spack develop: stage build artifacts in same root as non-dev builds (#41373)

Currently (outside of this PR) when you `spack develop` a path, this path is treated as the staging
directory (this means that for example all build artifacts are placed in the develop path).

This PR creates a separate staging directory for all `spack develop`ed builds. It looks like

```
# the stage root
/the-stage-root-for-all-spack-builds/
    spack-stage-<hash>
        # Spack packages inheriting CMakePackage put their build artifacts here
        spack-build-<hash>/
```

Unlike non-develop builds, there is no `spack-src` directory, `source_path` is the provided `dev_path`.
Instead, separately, in the `dev_path`, we have:

```
/dev/path/for/foo/
    build-{arch}-<hash> -> /the-stage-root-for-all-spack-builds/spack-stage-<hash>/
```

The main benefit of this is that build artifacts for out-of-source builds that are relative to
`Stage.path` are easily identified (and you can delete them with `spack clean`).

Other behavior added here:

- [x] A symlink is made from the `dev_path` to the stage directory. This symlink name incorporates
    spec details, so that multiple Spack environments that develop the same path will not conflict
    with one another

- [x] `spack cd` and `spack location` have added a `-c` shorthand for `--source-dir`

Spack builds can still change the develop path (in particular to keep track of applied patches), 
and for in-source builds, this doesn't change much (although logs would not be written into 
the develop path). Packages inheriting from `CMakePackage` should get this benefit
automatically though.
This commit is contained in:
Peter Scheibel 2024-03-14 13:32:01 -07:00 committed by GitHub
parent 22cb3815fe
commit ec517b40e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 295 additions and 125 deletions

View File

