summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/ci.py2
-rw-r--r--lib/spack/spack/cmd/ci.py2
-rw-r--r--lib/spack/spack/cmd/env.py69
-rw-r--r--lib/spack/spack/cmd/remove.py2
-rw-r--r--lib/spack/spack/environment/__init__.py6
-rw-r--r--lib/spack/spack/environment/environment.py855
-rw-r--r--lib/spack/spack/schema/ci.py2
-rw-r--r--lib/spack/spack/test/cmd/config.py11
-rw-r--r--lib/spack/spack/test/cmd/develop.py2
-rw-r--r--lib/spack/spack/test/cmd/env.py269
-rw-r--r--lib/spack/spack/test/cmd/install.py1
-rw-r--r--lib/spack/spack/test/env.py210
-rw-r--r--lib/spack/spack/test/environment.py47
-rw-r--r--lib/spack/spack/test/modules/lmod.py2
14 files changed, 978 insertions, 502 deletions
diff --git a/lib/spack/spack/ci.py b/lib/spack/spack/ci.py
index 5056cb250f..15e5b9aedd 100644
--- a/lib/spack/spack/ci.py
+++ b/lib/spack/spack/ci.py
@@ -750,7 +750,7 @@ def generate_gitlab_ci_yaml(
env.concretize()
env.write()
- yaml_root = ev.config_dict(env.yaml)
+ yaml_root = ev.config_dict(env.manifest)
# Get the joined "ci" config with all of the current scopes resolved
ci_config = cfg.get("ci")
diff --git a/lib/spack/spack/cmd/ci.py b/lib/spack/spack/cmd/ci.py
index d96f877d9a..12bd754e6a 100644
--- a/lib/spack/spack/cmd/ci.py
+++ b/lib/spack/spack/cmd/ci.py
@@ -227,7 +227,7 @@ def ci_reindex(args):
Use the active, gitlab-enabled environment to rebuild the buildcache
index for the associated mirror."""
env = spack.cmd.require_active_env(cmd_name="ci rebuild-index")
- yaml_root = ev.config_dict(env.yaml)
+ yaml_root = ev.config_dict(env.manifest)
if "mirrors" not in yaml_root or len(yaml_root["mirrors"].values()) < 1:
tty.die("spack ci rebuild-index requires an env containing a mirror")
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
index 3f06e1e4eb..29f6db412a 100644
--- a/lib/spack/spack/cmd/env.py
+++ b/lib/spack/spack/cmd/env.py
@@ -163,7 +163,7 @@ def env_activate(args):
env = create_temp_env_directory()
env_path = os.path.abspath(env)
short_name = os.path.basename(env_path)
- ev.Environment(env).write(regenerate=False)
+ ev.create_in_dir(env).write(regenerate=False)
# Managed environment
elif ev.exists(env_name_or_dir) and not args.dir:
@@ -301,16 +301,17 @@ def env_create(args):
# object could choose to enable a view by default. False means that
# the environment should not include a view.
with_view = None
- if args.envfile:
- with open(args.envfile) as f:
- _env_create(
- args.create_env, f, args.dir, with_view=with_view, keep_relative=args.keep_relative
- )
- else:
- _env_create(args.create_env, None, args.dir, with_view=with_view)
+
+ _env_create(
+ args.create_env,
+ init_file=args.envfile,
+ dir=args.dir,
+ with_view=with_view,
+ keep_relative=args.keep_relative,
+ )
-def _env_create(name_or_path, init_file=None, dir=False, with_view=None, keep_relative=False):
+def _env_create(name_or_path, *, init_file=None, dir=False, with_view=None, keep_relative=False):
"""Create a new environment, with an optional yaml description.
Arguments:
@@ -323,18 +324,21 @@ def _env_create(name_or_path, init_file=None, dir=False, with_view=None, keep_re
the new environment file, otherwise they may be made absolute if the
new environment is in a different location
"""
- if dir:
- env = ev.Environment(name_or_path, init_file, with_view, keep_relative)
- env.write()
- tty.msg("Created environment in %s" % env.path)
- tty.msg("You can activate this environment with:")
- tty.msg(" spack env activate %s" % env.path)
- else:
- env = ev.create(name_or_path, init_file, with_view, keep_relative)
- env.write()
+ if not dir:
+ env = ev.create(
+ name_or_path, init_file=init_file, with_view=with_view, keep_relative=keep_relative
+ )
tty.msg("Created environment '%s' in %s" % (name_or_path, env.path))
tty.msg("You can activate this environment with:")
tty.msg(" spack env activate %s" % (name_or_path))
+ return env
+
+ env = ev.create_in_dir(
+ name_or_path, init_file=init_file, with_view=with_view, keep_relative=keep_relative
+ )
+ tty.msg("Created environment in %s" % env.path)
+ tty.msg("You can activate this environment with:")
+ tty.msg(" spack env activate %s" % env.path)
return env
@@ -431,21 +435,22 @@ def env_view_setup_parser(subparser):
def env_view(args):
env = ev.active_environment()
- if env:
- if args.action == ViewAction.regenerate:
- env.regenerate_views()
- elif args.action == ViewAction.enable:
- if args.view_path:
- view_path = args.view_path
- else:
- view_path = env.view_path_default
- env.update_default_view(view_path)
- env.write()
- elif args.action == ViewAction.disable:
- env.update_default_view(None)
- env.write()
- else:
+ if not env:
tty.msg("No active environment")
+ return
+
+ if args.action == ViewAction.regenerate:
+ env.regenerate_views()
+ elif args.action == ViewAction.enable:
+ if args.view_path:
+ view_path = args.view_path
+ else:
+ view_path = env.view_path_default
+ env.update_default_view(view_path)
+ env.write()
+ elif args.action == ViewAction.disable:
+ env.update_default_view(path_or_bool=False)
+ env.write()
#
diff --git a/lib/spack/spack/cmd/remove.py b/lib/spack/spack/cmd/remove.py
index 3e1dde16f2..4fdb36a5e5 100644
--- a/lib/spack/spack/cmd/remove.py
+++ b/lib/spack/spack/cmd/remove.py
@@ -38,6 +38,6 @@ def remove(parser, args):
env.clear()
else:
for spec in spack.cmd.parse_specs(args.specs):
- tty.msg("Removing %s from environment %s" % (spec, env.name))
env.remove(spec, args.list_name, force=args.force)
+ tty.msg(f"{spec} has been removed from {env.manifest}")
env.write()
diff --git a/lib/spack/spack/environment/__init__.py b/lib/spack/spack/environment/__init__.py
index 7a3f4fbbb1..709c386d76 100644
--- a/lib/spack/spack/environment/__init__.py
+++ b/lib/spack/spack/environment/__init__.py
@@ -340,11 +340,14 @@ from .environment import (
all_environments,
config_dict,
create,
+ create_in_dir,
deactivate,
default_manifest_yaml,
default_view_name,
display_specs,
+ environment_dir_from_name,
exists,
+ initialize_environment_dir,
installed_specs,
is_env_dir,
is_latest_format,
@@ -369,11 +372,14 @@ __all__ = [
"all_environments",
"config_dict",
"create",
+ "create_in_dir",
"deactivate",
"default_manifest_yaml",
"default_view_name",
"display_specs",
+ "environment_dir_from_name",
"exists",
+ "initialize_environment_dir",
"installed_specs",
"is_env_dir",
"is_latest_format",
diff --git a/lib/spack/spack/environment/environment.py b/lib/spack/spack/environment/environment.py
index 31b14c4a86..eedca8daaa 100644
--- a/lib/spack/spack/environment/environment.py
+++ b/lib/spack/spack/environment/environment.py
@@ -3,9 +3,11 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import collections
+import collections.abc
import contextlib
import copy
import os
+import pathlib
import re
import shutil
import stat
@@ -14,7 +16,7 @@ import time
import urllib.parse
import urllib.request
import warnings
-from typing import List, Optional
+from typing import Any, Dict, List, Optional, Union
import ruamel.yaml as yaml
@@ -270,14 +272,87 @@ def read(name):
return Environment(root(name))
-def create(name, init_file=None, with_view=None, keep_relative=False):
- """Create a managed environment in Spack."""
+def create(
+ name: str,
+ init_file: Optional[Union[str, pathlib.Path]] = None,
+ with_view: Optional[Union[str, pathlib.Path, bool]] = None,
+ keep_relative: bool = False,
+) -> "Environment":
+ """Create a managed environment in Spack and returns it.
+
+ A managed environment is created in a root directory managed by this Spack instance, so that
+ Spack can keep track of them.
+
+ Args:
+ name: name of the managed environment
+ init_file: either a "spack.yaml" or a "spack.lock" file or None
+ with_view: whether a view should be maintained for the environment. If the value is a
+ 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
+ """
+ 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
+ )
+
+
+def create_in_dir(
+ manifest_dir: Union[str, pathlib.Path],
+ init_file: Optional[Union[str, pathlib.Path]] = None,
+ with_view: Optional[Union[str, pathlib.Path, bool]] = None,
+ keep_relative: bool = False,
+) -> "Environment":
+ """Create an environment in the directory passed as input and returns it.
+
+ Args:
+ manifest_dir: directory where to create the environment.
+ init_file: either a "spack.yaml" or a "spack.lock" file or None
+ with_view: whether a view should be maintained for the environment. If the value is a
+ 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
+ """
+ initialize_environment_dir(manifest_dir, envfile=init_file)
+
+ if with_view is None and keep_relative:
+ return Environment(manifest_dir)
+
+ manifest = EnvironmentManifestFile(manifest_dir)
+
+ if with_view is not None:
+ manifest.set_default_view(with_view)
+
+ 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)
+
+ manifest.flush()
+
+ return Environment(manifest_dir)
+
+
+def environment_dir_from_name(name: str, exists_ok: bool = True) -> str:
+ """Returns the directory associated with a named environment.
+
+ Args:
+ name: name of the environment
+ exists_ok: if False, raise an error if the environment exists already
+
+ Raises:
+ SpackEnvironmentError: if exists_ok is False and the environment exists already
+ """
+ if not exists_ok and exists(name):
+ raise SpackEnvironmentError(f"'{name}': environment already exists at {root(name)}")
+
+ ensure_env_root_path_exists()
+ validate_env_name(name)
+ return root(name)
+
+
+def ensure_env_root_path_exists():
if not os.path.isdir(env_root_path()):
fs.mkdirp(env_root_path())
- validate_env_name(name)
- if exists(name):
- raise SpackEnvironmentError("'%s': environment already exists at %s" % (name, root(name)))
- return Environment(root(name), init_file, with_view, keep_relative)
def config_dict(yaml_data):
@@ -313,7 +388,7 @@ def _read_yaml(str_or_file):
data = syaml.load_config(str_or_file)
filename = getattr(str_or_file, "name", None)
default_data = spack.config.validate(data, spack.schema.env.schema, filename)
- return (data, default_data)
+ return data, default_data
def _write_yaml(data, str_or_file):
@@ -399,7 +474,7 @@ def _error_on_nonempty_view_dir(new_root):
)
-class ViewDescriptor(object):
+class ViewDescriptor:
def __init__(
self,
base_path,
@@ -425,6 +500,10 @@ class ViewDescriptor(object):
def exclude_fn(self, spec):
return not any(spec.satisfies(e) for e in self.exclude)
+ def update_root(self, new_path):
+ self.raw_root = new_path
+ self.root = spack.util.path.canonicalize_path(new_path, default_wd=self.base)
+
def __eq__(self, other):
return all(
[
@@ -665,144 +744,77 @@ class ViewDescriptor(object):
tty.warn(msg)
-def _create_environment(*args, **kwargs):
- return Environment(*args, **kwargs)
+def _create_environment(path):
+ return Environment(path)
-class Environment(object):
- def __init__(self, path, init_file=None, with_view=None, keep_relative=False):
- """Create a new environment.
+class Environment:
+ """A Spack environment, which bundles together configuration and a list of specs."""
- The environment can be optionally initialized with either a
- spack.yaml or spack.lock file.
+ def __init__(self, manifest_dir: Union[str, pathlib.Path]) -> None:
+ """An environment can be constructed from a directory containing a "spack.yaml" file, and
+ optionally a consistent "spack.lock" file.
- Arguments:
- path (str): path to the root directory of this environment
- init_file (str or file object): filename or file object to
- initialize the environment
- with_view (str or bool): whether a view should be maintained for
- the environment. If the value is a string, it specifies the
- path to the view.
- keep_relative (bool): if True, develop paths are copied verbatim
- into the new environment file, otherwise they are made absolute
- when the environment path is different from init_file's
- directory.
+ Args:
+ manifest_dir: directory with the "spack.yaml" associated with the environment
"""
- self.path = os.path.abspath(path)
- self.init_file = init_file
- self.with_view = with_view
- self.keep_relative = keep_relative
+ self.path = os.path.abspath(str(manifest_dir))
self.txlock = lk.Lock(self._transaction_lock_path)
- # This attribute will be set properly from configuration
- # during concretization
self.unify = None
- self.new_specs = []
- self.new_installs = []
- self.clear()
-
- if init_file:
- # If we are creating the environment from an init file, we don't
- # need to lock, because there are no Spack operations that alter
- # the init file.
- with fs.open_if_filename(init_file) as f:
- if hasattr(f, "name") and f.name.endswith(".lock"):
- self._read_manifest(default_manifest_yaml())
- self._read_lockfile(f)
- self._set_user_specs_from_lockfile()
- else:
- self._read_manifest(f, raw_yaml=default_manifest_yaml())
+ self.new_specs: List[Spec] = []
+ self.new_installs: List[Spec] = []
+ self.views: Dict[str, ViewDescriptor] = {}
+
+ #: Specs from "spack.yaml"
+ self.spec_lists = {user_speclist_name: SpecList()}
+ #: Dev-build specs from "spack.yaml"
+ self.dev_specs: Dict[str, Any] = {}
+ #: User specs from the last concretization
+ self.concretized_user_specs: List[Spec] = []
+ #: Roots associated with the last concretization, in order
+ self.concretized_order: List[Spec] = []
+ #: Concretized specs by hash
+ self.specs_by_hash: Dict[str, Spec] = {}
+ #: Repository for this environment (memoized)
+ self._repo = None
+ #: Previously active environment
+ self._previous_active = None
- # Rewrite relative develop paths when initializing a new
- # environment in a different location from the spack.yaml file.
- if not keep_relative and hasattr(f, "name") and f.name.endswith(".yaml"):
- init_file_dir = os.path.abspath(os.path.dirname(f.name))
- self._rewrite_relative_paths_on_relocation(init_file_dir)
- else:
- with lk.ReadTransaction(self.txlock):
- self._read()
-
- if with_view is False:
- self.views = {}
- elif with_view is True:
- self.views = {default_view_name: ViewDescriptor(self.path, self.view_path_default)}
- elif isinstance(with_view, str):
- self.views = {default_view_name: ViewDescriptor(self.path, with_view)}
- # If with_view is None, then defer to the view settings determined by
- # the manifest file
+ with lk.ReadTransaction(self.txlock):
+ self.manifest = EnvironmentManifestFile(manifest_dir)
+ self._read()
def __reduce__(self):
- return _create_environment, (self.path, self.init_file, self.with_view, self.keep_relative)
-
- def _rewrite_relative_paths_on_relocation(self, init_file_dir):
- """When initializing the environment from a manifest file and we plan
- to store the environment in a different directory, we have to rewrite
- relative paths to absolute ones."""
- if init_file_dir == self.path:
- return
-
- for name, entry in self.dev_specs.items():
- dev_path = entry["path"]
- expanded_path = os.path.normpath(os.path.join(init_file_dir, entry["path"]))
-
- # Skip if the expanded path is the same (e.g. when absolute)
- if dev_path == expanded_path:
- continue
-
- tty.debug("Expanding develop path for {0} to {1}".format(name, expanded_path))
-
- self.dev_specs[name]["path"] = expanded_path
+ return _create_environment, (self.path,)
def _re_read(self):
- """Reinitialize the environment object if it has been written (this
- may not be true if the environment was just created in this running
- instance of Spack)."""
- if not os.path.exists(self.manifest_path):
- return
-
+ """Reinitialize the environment object."""
self.clear(re_read=True)
+ self.manifest = EnvironmentManifestFile(self.path)
self._read()
def _read(self):
- default_manifest = not os.path.exists(self.manifest_path)
- if default_manifest:
- # No manifest, use default yaml
- self._read_manifest(default_manifest_yaml())
- else:
- with open(self.manifest_path) as f:
- self._read_manifest(f)
+ self._construct_state_from_manifest()
if os.path.exists(self.lock_path):
with open(self.lock_path) as f:
read_lock_version = self._read_lockfile(f)
- if default_manifest:
- # No manifest, set user specs from lockfile
- self._set_user_specs_from_lockfile()
if read_lock_version == 1:
- tty.debug(
- "Storing backup of old lockfile {0} at {1}".format(
- self.lock_path, self._lock_backup_v1_path
- )
- )
+ tty.debug(f"Storing backup of {self.lock_path} at {self._lock_backup_v1_path}")
shutil.copy(self.lock_path, self._lock_backup_v1_path)
def write_transaction(self):
"""Get a write lock context manager for use in a `with` block."""
return lk.WriteTransaction(self.txlock, acquire=self._re_read)
- def _read_manifest(self, f, raw_yaml=None):
+ def _construct_state_from_manifest(self):
"""Read manifest file and set up user specs."""
- if raw_yaml:
- _, self.yaml = _read_yaml(f)
- self.raw_yaml, _ = _read_yaml(raw_yaml)
- else:
- self.raw_yaml, self.yaml = _read_yaml(f)
-
self.spec_lists = collections.OrderedDict()
- for item in config_dict(self.yaml).get("definitions", []):
+ for item in config_dict(self.manifest).get("definitions", []):
entry = copy.deepcopy(item)
when = _eval_conditional(entry.pop("when", "True"))
assert len(entry) == 1
@@ -814,13 +826,13 @@ class Environment(object):
else:
self.spec_lists[name] = user_specs
- spec_list = config_dict(self.yaml).get(user_speclist_name, [])
+ spec_list = config_dict(self.manifest).get(user_speclist_name, [])
user_specs = SpecList(
user_speclist_name, [s for s in spec_list if s], self.spec_lists.copy()
)
self.spec_lists[user_speclist_name] = user_specs
- enable_view = config_dict(self.yaml).get("view")
+ enable_view = config_dict(self.manifest).get("view")
# enable_view can be boolean, string, or None
if enable_view is True or enable_view is None:
self.views = {default_view_name: ViewDescriptor(self.path, self.view_path_default)}
@@ -836,13 +848,13 @@ class Environment(object):
self.views = {}
# Retrieve the current concretization strategy
- configuration = config_dict(self.yaml)
+ configuration = config_dict(self.manifest)
# Retrieve unification scheme for the concretizer
self.unify = spack.config.get("concretizer:unify", False)
# Retrieve dev-build packages:
- self.dev_specs = configuration.get("develop", {})
+ self.dev_specs = copy.deepcopy(configuration.get("develop", {}))
for name, entry in self.dev_specs.items():
# spec must include a concrete version
assert Spec(entry["spec"]).version.concrete
@@ -854,14 +866,6 @@ class Environment(object):
def user_specs(self):
return self.spec_lists[user_speclist_name]
- def _set_user_specs_from_lockfile(self):
- """Copy user_specs from a read-in lockfile."""
- self.spec_lists = {
- user_speclist_name: SpecList(
- user_speclist_name, [str(s) for s in self.concretized_user_specs]
- )
- }
-
def clear(self, re_read=False):
"""Clear the contents of the environment
@@ -876,7 +880,7 @@ class Environment(object):
self.concretized_user_specs = [] # user specs from last concretize
self.concretized_order = [] # roots of last concretize, in order
self.specs_by_hash = {} # concretized specs by hash
- self._repo = None # RepoPath for this env (memoized)
+ self.invalidate_repository_cache()
self._previous_active = None # previously active environment
if not re_read:
# things that cannot be recreated from file
@@ -970,7 +974,7 @@ class Environment(object):
# load config scopes added via 'include:', in reverse so that
# highest-precedence scopes are last.
- includes = config_dict(self.yaml).get("include", [])
+ includes = config_dict(self.manifest).get("include", [])
missing = []
for i, config_path in enumerate(reversed(includes)):
# allow paths to contain spack config/environment variables, etc.
@@ -1066,7 +1070,7 @@ class Environment(object):
config_name,
self.manifest_path,
spack.schema.env.schema,
- [spack.config.first_existing(self.raw_yaml, spack.schema.env.keys)],
+ [spack.config.first_existing(self.manifest, spack.schema.env.keys)],
)
def config_scopes(self):
@@ -1104,13 +1108,11 @@ class Environment(object):
spec = Spec(user_spec)
if list_name not in self.spec_lists:
- raise SpackEnvironmentError(
- "No list %s exists in environment %s" % (list_name, self.name)
- )
+ raise SpackEnvironmentError(f"No list {list_name} exists in environment {self.name}")
if list_name == user_speclist_name:
if not spec.name:
- raise SpackEnvironmentError("cannot add anonymous specs to an environment!")
+ raise SpackEnvironmentError("cannot add anonymous specs to an environment")
elif not spack.repo.path.exists(spec.name):
virtuals = spack.repo.path.provider_index.providers.keys()
if spec.name not in virtuals:
@@ -1122,6 +1124,10 @@ class Environment(object):
if not existing:
list_to_change.add(str(spec))
self.update_stale_references(list_name)
+ if list_name == user_speclist_name:
+ self.manifest.add_user_spec(str(user_spec))
+ else:
+ self.manifest.add_definition(str(user_spec), list_name=list_name)
return bool(not existing)
@@ -1159,7 +1165,7 @@ class Environment(object):
" specify a named list that is not a matrix"
)
- matches = list(x for x in list_to_change if x.satisfies(match_spec))
+ matches = list((idx, x) for idx, x in enumerate(list_to_change) if x.satisfies(match_spec))
if len(matches) == 0:
raise ValueError(
"There are no specs named {0} in {1}".format(match_spec.name, list_name)
@@ -1167,35 +1173,42 @@ class Environment(object):
elif len(matches) > 1 and not allow_changing_multiple_specs:
raise ValueError("{0} matches multiple specs".format(str(match_spec)))
- new_speclist = SpecList(list_name)
- for i, spec in enumerate(list_to_change):
- if spec.satisfies(match_spec):
- new_speclist.add(Spec.override(spec, change_spec))
+ for idx, spec in matches:
+ override_spec = Spec.override(spec, change_spec)
+ self.spec_lists[list_name].specs[idx] = override_spec
+ if list_name == user_speclist_name:
+ self.manifest.override_user_spec(str(override_spec), idx=idx)
else:
- new_speclist.add(spec)
-
- self.spec_lists[list_name] = new_speclist
- self.update_stale_references()
+ self.manifest.override_definition(
+ str(spec), override=str(override_spec), list_name=list_name
+ )
+ self.update_stale_references(from_list=list_name)
+ self._construct_state_from_manifest()
def remove(self, query_spec, list_name=user_speclist_name, force=False):
"""Remove specs from an environment that match a query_spec"""
+ err_msg_header = (
+ f"cannot remove {query_spec} from '{list_name}' definition "
+ f"in {self.manifest.manifest_file}"
+ )
query_spec = Spec(query_spec)
-
- list_to_change = self.spec_lists[list_name]
- matches = []
+ try:
+ list_to_change = self.spec_lists[list_name]
+ except KeyError as e:
+ msg = f"{err_msg_header}, since '{list_name}' does not exist"
+ raise SpackEnvironmentError(msg) from e
if not query_spec.concrete:
matches = [s for s in list_to_change if s.satisfies(query_spec)]
- if not matches:
+ else:
# concrete specs match against concrete specs in the env
# by dag hash.
specs_hashes = zip(self.concretized_user_specs, self.concretized_order)
-
matches = [s for s, h in specs_hashes if query_spec.dag_hash() == h]
if not matches:
- raise SpackEnvironmentError("Not found: {0}".format(query_spec))
+ raise SpackEnvironmentError(f"{err_msg_header}, no spec matches")
old_specs = set(self.user_specs)
new_specs = set()
@@ -1208,14 +1221,20 @@ class Environment(object):
except spack.spec_list.SpecListError:
# define new specs list
new_specs = set(self.user_specs)
- msg = "Spec '%s' is part of a spec matrix and " % spec
- msg += "cannot be removed from list '%s'." % list_to_change
+ msg = f"Spec '{spec}' is part of a spec matrix and "
+ msg += f"cannot be removed from list '{list_to_change}'."
if force:
msg += " It will be removed from the concrete specs."
- # Mock new specs so we can remove this spec from
- # concrete spec lists
+ # Mock new specs, so we can remove this spec from concrete spec lists
new_specs.remove(spec)
tty.warn(msg)
+ else:
+ if list_name == user_speclist_name:
+ for user_spec in matches:
+ self.manifest.remove_user_spec(str(user_spec))
+ else:
+ for user_spec in matches:
+ self.manifest.remove_definition(str(user_spec), list_name=list_name)
# If force, update stale concretized specs
for spec in old_specs - new_specs:
@@ -1280,7 +1299,9 @@ class Environment(object):
pkg_cls(spec).stage.steal_source(abspath)
# If it wasn't already in the list, append it
- self.dev_specs[spec.name] = {"path": path, "spec": str(spec)}
+ entry = {"path": path, "spec": str(spec)}
+ self.dev_specs[spec.name] = entry
+ self.manifest.add_develop_spec(spec.name, entry=entry.copy())
return True
def undevelop(self, spec):
@@ -1290,6 +1311,7 @@ class Environment(object):
spec = Spec(spec) # In case it's a spec object
if spec.name in self.dev_specs:
del self.dev_specs[spec.name]
+ self.manifest.remove_develop_spec(spec.name)
return True
return False
@@ -1541,18 +1563,64 @@ class Environment(object):
return self.views[default_view_name]
- def update_default_view(self, viewpath):
- name = default_view_name
- if name in self.views and self.default_view.root != viewpath:
- shutil.rmtree(self.default_view.root)
+ def update_default_view(self, path_or_bool: Union[str, bool]) -> None:
+ """Updates the path of the default view.
- if viewpath:
- if name in self.views:
- self.default_view.root = viewpath
- else:
- self.views[name] = ViewDescriptor(self.path, viewpath)
+ If the argument passed as input is False the default view is deleted, if present. The
+ manifest will have an entry "view: false".
+
+ If the argument passed as input is True a default view is created, if not already present.
+ The manifest will have an entry "view: true". If a default view is already declared, it
+ will be left untouched.
+
+ If the argument passed as input is a path a default view pointing to that path is created,
+ if not present already. If a default view is already declared, only its "root" will be
+ changed.
+
+ Args:
+ path_or_bool: either True, or False or a path
+ """
+ view_path = self.view_path_default if path_or_bool is True else path_or_bool
+
+ # We don't have the view, and we want to remove it
+ if default_view_name not in self.views and path_or_bool is False:
+ return
+
+ # We want to enable the view, but we have it already
+ if default_view_name in self.views and path_or_bool is True:
+ return
+
+ # We have the view, and we want to set it to the same path
+ if default_view_name in self.views and self.default_view.root == view_path:
+ return
+
+ self.delete_default_view()
+ if path_or_bool is False:
+ self.views.pop(default_view_name, None)
+ self.manifest.remove_default_view()
+ return
+
+ # If we had a default view already just update its path,
+ # else create a new one and add it to views
+ if default_view_name in self.views:
+ self.default_view.update_root(view_path)
else:
- self.views.pop(name, None)
+ self.views[default_view_name] = ViewDescriptor(self.path, view_path)
+
+ self.manifest.set_default_view(self._default_view_as_yaml())
+
+ def delete_default_view(self) -> None:
+ """Deletes the default view associated with this environment."""
+ if default_view_name not in self.views:
+ return
+
+ try:
+ view = pathlib.Path(self.default_view.root)
+ shutil.rmtree(view.resolve())
+ view.unlink()
+ except FileNotFoundError as e:
+ msg = f"[ENVIRONMENT] error trying to delete the default view: {str(e)}"
+ tty.debug(msg)
def regenerate_views(self):
if not self.views:
@@ -2094,7 +2162,7 @@ class Environment(object):
if self.specs_by_hash:
self.ensure_env_directory_exists(dot_env=True)
self.update_environment_repository()
- self.update_manifest()
+ self.manifest.flush()
# Write the lock file last. This is useful for Makefiles
# with `spack.lock: spack.yaml` rules, where the target
# should be newer than the prerequisite to avoid
@@ -2103,7 +2171,7 @@ class Environment(object):
else:
self.ensure_env_directory_exists(dot_env=False)
with fs.safe_remove(self.lock_path):
- self.update_manifest()
+ self.manifest.flush()
if regenerate:
self.regenerate_views()
@@ -2158,99 +2226,22 @@ class Environment(object):
)
warnings.warn(msg.format(self.name, self.name, ver))
- def update_manifest(self):
- """Update YAML manifest for this environment based on changes to
- spec lists and views and write it.
- """
- yaml_dict = config_dict(self.yaml)
- raw_yaml_dict = config_dict(self.raw_yaml)
- # invalidate _repo cache
- self._repo = None
- # put any changes in the definitions in the YAML
- for name, speclist in self.spec_lists.items():
- if name == user_speclist_name:
- # The primary list is handled differently
- continue
+ def _default_view_as_yaml(self):
+ """This internal function assumes the default view is set"""
+ path = self.default_view.raw_root
+ if (
+ self.default_view == ViewDescriptor(self.path, self.view_path_default)
+ and len(self.views) == 1
+ ):
+ return True
- active_yaml_lists = [
- x
- for x in yaml_dict.get("definitions", [])
- if name in x and _eval_conditional(x.get("when", "True"))
- ]
+ if self.default_view == ViewDescriptor(self.path, path) and len(self.views) == 1:
+ return path
- # Remove any specs in yaml that are not in internal representation
- for ayl in active_yaml_lists:
- # If it's not a string, it's a matrix. Those can't have changed
- # If it is a string that starts with '$', it's a reference.
- # Those also can't have changed.
- ayl[name][:] = [
- s
- for s in ayl.setdefault(name, [])
- if (not isinstance(s, str)) or s.startswith("$") or Spec(s) in speclist.specs
- ]
-
- # Put the new specs into the first active list from the yaml
- new_specs = [
- entry
- for entry in speclist.yaml_list
- if isinstance(entry, str)
- and not any(entry in ayl[name] for ayl in active_yaml_lists)
- ]
- list_for_new_specs = active_yaml_lists[0].setdefault(name, [])
- list_for_new_specs[:] = list_for_new_specs + new_specs
- # put the new user specs in the YAML.
- # This can be done directly because there can't be multiple definitions
- # nor when clauses for `specs` list.
- yaml_spec_list = yaml_dict.setdefault(user_speclist_name, [])
- yaml_spec_list[:] = self.user_specs.yaml_list
- # Construct YAML representation of view
- default_name = default_view_name
- if self.views and len(self.views) == 1 and default_name in self.views:
- path = self.default_view.raw_root
- if self.default_view == ViewDescriptor(self.path, self.view_path_default):
- view = True
- elif self.default_view == ViewDescriptor(self.path, path):
- view = path
- else:
- view = dict((name, view.to_dict()) for name, view in self.views.items())
- elif self.views:
- view = dict((name, view.to_dict()) for name, view in self.views.items())
- else:
- view = False
-
- yaml_dict["view"] = view
+ return self.default_view.to_dict()
- if self.dev_specs:
- # Remove entries that are mirroring defaults
- write_dev_specs = copy.deepcopy(self.dev_specs)
- for name, entry in write_dev_specs.items():
- if entry["path"] == name:
- del entry["path"]
- yaml_dict["develop"] = write_dev_specs
- else:
- yaml_dict.pop("develop", None)
-
- # Remove yaml sections that are shadowing defaults
- # construct garbage path to ensure we don't find a manifest by accident
- with fs.temp_cwd() as env_dir:
- bare_env = Environment(env_dir, with_view=self.view_path_default)
- keys_present = list(yaml_dict.keys())
- for key in keys_present:
- if yaml_dict[key] == config_dict(bare_env.yaml).get(key, None):
- if key not in raw_yaml_dict:
- del yaml_dict[key]
- # if all that worked, write out the manifest file at the top level
- # (we used to check whether the yaml had changed and not write it out
- # if it hadn't. We can't do that anymore because it could be the only
- # thing that changed is the "override" attribute on a config dict,
- # which would not show up in even a string comparison between the two
- # keys).
- changed = not yaml_equivalent(self.yaml, self.raw_yaml)
- written = os.path.exists(self.manifest_path)
- if changed or not written:
- self.raw_yaml = copy.deepcopy(self.yaml)
- with fs.write_tmp_and_move(os.path.realpath(self.manifest_path)) as f:
- _write_yaml(self.yaml, f)
+ def invalidate_repository_cache(self):
+ self._repo = None
def __enter__(self):
self._previous_active = _active_environment
@@ -2500,6 +2491,360 @@ def no_active_environment():
activate(env)
+def initialize_environment_dir(
+ environment_dir: Union[str, pathlib.Path], envfile: Optional[Union[str, pathlib.Path]]
+) -> None:
+ """Initialize an environment directory starting from an envfile.
+
+ The envfile can be either a "spack.yaml" manifest file, or a "spack.lock" file.
+
+ Args:
+ environment_dir: directory where the environment should be placed
+ envfile: manifest file or lockfile used to initialize the environment
+
+ Raises:
+ SpackEnvironmentError: if the directory can't be initialized
+ """
+ environment_dir = pathlib.Path(environment_dir)
+ target_lockfile = environment_dir / lockfile_name
+ target_manifest = environment_dir / manifest_name
+ if target_manifest.exists():
+ msg = f"cannot initialize environment, {target_manifest} already exists"
+ raise SpackEnvironmentError(msg)
+
+ if target_lockfile.exists():
+ msg = f"cannot initialize environment, {target_lockfile} already exists"
+ raise SpackEnvironmentError(msg)
+
+ def _ensure_env_dir():
+ try:
+ environment_dir.mkdir(parents=True, exist_ok=True)
+ except FileExistsError as e:
+ msg = f"cannot initialize the environment, '{environment_dir}' already exists"
+ raise SpackEnvironmentError(msg) from e
+
+ if envfile is None:
+ _ensure_env_dir()
+ target_manifest.write_text(default_manifest_yaml())
+ return
+
+ envfile = pathlib.Path(envfile)
+ if not envfile.exists() or not envfile.is_file():
+ msg = f"cannot initialize environment, {envfile} is not a valid file"
+ raise SpackEnvironmentError(msg)
+
+ if not str(envfile).endswith(manifest_name) and not str(envfile).endswith(lockfile_name):
+ msg = (
+ f"cannot initialize environment from '{envfile}', either a '{manifest_name}'"
+ f" or a '{lockfile_name}' file is needed"
+ )
+ raise SpackEnvironmentError(msg)
+
+ _ensure_env_dir()
+
+ # When we have a lockfile we should copy that and produce a consistent default manifest
+ if str(envfile).endswith(lockfile_name):
+ shutil.copy(envfile, target_lockfile)
+ # This constructor writes a spack.yaml which is consistent with the root
+ # specs in the spack.lock
+ EnvironmentManifestFile.from_lockfile(environment_dir)
+ return
+
+ shutil.copy(envfile, target_manifest)
+
+
+class EnvironmentManifestFile(collections.abc.Mapping):
+ """Manages the in-memory representation of a manifest file, and its synchronization
+ with the actual manifest on disk.
+ """
+
+ @staticmethod
+ def from_lockfile(manifest_dir: Union[pathlib.Path, str]) -> "EnvironmentManifestFile":
+ """Returns an environment manifest file compatible with the lockfile already present in
+ the environment directory.
+
+ This function also writes a spack.yaml file that is consistent with the spack.lock
+ already existing in the directory.
+
+ Args:
+ manifest_dir: directory where the lockfile is
+ """
+ manifest_dir = pathlib.Path(manifest_dir)
+ lockfile = manifest_dir / lockfile_name
+ with lockfile.open("r") as f:
+ data = sjson.load(f)
+ user_specs = data["roots"]
+
+ default_content = manifest_dir / manifest_name
+ default_content.write_text(default_manifest_yaml())
+ manifest = EnvironmentManifestFile(manifest_dir)
+ for item in user_specs:
+ manifest.add_user_spec(item["spec"])
+ manifest.flush()
+ return manifest
+
+ def __init__(self, manifest_dir: Union[pathlib.Path, str]) -> None:
+ self.manifest_dir = pathlib.Path(manifest_dir)
+ self.manifest_file = self.manifest_dir / manifest_name
+
+ if not self.manifest_file.exists():
+ msg = f"cannot find '{manifest_name}' in {self.manifest_dir}"
+ raise SpackEnvironmentError(msg)
+
+ with self.manifest_file.open() as f:
+ raw, with_defaults_added = _read_yaml(f)
+
+ #: Pristine YAML content, without defaults being added
+ self.pristine_yaml_content = raw
+ #: YAML content with defaults added by Spack, if they're missing
+ self.yaml_content = with_defaults_added
+ self.changed = False
+
+ def add_user_spec(self, user_spec: str) -> None:
+ """Appends the user spec passed as input to the list of root specs.
+
+ Args:
+ user_spec: user spec to be appended
+ """
+ config_dict(self.pristine_yaml_content)["specs"].append(user_spec)
+ config_dict(self.yaml_content)["specs"].append(user_spec)
+ self.changed = True
+
+ def remove_user_spec(self, user_spec: str) -> None:
+ """Removes the user spec passed as input from the list of root specs
+
+ Args:
+ user_spec: user spec to be removed
+
+ Raises:
+ SpackEnvironmentError: when the user spec is not in the list
+ """
+ try:
+ config_dict(self.pristine_yaml_content)["specs"].remove(user_spec)
+ config_dict(self.yaml_content)["specs"].remove(user_spec)
+ except ValueError as e:
+ msg = f"cannot remove {user_spec} from {self}, no such spec exists"
+ raise SpackEnvironmentError(msg) from e
+ self.changed = True
+
+ def override_user_spec(self, user_spec: str, idx: int) -> None:
+ """Overrides the user spec at index idx with the one passed as input.
+
+ Args:
+ user_spec: new user spec
+ idx: index of the spec to be overridden
+
+ Raises:
+ SpackEnvironmentError: when the user spec cannot be overridden
+ """
+ try:
+ config_dict(self.pristine_yaml_content)["specs"][idx] = user_spec
+ config_dict(self.yaml_content)["specs"][idx] = user_spec
+ except ValueError as e:
+ msg = f"cannot override {user_spec} from {self}"
+ raise SpackEnvironmentError(msg) from e
+ self.changed = True
+
+ def add_definition(self, user_spec: str, list_name: str) -> None:
+ """Appends a user spec to the first active definition mathing the name passed as argument.
+
+ Args:
+ user_spec: user spec to be appended
+ list_name: name of the definition where to append
+
+ Raises:
+ SpackEnvironmentError: is no valid definition exists already
+ """
+ defs = config_dict(self.pristine_yaml_content).get("definitions", [])
+ msg = f"cannot add {user_spec} to the '{list_name}' definition, no valid list exists"
+
+ for idx, item in self._iterate_on_definitions(defs, list_name=list_name, err_msg=msg):
+ item[list_name].append(user_spec)
+ break
+
+ config_dict(self.yaml_content)["definitions"][idx][list_name].append(user_spec)
+ self.changed = True
+
+ def remove_definition(self, user_spec: str, list_name: str) -> None:
+ """Removes a user spec from an active definition that matches the name passed as argument.
+
+ Args:
+ user_spec: user spec to be removed
+ list_name: name of the definition where to remove the spec from
+
+ Raises:
+ SpackEnvironmentError: if the user spec cannot be removed from the list,
+ or the list does not exist
+ """
+ defs = config_dict(self.pristine_yaml_content).get("definitions", [])
+ msg = (
+ f"cannot remove {user_spec} from the '{list_name}' definition, "
+ f"no valid list exists"
+ )
+
+ for idx, item in self._iterate_on_definitions(defs, list_name=list_name, err_msg=msg):
+ try:
+ item[list_name].remove(user_spec)
+ break
+ except ValueError:
+ pass
+
+ config_dict(self.yaml_content)["definitions"][idx][list_name].remove(user_spec)
+ self.changed = True
+
+ def override_definition(self, user_spec: str, *, override: str, list_name: str) -> None:
+ """Overrides a user spec from an active definition that matches the name passed
+ as argument.
+
+ Args:
+ user_spec: user spec to be overridden
+ override: new spec to be used
+ list_name: name of the definition where to override the spec
+
+ Raises:
+ SpackEnvironmentError: if the user spec cannot be overridden
+ """
+ defs = config_dict(self.pristine_yaml_content).get("definitions", [])
+ msg = f"cannot override {user_spec} with {override} in the '{list_name}' definition"
+
+ for idx, item in self._iterate_on_definitions(defs, list_name=list_name, err_msg=msg):
+ try:
+ sub_index = item[list_name].index(user_spec)
+ item[list_name][sub_index] = override
+ break
+ except ValueError:
+ pass
+
+ config_dict(self.yaml_content)["definitions"][idx][list_name][sub_index] = override
+ self.changed = True
+
+ def _iterate_on_definitions(self, definitions, *, list_name, err_msg):
+ """Iterates on definitions, returning the active ones matching a given name."""
+
+ def extract_name(_item):
+ names = list(x for x in _item if x != "when")
+ assert len(names) == 1, f"more than one name in {_item}"
+ return names[0]
+
+ for idx, item in enumerate(definitions):
+ name = extract_name(item)
+ if name != list_name:
+ continue
+
+ condition_str = item.get("when", "True")
+ if not _eval_conditional(condition_str):
+ continue
+
+ yield idx, item
+ else:
+ raise SpackEnvironmentError(err_msg)
+
+ def set_default_view(self, view: Union[bool, str, pathlib.Path, Dict[str, str]]) -> None:
+ """Sets the default view root in the manifest to the value passed as input.
+
+ Args:
+ view: If the value is a string or a path, it specifies the path to the view. If
+ True the default view is used for the environment, if False there's no view.
+ """
+ if isinstance(view, dict):
+ config_dict(self.pristine_yaml_content)["view"][default_view_name].update(view)
+ config_dict(self.yaml_content)["view"][default_view_name].update(view)
+ self.changed = True
+ return
+
+ if not isinstance(view, bool):
+ view = str(view)
+
+ config_dict(self.pristine_yaml_content)["view"] = view
+ config_dict(self.yaml_content)["view"] = view
+ self.changed = True
+
+ def remove_default_view(self) -> None:
+ """Removes the default view from the manifest file"""
+ view_data = config_dict(self.pristine_yaml_content).get("view")
+ if isinstance(view_data, collections.abc.Mapping):
+ config_dict(self.pristine_yaml_content)["view"].pop(default_view_name)
+ config_dict(self.yaml_content)["view"].pop(default_view_name)
+ self.changed = True
+ return
+
+ self.set_default_view(view=False)
+
+ def add_develop_spec(self, pkg_name: str, entry: Dict[str, str]) -> None:
+ """Adds a develop spec to the manifest file
+
+ Args:
+ pkg_name: name of the package to be developed
+ entry: spec and path of the developed package
+ """
+ # The environment sets the path to pkg_name is that is implicit
+ if entry["path"] == pkg_name:
+ entry.pop("path")
+
+ config_dict(self.pristine_yaml_content).setdefault("develop", {}).setdefault(
+ pkg_name, {}
+ ).update(entry)
+ config_dict(self.yaml_content).setdefault("develop", {}).setdefault(pkg_name, {}).update(
+ entry
+ )
+ self.changed = True
+
+ def remove_develop_spec(self, pkg_name: str) -> None:
+ """Removes a develop spec from the manifest file
+
+ Args:
+ pkg_name: package to be removed from development
+
+ Raises:
+ SpackEnvironmentError: if there is nothing to remove
+ """
+ try:
+ del config_dict(self.pristine_yaml_content)["develop"][pkg_name]
+ except KeyError as e:
+ msg = f"cannot remove '{pkg_name}' from develop specs in {self}, entry does not exist"
+ raise SpackEnvironmentError(msg) from e
+ del config_dict(self.yaml_content)["develop"][pkg_name]
+ self.changed = True
+
+ def absolutify_dev_paths(self, init_file_dir: Union[str, pathlib.Path]) -> None:
+ """Normalizes the dev paths in the environment with respect to the directory where the
+ initialization file resides.
+
+ Args:
+ init_file_dir: directory with the "spack.yaml" used to initialize the environment.
+ """
+ init_file_dir = pathlib.Path(init_file_dir).absolute()
+ for _, entry in config_dict(self.pristine_yaml_content).get("develop", {}).items():
+ expanded_path = os.path.normpath(str(init_file_dir / entry["path"]))
+ entry["path"] = str(expanded_path)
+
+ for _, entry in config_dict(self.yaml_content).get("develop", {}).items():
+ expanded_path = os.path.normpath(str(init_file_dir / entry["path"]))
+ entry["path"] = str(expanded_path)
+ self.changed = True
+
+ def flush(self) -> None:
+ """Synchronizes the object with the manifest file on disk."""
+ if not self.changed:
+ return
+
+ with fs.write_tmp_and_move(os.path.realpath(self.manifest_file)) as f:
+ _write_yaml(self.pristine_yaml_content, f)
+ self.changed = False
+
+ def __len__(self):
+ return len(self.yaml_content)
+
+ def __getitem__(self, key):
+ return self.yaml_content[key]
+
+ def __iter__(self):
+ return iter(self.yaml_content)
+
+ def __str__(self):
+ return str(self.manifest_file)
+
+
class SpackEnvironmentError(spack.error.SpackError):
"""Superclass for all errors to do with Spack environments."""
diff --git a/lib/spack/spack/schema/ci.py b/lib/spack/spack/schema/ci.py
index 7ff6fdb0d0..2c22f071de 100644
--- a/lib/spack/spack/schema/ci.py
+++ b/lib/spack/spack/schema/ci.py
@@ -204,7 +204,7 @@ def update(data):
# Warn if deprecated section is still in the environment
ci_env = ev.active_environment()
if ci_env:
- env_config = ev.config_dict(ci_env.yaml)
+ env_config = ev.config_dict(ci_env.manifest)
if "gitlab-ci" in env_config:
tty.die("Error: `gitlab-ci` section detected with `ci`, these are not compatible")
diff --git a/lib/spack/spack/test/cmd/config.py b/lib/spack/spack/test/cmd/config.py
index 03c310ca1e..a7793d65c7 100644
--- a/lib/spack/spack/test/cmd/config.py
+++ b/lib/spack/spack/test/cmd/config.py
@@ -91,17 +91,10 @@ def test_config_edit(mutable_config, working_env):
def test_config_get_gets_spack_yaml(mutable_mock_env_path):
- env = ev.create("test")
-
config("get", fail_on_error=False)
assert config.returncode == 1
- with env:
- config("get", fail_on_error=False)
- assert config.returncode == 1
-
- env.write()
-
+ with ev.create("test") as env:
assert "mpileaks" not in config("get")
env.add("mpileaks")
@@ -671,4 +664,4 @@ spack:
config("update", "-y", "config")
with ev.Environment(str(tmpdir)) as e:
- assert not e.raw_yaml["spack"]["config"]["ccache"]
+ assert not e.manifest.pristine_yaml_content["spack"]["config"]["ccache"]
diff --git a/lib/spack/spack/test/cmd/develop.py b/lib/spack/spack/test/cmd/develop.py
index 50441a6844..bea3aa3b8a 100644
--- a/lib/spack/spack/test/cmd/develop.py
+++ b/lib/spack/spack/test/cmd/develop.py
@@ -32,7 +32,7 @@ class TestDevelop(object):
assert dev_specs_entry["spec"] == str(spec)
# check yaml representation
- yaml = ev.config_dict(env.yaml)
+ yaml = ev.config_dict(env.manifest)
assert spec.name in yaml["develop"]
yaml_entry = yaml["develop"][spec.name]
assert yaml_entry["spec"] == str(spec)
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
index 348c46261c..c567f6ee25 100644
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -6,6 +6,7 @@ import filecmp
import glob
import io
import os
+import pathlib
import shutil
import sys
from argparse import Namespace
@@ -18,6 +19,7 @@ import llnl.util.link_tree
import spack.cmd.env
import spack.config
import spack.environment as ev
+import spack.environment.environment
import spack.environment.shell
import spack.error
import spack.modules
@@ -53,6 +55,18 @@ find = SpackCommand("find")
sep = os.sep
+@pytest.fixture()
+def environment_from_manifest(tmp_path):
+ """Returns a new environment named 'test' from the content of a manifest file."""
+
+ def _create(content):
+ spack_yaml = tmp_path / ev.manifest_name
+ spack_yaml.write_text(content)
+ return _env_create("test", init_file=str(spack_yaml))
+
+ return _create
+
+
def check_mpileaks_and_deps_in_view(viewdir):
"""Check that the expected install directories exist."""
assert os.path.exists(str(viewdir.join(".spack", "mpileaks")))
@@ -427,11 +441,11 @@ def test_environment_status(capsys, tmpdir):
with capsys.disabled():
assert "In environment test" in env("status")
- with ev.Environment("local_dir"):
+ with ev.create_in_dir("local_dir"):
with capsys.disabled():
assert os.path.join(os.getcwd(), "local_dir") in env("status")
- e = ev.Environment("myproject")
+ e = ev.create_in_dir("myproject")
e.write()
with tmpdir.join("myproject").as_cwd():
with e:
@@ -445,21 +459,20 @@ def test_env_status_broken_view(
mock_fetch,
mock_custom_repository,
install_mockery,
- tmpdir,
+ tmp_path,
):
- env_dir = str(tmpdir)
- with ev.Environment(env_dir):
+ with ev.create_in_dir(tmp_path):
install("--add", "trivial-install-test-package")
# switch to a new repo that doesn't include the installed package
# test that Spack detects the missing package and warns the user
with spack.repo.use_repositories(mock_custom_repository):
- with ev.Environment(env_dir):
+ with ev.Environment(tmp_path):
output = env("status")
assert "includes out of date packages or repos" in output
# Test that the warning goes away when it's fixed
- with ev.Environment(env_dir):
+ with ev.Environment(tmp_path):
output = env("status")
assert "includes out of date packages or repos" not in output
@@ -505,9 +518,9 @@ def test_env_repo():
assert pkg_cls.namespace == "builtin.mock"
-def test_user_removed_spec():
+def test_user_removed_spec(environment_from_manifest):
"""Ensure a user can remove from any position in the spack.yaml file."""
- initial_yaml = io.StringIO(
+ before = environment_from_manifest(
"""\
env:
specs:
@@ -516,8 +529,6 @@ env:
- libelf
"""
)
-
- before = ev.create("test", initial_yaml)
before.concretize()
before.write()
@@ -536,17 +547,16 @@ env:
after.concretize()
after.write()
- env_specs = after._get_environment_specs()
read = ev.read("test")
env_specs = read._get_environment_specs()
assert not any(x.name == "hypre" for x in env_specs)
-def test_init_from_lockfile(tmpdir):
+def test_init_from_lockfile(environment_from_manifest):
"""Test that an environment can be instantiated from a lockfile."""
- initial_yaml = io.StringIO(
- """\
+ e1 = environment_from_manifest(
+ """
env:
specs:
- mpileaks
@@ -554,11 +564,10 @@ env:
- libelf
"""
)
- e1 = ev.create("test", initial_yaml)
e1.concretize()
e1.write()
- e2 = ev.Environment(str(tmpdir), e1.lock_path)
+ e2 = _env_create("test2", init_file=e1.lock_path)
for s1, s2 in zip(e1.user_specs, e2.user_specs):
assert s1 == s2
@@ -571,10 +580,10 @@ env:
assert s1 == s2
-def test_init_from_yaml(tmpdir):
+def test_init_from_yaml(environment_from_manifest):
"""Test that an environment can be instantiated from a lockfile."""
- initial_yaml = io.StringIO(
- """\
+ e1 = environment_from_manifest(
+ """
env:
specs:
- mpileaks
@@ -582,11 +591,10 @@ env:
- libelf
"""
)
- e1 = ev.create("test", initial_yaml)
e1.concretize()
e1.write()
- e2 = ev.Environment(str(tmpdir), e1.manifest_path)
+ e2 = _env_create("test2", init_file=e1.manifest_path)
for s1, s2 in zip(e1.user_specs, e2.user_specs):
assert s1 == s2
@@ -597,13 +605,16 @@ env:
@pytest.mark.usefixtures("config")
-def test_env_view_external_prefix(tmpdir_factory, mutable_database, mock_packages):
- fake_prefix = tmpdir_factory.mktemp("a-prefix")
- fake_bin = fake_prefix.join("bin")
- fake_bin.ensure(dir=True)
-
- initial_yaml = io.StringIO(
- """\
+def test_env_view_external_prefix(tmp_path, mutable_database, mock_packages):
+ fake_prefix = tmp_path / "a-prefix"
+ fake_bin = fake_prefix / "bin"
+ fake_bin.mkdir(parents=True, exist_ok=False)
+
+ manifest_dir = tmp_path / "environment"
+ manifest_dir.mkdir(parents=True, exist_ok=False)
+ manifest_file = manifest_dir / ev.manifest_name
+ manifest_file.write_text(
+ """
env:
specs:
- a
@@ -627,7 +638,7 @@ packages:
test_scope = spack.config.InternalConfigScope("env-external-test", data=external_config_dict)
with spack.config.override(test_scope):
- e = ev.create("test", initial_yaml)
+ e = ev.create("test", manifest_file)
e.concretize()
# Note: normally installing specs in a test environment requires doing
# a fake install, but not for external specs since no actions are
@@ -672,8 +683,9 @@ env:
assert "test" not in out
-def test_env_with_config():
- test_config = """\
+def test_env_with_config(environment_from_manifest):
+ e = environment_from_manifest(
+ """
env:
specs:
- mpileaks
@@ -681,26 +693,22 @@ env:
mpileaks:
version: [2.2]
"""
- _env_create("test", io.StringIO(test_config))
-
- e = ev.read("test")
+ )
with e:
e.concretize()
assert any(x.intersects("mpileaks@2.2") for x in e._get_environment_specs())
-def test_with_config_bad_include():
- env_name = "test_bad_include"
- test_config = """\
+def test_with_config_bad_include(environment_from_manifest):
+ e = environment_from_manifest(
+ """
spack:
include:
- /no/such/directory
- no/such/file.yaml
"""
- _env_create(env_name, io.StringIO(test_config))
-
- e = ev.read(env_name)
+ )
with pytest.raises(spack.config.ConfigFileError) as exc:
with e:
e.concretize()
@@ -712,17 +720,19 @@ spack:
assert ev.active_environment() is None
-def test_env_with_include_config_files_same_basename():
- test_config = """\
- env:
- include:
- - ./path/to/included-config.yaml
- - ./second/path/to/include-config.yaml
- specs:
- [libelf, mpileaks]
- """
+def test_env_with_include_config_files_same_basename(environment_from_manifest):
+ e = environment_from_manifest(
+ """
+env:
+ include:
+ - ./path/to/included-config.yaml
+ - ./second/path/to/include-config.yaml
+ specs:
+ - libelf
+ - mpileaks
+"""
+ )
- _env_create("test", io.StringIO(test_config))
e = ev.read("test")
fs.mkdirp(os.path.join(e.path, "path", "to"))
@@ -781,14 +791,20 @@ env:
)
-def test_env_with_included_config_file(packages_file):
+def test_env_with_included_config_file(environment_from_manifest, packages_file):
"""Test inclusion of a relative packages configuration file added to an
- existing environment."""
+ existing environment.
+ """
include_filename = "included-config.yaml"
- test_config = mpileaks_env_config(os.path.join(".", include_filename))
-
- _env_create("test", io.StringIO(test_config))
- e = ev.read("test")
+ e = environment_from_manifest(
+ f"""\
+env:
+ include:
+ - {os.path.join(".", include_filename)}
+ specs:
+ - mpileaks
+"""
+ )
included_path = os.path.join(e.path, include_filename)
shutil.move(packages_file.strpath, included_path)
@@ -830,7 +846,7 @@ def test_env_with_included_config_missing_file(tmpdir, mutable_empty_config):
ev.activate(env)
-def test_env_with_included_config_scope(tmpdir, packages_file):
+def test_env_with_included_config_scope(environment_from_manifest, packages_file):
"""Test inclusion of a package file from the environment's configuration
stage directory. This test is intended to represent a case where a remote
file has already been staged."""
@@ -838,15 +854,10 @@ def test_env_with_included_config_scope(tmpdir, packages_file):
# Configure the environment to include file(s) from the environment's
# remote configuration stage directory.
- test_config = mpileaks_env_config(config_scope_path)
-
- # Create the environment
- _env_create("test", io.StringIO(test_config))
-
- e = ev.read("test")
+ e = environment_from_manifest(mpileaks_env_config(config_scope_path))
# Copy the packages.yaml file to the environment configuration
- # directory so it is picked up during concretization. (Using
+ # directory, so it is picked up during concretization. (Using
# copy instead of rename in case the fixture scope changes.)
fs.mkdirp(config_scope_path)
include_filename = os.path.basename(packages_file.strpath)
@@ -861,14 +872,11 @@ def test_env_with_included_config_scope(tmpdir, packages_file):
assert any(x.satisfies("mpileaks@2.2") for x in e._get_environment_specs())
-def test_env_with_included_config_var_path(packages_file):
+def test_env_with_included_config_var_path(environment_from_manifest, packages_file):
"""Test inclusion of a package configuration file with path variables
"staged" in the environment's configuration stage directory."""
config_var_path = os.path.join("$tempdir", "included-config.yaml")
- test_config = mpileaks_env_config(config_var_path)
-
- _env_create("test", io.StringIO(test_config))
- e = ev.read("test")
+ e = environment_from_manifest(mpileaks_env_config(config_var_path))
config_real_path = substitute_path_variables(config_var_path)
fs.mkdirp(os.path.dirname(config_real_path))
@@ -881,8 +889,9 @@ def test_env_with_included_config_var_path(packages_file):
assert any(x.satisfies("mpileaks@2.2") for x in e._get_environment_specs())
-def test_env_config_precedence():
- test_config = """\
+def test_env_config_precedence(environment_from_manifest):
+ e = environment_from_manifest(
+ """
env:
packages:
libelf:
@@ -892,9 +901,7 @@ env:
specs:
- mpileaks
"""
- _env_create("test", io.StringIO(test_config))
- e = ev.read("test")
-
+ )
with open(os.path.join(e.path, "included-config.yaml"), "w") as f:
f.write(
"""\
@@ -916,8 +923,9 @@ packages:
assert any(x.satisfies("libelf@0.8.12") for x in e._get_environment_specs())
-def test_included_config_precedence():
- test_config = """\
+def test_included_config_precedence(environment_from_manifest):
+ e = environment_from_manifest(
+ """
env:
include:
- ./high-config.yaml # this one should take precedence
@@ -925,8 +933,7 @@ env:
specs:
- mpileaks
"""
- _env_create("test", io.StringIO(test_config))
- e = ev.read("test")
+ )
with open(os.path.join(e.path, "high-config.yaml"), "w") as f:
f.write(
@@ -970,7 +977,7 @@ env:
with tmpdir.as_cwd():
with pytest.raises(spack.config.ConfigFormatError) as e:
env("create", "test", "./spack.yaml")
- assert "./spack.yaml:2" in str(e)
+ assert "spack.yaml:2" in str(e)
assert "'spacks' was unexpected" in str(e)
@@ -1255,14 +1262,17 @@ def test_env_without_view_install(tmpdir, mock_stage, mock_fetch, install_mocker
check_mpileaks_and_deps_in_view(view_dir)
-def test_env_config_view_default(tmpdir, mock_stage, mock_fetch, install_mockery):
+def test_env_config_view_default(
+ environment_from_manifest, mock_stage, mock_fetch, install_mockery
+):
# This config doesn't mention whether a view is enabled
- test_config = """\
+ environment_from_manifest(
+ """
env:
specs:
- mpileaks
"""
- _env_create("test", io.StringIO(test_config))
+ )
with ev.read("test"):
install("--fake")
@@ -1879,7 +1889,9 @@ env:
test = ev.read("test")
- packages_lists = list(filter(lambda x: "packages" in x, test.yaml["env"]["definitions"]))
+ packages_lists = list(
+ filter(lambda x: "packages" in x, test.manifest["env"]["definitions"])
+ )
assert len(packages_lists) == 2
assert "callpath" not in packages_lists[0]["packages"]
@@ -2557,7 +2569,9 @@ spack:
def _write_helper_raise(self):
raise RuntimeError("some error")
- monkeypatch.setattr(ev.Environment, "update_manifest", _write_helper_raise)
+ monkeypatch.setattr(
+ spack.environment.environment.EnvironmentManifestFile, "flush", _write_helper_raise
+ )
with ev.Environment(str(tmpdir)) as e:
e.concretize(force=True)
with pytest.raises(RuntimeError):
@@ -2615,25 +2629,6 @@ def test_rewrite_rel_dev_path_named_env(tmpdir):
assert e.dev_specs["mypkg2"]["path"] == sep + os.path.join("some", "other", "path")
-def test_rewrite_rel_dev_path_original_dir(tmpdir):
- """Relative devevelop paths should not be rewritten when initializing an
- environment with root path set to the same directory"""
- init_env, _, _, spack_yaml = _setup_develop_packages(tmpdir)
- with ev.Environment(str(init_env), str(spack_yaml)) as e:
- assert e.dev_specs["mypkg1"]["path"] == "../build_folder"
- assert e.dev_specs["mypkg2"]["path"] == "/some/other/path"
-
-
-def test_rewrite_rel_dev_path_create_original_dir(tmpdir):
- """Relative develop paths should not be rewritten when creating an
- environment in the original directory"""
- init_env, _, _, spack_yaml = _setup_develop_packages(tmpdir)
- env("create", "-d", str(init_env), str(spack_yaml))
- with ev.Environment(str(init_env)) as e:
- assert e.dev_specs["mypkg1"]["path"] == "../build_folder"
- assert e.dev_specs["mypkg2"]["path"] == "/some/other/path"
-
-
def test_does_not_rewrite_rel_dev_path_when_keep_relative_is_set(tmpdir):
"""Relative develop paths should not be rewritten when --keep-relative is
passed to create"""
@@ -2659,8 +2654,9 @@ def test_custom_version_concretize_together(tmpdir):
assert any("hdf5@myversion" in spec for _, spec in e.concretized_specs())
-def test_modules_relative_to_views(tmpdir, install_mockery, mock_fetch):
- spack_yaml = """
+def test_modules_relative_to_views(environment_from_manifest, install_mockery, mock_fetch):
+ environment_from_manifest(
+ """
spack:
specs:
- trivial-install-test-package
@@ -2671,7 +2667,7 @@ spack:
roots:
tcl: modules
"""
- _env_create("test", io.StringIO(spack_yaml))
+ )
with ev.read("test") as e:
install()
@@ -2690,8 +2686,9 @@ spack:
assert spec.prefix not in contents
-def test_multiple_modules_post_env_hook(tmpdir, install_mockery, mock_fetch):
- spack_yaml = """
+def test_multiple_modules_post_env_hook(environment_from_manifest, install_mockery, mock_fetch):
+ environment_from_manifest(
+ """
spack:
specs:
- trivial-install-test-package
@@ -2706,7 +2703,7 @@ spack:
roots:
tcl: full_modules
"""
- _env_create("test", io.StringIO(spack_yaml))
+ )
with ev.read("test") as e:
install()
@@ -2818,17 +2815,17 @@ def test_env_view_fail_if_symlink_points_elsewhere(tmpdir, install_mockery, mock
assert os.path.isdir(non_view_dir)
-def test_failed_view_cleanup(tmpdir, mock_stage, mock_fetch, install_mockery):
+def test_failed_view_cleanup(tmp_path, mock_stage, mock_fetch, install_mockery):
"""Tests whether Spack cleans up after itself when a view fails to create"""
- view = str(tmpdir.join("view"))
- with ev.create("env", with_view=view):
+ view_dir = tmp_path / "view"
+ with ev.create("env", with_view=str(view_dir)):
add("libelf")
install("--fake")
# Save the current view directory.
- resolved_view = os.path.realpath(view)
- all_views = os.path.dirname(resolved_view)
- views_before = os.listdir(all_views)
+ resolved_view = view_dir.resolve(strict=True)
+ all_views = resolved_view.parent
+ views_before = list(all_views.iterdir())
# Add a spec that results in view clash when creating a view
with ev.read("env"):
@@ -2838,9 +2835,9 @@ def test_failed_view_cleanup(tmpdir, mock_stage, mock_fetch, install_mockery):
# Make sure there is no broken view in the views directory, and the current
# view is the original view from before the failed regenerate attempt.
- views_after = os.listdir(all_views)
+ views_after = list(all_views.iterdir())
assert views_before == views_after
- assert os.path.samefile(resolved_view, view)
+ assert view_dir.samefile(resolved_view), view_dir
def test_environment_view_target_already_exists(tmpdir, mock_stage, mock_fetch, install_mockery):
@@ -2941,9 +2938,9 @@ def test_read_old_lock_and_write_new(config, tmpdir, lockfile):
shadowed_hash = dag_hash
# make an env out of the old lockfile -- env should be able to read v1/v2/v3
- test_lockfile_path = str(tmpdir.join("test.lock"))
+ test_lockfile_path = str(tmpdir.join("spack.lock"))
shutil.copy(lockfile_path, test_lockfile_path)
- _env_create("test", test_lockfile_path, with_view=False)
+ _env_create("test", init_file=test_lockfile_path, with_view=False)
# re-read the old env as a new lockfile
e = ev.read("test")
@@ -2958,24 +2955,24 @@ def test_read_old_lock_and_write_new(config, tmpdir, lockfile):
assert old_hashes == hashes
-def test_read_v1_lock_creates_backup(config, tmpdir):
+def test_read_v1_lock_creates_backup(config, tmp_path):
"""When reading a version-1 lockfile, make sure that a backup of that file
is created.
"""
- # read in the JSON from a legacy v1 lockfile
- v1_lockfile_path = os.path.join(spack.paths.test_path, "data", "legacy_env", "v1.lock")
-
- # make an env out of the old lockfile
- test_lockfile_path = str(tmpdir.join(ev.lockfile_name))
+ v1_lockfile_path = pathlib.Path(spack.paths.test_path) / "data" / "legacy_env" / "v1.lock"
+ test_lockfile_path = tmp_path / "init" / ev.lockfile_name
+ test_lockfile_path.parent.mkdir(parents=True, exist_ok=False)
shutil.copy(v1_lockfile_path, test_lockfile_path)
- e = ev.Environment(str(tmpdir))
+ e = ev.create_in_dir(tmp_path, init_file=test_lockfile_path)
assert os.path.exists(e._lock_backup_v1_path)
assert filecmp.cmp(e._lock_backup_v1_path, v1_lockfile_path)
@pytest.mark.parametrize("lockfile", ["v1", "v2", "v3"])
-def test_read_legacy_lockfile_and_reconcretize(mock_stage, mock_fetch, install_mockery, lockfile):
+def test_read_legacy_lockfile_and_reconcretize(
+ mock_stage, mock_fetch, install_mockery, lockfile, tmp_path
+):
# In legacy lockfiles v2 and v3 (keyed by build hash), there may be multiple
# versions of the same spec with different build dependencies, which means
# they will have different build hashes but the same DAG hash.
@@ -2985,9 +2982,10 @@ def test_read_legacy_lockfile_and_reconcretize(mock_stage, mock_fetch, install_m
# After reconcretization with the *new*, finer-grained DAG hash, there should no
# longer be conflicts, and the previously conflicting specs can coexist in the
# same environment.
- legacy_lockfile_path = os.path.join(
- spack.paths.test_path, "data", "legacy_env", "%s.lock" % lockfile
- )
+ test_path = pathlib.Path(spack.paths.test_path)
+ lockfile_content = test_path / "data" / "legacy_env" / f"{lockfile}.lock"
+ legacy_lockfile_path = tmp_path / ev.lockfile_name
+ shutil.copy(lockfile_content, legacy_lockfile_path)
# The order of the root specs in this environment is:
# [
@@ -2997,7 +2995,7 @@ def test_read_legacy_lockfile_and_reconcretize(mock_stage, mock_fetch, install_m
# So in v2 and v3 lockfiles we have two versions of dttop with the same DAG
# hash but different build hashes.
- env("create", "test", legacy_lockfile_path)
+ env("create", "test", str(legacy_lockfile_path))
test = ev.read("test")
assert len(test.specs_by_hash) == 1
@@ -3154,7 +3152,7 @@ def test_depfile_phony_convenience_targets(
each package if "--make-prefix" is absent."""
make = Executable("make")
with fs.working_dir(str(tmpdir)):
- with ev.Environment("."):
+ with ev.create_in_dir("."):
add("dttop")
concretize()
@@ -3277,10 +3275,11 @@ def test_env_include_packages_url(
assert "openmpi" in cfg["all"]["providers"]["mpi"]
-def test_relative_view_path_on_command_line_is_made_absolute(tmpdir, config):
- with fs.working_dir(str(tmpdir)):
+def test_relative_view_path_on_command_line_is_made_absolute(tmp_path, config):
+ with fs.working_dir(str(tmp_path)):
env("create", "--with-view", "view", "--dir", "env")
environment = ev.Environment(os.path.join(".", "env"))
+ environment.regenerate_views()
assert os.path.samefile("view", environment.default_view.root)
diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py
index 1eae5c6c5f..05b0ff3404 100644
--- a/lib/spack/spack/test/cmd/install.py
+++ b/lib/spack/spack/test/cmd/install.py
@@ -799,6 +799,7 @@ def test_install_no_add_in_env(tmpdir, mock_fetch, install_mockery, mutable_mock
e.add("a")
e.add("a ~bvv")
e.concretize()
+ e.write()
env_specs = e.all_specs()
a_spec = None
diff --git a/lib/spack/spack/test/env.py b/lib/spack/spack/test/env.py
index 77ccd7889e..913eb7bf9e 100644
--- a/lib/spack/spack/test/env.py
+++ b/lib/spack/spack/test/env.py
@@ -3,8 +3,8 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Test environment internals without CLI"""
-import io
import os
+import pickle
import sys
import pytest
@@ -13,16 +13,28 @@ import llnl.util.filesystem as fs
import spack.environment as ev
import spack.spec
+from spack.environment.environment import SpackEnvironmentViewError, _error_on_nonempty_view_dir
pytestmark = pytest.mark.skipif(
sys.platform == "win32", reason="Envs are not supported on windows"
)
-def test_hash_change_no_rehash_concrete(tmpdir, mock_packages, config):
+class TestDirectoryInitialization:
+ def test_environment_dir_from_name(self, mutable_mock_env_path):
+ """Test the function mapping a managed environment name to its folder."""
+ env = ev.create("test")
+ environment_dir = ev.environment_dir_from_name("test")
+ assert env.path == environment_dir
+ with pytest.raises(ev.SpackEnvironmentError, match="environment already exists"):
+ ev.environment_dir_from_name("test", exists_ok=False)
+
+
+def test_hash_change_no_rehash_concrete(tmp_path, mock_packages, config):
# create an environment
- env_path = tmpdir.mkdir("env_dir").strpath
- env = ev.Environment(env_path)
+ env_path = tmp_path / "env_dir"
+ env_path.mkdir(exist_ok=False)
+ env = ev.create_in_dir(env_path)
env.write()
# add a spec with a rewritten build hash
@@ -48,9 +60,10 @@ def test_hash_change_no_rehash_concrete(tmpdir, mock_packages, config):
assert read_in.specs_by_hash[read_in.concretized_order[0]]._hash == new_hash
-def test_env_change_spec(tmpdir, mock_packages, config):
- env_path = tmpdir.mkdir("env_dir").strpath
- env = ev.Environment(env_path)
+def test_env_change_spec(tmp_path, mock_packages, config):
+ env_path = tmp_path / "env_dir"
+ env_path.mkdir(exist_ok=False)
+ env = ev.create_in_dir(env_path)
env.write()
spec = spack.spec.Spec("mpileaks@2.1~shared+debug")
@@ -80,9 +93,10 @@ env:
"""
-def test_env_change_spec_in_definition(tmpdir, mock_packages, config, mutable_mock_env_path):
- initial_yaml = io.StringIO(_test_matrix_yaml)
- e = ev.create("test", initial_yaml)
+def test_env_change_spec_in_definition(tmp_path, mock_packages, config, mutable_mock_env_path):
+ manifest_file = tmp_path / ev.manifest_name
+ manifest_file.write_text(_test_matrix_yaml)
+ e = ev.create("test", manifest_file)
e.concretize()
e.write()
@@ -96,10 +110,11 @@ def test_env_change_spec_in_definition(tmpdir, mock_packages, config, mutable_mo
def test_env_change_spec_in_matrix_raises_error(
- tmpdir, mock_packages, config, mutable_mock_env_path
+ tmp_path, mock_packages, config, mutable_mock_env_path
):
- initial_yaml = io.StringIO(_test_matrix_yaml)
- e = ev.create("test", initial_yaml)
+ manifest_file = tmp_path / ev.manifest_name
+ manifest_file.write_text(_test_matrix_yaml)
+ e = ev.create("test", manifest_file)
e.concretize()
e.write()
@@ -131,8 +146,8 @@ def test_user_view_path_is_not_canonicalized_in_yaml(tmpdir, config):
# Serialize environment with relative view path
with fs.working_dir(str(tmpdir)):
- fst = ev.Environment(env_path, with_view=view)
- fst.write()
+ fst = ev.create_in_dir(env_path, with_view=view)
+ fst.regenerate_views()
# The view link should be created
assert os.path.isdir(absolute_view)
@@ -141,7 +156,7 @@ def test_user_view_path_is_not_canonicalized_in_yaml(tmpdir, config):
# and also check that the getter is pointing to the right dir.
with fs.working_dir(str(tmpdir)):
snd = ev.Environment(env_path)
- assert snd.yaml["spack"]["view"] == view
+ assert snd.manifest["spack"]["view"] == view
assert os.path.samefile(snd.default_view.root, absolute_view)
@@ -184,8 +199,167 @@ def test_roundtrip_spack_yaml_with_comments(original_content, mock_packages, con
spack_yaml = tmp_path / "spack.yaml"
spack_yaml.write_text(original_content)
- e = ev.Environment(str(tmp_path))
- e.update_manifest()
+ e = ev.Environment(tmp_path)
+ e.manifest.flush()
content = spack_yaml.read_text()
assert content == original_content
+
+
+def test_adding_anonymous_specs_to_env_fails(tmp_path):
+ """Tests that trying to add an anonymous spec to the 'specs' section of an environment
+ raises an exception
+ """
+ env = ev.create_in_dir(tmp_path)
+ with pytest.raises(ev.SpackEnvironmentError, match="cannot add anonymous"):
+ env.add("%gcc")
+
+
+def test_removing_from_non_existing_list_fails(tmp_path):
+ """Tests that trying to remove a spec from a non-existing definition fails."""
+ env = ev.create_in_dir(tmp_path)
+ with pytest.raises(ev.SpackEnvironmentError, match="'bar' does not exist"):
+ env.remove("%gcc", list_name="bar")
+
+
+@pytest.mark.parametrize(
+ "init_view,update_value",
+ [
+ (True, False),
+ (True, "./view"),
+ (False, True),
+ ("./view", True),
+ ("./view", False),
+ (True, True),
+ (False, False),
+ ],
+)
+def test_update_default_view(init_view, update_value, tmp_path, mock_packages, config):
+ """Tests updating the default view with different values."""
+ env = ev.create_in_dir(tmp_path, with_view=init_view)
+ env.update_default_view(update_value)
+ env.write(regenerate=True)
+ if not isinstance(update_value, bool):
+ assert env.default_view.raw_root == update_value
+
+ expected_value = update_value
+ if isinstance(init_view, str) and update_value is True:
+ expected_value = init_view
+
+ assert env.manifest.pristine_yaml_content["spack"]["view"] == expected_value
+
+
+@pytest.mark.parametrize(
+ "initial_content,update_value,expected_view",
+ [
+ (
+ """
+spack:
+ specs:
+ - mpileaks
+ view:
+ default:
+ root: ./view-gcc
+ select: ['%gcc']
+ link_type: symlink
+""",
+ "./another-view",
+ {"root": "./another-view", "select": ["%gcc"], "link_type": "symlink"},
+ ),
+ (
+ """
+spack:
+ specs:
+ - mpileaks
+ view:
+ default:
+ root: ./view-gcc
+ select: ['%gcc']
+ link_type: symlink
+""",
+ True,
+ {"root": "./view-gcc", "select": ["%gcc"], "link_type": "symlink"},
+ ),
+ ],
+)
+def test_update_default_complex_view(
+ initial_content, update_value, expected_view, tmp_path, mock_packages, config
+):
+ spack_yaml = tmp_path / "spack.yaml"
+ spack_yaml.write_text(initial_content)
+
+ env = ev.Environment(tmp_path)
+ env.update_default_view(update_value)
+ env.write(regenerate=True)
+
+ assert env.default_view.to_dict() == expected_view
+
+
+@pytest.mark.parametrize("filename", [ev.manifest_name, ev.lockfile_name])
+def test_cannot_initialize_in_dir_with_init_file(tmp_path, filename):
+ """Tests that initializing an environment in a directory with an already existing
+ spack.yaml or spack.lock raises an exception.
+ """
+ init_file = tmp_path / filename
+ init_file.touch()
+ with pytest.raises(ev.SpackEnvironmentError, match="cannot initialize"):
+ ev.create_in_dir(tmp_path)
+
+
+def test_cannot_initiliaze_if_dirname_exists_as_a_file(tmp_path):
+ """Tests that initializing an environment using as a location an existing file raises
+ an error.
+ """
+ dir_name = tmp_path / "dir"
+ dir_name.touch()
+ with pytest.raises(ev.SpackEnvironmentError, match="cannot initialize"):
+ ev.create_in_dir(dir_name)
+
+
+def test_cannot_initiliaze_if_init_file_does_not_exist(tmp_path):
+ """Tests that initializing an environment passing a non-existing init file raises an error."""
+ init_file = tmp_path / ev.manifest_name
+ with pytest.raises(ev.SpackEnvironmentError, match="cannot initialize"):
+ ev.create_in_dir(tmp_path, init_file=init_file)
+
+
+def test_cannot_initialize_from_random_file(tmp_path):
+ init_file = tmp_path / "foo.txt"
+ init_file.touch()
+ with pytest.raises(ev.SpackEnvironmentError, match="cannot initialize"):
+ ev.create_in_dir(tmp_path, init_file=init_file)
+
+
+def test_environment_pickle(tmp_path):
+ env1 = ev.create_in_dir(tmp_path)
+ obj = pickle.dumps(env1)
+ env2 = pickle.loads(obj)
+ assert isinstance(env2, ev.Environment)
+
+
+def test_error_on_nonempty_view_dir(tmpdir):
+ """Error when the target is not an empty dir"""
+ with tmpdir.as_cwd():
+ os.mkdir("empty_dir")
+ os.mkdir("nonempty_dir")
+ with open(os.path.join("nonempty_dir", "file"), "wb"):
+ pass
+ os.symlink("empty_dir", "symlinked_empty_dir")
+ os.symlink("does_not_exist", "broken_link")
+ os.symlink("broken_link", "file")
+
+ # This is OK.
+ _error_on_nonempty_view_dir("empty_dir")
+
+ # This is not OK.
+ with pytest.raises(SpackEnvironmentViewError):
+ _error_on_nonempty_view_dir("nonempty_dir")
+
+ with pytest.raises(SpackEnvironmentViewError):
+ _error_on_nonempty_view_dir("symlinked_empty_dir")
+
+ with pytest.raises(SpackEnvironmentViewError):
+ _error_on_nonempty_view_dir("broken_link")
+
+ with pytest.raises(SpackEnvironmentViewError):
+ _error_on_nonempty_view_dir("file")
diff --git a/lib/spack/spack/test/environment.py b/lib/spack/spack/test/environment.py
deleted file mode 100644
index 2f363dfdca..0000000000
--- a/lib/spack/spack/test/environment.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
-# Spack Project Developers. See the top-level COPYRIGHT file for details.
-#
-# SPDX-License-Identifier: (Apache-2.0 OR MIT)
-
-import os
-import pickle
-
-import pytest
-
-from spack.environment import Environment
-from spack.environment.environment import SpackEnvironmentViewError, _error_on_nonempty_view_dir
-
-
-def test_environment_pickle(tmpdir):
- env1 = Environment(str(tmpdir))
- obj = pickle.dumps(env1)
- env2 = pickle.loads(obj)
- assert isinstance(env2, Environment)
-
-
-def test_error_on_nonempty_view_dir(tmpdir):
- """Error when the target is not an empty dir"""
- with tmpdir.as_cwd():
- os.mkdir("empty_dir")
- os.mkdir("nonempty_dir")
- with open(os.path.join("nonempty_dir", "file"), "wb"):
- pass
- os.symlink("empty_dir", "symlinked_empty_dir")
- os.symlink("does_not_exist", "broken_link")
- os.symlink("broken_link", "file")
-
- # This is OK.
- _error_on_nonempty_view_dir("empty_dir")
-
- # This is not OK.
- with pytest.raises(SpackEnvironmentViewError):
- _error_on_nonempty_view_dir("nonempty_dir")
-
- with pytest.raises(SpackEnvironmentViewError):
- _error_on_nonempty_view_dir("symlinked_empty_dir")
-
- with pytest.raises(SpackEnvironmentViewError):
- _error_on_nonempty_view_dir("broken_link")
-
- with pytest.raises(SpackEnvironmentViewError):
- _error_on_nonempty_view_dir("file")
diff --git a/lib/spack/spack/test/modules/lmod.py b/lib/spack/spack/test/modules/lmod.py
index 30e1b58905..0b3d2bf597 100644
--- a/lib/spack/spack/test/modules/lmod.py
+++ b/lib/spack/spack/test/modules/lmod.py
@@ -335,7 +335,7 @@ class TestLmod(object):
def test_modules_relative_to_view(
self, tmpdir, modulefile_content, module_configuration, install_mockery, mock_fetch
):
- with ev.Environment(str(tmpdir), with_view=True) as e:
+ with ev.create_in_dir(str(tmpdir), with_view=True) as e:
module_configuration("with_view")
install("--add", "cmake")