diff --git a/lib/spack/docs/environments.rst b/lib/spack/docs/environments.rst index 78a903a77e..193258e047 100644 --- a/lib/spack/docs/environments.rst +++ b/lib/spack/docs/environments.rst @@ -460,6 +460,125 @@ Sourcing that file in Bash will make the environment available to the user; and can be included in ``.bashrc`` files, etc. The ``loads`` file may also be copied out of the environment, renamed, etc. + +.. _environment_include_concrete: + +------------------------------ +Included Concrete Environments +------------------------------ + +Spack environments can create an environment based off of information in already +established environments. You can think of it as a combination of existing +environments. It will gather information from the existing environment's +``spack.lock`` and use that during the creation of this included concrete +environment. When an included concrete environment is created it will generate +a ``spack.lock`` file for the newly created environment. + + +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Creating included environments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +To create a combined concrete environment, you must have at least one existing +concrete environment. You will use the command ``spack env create`` with the +argument ``--include-concrete`` followed by the name or path of the environment +you'd like to include. Here is an example of how to create a combined environment +from the command line. + +.. code-block:: console + + $ spack env create myenv + $ spack -e myenv add python + $ spack -e myenv concretize + $ spack env create --include-concrete myenv included_env + + +You can also include an environment directly in the ``spack.yaml`` file. It +involves adding the ``include_concrete`` heading in the yaml followed by the +absolute path to the independent environments. + +.. code-block:: yaml + + spack: + specs: [] + concretizer: + unify: true + include_concrete: + - /absolute/path/to/environment1 + - /absolute/path/to/environment2 + + +Once the ``spack.yaml`` has been updated you must concretize the environment to +get the concrete specs from the included environments. + +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Updating an included environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If changes were made to the base environment and you want that reflected in the +included environment you will need to reconcretize both the base environment and the +included environment for the change to be implemented. For example: + +.. code-block:: console + + $ spack env create myenv + $ spack -e myenv add python + $ spack -e myenv concretize + $ spack env create --include-concrete myenv included_env + + + $ spack -e myenv find + ==> In environment myenv + ==> Root specs + python + + ==> 0 installed packages + + + $ spack -e included_env find + ==> In environment included_env + ==> No root specs + ==> Included specs + python + + ==> 0 installed packages + +Here we see that ``included_env`` has access to the python package through +the ``myenv`` environment. But if we were to add another spec to ``myenv``, +``included_env`` will not be able to access the new information. + +.. code-block:: console + + $ spack -e myenv add perl + $ spack -e myenv concretize + $ spack -e myenv find + ==> In environment myenv + ==> Root specs + perl python + + ==> 0 installed packages + + + $ spack -e included_env find + ==> In environment included_env + ==> No root specs + ==> Included specs + python + + ==> 0 installed packages + +It isn't until you run the ``spack concretize`` command that the combined +environment will get the updated information from the reconcretized base environmennt. + +.. code-block:: console + + $ spack -e included_env concretize + $ spack -e included_env find + ==> In environment included_env + ==> No root specs + ==> Included specs + perl python + + ==> 0 installed packages + .. _environment-configuration: ------------------------ @@ -811,6 +930,7 @@ For example, the following environment has three root packages: This allows for a much-needed reduction in redundancy between packages and constraints. + ---------------- Filesystem Views ---------------- @@ -1044,7 +1164,7 @@ other targets to depend on the environment installation. A typical workflow is as follows: -.. code:: console +.. code-block:: console spack env create -d . spack -e . add perl @@ -1137,7 +1257,7 @@ its dependencies. This can be useful when certain flags should only apply to dependencies. Below we show a use case where a spec is installed with verbose output (``spack install --verbose``) while its dependencies are installed silently: -.. code:: console +.. code-block:: console $ spack env depfile -o Makefile @@ -1159,7 +1279,7 @@ This can be accomplished through the generated ``[/]SPACK_PACKAGE_IDS`` variable. Assuming we have an active and concrete environment, we generate the associated ``Makefile`` with a prefix ``example``: -.. code:: console +.. code-block:: console $ spack env depfile -o env.mk --make-prefix example diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py index 13f67e74e6..2ccb88fd1a 100644 --- a/lib/spack/spack/cmd/env.py +++ b/lib/spack/spack/cmd/env.py @@ -10,7 +10,7 @@ import sys import tempfile from pathlib import Path -from typing import Optional +from typing import List, Optional import llnl.string as string import llnl.util.filesystem as fs @@ -87,6 +87,9 @@ def env_create_setup_parser(subparser): default=None, help="either a lockfile (must end with '.json' or '.lock') or a manifest file", ) + subparser.add_argument( + "--include-concrete", action="append", help="name of old environment to copy specs from" + ) def env_create(args): @@ -104,12 +107,17 @@ def env_create(args): # the environment should not include a view. with_view = None + include_concrete = None + if hasattr(args, "include_concrete"): + include_concrete = args.include_concrete + env = _env_create( args.env_name, init_file=args.envfile, dir=args.dir or os.path.sep in args.env_name or args.env_name in (".", ".."), with_view=with_view, keep_relative=args.keep_relative, + include_concrete=include_concrete, ) # Generate views, only really useful for environments created from spack.lock files. @@ -123,31 +131,43 @@ def _env_create( dir: bool = False, with_view: Optional[str] = None, keep_relative: bool = False, + include_concrete: Optional[List[str]] = None, ): """Create a new environment, with an optional yaml description. Arguments: - name_or_path: name of the environment to create, or path to it - init_file: optional initialization file -- can be a JSON lockfile (*.lock, *.json) or YAML - manifest file - dir: if True, create an environment in a directory instead of a managed environment - keep_relative: if True, develop paths are copied verbatim into the new environment file, - otherwise they may be made absolute if the new environment is in a different location + name_or_path (str): name of the environment to create, or path to it + init_file (str or file): optional initialization file -- can be + a JSON lockfile (*.lock, *.json) or YAML manifest file + dir (bool): if True, create an environment in a directory instead + of a named environment + keep_relative (bool): if True, develop paths are copied verbatim into + the new environment file, otherwise they may be made absolute if the + new environment is in a different location + include_concrete (list): list of the included concrete environments """ if not dir: env = ev.create( - name_or_path, init_file=init_file, with_view=with_view, keep_relative=keep_relative + name_or_path, + init_file=init_file, + with_view=with_view, + keep_relative=keep_relative, + include_concrete=include_concrete, ) tty.msg( colorize( - f"Created environment @c{{{cescape(env.name)}}} in: @c{{{cescape(env.path)}}}" + f"Created environment @c{{{cescape(name_or_path)}}} in: @c{{{cescape(env.path)}}}" ) ) else: env = ev.create_in_dir( - name_or_path, init_file=init_file, with_view=with_view, keep_relative=keep_relative + name_or_path, + init_file=init_file, + with_view=with_view, + keep_relative=keep_relative, + include_concrete=include_concrete, ) - tty.msg(colorize(f"Created anonymous environment in: @c{{{cescape(env.path)}}}")) + tty.msg(colorize(f"Created independent environment in: @c{{{cescape(env.path)}}}")) tty.msg(f"Activate with: {colorize(f'@c{{spack env activate {cescape(name_or_path)}}}')}") return env @@ -434,6 +454,12 @@ def env_remove_setup_parser(subparser): """remove an existing environment""" subparser.add_argument("rm_env", metavar="env", nargs="+", help="environment(s) to remove") arguments.add_common_arguments(subparser, ["yes_to_all"]) + subparser.add_argument( + "-f", + "--force", + action="store_true", + help="remove the environment even if it is included in another environment", + ) def env_remove(args): @@ -443,13 +469,35 @@ def env_remove(args): and manifests embedded in repositories should be removed manually. """ read_envs = [] + valid_envs = [] bad_envs = [] - for env_name in args.rm_env: + invalid_envs = [] + + for env_name in ev.all_environment_names(): try: env = ev.read(env_name) - read_envs.append(env) + valid_envs.append(env_name) + + if env_name in args.rm_env: + read_envs.append(env) except (spack.config.ConfigFormatError, ev.SpackEnvironmentConfigError): - bad_envs.append(env_name) + invalid_envs.append(env_name) + + if env_name in args.rm_env: + bad_envs.append(env_name) + + # Check if env is linked to another before trying to remove + for name in valid_envs: + # don't check if environment is included to itself + if name == env_name: + continue + environ = ev.Environment(ev.root(name)) + if ev.root(env_name) in environ.included_concrete_envs: + msg = f'Environment "{env_name}" is being used by environment "{name}"' + if args.force: + tty.warn(msg) + else: + tty.die(msg) if not args.yes_to_all: environments = string.plural(len(args.rm_env), "environment", show_n=False) diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index d1917a73b5..c4e2c77552 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) +import copy import sys import llnl.util.lang @@ -271,6 +272,27 @@ def root_decorator(spec, string): print() + if env.included_concrete_envs: + tty.msg("Included specs") + + # Root specs cannot be displayed with prefixes, since those are not + # set for abstract specs. Same for hashes + root_args = copy.copy(args) + root_args.paths = False + + # Roots are displayed with variants, etc. so that we can see + # specifically what the user asked for. + cmd.display_specs( + env.included_user_specs, + root_args, + decorator=lambda s, f: color.colorize("@*{%s}" % f), + namespace=True, + show_flags=True, + show_full_compiler=True, + variants=True, + ) + print() + if args.show_concretized: tty.msg("Concretized roots") cmd.display_specs(env.specs_by_hash.values(), args, decorator=decorator) diff --git a/lib/spack/spack/environment/__init__.py b/lib/spack/spack/environment/__init__.py index e6521aed87..fb083594e0 100644 --- a/lib/spack/spack/environment/__init__.py +++ b/lib/spack/spack/environment/__init__.py @@ -34,6 +34,9 @@ * ``spec``: a string representation of the abstract spec that was concretized 4. ``concrete_specs``: a dictionary containing the specs in the environment. + 5. ``include_concrete`` (dictionary): an optional dictionary that includes the roots + and concrete specs from the included environments, keyed by the path to that + environment Compatibility ------------- @@ -50,26 +53,37 @@ - ``v2`` - ``v3`` - ``v4`` + - ``v5`` * - ``v0.12:0.14`` - ✅ - - - + - * - ``v0.15:0.16`` - ✅ - ✅ - - + - * - ``v0.17`` - ✅ - ✅ - ✅ - + - * - ``v0.18:`` - ✅ - ✅ - ✅ - ✅ + - + * - ``v0.22:`` + - ✅ + - ✅ + - ✅ + - ✅ + - ✅ Version 1 --------- @@ -334,6 +348,118 @@ } } } + + +Version 5 +--------- + +Version 5 doesn't change the top-level lockfile format, but an optional dictionary is +added. The dictionary has the ``root`` and ``concrete_specs`` of the included +environments, which are keyed by the path to that environment. Since this is optional +if the environment does not have any included environments ``include_concrete`` will +not be a part of the lockfile. + +.. code-block:: json + + { + "_meta": { + "file-type": "spack-lockfile", + "lockfile-version": 5, + "specfile-version": 3 + }, + "roots": [ + { + "hash": "", + "spec": "" + }, + { + "hash": "", + "spec": "" + } + ], + "concrete_specs": { + "": { + "... ...": { }, + "dependencies": [ + { + "name": "depname_1", + "hash": "", + "type": ["build", "link"] + }, + { + "name": "depname_2", + "hash": "", + "type": ["build", "link"] + } + ], + "hash": "", + }, + "": { + "... ...": { }, + "dependencies": [ + { + "name": "depname_3", + "hash": "", + "type": ["build", "link"] + }, + { + "name": "depname_4", + "hash": "", + "type": ["build", "link"] + } + ], + "hash": "" + } + } + "include_concrete": { + "": { + "roots": [ + { + "hash": "", + "spec": "" + }, + { + "hash": "", + "spec": "" + } + ], + "concrete_specs": { + "": { + "... ...": { }, + "dependencies": [ + { + "name": "depname_1", + "hash": "", + "type": ["build", "link"] + }, + { + "name": "depname_2", + "hash": "", + "type": ["build", "link"] + } + ], + "hash": "", + }, + "": { + "... ...": { }, + "dependencies": [ + { + "name": "depname_3", + "hash": "", + "type": ["build", "link"] + }, + { + "name": "depname_4", + "hash": "", + "type": ["build", "link"] + } + ], + "hash": "" + } + } + } + } + } """ from .environment import ( diff --git a/lib/spack/spack/environment/environment.py b/lib/spack/spack/environment/environment.py index f14919adb6..562413c234 100644 --- a/lib/spack/spack/environment/environment.py +++ b/lib/spack/spack/environment/environment.py @@ -16,7 +16,7 @@ import urllib.parse import urllib.request import warnings -from typing import Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union import llnl.util.filesystem as fs import llnl.util.tty as tty @@ -159,6 +159,8 @@ def default_manifest_yaml(): default_view_name = "default" # Default behavior to link all packages into views (vs. only root packages) default_view_link = "all" +# The name for any included concrete specs +included_concrete_name = "include_concrete" def installed_specs(): @@ -293,6 +295,7 @@ def create( init_file: Optional[Union[str, pathlib.Path]] = None, with_view: Optional[Union[str, pathlib.Path, bool]] = None, keep_relative: bool = False, + include_concrete: Optional[List[str]] = None, ) -> "Environment": """Create a managed environment in Spack and returns it. @@ -309,10 +312,15 @@ def create( string, it specifies the path to the view keep_relative: if True, develop paths are copied verbatim into the new environment file, otherwise they are made absolute + include_concrete: list of concrete environment names/paths to be included """ environment_dir = environment_dir_from_name(name, exists_ok=False) return create_in_dir( - environment_dir, init_file=init_file, with_view=with_view, keep_relative=keep_relative + environment_dir, + init_file=init_file, + with_view=with_view, + keep_relative=keep_relative, + include_concrete=include_concrete, ) @@ -321,6 +329,7 @@ def create_in_dir( init_file: Optional[Union[str, pathlib.Path]] = None, with_view: Optional[Union[str, pathlib.Path, bool]] = None, keep_relative: bool = False, + include_concrete: Optional[List[str]] = None, ) -> "Environment": """Create an environment in the directory passed as input and returns it. @@ -334,6 +343,7 @@ def create_in_dir( string, it specifies the path to the view keep_relative: if True, develop paths are copied verbatim into the new environment file, otherwise they are made absolute + include_concrete: concrete environment names/paths to be included """ initialize_environment_dir(root, envfile=init_file) @@ -346,6 +356,12 @@ def create_in_dir( if with_view is not None: manifest.set_default_view(with_view) + if include_concrete is not None: + set_included_envs_to_env_paths(include_concrete) + validate_included_envs_exists(include_concrete) + validate_included_envs_concrete(include_concrete) + manifest.set_include_concrete(include_concrete) + manifest.flush() except (spack.config.ConfigFormatError, SpackEnvironmentConfigError) as e: @@ -367,6 +383,14 @@ def create_in_dir( return env + # Must be done after environment is initialized + if include_concrete: + manifest.set_include_concrete(include_concrete) + + if not keep_relative and init_file is not None and str(init_file).endswith(manifest_name): + init_file = pathlib.Path(init_file) + manifest.absolutify_dev_paths(init_file.parent) + def _rewrite_relative_dev_paths_on_relocation(env, init_file_dir): """When initializing the environment from a manifest file and we plan @@ -419,6 +443,67 @@ def ensure_env_root_path_exists(): fs.mkdirp(env_root_path()) +def set_included_envs_to_env_paths(include_concrete: List[str]) -> None: + """If the included environment(s) is the environment name + it is replaced by the path to the environment + + Args: + include_concrete: list of env name or path to env""" + + for i, env_name in enumerate(include_concrete): + if is_env_dir(env_name): + include_concrete[i] = env_name + elif exists(env_name): + include_concrete[i] = root(env_name) + + +def validate_included_envs_exists(include_concrete: List[str]) -> None: + """Checks that all of the included environments exist + + Args: + include_concrete: list of already existing concrete environments to include + + Raises: + SpackEnvironmentError: if any of the included environments do not exist + """ + + missing_envs = set() + + for i, env_name in enumerate(include_concrete): + if not is_env_dir(env_name): + missing_envs.add(env_name) + + if missing_envs: + msg = "The following environment(s) are missing: {0}".format(", ".join(missing_envs)) + raise SpackEnvironmentError(msg) + + +def validate_included_envs_concrete(include_concrete: List[str]) -> None: + """Checks that all of the included environments are concrete + + Args: + include_concrete: list of already existing concrete environments to include + + Raises: + SpackEnvironmentError: if any of the included environments are not concrete + """ + + non_concrete_envs = set() + + for env_path in include_concrete: + if not os.path.exists(Environment(env_path).lock_path): + non_concrete_envs.add(Environment(env_path).name) + + if non_concrete_envs: + msg = "The following environment(s) are not concrete: {0}\n" "Please run:".format( + ", ".join(non_concrete_envs) + ) + for env in non_concrete_envs: + msg += f"\n\t`spack -e {env} concretize`" + + raise SpackEnvironmentError(msg) + + def all_environment_names(): """List the names of environments that currently exist.""" # just return empty if the env path does not exist. A read-only @@ -821,6 +906,18 @@ def __init__(self, manifest_dir: Union[str, pathlib.Path]) -> None: self.specs_by_hash: Dict[str, Spec] = {} #: Repository for this environment (memoized) self._repo = None + + #: Environment paths for concrete (lockfile) included environments + self.included_concrete_envs: List[str] = [] + #: First-level included concretized spec data from/to the lockfile. + self.included_concrete_spec_data: Dict[str, Dict[str, List[str]]] = {} + #: User specs from included environments from the last concretization + self.included_concretized_user_specs: Dict[str, List[Spec]] = {} + #: Roots from included environments with the last concretization, in order + self.included_concretized_order: Dict[str, List[str]] = {} + #: Concretized specs by hash from the included environments + self.included_specs_by_hash: Dict[str, Dict[str, Spec]] = {} + #: Previously active environment self._previous_active = None self._dev_specs = None @@ -858,7 +955,7 @@ def _read(self): if os.path.exists(self.lock_path): with open(self.lock_path) as f: - read_lock_version = self._read_lockfile(f) + read_lock_version = self._read_lockfile(f)["_meta"]["lockfile-version"] if read_lock_version == 1: tty.debug(f"Storing backup of {self.lock_path} at {self._lock_backup_v1_path}") @@ -926,6 +1023,20 @@ def add_view(name, values): if self.views == dict(): self.views[default_view_name] = ViewDescriptor(self.path, self.view_path_default) + def _process_concrete_includes(self): + """Extract and load into memory included concrete spec data.""" + self.included_concrete_envs = self.manifest[TOP_LEVEL_KEY].get(included_concrete_name, []) + + if self.included_concrete_envs: + if os.path.exists(self.lock_path): + with open(self.lock_path) as f: + data = self._read_lockfile(f) + + if included_concrete_name in data: + self.included_concrete_spec_data = data[included_concrete_name] + else: + self.include_concrete_envs() + def _construct_state_from_manifest(self): """Set up user specs and views from the manifest file.""" self.spec_lists = collections.OrderedDict() @@ -942,6 +1053,31 @@ def _construct_state_from_manifest(self): self.spec_lists[user_speclist_name] = user_specs self._process_view(spack.config.get("view", True)) + self._process_concrete_includes() + + def all_concretized_user_specs(self) -> List[Spec]: + """Returns all of the concretized user specs of the environment and + its included environment(s).""" + concretized_user_specs = self.concretized_user_specs[:] + for included_specs in self.included_concretized_user_specs.values(): + for included in included_specs: + # Don't duplicate included spec(s) + if included not in concretized_user_specs: + concretized_user_specs.append(included) + + return concretized_user_specs + + def all_concretized_orders(self) -> List[str]: + """Returns all of the concretized order of the environment and + its included environment(s).""" + concretized_order = self.concretized_order[:] + for included_concretized_order in self.included_concretized_order.values(): + for included in included_concretized_order: + # Don't duplicate included spec(s) + if included not in concretized_order: + concretized_order.append(included) + + return concretized_order @property def user_specs(self): @@ -966,6 +1102,26 @@ def _read_dev_specs(self): dev_specs[name] = local_entry return dev_specs + @property + def included_user_specs(self) -> SpecList: + """Included concrete user (or root) specs from last concretization.""" + spec_list = SpecList() + + if not self.included_concrete_envs: + return spec_list + + def add_root_specs(included_concrete_specs): + # add specs from the include *and* any nested includes it may have + for env, info in included_concrete_specs.items(): + for root_list in info["roots"]: + spec_list.add(root_list["spec"]) + + if "include_concrete" in info: + add_root_specs(info["include_concrete"]) + + add_root_specs(self.included_concrete_spec_data) + return spec_list + def clear(self, re_read=False): """Clear the contents of the environment @@ -977,9 +1133,15 @@ def clear(self, re_read=False): self.spec_lists[user_speclist_name] = SpecList() self._dev_specs = {} - self.concretized_user_specs = [] # user specs from last concretize self.concretized_order = [] # roots of last concretize, in order + self.concretized_user_specs = [] # user specs from last concretize self.specs_by_hash = {} # concretized specs by hash + + self.included_concrete_spec_data = {} # concretized specs from lockfile of included envs + self.included_concretized_order = {} # root specs of the included envs, keyed by env path + self.included_concretized_user_specs = {} # user specs from last concretize's included env + self.included_specs_by_hash = {} # concretized specs by hash from the included envs + self.invalidate_repository_cache() self._previous_active = None # previously active environment if not re_read: @@ -1033,6 +1195,55 @@ def scope_name(self): """Name of the config scope of this environment's manifest file.""" return self.manifest.scope_name + def include_concrete_envs(self): + """Copy and save the included envs' specs internally""" + + lockfile_meta = None + root_hash_seen = set() + concrete_hash_seen = set() + self.included_concrete_spec_data = {} + + for env_path in self.included_concrete_envs: + # Check that environment exists + if not is_env_dir(env_path): + raise SpackEnvironmentError(f"Unable to find env at {env_path}") + + env = Environment(env_path) + + with open(env.lock_path) as f: + lockfile_as_dict = env._read_lockfile(f) + + # Lockfile_meta must match each env and use at least format version 5 + if lockfile_meta is None: + lockfile_meta = lockfile_as_dict["_meta"] + elif lockfile_meta != lockfile_as_dict["_meta"]: + raise SpackEnvironmentError("All lockfile _meta values must match") + elif lockfile_meta["lockfile-version"] < 5: + raise SpackEnvironmentError("The lockfile format must be at version 5 or higher") + + # Copy unique root specs from env + self.included_concrete_spec_data[env_path] = {"roots": []} + for root_dict in lockfile_as_dict["roots"]: + if root_dict["hash"] not in root_hash_seen: + self.included_concrete_spec_data[env_path]["roots"].append(root_dict) + root_hash_seen.add(root_dict["hash"]) + + # Copy unique concrete specs from env + for concrete_spec in lockfile_as_dict["concrete_specs"]: + if concrete_spec not in concrete_hash_seen: + self.included_concrete_spec_data[env_path].update( + {"concrete_specs": lockfile_as_dict["concrete_specs"]} + ) + concrete_hash_seen.add(concrete_spec) + + if "include_concrete" in lockfile_as_dict.keys(): + self.included_concrete_spec_data[env_path]["include_concrete"] = lockfile_as_dict[ + "include_concrete" + ] + + self._read_lockfile_dict(self._to_lockfile_dict()) + self.write() + def destroy(self): """Remove this environment from Spack entirely.""" shutil.rmtree(self.path) @@ -1232,6 +1443,10 @@ def concretize(self, force=False, tests=False): for spec in set(self.concretized_user_specs) - set(self.user_specs): self.deconcretize(spec, concrete=False) + # If a combined env, check updated spec is in the linked envs + if self.included_concrete_envs: + self.include_concrete_envs() + # Pick the right concretization strategy if self.unify == "when_possible": return self._concretize_together_where_possible(tests=tests) @@ -1704,8 +1919,14 @@ def _partition_roots_by_install_status(self): of per spec.""" installed, uninstalled = [], [] with spack.store.STORE.db.read_transaction(): - for concretized_hash in self.concretized_order: - spec = self.specs_by_hash[concretized_hash] + for concretized_hash in self.all_concretized_orders(): + if concretized_hash in self.specs_by_hash: + spec = self.specs_by_hash[concretized_hash] + else: + for env_path in self.included_specs_by_hash.keys(): + if concretized_hash in self.included_specs_by_hash[env_path]: + spec = self.included_specs_by_hash[env_path][concretized_hash] + break if not spec.installed or ( spec.satisfies("dev_path=*") or spec.satisfies("^dev_path=*") ): @@ -1785,8 +2006,14 @@ def added_specs(self): def concretized_specs(self): """Tuples of (user spec, concrete spec) for all concrete specs.""" - for s, h in zip(self.concretized_user_specs, self.concretized_order): - yield (s, self.specs_by_hash[h]) + for s, h in zip(self.all_concretized_user_specs(), self.all_concretized_orders()): + if h in self.specs_by_hash: + yield (s, self.specs_by_hash[h]) + else: + for env_path in self.included_specs_by_hash.keys(): + if h in self.included_specs_by_hash[env_path]: + yield (s, self.included_specs_by_hash[env_path][h]) + break def concrete_roots(self): """Same as concretized_specs, except it returns the list of concrete @@ -1915,8 +2142,7 @@ def _get_environment_specs(self, recurse_dependencies=True): If these specs appear under different user_specs, only one copy is added to the list returned. """ - specs = [self.specs_by_hash[h] for h in self.concretized_order] - + specs = [self.specs_by_hash[h] for h in self.all_concretized_orders()] if recurse_dependencies: specs.extend( traverse.traverse_nodes( @@ -1961,31 +2187,76 @@ def _to_lockfile_dict(self): "concrete_specs": concrete_specs, } + if self.included_concrete_envs: + data[included_concrete_name] = self.included_concrete_spec_data + return data def _read_lockfile(self, file_or_json): """Read a lockfile from a file or from a raw string.""" lockfile_dict = sjson.load(file_or_json) self._read_lockfile_dict(lockfile_dict) - return lockfile_dict["_meta"]["lockfile-version"] + return lockfile_dict + + def set_included_concretized_user_specs( + self, + env_name: str, + env_info: Dict[str, Dict[str, Any]], + included_json_specs_by_hash: Dict[str, Dict[str, Any]], + ) -> Dict[str, Dict[str, Any]]: + """Sets all of the concretized user specs from included environments + to include those from nested included environments. + + Args: + env_name: the name (technically the path) of the included environment + env_info: included concrete environment data + included_json_specs_by_hash: concrete spec data keyed by hash + + Returns: updated specs_by_hash + """ + self.included_concretized_order[env_name] = [] + self.included_concretized_user_specs[env_name] = [] + + def add_specs(name, info, specs_by_hash): + # Add specs from the environment as well as any of its nested + # environments. + for root_info in info["roots"]: + self.included_concretized_order[name].append(root_info["hash"]) + self.included_concretized_user_specs[name].append(Spec(root_info["spec"])) + if "concrete_specs" in info: + specs_by_hash.update(info["concrete_specs"]) + + if included_concrete_name in info: + for included_name, included_info in info[included_concrete_name].items(): + if included_name not in self.included_concretized_order: + self.included_concretized_order[included_name] = [] + self.included_concretized_user_specs[included_name] = [] + add_specs(included_name, included_info, specs_by_hash) + + add_specs(env_name, env_info, included_json_specs_by_hash) + return included_json_specs_by_hash def _read_lockfile_dict(self, d): """Read a lockfile dictionary into this environment.""" self.specs_by_hash = {} + self.included_specs_by_hash = {} + self.included_concretized_user_specs = {} + self.included_concretized_order = {} roots = d["roots"] self.concretized_user_specs = [Spec(r["spec"]) for r in roots] self.concretized_order = [r["hash"] for r in roots] json_specs_by_hash = d["concrete_specs"] + included_json_specs_by_hash = {} - # Track specs by their lockfile key. Currently spack uses the finest - # grained hash as the lockfile key, while older formats used the build - # hash or a previous incarnation of the DAG hash (one that did not - # include build deps or package hash). - specs_by_hash = {} + if included_concrete_name in d: + for env_name, env_info in d[included_concrete_name].items(): + included_json_specs_by_hash.update( + self.set_included_concretized_user_specs( + env_name, env_info, included_json_specs_by_hash + ) + ) - # Track specs by their DAG hash, allows handling DAG hash collisions - first_seen = {} current_lockfile_format = d["_meta"]["lockfile-version"] try: reader = READER_CLS[current_lockfile_format] @@ -1998,6 +2269,39 @@ def _read_lockfile_dict(self, d): msg += " You need to use a newer Spack version." raise SpackEnvironmentError(msg) + first_seen, self.concretized_order = self.filter_specs( + reader, json_specs_by_hash, self.concretized_order + ) + + for spec_dag_hash in self.concretized_order: + self.specs_by_hash[spec_dag_hash] = first_seen[spec_dag_hash] + + if any(self.included_concretized_order.values()): + first_seen = {} + + for env_name, concretized_order in self.included_concretized_order.items(): + filtered_spec, self.included_concretized_order[env_name] = self.filter_specs( + reader, included_json_specs_by_hash, concretized_order + ) + first_seen.update(filtered_spec) + + for env_path, spec_hashes in self.included_concretized_order.items(): + self.included_specs_by_hash[env_path] = {} + for spec_dag_hash in spec_hashes: + self.included_specs_by_hash[env_path].update( + {spec_dag_hash: first_seen[spec_dag_hash]} + ) + + def filter_specs(self, reader, json_specs_by_hash, order_concretized): + # Track specs by their lockfile key. Currently spack uses the finest + # grained hash as the lockfile key, while older formats used the build + # hash or a previous incarnation of the DAG hash (one that did not + # include build deps or package hash). + specs_by_hash = {} + + # Track specs by their DAG hash, allows handling DAG hash collisions + first_seen = {} + # First pass: Put each spec in the map ignoring dependencies for lockfile_key, node_dict in json_specs_by_hash.items(): spec = reader.from_node_dict(node_dict) @@ -2020,7 +2324,8 @@ def _read_lockfile_dict(self, d): # keep. This is only required as long as we support older lockfile # formats where the mapping from DAG hash to lockfile key is possibly # one-to-many. - for lockfile_key in self.concretized_order: + + for lockfile_key in order_concretized: for s in specs_by_hash[lockfile_key].traverse(): if s.dag_hash() not in first_seen: first_seen[s.dag_hash()] = s @@ -2028,12 +2333,10 @@ def _read_lockfile_dict(self, d): # Now make sure concretized_order and our internal specs dict # contains the keys used by modern spack (i.e. the dag_hash # that includes build deps and package hash). - self.concretized_order = [ - specs_by_hash[h_key].dag_hash() for h_key in self.concretized_order - ] - for spec_dag_hash in self.concretized_order: - self.specs_by_hash[spec_dag_hash] = first_seen[spec_dag_hash] + order_concretized = [specs_by_hash[h_key].dag_hash() for h_key in order_concretized] + + return first_seen, order_concretized def write(self, regenerate: bool = True) -> None: """Writes an in-memory environment to its location on disk. @@ -2046,7 +2349,7 @@ def write(self, regenerate: bool = True) -> None: regenerate: regenerate views and run post-write hooks as well as writing if True. """ self.manifest_uptodate_or_warn() - if self.specs_by_hash: + if self.specs_by_hash or self.included_concrete_envs: self.ensure_env_directory_exists(dot_env=True) self.update_environment_repository() self.manifest.flush() @@ -2545,6 +2848,19 @@ def override_user_spec(self, user_spec: str, idx: int) -> None: raise SpackEnvironmentError(msg) from e self.changed = True + def set_include_concrete(self, include_concrete: List[str]) -> None: + """Sets the included concrete environments in the manifest to the value(s) passed as input. + + Args: + include_concrete: list of already existing concrete environments to include + """ + self.pristine_configuration[included_concrete_name] = [] + + for env_path in include_concrete: + self.pristine_configuration[included_concrete_name].append(env_path) + + self.changed = True + def add_definition(self, user_spec: str, list_name: str) -> None: """Appends a user spec to the first active definition matching the name passed as argument. diff --git a/lib/spack/spack/schema/env.py b/lib/spack/spack/schema/env.py index d2df795a3d..8b37f3e236 100644 --- a/lib/spack/spack/schema/env.py +++ b/lib/spack/spack/schema/env.py @@ -35,6 +35,7 @@ { "include": {"type": "array", "default": [], "items": {"type": "string"}}, "specs": spec_list_schema, + "include_concrete": {"type": "array", "default": [], "items": {"type": "string"}}, }, ), } diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py index a05222db6b..e1136e3bbe 100644 --- a/lib/spack/spack/test/cmd/env.py +++ b/lib/spack/spack/test/cmd/env.py @@ -60,6 +60,27 @@ sep = os.sep +def setup_combined_multiple_env(): + env("create", "test1") + test1 = ev.read("test1") + with test1: + add("zlib") + test1.concretize() + test1.write() + + env("create", "test2") + test2 = ev.read("test2") + with test2: + add("libelf") + test2.concretize() + test2.write() + + env("create", "--include-concrete", "test1", "--include-concrete", "test2", "combined_env") + combined = ev.read("combined_env") + + return test1, test2, combined + + @pytest.fixture() def environment_from_manifest(tmp_path): """Returns a new environment named 'test' from the content of a manifest file.""" @@ -369,6 +390,29 @@ def test_env_install_single_spec(install_mockery, mock_fetch): assert e.specs_by_hash[e.concretized_order[0]].name == "cmake-client" +@pytest.mark.parametrize("unify", [True, False, "when_possible"]) +def test_env_install_include_concrete_env(unify, install_mockery, mock_fetch): + test1, test2, combined = setup_combined_multiple_env() + + combined.concretize() + combined.write() + + combined.unify = unify + + with combined: + install() + + test1_roots = test1.concretized_order + test2_roots = test2.concretized_order + combined_included_roots = combined.included_concretized_order + + for spec in combined.all_specs(): + assert spec.installed + + assert test1_roots == combined_included_roots[test1.path] + assert test2_roots == combined_included_roots[test2.path] + + def test_env_roots_marked_explicit(install_mockery, mock_fetch): install = SpackCommand("install") install("dependent-install") @@ -557,6 +601,41 @@ def test_remove_command(): assert "mpileaks@" not in find("--show-concretized") +def test_bad_remove_included_env(): + env("create", "test") + test = ev.read("test") + + with test: + add("mpileaks") + + test.concretize() + test.write() + + env("create", "--include-concrete", "test", "combined_env") + + with pytest.raises(SpackCommandError): + env("remove", "test") + + +def test_force_remove_included_env(): + env("create", "test") + test = ev.read("test") + + with test: + add("mpileaks") + + test.concretize() + test.write() + + env("create", "--include-concrete", "test", "combined_env") + + rm_output = env("remove", "-f", "-y", "test") + list_output = env("list") + + assert '"test" is being used by environment "combined_env"' in rm_output + assert "test" not in list_output + + def test_environment_status(capsys, tmpdir): with tmpdir.as_cwd(): with capsys.disabled(): @@ -1636,6 +1715,275 @@ def test_env_without_view_install(tmpdir, mock_stage, mock_fetch, install_mocker check_mpileaks_and_deps_in_view(view_dir) +@pytest.mark.parametrize("env_name", [True, False]) +def test_env_include_concrete_env_yaml(env_name): + env("create", "test") + test = ev.read("test") + + with test: + add("mpileaks") + test.concretize() + test.write() + + environ = "test" if env_name else test.path + + env("create", "--include-concrete", environ, "combined_env") + + combined = ev.read("combined_env") + combined_yaml = combined.manifest["spack"] + + assert "include_concrete" in combined_yaml + assert test.path in combined_yaml["include_concrete"] + + +def test_env_bad_include_concrete_env(): + with pytest.raises(ev.SpackEnvironmentError): + env("create", "--include-concrete", "nonexistant_env", "combined_env") + + +def test_env_not_concrete_include_concrete_env(): + env("create", "test") + test = ev.read("test") + + with test: + add("mpileaks") + + with pytest.raises(ev.SpackEnvironmentError): + env("create", "--include-concrete", "test", "combined_env") + + +def test_env_multiple_include_concrete_envs(): + test1, test2, combined = setup_combined_multiple_env() + + combined_yaml = combined.manifest["spack"] + + assert test1.path in combined_yaml["include_concrete"][0] + assert test2.path in combined_yaml["include_concrete"][1] + + # No local specs in the combined env + assert not combined_yaml["specs"] + + +def test_env_include_concrete_envs_lockfile(): + test1, test2, combined = setup_combined_multiple_env() + + combined_yaml = combined.manifest["spack"] + + assert "include_concrete" in combined_yaml + assert test1.path in combined_yaml["include_concrete"] + + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert set( + entry["hash"] for entry in lockfile_as_dict["include_concrete"][test1.path]["roots"] + ) == set(test1.specs_by_hash) + assert set( + entry["hash"] for entry in lockfile_as_dict["include_concrete"][test2.path]["roots"] + ) == set(test2.specs_by_hash) + + +def test_env_include_concrete_add_env(): + test1, test2, combined = setup_combined_multiple_env() + + # crete new env & crecretize + env("create", "new") + new_env = ev.read("new") + with new_env: + add("mpileaks") + + new_env.concretize() + new_env.write() + + # add new env to combined + combined.included_concrete_envs.append(new_env.path) + + # assert thing haven't changed yet + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert new_env.path not in lockfile_as_dict["include_concrete"].keys() + + # concretize combined env with new env + combined.concretize() + combined.write() + + # assert changes + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert new_env.path in lockfile_as_dict["include_concrete"].keys() + + +def test_env_include_concrete_remove_env(): + test1, test2, combined = setup_combined_multiple_env() + + # remove test2 from combined + combined.included_concrete_envs = [test1.path] + + # assert test2 is still in combined's lockfile + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert test2.path in lockfile_as_dict["include_concrete"].keys() + + # reconcretize combined + combined.concretize() + combined.write() + + # assert test2 is not in combined's lockfile + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert test2.path not in lockfile_as_dict["include_concrete"].keys() + + +@pytest.mark.parametrize("unify", [True, False, "when_possible"]) +def test_env_include_concrete_env_reconcretized(unify): + """Double check to make sure that concrete_specs for the local specs is empty + after recocnretizing. + """ + _, _, combined = setup_combined_multiple_env() + + combined.unify = unify + + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert not lockfile_as_dict["roots"] + assert not lockfile_as_dict["concrete_specs"] + + combined.concretize() + combined.write() + + with open(combined.lock_path) as f: + lockfile_as_dict = combined._read_lockfile(f) + + assert not lockfile_as_dict["roots"] + assert not lockfile_as_dict["concrete_specs"] + + +def test_concretize_include_concrete_env(): + test1, _, combined = setup_combined_multiple_env() + + with test1: + add("mpileaks") + test1.concretize() + test1.write() + + assert Spec("mpileaks") in test1.concretized_user_specs + assert Spec("mpileaks") not in combined.included_concretized_user_specs[test1.path] + + combined.concretize() + combined.write() + + assert Spec("mpileaks") in combined.included_concretized_user_specs[test1.path] + + +def test_concretize_nested_include_concrete_envs(): + env("create", "test1") + test1 = ev.read("test1") + with test1: + add("zlib") + test1.concretize() + test1.write() + + env("create", "--include-concrete", "test1", "test2") + test2 = ev.read("test2") + with test2: + add("libelf") + test2.concretize() + test2.write() + + env("create", "--include-concrete", "test2", "test3") + test3 = ev.read("test3") + + with open(test3.lock_path) as f: + lockfile_as_dict = test3._read_lockfile(f) + + assert test2.path in lockfile_as_dict["include_concrete"] + assert test1.path in lockfile_as_dict["include_concrete"][test2.path]["include_concrete"] + + assert Spec("zlib") in test3.included_concretized_user_specs[test1.path] + + +def test_concretize_nested_included_concrete(): + """Confirm that nested included environments use specs concretized at + environment creation time and change with reconcretization.""" + env("create", "test1") + test1 = ev.read("test1") + with test1: + add("zlib") + test1.concretize() + test1.write() + + # test2 should include test1 with zlib + env("create", "--include-concrete", "test1", "test2") + test2 = ev.read("test2") + with test2: + add("libelf") + test2.concretize() + test2.write() + + assert Spec("zlib") in test2.included_concretized_user_specs[test1.path] + + # Modify/re-concretize test1 to replace zlib with mpileaks + with test1: + remove("zlib") + add("mpileaks") + test1.concretize() + test1.write() + + # test3 should include the latest concretization of test1 + env("create", "--include-concrete", "test1", "test3") + test3 = ev.read("test3") + with test3: + add("callpath") + test3.concretize() + test3.write() + + included_specs = test3.included_concretized_user_specs[test1.path] + assert len(included_specs) == 1 + assert Spec("mpileaks") in included_specs + + # The last concretization of test4's included environments should have test2 + # with the original concretized test1 spec and test3 with the re-concretized + # test1 spec. + env("create", "--include-concrete", "test2", "--include-concrete", "test3", "test4") + test4 = ev.read("test4") + + def included_included_spec(path1, path2): + included_path1 = test4.included_concrete_spec_data[path1] + included_path2 = included_path1["include_concrete"][path2] + return included_path2["roots"][0]["spec"] + + included_test2_test1 = included_included_spec(test2.path, test1.path) + assert "zlib" in included_test2_test1 + + included_test3_test1 = included_included_spec(test3.path, test1.path) + assert "mpileaks" in included_test3_test1 + + # test4's concretized specs should reflect the original concretization. + concrete_specs = [s for s, _ in test4.concretized_specs()] + expected = [Spec(s) for s in ["libelf", "zlib", "mpileaks", "callpath"]] + assert all(s in concrete_specs for s in expected) + + # Re-concretize test2 to reflect the new concretization of included test1 + # to remove zlib and write it out so it can be picked up by test4. + # Re-concretize test4 to reflect the re-concretization of included test2 + # and ensure that its included specs are up-to-date + test2.concretize() + test2.write() + test4.concretize() + + concrete_specs = [s for s, _ in test4.concretized_specs()] + assert Spec("zlib") not in concrete_specs + + # Expecting mpileaks to appear only once + expected = [Spec(s) for s in ["libelf", "mpileaks", "callpath"]] + assert len(concrete_specs) == 3 and all(s in concrete_specs for s in expected) + + def test_env_config_view_default( environment_from_manifest, mock_stage, mock_fetch, install_mockery ): diff --git a/lib/spack/spack/test/cmd/find.py b/lib/spack/spack/test/cmd/find.py index e42a938e38..37a0a7ff14 100644 --- a/lib/spack/spack/test/cmd/find.py +++ b/lib/spack/spack/test/cmd/find.py @@ -349,6 +349,87 @@ def test_find_prefix_in_env( # Would throw error on regression +def test_find_specs_include_concrete_env(mutable_mock_env_path, config, mutable_mock_repo, tmpdir): + path = tmpdir.join("spack.yaml") + + with tmpdir.as_cwd(): + with open(str(path), "w") as f: + f.write( + """\ +spack: + specs: + - mpileaks +""" + ) + env("create", "test1", "spack.yaml") + + test1 = ev.read("test1") + test1.concretize() + test1.write() + + with tmpdir.as_cwd(): + with open(str(path), "w") as f: + f.write( + """\ +spack: + specs: + - libelf +""" + ) + env("create", "test2", "spack.yaml") + + test2 = ev.read("test2") + test2.concretize() + test2.write() + + env("create", "--include-concrete", "test1", "--include-concrete", "test2", "combined_env") + + with ev.read("combined_env"): + output = find() + + assert "No root specs" in output + assert "Included specs" in output + assert "mpileaks" in output + assert "libelf" in output + + +def test_find_specs_nested_include_concrete_env( + mutable_mock_env_path, config, mutable_mock_repo, tmpdir +): + path = tmpdir.join("spack.yaml") + + with tmpdir.as_cwd(): + with open(str(path), "w") as f: + f.write( + """\ +spack: + specs: + - mpileaks +""" + ) + env("create", "test1", "spack.yaml") + + test1 = ev.read("test1") + test1.concretize() + test1.write() + + env("create", "--include-concrete", "test1", "test2") + test2 = ev.read("test2") + test2.add("libelf") + test2.concretize() + test2.write() + + env("create", "--include-concrete", "test2", "test3") + + with ev.read("test3"): + output = find() + + assert "No root specs" in output + assert "Included specs" in output + assert "mpileaks" in output + assert "libelf" in output + + def test_find_loaded(database, working_env): output = find("--loaded", "--group") assert output == "" diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash index 387b364189..1f73849fc8 100755 --- a/share/spack/spack-completion.bash +++ b/share/spack/spack-completion.bash @@ -1052,7 +1052,7 @@ _spack_env_deactivate() { _spack_env_create() { if $list_options then - SPACK_COMPREPLY="-h --help -d --dir --keep-relative --without-view --with-view" + SPACK_COMPREPLY="-h --help -d --dir --keep-relative --without-view --with-view --include-concrete" else _environments fi @@ -1061,7 +1061,7 @@ _spack_env_create() { _spack_env_remove() { if $list_options then - SPACK_COMPREPLY="-h --help -y --yes-to-all" + SPACK_COMPREPLY="-h --help -y --yes-to-all -f --force" else _environments fi @@ -1070,7 +1070,7 @@ _spack_env_remove() { _spack_env_rm() { if $list_options then - SPACK_COMPREPLY="-h --help -y --yes-to-all" + SPACK_COMPREPLY="-h --help -y --yes-to-all -f --force" else _environments fi diff --git a/share/spack/spack-completion.fish b/share/spack/spack-completion.fish index 3818b12f1b..63abb4864e 100755 --- a/share/spack/spack-completion.fish +++ b/share/spack/spack-completion.fish @@ -1538,7 +1538,7 @@ complete -c spack -n '__fish_spack_using_command env deactivate' -l pwsh -f -a s complete -c spack -n '__fish_spack_using_command env deactivate' -l pwsh -d 'print pwsh commands to activate the environment' # spack env create -set -g __fish_spack_optspecs_spack_env_create h/help d/dir keep-relative without-view with-view= +set -g __fish_spack_optspecs_spack_env_create h/help d/dir keep-relative without-view with-view= include-concrete= complete -c spack -n '__fish_spack_using_command_pos 0 env create' -f -a '(__fish_spack_environments)' complete -c spack -n '__fish_spack_using_command env create' -s h -l help -f -a help complete -c spack -n '__fish_spack_using_command env create' -s h -l help -d 'show this help message and exit' @@ -1550,22 +1550,28 @@ complete -c spack -n '__fish_spack_using_command env create' -l without-view -f complete -c spack -n '__fish_spack_using_command env create' -l without-view -d 'do not maintain a view for this environment' complete -c spack -n '__fish_spack_using_command env create' -l with-view -r -f -a with_view complete -c spack -n '__fish_spack_using_command env create' -l with-view -r -d 'specify that this environment should maintain a view at the specified path (by default the view is maintained in the environment directory)' +complete -c spack -n '__fish_spack_using_command env create' -l include-concrete -r -f -a include_concrete +complete -c spack -n '__fish_spack_using_command env create' -l include-concrete -r -d 'name of old environment to copy specs from' # spack env remove -set -g __fish_spack_optspecs_spack_env_remove h/help y/yes-to-all +set -g __fish_spack_optspecs_spack_env_remove h/help y/yes-to-all f/force complete -c spack -n '__fish_spack_using_command_pos_remainder 0 env remove' -f -a '(__fish_spack_environments)' complete -c spack -n '__fish_spack_using_command env remove' -s h -l help -f -a help complete -c spack -n '__fish_spack_using_command env remove' -s h -l help -d 'show this help message and exit' complete -c spack -n '__fish_spack_using_command env remove' -s y -l yes-to-all -f -a yes_to_all complete -c spack -n '__fish_spack_using_command env remove' -s y -l yes-to-all -d 'assume "yes" is the answer to every confirmation request' +complete -c spack -n '__fish_spack_using_command env remove' -s f -l force -f -a force +complete -c spack -n '__fish_spack_using_command env remove' -s f -l force -d 'remove the environment even if it is included in another environment' # spack env rm -set -g __fish_spack_optspecs_spack_env_rm h/help y/yes-to-all +set -g __fish_spack_optspecs_spack_env_rm h/help y/yes-to-all f/force complete -c spack -n '__fish_spack_using_command_pos_remainder 0 env rm' -f -a '(__fish_spack_environments)' complete -c spack -n '__fish_spack_using_command env rm' -s h -l help -f -a help complete -c spack -n '__fish_spack_using_command env rm' -s h -l help -d 'show this help message and exit' complete -c spack -n '__fish_spack_using_command env rm' -s y -l yes-to-all -f -a yes_to_all complete -c spack -n '__fish_spack_using_command env rm' -s y -l yes-to-all -d 'assume "yes" is the answer to every confirmation request' +complete -c spack -n '__fish_spack_using_command env rm' -s f -l force -f -a force +complete -c spack -n '__fish_spack_using_command env rm' -s f -l force -d 'remove the environment even if it is included in another environment' # spack env rename set -g __fish_spack_optspecs_spack_env_rename h/help d/dir f/force