@ -53,6 +53,7 @@ def setup_parser(subparser):
"-S", "--stages", action="store_true", help="top level stage directory" "-S", "--stages", action="store_true", help="top level stage directory"
) )
directories.add_argument( directories.add_argument(
"-c",
"--source-dir", "--source-dir",
action="store_true", action="store_true",
help="source directory for a spec (requires it to be staged first)", help="source directory for a spec (requires it to be staged first)",

View File

@ -64,7 +64,7 @@
install_test_root, install_test_root,
) )
from spack.installer import InstallError, PackageInstaller from spack.installer import InstallError, PackageInstaller
from spack.stage import DIYStage, ResourceStage, Stage, StageComposite, compute_stage_name from spack.stage import DevelopStage, ResourceStage, Stage, StageComposite, compute_stage_name
from spack.util.executable import ProcessError, which from spack.util.executable import ProcessError, which
from spack.util.package_hash import package_hash from spack.util.package_hash import package_hash
from spack.version import GitVersion, StandardVersion from spack.version import GitVersion, StandardVersion
@ -1075,7 +1075,12 @@ def _make_stage(self):
# If it's a dev package (not transitively), use a DIY stage object # If it's a dev package (not transitively), use a DIY stage object
dev_path_var = self.spec.variants.get("dev_path", None) dev_path_var = self.spec.variants.get("dev_path", None)
if dev_path_var: if dev_path_var:
return DIYStage(dev_path_var.value) dev_path = dev_path_var.value
link_format = spack.config.get("config:develop_stage_link")
if not link_format:
link_format = "build-{arch}-{hash:7}"
stage_link = self.spec.format_path(link_format)
return DevelopStage(compute_stage_name(self.spec), dev_path, stage_link)
# To fetch the current version # To fetch the current version
source_stage = self._make_root_stage(self.fetcher) source_stage = self._make_root_stage(self.fetcher)
@ -1407,7 +1412,7 @@ def do_fetch(self, mirror_only=False):
return return
checksum = spack.config.get("config:checksum") checksum = spack.config.get("config:checksum")
fetch = self.stage.managed_by_spack fetch = self.stage.needs_fetching
if ( if (
checksum checksum
and fetch and fetch
@ -1480,9 +1485,6 @@ def do_stage(self, mirror_only=False):
if self.has_code: if self.has_code:
self.do_fetch(mirror_only) self.do_fetch(mirror_only)
self.stage.expand_archive() self.stage.expand_archive()
if not os.listdir(self.stage.path):
raise spack.error.FetchError("Archive was empty for %s" % self.name)
else: else:
# Support for post-install hooks requires a stage.source_path # Support for post-install hooks requires a stage.source_path
fsys.mkdirp(self.stage.source_path) fsys.mkdirp(self.stage.source_path)
@ -1516,7 +1518,7 @@ def do_patch(self):
# If we encounter an archive that failed to patch, restage it # If we encounter an archive that failed to patch, restage it
# so that we can apply all the patches again. # so that we can apply all the patches again.
if os.path.isfile(bad_file): if os.path.isfile(bad_file):
if self.stage.managed_by_spack: if self.stage.requires_patch_success:
tty.debug("Patching failed last time. Restaging.") tty.debug("Patching failed last time. Restaging.")
self.stage.restage() self.stage.restage()
else: else:
@ -1537,6 +1539,8 @@ def do_patch(self):
tty.msg("No patches needed for {0}".format(self.name)) tty.msg("No patches needed for {0}".format(self.name))
return return
errors = []
# Apply all the patches for specs that match this one # Apply all the patches for specs that match this one
patched = False patched = False
for patch in patches: for patch in patches:
@ -1546,12 +1550,16 @@ def do_patch(self):
tty.msg("Applied patch {0}".format(patch.path_or_url)) tty.msg("Applied patch {0}".format(patch.path_or_url))
patched = True patched = True
except spack.error.SpackError as e: except spack.error.SpackError as e:
tty.debug(e)
# Touch bad file if anything goes wrong. # Touch bad file if anything goes wrong.
tty.msg("Patch %s failed." % patch.path_or_url)
fsys.touch(bad_file) fsys.touch(bad_file)
raise error_msg = f"Patch {patch.path_or_url} failed."
if self.stage.requires_patch_success:
tty.msg(error_msg)
raise
else:
tty.debug(error_msg)
tty.debug(e)
errors.append(e)
if has_patch_fun: if has_patch_fun:
try: try:
@ -1569,24 +1577,29 @@ def do_patch(self):
# printed a message for each patch. # printed a message for each patch.
tty.msg("No patches needed for {0}".format(self.name)) tty.msg("No patches needed for {0}".format(self.name))
except spack.error.SpackError as e: except spack.error.SpackError as e:
tty.debug(e)
# Touch bad file if anything goes wrong. # Touch bad file if anything goes wrong.
tty.msg("patch() function failed for {0}".format(self.name))
fsys.touch(bad_file) fsys.touch(bad_file)
raise error_msg = f"patch() function failed for {self.name}"
if self.stage.requires_patch_success:
tty.msg(error_msg)
raise
else:
tty.debug(error_msg)
tty.debug(e)
errors.append(e)
# Get rid of any old failed file -- patches have either succeeded if not errors:
# or are not needed. This is mostly defensive -- it's needed # Get rid of any old failed file -- patches have either succeeded
# if the restage() method doesn't clean *everything* (e.g., for a repo) # or are not needed. This is mostly defensive -- it's needed
if os.path.isfile(bad_file): # if we didn't restage
os.remove(bad_file) if os.path.isfile(bad_file):
os.remove(bad_file)
# touch good or no patches file so that we skip next time. # touch good or no patches file so that we skip next time.
if patched: if patched:
fsys.touch(good_file) fsys.touch(good_file)
else: else:
fsys.touch(no_patches_file) fsys.touch(no_patches_file)
@classmethod @classmethod
def all_patches(cls): def all_patches(cls):

View File

@ -63,6 +63,7 @@
"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}] "oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]
}, },
"stage_name": {"type": "string"}, "stage_name": {"type": "string"},
"develop_stage_link": {"type": "string"},
"test_stage": {"type": "string"}, "test_stage": {"type": "string"},
"extensions": {"type": "array", "items": {"type": "string"}}, "extensions": {"type": "array", "items": {"type": "string"}},
"template_dirs": {"type": "array", "items": {"type": "string"}}, "template_dirs": {"type": "array", "items": {"type": "string"}},

