Allow users to specify root env dir (#32836)
* Allow users to specify root env dir Environments managed by spack have some advantages over anonymous Environments but they are tucked away inside spack's directory tree. This PR gives users the ability to specify where the environments should live. See #32823 This is also taken as an opportunity to ensure that all references are to "managed environments", rather than "named environments". Prior to this PR some references to the latter persisted. Co-authored-by: Tom Scogland <scogland1@llnl.gov> Co-authored-by: Tamara Dahlgren <35777542+tldahlgren@users.noreply.github.com> Co-authored-by: Gregory Becker <becker33@llnl.gov>
This commit is contained in:
parent
0a233ce83a
commit
b8d15e816b
13 changed files with 97 additions and 29 deletions
|
@ -72,6 +72,7 @@ config:
|
||||||
root: $TMP_DIR/install
|
root: $TMP_DIR/install
|
||||||
misc_cache: $$user_cache_path/cache
|
misc_cache: $$user_cache_path/cache
|
||||||
source_cache: $$user_cache_path/source
|
source_cache: $$user_cache_path/source
|
||||||
|
environments_root: $TMP_DIR/envs
|
||||||
EOF
|
EOF
|
||||||
cat >"$SPACK_USER_CONFIG_PATH/bootstrap.yaml" <<EOF
|
cat >"$SPACK_USER_CONFIG_PATH/bootstrap.yaml" <<EOF
|
||||||
bootstrap:
|
bootstrap:
|
||||||
|
|
|
@ -81,6 +81,10 @@ config:
|
||||||
source_cache: $spack/var/spack/cache
|
source_cache: $spack/var/spack/cache
|
||||||
|
|
||||||
|
|
||||||
|
## Directory where spack managed environments are created and stored
|
||||||
|
# environments_root: $spack/var/spack/environments
|
||||||
|
|
||||||
|
|
||||||
# Cache directory for miscellaneous files, like the package index.
|
# Cache directory for miscellaneous files, like the package index.
|
||||||
# This can be purged with `spack clean --misc-cache`
|
# This can be purged with `spack clean --misc-cache`
|
||||||
misc_cache: $user_cache_path/cache
|
misc_cache: $user_cache_path/cache
|
||||||
|
|
|
@ -58,9 +58,9 @@ Using Environments
|
||||||
Here we follow a typical use case of creating, concretizing,
|
Here we follow a typical use case of creating, concretizing,
|
||||||
installing and loading an environment.
|
installing and loading an environment.
|
||||||
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
Creating a named Environment
|
Creating a managed Environment
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
An environment is created by:
|
An environment is created by:
|
||||||
|
|
||||||
|
@ -72,7 +72,8 @@ Spack then creates the directory ``var/spack/environments/myenv``.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
All named environments are stored in the ``var/spack/environments`` folder.
|
All managed environments by default are stored in the ``var/spack/environments`` folder.
|
||||||
|
This location can be changed by setting the ``environments_root`` variable in ``config.yaml``.
|
||||||
|
|
||||||
In the ``var/spack/environments/myenv`` directory, Spack creates the
|
In the ``var/spack/environments/myenv`` directory, Spack creates the
|
||||||
file ``spack.yaml`` and the hidden directory ``.spack-env``.
|
file ``spack.yaml`` and the hidden directory ``.spack-env``.
|
||||||
|
|
|
@ -165,7 +165,7 @@ def env_activate(args):
|
||||||
short_name = os.path.basename(env_path)
|
short_name = os.path.basename(env_path)
|
||||||
ev.Environment(env).write(regenerate=False)
|
ev.Environment(env).write(regenerate=False)
|
||||||
|
|
||||||
# Named environment
|
# Managed environment
|
||||||
elif ev.exists(env_name_or_dir) and not args.dir:
|
elif ev.exists(env_name_or_dir) and not args.dir:
|
||||||
env_path = ev.root(env_name_or_dir)
|
env_path = ev.root(env_name_or_dir)
|
||||||
short_name = env_name_or_dir
|
short_name = env_name_or_dir
|
||||||
|
|
|
@ -95,7 +95,7 @@ def location(parser, args):
|
||||||
spack.cmd.require_active_env("location -e")
|
spack.cmd.require_active_env("location -e")
|
||||||
path = ev.active_environment().path
|
path = ev.active_environment().path
|
||||||
else:
|
else:
|
||||||
# Get named environment path
|
# Get path of requested environment
|
||||||
if not ev.exists(args.location_env):
|
if not ev.exists(args.location_env):
|
||||||
tty.die("no such environment: '%s'" % args.location_env)
|
tty.die("no such environment: '%s'" % args.location_env)
|
||||||
path = ev.root(args.location_env)
|
path = ev.root(args.location_env)
|
||||||
|
|
|
@ -62,8 +62,8 @@
|
||||||
_active_environment = None
|
_active_environment = None
|
||||||
|
|
||||||
|
|
||||||
#: path where environments are stored in the spack tree
|
#: default path where environments are stored in the spack tree
|
||||||
env_path = os.path.join(spack.paths.var_path, "environments")
|
default_env_path = os.path.join(spack.paths.var_path, "environments")
|
||||||
|
|
||||||
|
|
||||||
#: Name of the input yaml file for an environment
|
#: Name of the input yaml file for an environment
|
||||||
|
@ -78,6 +78,26 @@
|
||||||
env_subdir_name = ".spack-env"
|
env_subdir_name = ".spack-env"
|
||||||
|
|
||||||
|
|
||||||
|
def env_root_path():
|
||||||
|
"""Override default root path if the user specified it"""
|
||||||
|
return spack.util.path.canonicalize_path(
|
||||||
|
spack.config.get("config:environments_root", default=default_env_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_disallowed_env_config_mods(scopes):
|
||||||
|
for scope in scopes:
|
||||||
|
with spack.config.use_configuration(scope):
|
||||||
|
if spack.config.get("config:environments_root"):
|
||||||
|
raise SpackEnvironmentError(
|
||||||
|
"Spack environments are prohibited from modifying 'config:environments_root' "
|
||||||
|
"because it can make the definition of the environment ill-posed. Please "
|
||||||
|
"remove from your environment and place it in a permanent scope such as "
|
||||||
|
"defaults, system, site, etc."
|
||||||
|
)
|
||||||
|
return scopes
|
||||||
|
|
||||||
|
|
||||||
def default_manifest_yaml():
|
def default_manifest_yaml():
|
||||||
"""default spack.yaml file to put in new environments"""
|
"""default spack.yaml file to put in new environments"""
|
||||||
return """\
|
return """\
|
||||||
|
@ -214,7 +234,7 @@ def active_environment():
|
||||||
|
|
||||||
def _root(name):
|
def _root(name):
|
||||||
"""Non-validating version of root(), to be used internally."""
|
"""Non-validating version of root(), to be used internally."""
|
||||||
return os.path.join(env_path, name)
|
return os.path.join(env_root_path(), name)
|
||||||
|
|
||||||
|
|
||||||
def root(name):
|
def root(name):
|
||||||
|
@ -249,10 +269,12 @@ def read(name):
|
||||||
|
|
||||||
|
|
||||||
def create(name, init_file=None, with_view=None, keep_relative=False):
|
def create(name, init_file=None, with_view=None, keep_relative=False):
|
||||||
"""Create a named environment in Spack."""
|
"""Create a managed environment in Spack."""
|
||||||
|
if not os.path.isdir(env_root_path()):
|
||||||
|
fs.mkdirp(env_root_path())
|
||||||
validate_env_name(name)
|
validate_env_name(name)
|
||||||
if exists(name):
|
if exists(name):
|
||||||
raise SpackEnvironmentError("'%s': environment already exists" % name)
|
raise SpackEnvironmentError("'%s': environment already exists at %s" % (name, root(name)))
|
||||||
return Environment(root(name), init_file, with_view, keep_relative)
|
return Environment(root(name), init_file, with_view, keep_relative)
|
||||||
|
|
||||||
|
|
||||||
|
@ -266,10 +288,10 @@ def all_environment_names():
|
||||||
"""List the names of environments that currently exist."""
|
"""List the names of environments that currently exist."""
|
||||||
# just return empty if the env path does not exist. A read-only
|
# just return empty if the env path does not exist. A read-only
|
||||||
# operation like list should not try to create a directory.
|
# operation like list should not try to create a directory.
|
||||||
if not os.path.exists(env_path):
|
if not os.path.exists(env_root_path()):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
candidates = sorted(os.listdir(env_path))
|
candidates = sorted(os.listdir(env_root_path()))
|
||||||
names = []
|
names = []
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
yaml_path = os.path.join(_root(candidate), manifest_name)
|
yaml_path = os.path.join(_root(candidate), manifest_name)
|
||||||
|
@ -279,7 +301,7 @@ def all_environment_names():
|
||||||
|
|
||||||
|
|
||||||
def all_environments():
|
def all_environments():
|
||||||
"""Generator for all named Environments."""
|
"""Generator for all managed Environments."""
|
||||||
for name in all_environment_names():
|
for name in all_environment_names():
|
||||||
yield read(name)
|
yield read(name)
|
||||||
|
|
||||||
|
@ -859,14 +881,14 @@ def clear(self, re_read=False):
|
||||||
@property
|
@property
|
||||||
def internal(self):
|
def internal(self):
|
||||||
"""Whether this environment is managed by Spack."""
|
"""Whether this environment is managed by Spack."""
|
||||||
return self.path.startswith(env_path)
|
return self.path.startswith(env_root_path())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Human-readable representation of the environment.
|
"""Human-readable representation of the environment.
|
||||||
|
|
||||||
This is the path for directory environments, and just the name
|
This is the path for directory environments, and just the name
|
||||||
for named environments.
|
for managed environments.
|
||||||
"""
|
"""
|
||||||
if self.internal:
|
if self.internal:
|
||||||
return os.path.basename(self.path)
|
return os.path.basename(self.path)
|
||||||
|
@ -1044,7 +1066,9 @@ def env_file_config_scope(self):
|
||||||
|
|
||||||
def config_scopes(self):
|
def config_scopes(self):
|
||||||
"""A list of all configuration scopes for this environment."""
|
"""A list of all configuration scopes for this environment."""
|
||||||
return self.included_config_scopes() + [self.env_file_config_scope()]
|
return check_disallowed_env_config_mods(
|
||||||
|
self.included_config_scopes() + [self.env_file_config_scope()]
|
||||||
|
)
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
"""Remove this environment from Spack entirely."""
|
"""Remove this environment from Spack entirely."""
|
||||||
|
|
|
@ -459,7 +459,7 @@ def make_argument_parser(**kwargs):
|
||||||
dest="env_dir",
|
dest="env_dir",
|
||||||
metavar="DIR",
|
metavar="DIR",
|
||||||
action="store",
|
action="store",
|
||||||
help="run with an environment directory (ignore named environments)",
|
help="run with an environment directory (ignore managed environments)",
|
||||||
)
|
)
|
||||||
env_group.add_argument(
|
env_group.add_argument(
|
||||||
"-E",
|
"-E",
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
"license_dir": {"type": "string"},
|
"license_dir": {"type": "string"},
|
||||||
"source_cache": {"type": "string"},
|
"source_cache": {"type": "string"},
|
||||||
"misc_cache": {"type": "string"},
|
"misc_cache": {"type": "string"},
|
||||||
|
"environments_root": {"type": "string"},
|
||||||
"connect_timeout": {"type": "integer", "minimum": 0},
|
"connect_timeout": {"type": "integer", "minimum": 0},
|
||||||
"verify_ssl": {"type": "boolean"},
|
"verify_ssl": {"type": "boolean"},
|
||||||
"suppress_gpg_warnings": {"type": "boolean"},
|
"suppress_gpg_warnings": {"type": "boolean"},
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
import spack.environment as ev
|
import spack.environment as ev
|
||||||
from spack.main import SpackCommand
|
from spack.main import SpackCommand
|
||||||
|
|
||||||
# everything here uses the mock_env_path
|
pytestmark = pytest.mark.usefixtures("config", "mutable_mock_repo")
|
||||||
pytestmark = pytest.mark.usefixtures("mutable_mock_env_path", "config", "mutable_mock_repo")
|
|
||||||
|
|
||||||
env = SpackCommand("env")
|
env = SpackCommand("env")
|
||||||
add = SpackCommand("add")
|
add = SpackCommand("add")
|
||||||
|
@ -21,7 +20,7 @@
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("unify", unification_strategies)
|
@pytest.mark.parametrize("unify", unification_strategies)
|
||||||
def test_concretize_all_test_dependencies(unify):
|
def test_concretize_all_test_dependencies(unify, mutable_mock_env_path):
|
||||||
"""Check all test dependencies are concretized."""
|
"""Check all test dependencies are concretized."""
|
||||||
env("create", "test")
|
env("create", "test")
|
||||||
|
|
||||||
|
@ -33,7 +32,7 @@ def test_concretize_all_test_dependencies(unify):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("unify", unification_strategies)
|
@pytest.mark.parametrize("unify", unification_strategies)
|
||||||
def test_concretize_root_test_dependencies_not_recursive(unify):
|
def test_concretize_root_test_dependencies_not_recursive(unify, mutable_mock_env_path):
|
||||||
"""Check that test dependencies are not concretized recursively."""
|
"""Check that test dependencies are not concretized recursively."""
|
||||||
env("create", "test")
|
env("create", "test")
|
||||||
|
|
||||||
|
@ -45,7 +44,7 @@ def test_concretize_root_test_dependencies_not_recursive(unify):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("unify", unification_strategies)
|
@pytest.mark.parametrize("unify", unification_strategies)
|
||||||
def test_concretize_root_test_dependencies_are_concretized(unify):
|
def test_concretize_root_test_dependencies_are_concretized(unify, mutable_mock_env_path):
|
||||||
"""Check that root test dependencies are concretized."""
|
"""Check that root test dependencies are concretized."""
|
||||||
env("create", "test")
|
env("create", "test")
|
||||||
|
|
||||||
|
|
|
@ -3222,3 +3222,20 @@ def test_relative_view_path_on_command_line_is_made_absolute(tmpdir, config):
|
||||||
env("create", "--with-view", "view", "--dir", "env")
|
env("create", "--with-view", "view", "--dir", "env")
|
||||||
environment = ev.Environment(os.path.join(".", "env"))
|
environment = ev.Environment(os.path.join(".", "env"))
|
||||||
assert os.path.samefile("view", environment.default_view.root)
|
assert os.path.samefile("view", environment.default_view.root)
|
||||||
|
|
||||||
|
|
||||||
|
def test_environment_created_in_users_location(mutable_config, tmpdir):
|
||||||
|
"""Test that an environment is created in a location based on the config"""
|
||||||
|
spack.config.set("config:environments_root", str(tmpdir.join("envs")))
|
||||||
|
env_dir = spack.config.get("config:environments_root")
|
||||||
|
|
||||||
|
assert tmpdir.strpath in env_dir
|
||||||
|
assert not os.path.isdir(env_dir)
|
||||||
|
|
||||||
|
dir_name = "user_env"
|
||||||
|
env("create", dir_name)
|
||||||
|
out = env("list")
|
||||||
|
|
||||||
|
assert dir_name in out
|
||||||
|
assert env_dir in ev.root(dir_name)
|
||||||
|
assert os.path.isdir(os.path.join(env_dir, dir_name))
|
||||||
|
|
|
@ -225,7 +225,7 @@ class TestUninstallFromEnv(object):
|
||||||
concretize = SpackCommand("concretize")
|
concretize = SpackCommand("concretize")
|
||||||
find = SpackCommand("find")
|
find = SpackCommand("find")
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="function")
|
||||||
def environment_setup(
|
def environment_setup(
|
||||||
self, mutable_mock_env_path, config, mock_packages, mutable_database, install_mockery
|
self, mutable_mock_env_path, config, mock_packages, mutable_database, install_mockery
|
||||||
):
|
):
|
||||||
|
@ -244,6 +244,9 @@ def environment_setup(
|
||||||
TestUninstallFromEnv.add("diamond-link-bottom")
|
TestUninstallFromEnv.add("diamond-link-bottom")
|
||||||
TestUninstallFromEnv.concretize()
|
TestUninstallFromEnv.concretize()
|
||||||
install("--fake")
|
install("--fake")
|
||||||
|
yield "environment_setup"
|
||||||
|
TestUninstallFromEnv.env("rm", "e1", "-y")
|
||||||
|
TestUninstallFromEnv.env("rm", "e2", "-y")
|
||||||
|
|
||||||
def test_basic_env_sanity(self, environment_setup):
|
def test_basic_env_sanity(self, environment_setup):
|
||||||
for env_name in ["e1", "e2"]:
|
for env_name in ["e1", "e2"]:
|
||||||
|
|
|
@ -1535,14 +1535,14 @@ def get_rev():
|
||||||
yield t
|
yield t
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture(scope="function")
|
||||||
def mutable_mock_env_path(tmpdir_factory):
|
def mutable_mock_env_path(tmpdir_factory, mutable_config):
|
||||||
"""Fixture for mocking the internal spack environments directory."""
|
"""Fixture for mocking the internal spack environments directory."""
|
||||||
saved_path = ev.environment.env_path
|
saved_path = ev.environment.default_env_path
|
||||||
mock_path = tmpdir_factory.mktemp("mock-env-path")
|
mock_path = tmpdir_factory.mktemp("mock-env-path")
|
||||||
ev.environment.env_path = str(mock_path)
|
ev.environment.default_env_path = str(mock_path)
|
||||||
yield mock_path
|
yield mock_path
|
||||||
ev.environment.env_path = saved_path
|
ev.environment.default_env_path = saved_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|
|
@ -143,3 +143,21 @@ def test_user_view_path_is_not_canonicalized_in_yaml(tmpdir, config):
|
||||||
snd = ev.Environment(env_path)
|
snd = ev.Environment(env_path)
|
||||||
assert snd.yaml["spack"]["view"] == view
|
assert snd.yaml["spack"]["view"] == view
|
||||||
assert os.path.samefile(snd.default_view.root, absolute_view)
|
assert os.path.samefile(snd.default_view.root, absolute_view)
|
||||||
|
|
||||||
|
|
||||||
|
def test_environment_cant_modify_environments_root(tmpdir):
|
||||||
|
filename = str(tmpdir.join("spack.yaml"))
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(
|
||||||
|
"""\
|
||||||
|
spack:
|
||||||
|
config:
|
||||||
|
environments_root: /a/black/hole
|
||||||
|
view: false
|
||||||
|
specs: []
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
with pytest.raises(ev.SpackEnvironmentError):
|
||||||
|
e = ev.Environment(tmpdir.strpath)
|
||||||
|
ev.activate(e)
|
||||||
|
|
Loading…
Reference in a new issue