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 commit is contained in:
psakiev 2022-09-26 22:31:29 -06:00
parent c25b7ea898
commit 3f2e77e5fa
6 changed files with 51 additions and 21 deletions

View File

@ -76,6 +76,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

View File

@ -63,7 +63,10 @@
#: path where environments are stored in the spack tree #: path where environments are stored in the spack tree
env_path = os.path.join(spack.paths.var_path, "environments") # circular dependency to use canonicalize_path but we want to ignore an active env anyways
# env_path = spack.config.get(os.path.expandvars(os.path.expanduser("config:environments_root")))
def env_path():
return spack.util.path.canonicalize_path(spack.config.get("config:environments_root"), False)
#: Name of the input yaml file for an environment #: Name of the input yaml file for an environment
@ -205,7 +208,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_path(), name)
def root(name): def root(name):
@ -257,10 +260,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_path()):
return [] return []
candidates = sorted(os.listdir(env_path)) candidates = sorted(os.listdir(env_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)
@ -842,7 +845,7 @@ 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_path())
@property @property
def name(self): def name(self):

View File

@ -52,6 +52,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"},

View File

@ -28,6 +28,9 @@
import spack.schema.repos import spack.schema.repos
import spack.util.path as spack_path import spack.util.path as spack_path
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
from spack.main import SpackCommand
env = SpackCommand("env")
# sample config data # sample config data
config_low = { config_low = {
@ -1005,6 +1008,7 @@ def test_good_env_yaml(tmpdir):
config: config:
verify_ssl: False verify_ssl: False
dirty: False dirty: False
environtments_dir: ~/my/env/location
repos: repos:
- ~/my/repo/location - ~/my/repo/location
mirrors: mirrors:
@ -1431,3 +1435,18 @@ def test_config_file_read_invalid_yaml(tmpdir, mutable_empty_config):
with pytest.raises(spack.config.ConfigFileError, match="parsing yaml"): with pytest.raises(spack.config.ConfigFileError, match="parsing yaml"):
spack.config.read_config_file(filename) spack.config.read_config_file(filename)
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)
os.makedirs(env_dir)
env('create', 'test')
out = env('list')
assert 'test' in out
assert env_dir in ev.root('test')
assert os.path.isdir(os.path.join(env_dir, 'test'))

View File

@ -1493,11 +1493,13 @@ def get_rev():
@pytest.fixture() @pytest.fixture()
def mutable_mock_env_path(tmpdir_factory): def mutable_mock_env_path(tmpdir_factory):
"""Fixture for mocking the internal spack environments directory.""" """Fixture for mocking the internal spack environments directory."""
saved_path = ev.environment.env_path saved_path_fun = ev.environment.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) def mock_fun():
return str(mock_path)
ev.environment.env_path = mock_fun
yield mock_path yield mock_path
ev.environment.env_path = saved_path ev.environment.env_path = saved_path_fun
@pytest.fixture() @pytest.fixture()

View File

@ -226,7 +226,7 @@ def convert_to_platform_path(path):
return format_os_path(path, mode=Path.platform_path) return format_os_path(path, mode=Path.platform_path)
def substitute_config_variables(path): def substitute_config_variables(path, allow_env=True):
"""Substitute placeholders into paths. """Substitute placeholders into paths.
Spack allows paths in configs to have some placeholders, as follows: Spack allows paths in configs to have some placeholders, as follows:
@ -242,9 +242,10 @@ def substitute_config_variables(path):
replaced if there is an active environment, and should only be used in replaced if there is an active environment, and should only be used in
environment yaml files. environment yaml files.
""" """
_replacements = replacements()
if allow_env:
import spack.environment as ev # break circular import spack.environment as ev # break circular
_replacements = replacements()
env = ev.active_environment() env = ev.active_environment()
if env: if env:
_replacements.update({"env": env.path}) _replacements.update({"env": env.path})
@ -261,9 +262,9 @@ def repl(match):
return re.sub(r"(\$\w+\b|\$\{\w+\})", repl, path) return re.sub(r"(\$\w+\b|\$\{\w+\})", repl, path)
def substitute_path_variables(path): def substitute_path_variables(path, allow_env = True):
"""Substitute config vars, expand environment vars, expand user home.""" """Substitute config vars, expand environment vars, expand user home."""
path = substitute_config_variables(path) path = substitute_config_variables(path, allow_env)
path = os.path.expandvars(path) path = os.path.expandvars(path)
path = os.path.expanduser(path) path = os.path.expanduser(path)
return path return path
@ -305,7 +306,7 @@ def add_padding(path, length):
return os.path.join(path, padding) return os.path.join(path, padding)
def canonicalize_path(path): def canonicalize_path(path, allow_env = True):
"""Same as substitute_path_variables, but also take absolute path. """Same as substitute_path_variables, but also take absolute path.
Arguments: Arguments:
@ -321,7 +322,7 @@ def canonicalize_path(path):
filename = os.path.dirname(path._start_mark.name) filename = os.path.dirname(path._start_mark.name)
assert path._start_mark.name == path._end_mark.name assert path._start_mark.name == path._end_mark.name
path = substitute_path_variables(path) path = substitute_path_variables(path, allow_env)
if not os.path.isabs(path): if not os.path.isabs(path):
if filename: if filename:
path = os.path.join(filename, path) path = os.path.join(filename, path)