View File

@ -208,7 +208,103 @@ def _mirror_roots():
] ]
class Stage: class LockableStagingDir:
"""A directory whose lifetime can be managed with a context
manager (but persists if the user requests it). Instances can have
a specified name and if they do, then for all instances that have
the same name, only one can enter the context manager at a time.
"""
def __init__(self, name, path, keep, lock):
# TODO: This uses a protected member of tempfile, but seemed the only
# TODO: way to get a temporary name. It won't be the same as the
# TODO: temporary stage area in _stage_root.
self.name = name
if name is None:
self.name = stage_prefix + next(tempfile._get_candidate_names())
# Use the provided path or construct an optionally named stage path.
if path is not None:
self.path = path
else:
self.path = os.path.join(get_stage_root(), self.name)
# Flag to decide whether to delete the stage folder on exit or not
self.keep = keep
# File lock for the stage directory. We use one file for all
# stage locks. See spack.database.Database.prefix_locker.lock for
# details on this approach.
self._lock = None
self._use_locks = lock
# When stages are reused, we need to know whether to re-create
# it. This marks whether it has been created/destroyed.
self.created = False
def _get_lock(self):
if not self._lock:
sha1 = hashlib.sha1(self.name.encode("utf-8")).digest()
lock_id = prefix_bits(sha1, bit_length(sys.maxsize))
stage_lock_path = os.path.join(get_stage_root(), ".lock")
self._lock = spack.util.lock.Lock(
stage_lock_path, start=lock_id, length=1, desc=self.name
)
return self._lock
def __enter__(self):
"""
Entering a stage context will create the stage directory
Returns:
self
"""
if self._use_locks:
self._get_lock().acquire_write(timeout=60)
self.create()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Exiting from a stage context will delete the stage directory unless:
- it was explicitly requested not to do so
- an exception has been raised
Args:
exc_type: exception type
exc_val: exception value
exc_tb: exception traceback
Returns:
Boolean
"""
# Delete when there are no exceptions, unless asked to keep.
if exc_type is None and not self.keep:
self.destroy()
if self._use_locks:
self._get_lock().release_write()
def create(self):
"""
Ensures the top-level (config:build_stage) directory exists.
"""
# User has full permissions and group has only read permissions
if not os.path.exists(self.path):
mkdirp(self.path, mode=stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)
elif not os.path.isdir(self.path):
os.remove(self.path)
mkdirp(self.path, mode=stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)
# Make sure we can actually do something with the stage we made.
ensure_access(self.path)
self.created = True
def destroy(self):
raise NotImplementedError(f"{self.__class__.__name__} is abstract")
class Stage(LockableStagingDir):
"""Manages a temporary stage directory for building. """Manages a temporary stage directory for building.
A Stage object is a context manager that handles a directory where A Stage object is a context manager that handles a directory where
@ -251,7 +347,8 @@ class Stage:
""" """
#: Most staging is managed by Spack. DIYStage is one exception. #: Most staging is managed by Spack. DIYStage is one exception.
managed_by_spack = True needs_fetching = True
requires_patch_success = True
def __init__( def __init__(
self, self,
@ -297,6 +394,8 @@ def __init__(
The search function that provides the fetch strategy The search function that provides the fetch strategy
instance. instance.
""" """
super().__init__(name, path, keep, lock)
# TODO: fetch/stage coupling needs to be reworked -- the logic # TODO: fetch/stage coupling needs to be reworked -- the logic
# TODO: here is convoluted and not modular enough. # TODO: here is convoluted and not modular enough.
if isinstance(url_or_fetch_strategy, str): if isinstance(url_or_fetch_strategy, str):
@ -314,72 +413,8 @@ def __init__(
self.srcdir = None self.srcdir = None
# TODO: This uses a protected member of tempfile, but seemed the only
# TODO: way to get a temporary name. It won't be the same as the
# TODO: temporary stage area in _stage_root.
self.name = name
if name is None:
self.name = stage_prefix + next(tempfile._get_candidate_names())
self.mirror_paths = mirror_paths self.mirror_paths = mirror_paths
# Use the provided path or construct an optionally named stage path.
if path is not None:
self.path = path
else:
self.path = os.path.join(get_stage_root(), self.name)
# Flag to decide whether to delete the stage folder on exit or not
self.keep = keep
# File lock for the stage directory. We use one file for all
# stage locks. See spack.database.Database.prefix_locker.lock for
# details on this approach.
self._lock = None
if lock:
sha1 = hashlib.sha1(self.name.encode("utf-8")).digest()
lock_id = prefix_bits(sha1, bit_length(sys.maxsize))
stage_lock_path = os.path.join(get_stage_root(), ".lock")
self._lock = spack.util.lock.Lock(
stage_lock_path, start=lock_id, length=1, desc=self.name
)
# When stages are reused, we need to know whether to re-create
# it. This marks whether it has been created/destroyed.
self.created = False
def __enter__(self):
"""
Entering a stage context will create the stage directory
Returns:
self
"""
if self._lock is not None:
self._lock.acquire_write(timeout=60)
self.create()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Exiting from a stage context will delete the stage directory unless:
- it was explicitly requested not to do so
- an exception has been raised
Args:
exc_type: exception type
exc_val: exception value
exc_tb: exception traceback
Returns:
Boolean
"""
# Delete when there are no exceptions, unless asked to keep.
if exc_type is None and not self.keep:
self.destroy()
if self._lock is not None:
self._lock.release_write()
@property @property
def expected_archive_files(self): def expected_archive_files(self):
"""Possible archive file paths.""" """Possible archive file paths."""
@ -631,21 +666,6 @@ def restage(self):
""" """
self.fetcher.reset() self.fetcher.reset()
def create(self):
"""
Ensures the top-level (config:build_stage) directory exists.
"""
# User has full permissions and group has only read permissions
if not os.path.exists(self.path):
mkdirp(self.path, mode=stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)
elif not os.path.isdir(self.path):
os.remove(self.path)
mkdirp(self.path, mode=stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)
# Make sure we can actually do something with the stage we made.
ensure_access(self.path)
self.created = True
def destroy(self): def destroy(self):
"""Removes this stage directory.""" """Removes this stage directory."""
remove_linked_tree(self.path) remove_linked_tree(self.path)
@ -752,7 +772,8 @@ def __init__(self):
"cache_mirror", "cache_mirror",
"steal_source", "steal_source",
"disable_mirrors", "disable_mirrors",
"managed_by_spack", "needs_fetching",
"requires_patch_success",
] ]
) )
@ -808,8 +829,8 @@ class DIYStage:
directory naming convention. directory naming convention.
""" """
"""DIY staging is, by definition, not managed by Spack.""" needs_fetching = False
managed_by_spack = False requires_patch_success = False
def __init__(self, path): def __init__(self, path):
if path is None: if path is None:
@ -857,6 +878,65 @@ def cache_local(self):
tty.debug("Sources for DIY stages are not cached") tty.debug("Sources for DIY stages are not cached")
class DevelopStage(LockableStagingDir):
needs_fetching = False
requires_patch_success = False
def __init__(self, name, dev_path, reference_link):
super().__init__(name=name, path=None, keep=False, lock=True)
self.dev_path = dev_path
self.source_path = dev_path
# The path of a link that will point to this stage
if os.path.isabs(reference_link):
link_path = reference_link
else:
link_path = os.path.join(self.source_path, reference_link)
if not os.path.isdir(os.path.dirname(link_path)):
raise StageError(f"The directory containing {link_path} must exist")
self.reference_link = link_path
@property
def archive_file(self):
return None
def fetch(self, *args, **kwargs):
tty.debug("No fetching needed for develop stage.")
def check(self):
tty.debug("No checksum needed for develop stage.")
def expand_archive(self):
tty.debug("No expansion needed for develop stage.")
@property
def expanded(self):
"""Returns True since the source_path must exist."""
return True
def create(self):
super().create()
try:
llnl.util.symlink.symlink(self.path, self.reference_link)
except (llnl.util.symlink.AlreadyExistsError, FileExistsError):
pass
def destroy(self):
# Destroy all files, but do not follow symlinks
try:
shutil.rmtree(self.path)
except FileNotFoundError:
pass
self.created = False
def restage(self):
self.destroy()
self.create()
def cache_local(self):
tty.debug("Sources for Develop stages are not cached")
def ensure_access(file): def ensure_access(file):
"""Ensure we can access a directory and die with an error if we can't.""" """Ensure we can access a directory and die with an error if we can't."""
if not can_access(file): if not can_access(file):

View File

@ -41,6 +41,7 @@ def test_dev_build_basics(tmpdir, install_mockery):
assert os.path.exists(str(tmpdir)) assert os.path.exists(str(tmpdir))
@pytest.mark.disable_clean_stage_check
def test_dev_build_before(tmpdir, install_mockery): def test_dev_build_before(tmpdir, install_mockery):
spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized() spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized()
@ -57,6 +58,7 @@ def test_dev_build_before(tmpdir, install_mockery):
assert not os.path.exists(spec.prefix) assert not os.path.exists(spec.prefix)
@pytest.mark.disable_clean_stage_check
def test_dev_build_until(tmpdir, install_mockery): def test_dev_build_until(tmpdir, install_mockery):
spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized() spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized()
@ -74,6 +76,7 @@ def test_dev_build_until(tmpdir, install_mockery):
assert not spack.store.STORE.db.query(spec, installed=True) assert not spack.store.STORE.db.query(spec, installed=True)
@pytest.mark.disable_clean_stage_check
def test_dev_build_until_last_phase(tmpdir, install_mockery): def test_dev_build_until_last_phase(tmpdir, install_mockery):
# Test that we ignore the last_phase argument if it is already last # Test that we ignore the last_phase argument if it is already last
spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized() spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized()
@ -93,6 +96,7 @@ def test_dev_build_until_last_phase(tmpdir, install_mockery):
assert os.path.exists(str(tmpdir)) assert os.path.exists(str(tmpdir))
@pytest.mark.disable_clean_stage_check
def test_dev_build_before_until(tmpdir, install_mockery, capsys): def test_dev_build_before_until(tmpdir, install_mockery, capsys):
spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized() spec = spack.spec.Spec(f"dev-build-test-install@0.0.0 dev_path={tmpdir}").concretized()
@ -130,6 +134,7 @@ def mock_module_noop(*args):
pass pass
@pytest.mark.disable_clean_stage_check
def test_dev_build_drop_in(tmpdir, mock_packages, monkeypatch, install_mockery, working_env): def test_dev_build_drop_in(tmpdir, mock_packages, monkeypatch, install_mockery, working_env):
monkeypatch.setattr(os, "execvp", print_spack_cc) monkeypatch.setattr(os, "execvp", print_spack_cc)
monkeypatch.setattr(spack.build_environment, "module", mock_module_noop) monkeypatch.setattr(spack.build_environment, "module", mock_module_noop)

View File

@ -14,6 +14,7 @@
import spack.spec import spack.spec
from spack.main import SpackCommand from spack.main import SpackCommand
add = SpackCommand("add")
develop = SpackCommand("develop") develop = SpackCommand("develop")
env = SpackCommand("env") env = SpackCommand("env")
@ -192,14 +193,16 @@ def test_develop_full_git_repo(
finally: finally:
spec.package.do_clean() spec.package.do_clean()
# Now use "spack develop": look at the resulting stage directory and make # Now use "spack develop": look at the resulting dev_path and make
# sure the git repo pulled includes the full branch history (or rather, # sure the git repo pulled includes the full branch history (or rather,
# more than just one commit). # more than just one commit).
env("create", "test") env("create", "test")
with ev.read("test"): with ev.read("test") as e:
add("git-test-commit")
develop("git-test-commit@1.2") develop("git-test-commit@1.2")
location = SpackCommand("location") e.concretize()
develop_stage_dir = location("git-test-commit").strip() spec = e.all_specs()[0]
commits = _git_commit_list(develop_stage_dir) develop_dir = spec.variants["dev_path"].value
commits = _git_commit_list(develop_dir)
assert len(commits) > 1 assert len(commits) > 1

View File

@ -268,7 +268,7 @@ def trigger_bad_patch(pkg):
def test_patch_failure_develop_spec_exits_gracefully( def test_patch_failure_develop_spec_exits_gracefully(
mock_packages, config, install_mockery, mock_fetch, tmpdir mock_packages, config, install_mockery, mock_fetch, tmpdir, mock_stage
): ):
""" """
ensure that a failing patch does not trigger exceptions ensure that a failing patch does not trigger exceptions

View File

@ -22,7 +22,7 @@
import spack.util.executable import spack.util.executable
import spack.util.url as url_util import spack.util.url as url_util
from spack.resource import Resource from spack.resource import Resource
from spack.stage import DIYStage, ResourceStage, Stage, StageComposite from spack.stage import DevelopStage, DIYStage, ResourceStage, Stage, StageComposite
from spack.util.path import canonicalize_path from spack.util.path import canonicalize_path
# The following values are used for common fetch and stage mocking fixtures: # The following values are used for common fetch and stage mocking fixtures:
@ -145,7 +145,7 @@ def check_destroy(stage, stage_name):
assert not os.path.exists(stage_path) assert not os.path.exists(stage_path)
# tmp stage needs to remove tmp dir too. # tmp stage needs to remove tmp dir too.
if not stage.managed_by_spack: if not isinstance(stage, DIYStage):
target = os.path.realpath(stage_path) target = os.path.realpath(stage_path)
assert not os.path.exists(target) assert not os.path.exists(target)
@ -857,6 +857,73 @@ def test_diystage_preserve_file(self, tmpdir):
_file.read() == _readme_contents _file.read() == _readme_contents
def _create_files_from_tree(base, tree):
for name, content in tree.items():
sub_base = os.path.join(base, name)
if isinstance(content, dict):
os.mkdir(sub_base)
_create_files_from_tree(sub_base, content)
else:
assert (content is None) or (isinstance(content, str))
with open(sub_base, "w") as f:
if content:
f.write(content)
def _create_tree_from_dir_recursive(path):
if os.path.islink(path):
return os.readlink(path)
elif os.path.isdir(path):
tree = {}
for name in os.listdir(path):
sub_path = os.path.join(path, name)
tree[name] = _create_tree_from_dir_recursive(sub_path)
return tree
else:
with open(path, "r") as f:
content = f.read() or None
return content
@pytest.fixture
def develop_path(tmpdir):
dir_structure = {"a1": {"b1": None, "b2": "b1content"}, "a2": None}
srcdir = str(tmpdir.join("test-src"))
os.mkdir(srcdir)
_create_files_from_tree(srcdir, dir_structure)
yield dir_structure, srcdir
class TestDevelopStage:
def test_sanity_check_develop_path(self, develop_path):
_, srcdir = develop_path
with open(os.path.join(srcdir, "a1", "b2")) as f:
assert f.read() == "b1content"
assert os.path.exists(os.path.join(srcdir, "a2"))
def test_develop_stage(self, develop_path, tmp_build_stage_dir):
"""Check that (a) develop stages update the given
`dev_path` with a symlink that points to the stage dir and
(b) that destroying the stage does not destroy `dev_path`
"""
devtree, srcdir = develop_path
stage = DevelopStage("test-stage", srcdir, reference_link="link-to-stage")
stage.create()
srctree1 = _create_tree_from_dir_recursive(stage.source_path)
assert os.path.samefile(srctree1["link-to-stage"], stage.path)
del srctree1["link-to-stage"]
assert srctree1 == devtree
stage.destroy()
# Make sure destroying the stage doesn't change anything
# about the path
assert not os.path.exists(stage.path)
srctree2 = _create_tree_from_dir_recursive(srcdir)
del srctree2["link-to-stage"] # Note the symlink persists but is broken
assert srctree2 == devtree
def test_stage_create_replace_path(tmp_build_stage_dir): def test_stage_create_replace_path(tmp_build_stage_dir):
"""Ensure stage creation replaces a non-directory path.""" """Ensure stage creation replaces a non-directory path."""
_, test_stage_path = tmp_build_stage_dir _, test_stage_path = tmp_build_stage_dir

View File

@ -668,7 +668,7 @@ _spack_buildcache_rebuild_index() {
_spack_cd() { _spack_cd() {
if $list_options if $list_options
then then
SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages --source-dir -b --build-dir -e --env --first" SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -c --source-dir -b --build-dir -e --env --first"
else else
_all_packages _all_packages
fi fi
@ -1347,7 +1347,7 @@ _spack_load() {
_spack_location() { _spack_location() {
if $list_options if $list_options
then then
SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages --source-dir -b --build-dir -e --env --first" SPACK_COMPREPLY="-h --help -m --module-dir -r --spack-root -i --install-dir -p --package-dir -P --packages -s --stage-dir -S --stages -c --source-dir -b --build-dir -e --env --first"
else else
_all_packages _all_packages
fi fi

View File

@ -880,7 +880,7 @@ complete -c spack -n '__fish_spack_using_command buildcache rebuild-index' -s k
complete -c spack -n '__fish_spack_using_command buildcache rebuild-index' -s k -l keys -d 'if provided, key index will be updated as well as package index' complete -c spack -n '__fish_spack_using_command buildcache rebuild-index' -s k -l keys -d 'if provided, key index will be updated as well as package index'
# spack cd # spack cd
set -g __fish_spack_optspecs_spack_cd h/help m/module-dir r/spack-root i/install-dir p/package-dir P/packages s/stage-dir S/stages source-dir b/build-dir e/env= first set -g __fish_spack_optspecs_spack_cd h/help m/module-dir r/spack-root i/install-dir p/package-dir P/packages s/stage-dir S/stages c/source-dir b/build-dir e/env= first
complete -c spack -n '__fish_spack_using_command_pos_remainder 0 cd' -f -k -a '(__fish_spack_specs)' complete -c spack -n '__fish_spack_using_command_pos_remainder 0 cd' -f -k -a '(__fish_spack_specs)'
complete -c spack -n '__fish_spack_using_command cd' -s h -l help -f -a help complete -c spack -n '__fish_spack_using_command cd' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command cd' -s h -l help -d 'show this help message and exit' complete -c spack -n '__fish_spack_using_command cd' -s h -l help -d 'show this help message and exit'
@ -898,8 +898,8 @@ complete -c spack -n '__fish_spack_using_command cd' -s s -l stage-dir -f -a sta
complete -c spack -n '__fish_spack_using_command cd' -s s -l stage-dir -d 'stage directory for a spec' complete -c spack -n '__fish_spack_using_command cd' -s s -l stage-dir -d 'stage directory for a spec'
complete -c spack -n '__fish_spack_using_command cd' -s S -l stages -f -a stages complete -c spack -n '__fish_spack_using_command cd' -s S -l stages -f -a stages
complete -c spack -n '__fish_spack_using_command cd' -s S -l stages -d 'top level stage directory' complete -c spack -n '__fish_spack_using_command cd' -s S -l stages -d 'top level stage directory'
complete -c spack -n '__fish_spack_using_command cd' -l source-dir -f -a source_dir complete -c spack -n '__fish_spack_using_command cd' -s c -l source-dir -f -a source_dir
complete -c spack -n '__fish_spack_using_command cd' -l source-dir -d 'source directory for a spec (requires it to be staged first)' complete -c spack -n '__fish_spack_using_command cd' -s c -l source-dir -d 'source directory for a spec (requires it to be staged first)'
complete -c spack -n '__fish_spack_using_command cd' -s b -l build-dir -f -a build_dir complete -c spack -n '__fish_spack_using_command cd' -s b -l build-dir -f -a build_dir
complete -c spack -n '__fish_spack_using_command cd' -s b -l build-dir -d 'build directory for a spec (requires it to be staged first)' complete -c spack -n '__fish_spack_using_command cd' -s b -l build-dir -d 'build directory for a spec (requires it to be staged first)'
complete -c spack -n '__fish_spack_using_command cd' -s e -l env -r -f -a location_env complete -c spack -n '__fish_spack_using_command cd' -s e -l env -r -f -a location_env
@ -2089,7 +2089,7 @@ complete -c spack -n '__fish_spack_using_command load' -l list -f -a list
complete -c spack -n '__fish_spack_using_command load' -l list -d 'show loaded packages: same as `spack find --loaded`' complete -c spack -n '__fish_spack_using_command load' -l list -d 'show loaded packages: same as `spack find --loaded`'
# spack location # spack location
set -g __fish_spack_optspecs_spack_location h/help m/module-dir r/spack-root i/install-dir p/package-dir P/packages s/stage-dir S/stages source-dir b/build-dir e/env= first set -g __fish_spack_optspecs_spack_location h/help m/module-dir r/spack-root i/install-dir p/package-dir P/packages s/stage-dir S/stages c/source-dir b/build-dir e/env= first
complete -c spack -n '__fish_spack_using_command_pos_remainder 0 location' -f -k -a '(__fish_spack_specs)' complete -c spack -n '__fish_spack_using_command_pos_remainder 0 location' -f -k -a '(__fish_spack_specs)'
complete -c spack -n '__fish_spack_using_command location' -s h -l help -f -a help complete -c spack -n '__fish_spack_using_command location' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command location' -s h -l help -d 'show this help message and exit' complete -c spack -n '__fish_spack_using_command location' -s h -l help -d 'show this help message and exit'
@ -2107,8 +2107,8 @@ complete -c spack -n '__fish_spack_using_command location' -s s -l stage-dir -f
complete -c spack -n '__fish_spack_using_command location' -s s -l stage-dir -d 'stage directory for a spec' complete -c spack -n '__fish_spack_using_command location' -s s -l stage-dir -d 'stage directory for a spec'
complete -c spack -n '__fish_spack_using_command location' -s S -l stages -f -a stages complete -c spack -n '__fish_spack_using_command location' -s S -l stages -f -a stages
complete -c spack -n '__fish_spack_using_command location' -s S -l stages -d 'top level stage directory' complete -c spack -n '__fish_spack_using_command location' -s S -l stages -d 'top level stage directory'
complete -c spack -n '__fish_spack_using_command location' -l source-dir -f -a source_dir complete -c spack -n '__fish_spack_using_command location' -s c -l source-dir -f -a source_dir
complete -c spack -n '__fish_spack_using_command location' -l source-dir -d 'source directory for a spec (requires it to be staged first)' complete -c spack -n '__fish_spack_using_command location' -s c -l source-dir -d 'source directory for a spec (requires it to be staged first)'
complete -c spack -n '__fish_spack_using_command location' -s b -l build-dir -f -a build_dir complete -c spack -n '__fish_spack_using_command location' -s b -l build-dir -f -a build_dir
complete -c spack -n '__fish_spack_using_command location' -s b -l build-dir -d 'build directory for a spec (requires it to be staged first)' complete -c spack -n '__fish_spack_using_command location' -s b -l build-dir -d 'build directory for a spec (requires it to be staged first)'
complete -c spack -n '__fish_spack_using_command location' -s e -l env -r -f -a location_env complete -c spack -n '__fish_spack_using_command location' -s e -l env -r -f -a location_env