Maintain a view for an environment (#10017)

Environments are nowm by default, created with views.  When activated, if an environment includes a view, this view will be added to `PATH`, `CPATH`, and other shell variables to expose the Spack environment in the user's shell.

Example:

```
spack env create e1 #by default this will maintain a view in the directory Spack maintains for the env
spack env create e1 --with-view=/abs/path/to/anywhere
spack env create e1 --without-view
```

The `spack.yaml` manifest file now looks like this:

```
spack:
  specs:
  - python
  view: true #or false, or a string
```

These commands can be used to control the view configuration for the active environment, without hand-editing the `spack.yaml` file:

```
spack env view enable
spack env view envable /abs/path/to/anywhere
spack env view disable
```

Views are automatically updated when specs are installed to an environment. A view only maintains one copy of any package. An environment may refer to a package multiple times, in particular if it appears as a dependency. This PR establishes a prioritization for which environment specs are added to views: a spec has higher priority if it was concretized first. This does not necessarily exactly match the order in which specs were added, for example, given `X->Z` and `Y->Z'`:

```
spack env activate e1
spack add X
spack install Y # immediately concretizes and installs Y and Z'
spack install # concretizes X and Z
```

In this case `Z'` will be favored over `Z`. 

Specs in the environment must be concrete and installed to be added to the view, so there is another minor ordering effect: by default the view maintained for the environment ignores file conflicts between packages. If packages are not installed in order, and there are file conflicts, then the version chosen depends on the order.

Both ordering issues are avoided if `spack install`/`spack add` and `spack install <spec>` are not mixed.
This commit is contained in:
Peter Scheibel 2019-04-10 16:00:12 -07:00 committed by Todd Gamblin
parent 8f1ebfc73c
commit ea1de6b941
7 changed files with 570 additions and 115 deletions

View file

@ -703,14 +703,31 @@ def set_executable(path):
os.chmod(path, mode)
def remove_empty_directories(root):
"""Ascend up from the leaves accessible from `root` and remove empty
directories.
Parameters:
root (str): path where to search for empty directories
"""
for dirpath, subdirs, files in os.walk(root, topdown=False):
for sd in subdirs:
sdp = os.path.join(dirpath, sd)
try:
os.rmdir(sdp)
except OSError:
pass
def remove_dead_links(root):
"""Removes any dead link that is present in root.
"""Recursively removes any dead link that is present in root.
Parameters:
root (str): path where to search for dead links
"""
for file in os.listdir(root):
path = join_path(root, file)
for dirpath, subdirs, files in os.walk(root, topdown=False):
for f in files:
path = join_path(dirpath, f)
remove_if_dead_link(path)

View file

@ -5,6 +5,7 @@
import os
import sys
from collections import namedtuple
import llnl.util.tty as tty
import llnl.util.filesystem as fs
@ -20,6 +21,7 @@
import spack.environment as ev
import spack.util.string as string
description = "manage virtual environments"
section = "environments"
level = "short"
@ -34,6 +36,7 @@
['list', 'ls'],
['status', 'st'],
'loads',
'view',
]
@ -49,10 +52,20 @@ def env_activate_setup_parser(subparser):
shells.add_argument(
'--csh', action='store_const', dest='shell', const='csh',
help="print csh commands to activate the environment")
shells.add_argument(
view_options = subparser.add_mutually_exclusive_group()
view_options.add_argument(
'-v', '--with-view', action='store_const', dest='with_view',
const=True, default=True,
help="update PATH etc. with associated view")
view_options.add_argument(
'-V', '--without-view', action='store_const', dest='with_view',
const=False, default=True,
help="do not update PATH etc. with associated view")
subparser.add_argument(
'-d', '--dir', action='store_true', default=False,
help="force spack to treat env as a directory, not a name")
subparser.add_argument(
'-p', '--prompt', action='store_true', default=False,
help="decorate the command line prompt when activating")
@ -93,25 +106,13 @@ def env_activate(args):
if spack_env == os.environ.get('SPACK_ENV'):
tty.die("Environment %s is already active" % args.activate_env)
if args.shell == 'csh':
# TODO: figure out how to make color work for csh
sys.stdout.write('setenv SPACK_ENV %s;\n' % spack_env)
sys.stdout.write('alias despacktivate "spack env deactivate";\n')
if args.prompt:
sys.stdout.write('if (! $?SPACK_OLD_PROMPT ) '
'setenv SPACK_OLD_PROMPT "${prompt}";\n')
sys.stdout.write('set prompt="%s ${prompt}";\n' % env_prompt)
else:
if 'color' in os.environ['TERM']:
env_prompt = colorize('@G{%s} ' % env_prompt, color=True)
sys.stdout.write('export SPACK_ENV=%s;\n' % spack_env)
sys.stdout.write("alias despacktivate='spack env deactivate';\n")
if args.prompt:
sys.stdout.write('if [ -z "${SPACK_OLD_PS1}" ]; then\n')
sys.stdout.write('export SPACK_OLD_PS1="${PS1}"; fi;\n')
sys.stdout.write('export PS1="%s ${PS1}";\n' % env_prompt)
active_env = ev.get_env(namedtuple('args', ['env'])(env),
'activate')
cmds = ev.activate(
active_env, add_view=args.with_view, shell=args.shell,
prompt=env_prompt if args.prompt else None
)
sys.stdout.write(cmds)
#
@ -146,20 +147,8 @@ def env_deactivate(args):
if 'SPACK_ENV' not in os.environ:
tty.die('No environment is currently active.')
if args.shell == 'csh':
sys.stdout.write('unsetenv SPACK_ENV;\n')
sys.stdout.write('if ( $?SPACK_OLD_PROMPT ) '
'set prompt="$SPACK_OLD_PROMPT" && '
'unsetenv SPACK_OLD_PROMPT;\n')
sys.stdout.write('unalias despacktivate;\n')
else:
sys.stdout.write('unset SPACK_ENV; export SPACK_ENV;\n')
sys.stdout.write('unalias despacktivate;\n')
sys.stdout.write('if [ -n "$SPACK_OLD_PS1" ]; then\n')
sys.stdout.write('export PS1="$SPACK_OLD_PS1";\n')
sys.stdout.write('unset SPACK_OLD_PS1; export SPACK_OLD_PS1;\n')
sys.stdout.write('fi;\n')
cmds = ev.deactivate(shell=args.shell)
sys.stdout.write(cmds)
#
@ -172,20 +161,40 @@ def env_create_setup_parser(subparser):
subparser.add_argument(
'-d', '--dir', action='store_true',
help='create an environment in a specific directory')
view_opts = subparser.add_mutually_exclusive_group()
view_opts.add_argument(
'--without-view', action='store_true',
help='do not maintain a view for this environment')
view_opts.add_argument(
'--with-view',
help='specify that this environment should maintain a view at the'
' specified path (by default the view is maintained in the'
' environment directory)')
subparser.add_argument(
'envfile', nargs='?', default=None,
help='optional init file; can be spack.yaml or spack.lock')
def env_create(args):
if args.with_view:
with_view = args.with_view
elif args.without_view:
with_view = False
else:
# Note that 'None' means unspecified, in which case the Environment
# 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)
_env_create(args.create_env, f, args.dir,
with_view=with_view)
else:
_env_create(args.create_env, None, args.dir)
_env_create(args.create_env, None, args.dir,
with_view=with_view)
def _env_create(name_or_path, init_file=None, dir=False):
def _env_create(name_or_path, init_file=None, dir=False, with_view=None):
"""Create a new environment, with an optional yaml description.
Arguments:
@ -196,11 +205,11 @@ def _env_create(name_or_path, init_file=None, dir=False):
of a named environment
"""
if dir:
env = ev.Environment(name_or_path, init_file)
env = ev.Environment(name_or_path, init_file, with_view)
env.write()
tty.msg("Created environment in %s" % env.path)
else:
env = ev.create(name_or_path, init_file)
env = ev.create(name_or_path, init_file, with_view)
env.write()
tty.msg("Created environment '%s' in %s" % (name_or_path, env.path))
return env
@ -272,6 +281,50 @@ def env_list(args):
colify(color_names, indent=4)
class ViewAction(object):
regenerate = 'regenerate'
enable = 'enable'
disable = 'disable'
@staticmethod
def actions():
return [ViewAction.regenerate, ViewAction.enable, ViewAction.disable]
#
# env view
#
def env_view_setup_parser(subparser):
"""manage a view associated with the environment"""
subparser.add_argument(
'action', choices=ViewAction.actions(),
help="action to take for the environment's view")
subparser.add_argument(
'view_path', nargs='?',
help="when enabling a view, optionally set the path manually"
)
def env_view(args):
env = ev.get_env(args, 'env view')
if env:
if args.action == ViewAction.regenerate:
env.regenerate_view()
elif args.action == ViewAction.enable:
if args.view_path:
view_path = args.view_path
else:
view_path = env.default_view_path
env.update_view(view_path)
env.write()
elif args.action == ViewAction.disable:
env.update_view(None)
env.write()
else:
tty.msg("No active environment")
#
# env status
#

View file

@ -9,9 +9,11 @@
import shutil
import ruamel.yaml
import six
import llnl.util.filesystem as fs
import llnl.util.tty as tty
from llnl.util.tty.color import colorize
import spack.error
import spack.repo
@ -20,7 +22,9 @@
import spack.util.spack_json as sjson
import spack.config
from spack.spec import Spec
from spack.filesystem_view import YamlFilesystemView
from spack.util.environment import EnvironmentModifications
#: environment variable used to indicate the active environment
spack_env_var = 'SPACK_ENV'
@ -56,6 +60,7 @@
# add package specs to the `specs` list
specs:
-
view: true
"""
#: regex for validating enviroment names
valid_environment_name_re = r'^\w[\w-]*$'
@ -79,7 +84,9 @@ def validate_env_name(name):
return name
def activate(env, use_env_repo=False):
def activate(
env, use_env_repo=False, add_view=True, shell='sh', prompt=None
):
"""Activate an environment.
To activate an environment, we add its configuration scope to the
@ -90,8 +97,12 @@ def activate(env, use_env_repo=False):
env (Environment): the environment to activate
use_env_repo (bool): use the packages exactly as they appear in the
environment's repository
add_view (bool): generate commands to add view to path variables
shell (string): One of `sh`, `csh`.
prompt (string): string to add to the users prompt, or None
TODO: Add support for views here. Activation should set up the shell
Returns:
cmds: Shell commands to activate environment.
TODO: environment to use the activated spack environment.
"""
global _active_environment
@ -103,13 +114,41 @@ def activate(env, use_env_repo=False):
tty.debug("Using environmennt '%s'" % _active_environment.name)
# Construct the commands to run
cmds = ''
if shell == 'csh':
# TODO: figure out how to make color work for csh
cmds += 'setenv SPACK_ENV %s;\n' % env.path
cmds += 'alias despacktivate "spack env deactivate";\n'
if prompt:
cmds += 'if (! $?SPACK_OLD_PROMPT ) '
cmds += 'setenv SPACK_OLD_PROMPT "${prompt}";\n'
cmds += 'set prompt="%s ${prompt}";\n' % prompt
else:
if 'color' in os.environ['TERM'] and prompt:
prompt = colorize('@G{%s} ' % prompt, color=True)
def deactivate():
cmds += 'export SPACK_ENV=%s;\n' % env.path
cmds += "alias despacktivate='spack env deactivate';\n"
if prompt:
cmds += 'if [ -z "${SPACK_OLD_PS1}" ]; then\n'
cmds += 'export SPACK_OLD_PS1="${PS1}"; fi;\n'
cmds += 'export PS1="%s ${PS1}";\n' % prompt
if add_view and env._view_path:
cmds += env.add_view_to_shell(shell)
return cmds
def deactivate(shell='sh'):
"""Undo any configuration or repo settings modified by ``activate()``.
Arguments:
shell (string): One of `sh`, `csh`. Shell style to use.
Returns:
(bool): True if an environment was deactivated, False if no
environment was active.
(string): shell commands for `shell` to undo environment variables
"""
global _active_environment
@ -123,9 +162,29 @@ def deactivate():
if _active_environment._repo:
spack.repo.path.remove(_active_environment._repo)
cmds = ''
if shell == 'csh':
cmds += 'unsetenv SPACK_ENV;\n'
cmds += 'if ( $?SPACK_OLD_PROMPT ) '
cmds += 'set prompt="$SPACK_OLD_PROMPT" && '
cmds += 'unsetenv SPACK_OLD_PROMPT;\n'
cmds += 'unalias despacktivate;\n'
else:
cmds += 'unset SPACK_ENV; export SPACK_ENV;\n'
cmds += 'unalias despacktivate;\n'
cmds += 'if [ -n "$SPACK_OLD_PS1" ]; then\n'
cmds += 'export PS1="$SPACK_OLD_PS1";\n'
cmds += 'unset SPACK_OLD_PS1; export SPACK_OLD_PS1;\n'
cmds += 'fi;\n'
if _active_environment._view_path:
cmds += _active_environment.rm_view_from_shell(shell)
tty.debug("Deactivated environmennt '%s'" % _active_environment.name)
_active_environment = None
return cmds
def find_environment(args):
"""Find active environment from args, spack.yaml, or environment variable.
@ -265,12 +324,12 @@ def read(name):
return Environment(root(name))
def create(name, init_file=None):
def create(name, init_file=None, with_view=None):
"""Create a named environment in Spack."""
validate_env_name(name)
if exists(name):
raise SpackEnvironmentError("'%s': environment already exists" % name)
return Environment(root(name), init_file)
return Environment(root(name), init_file, with_view)
def config_dict(yaml_data):
@ -327,7 +386,7 @@ def _write_yaml(data, str_or_file):
class Environment(object):
def __init__(self, path, init_file=None):
def __init__(self, path, init_file=None, with_view=None):
"""Create a new environment.
The environment can be optionally initialized with either a
@ -337,39 +396,41 @@ def __init__(self, path, init_file=None):
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.
"""
self.path = os.path.abspath(path)
self.clear()
if init_file:
# initialize the environment from a file if provided
with fs.open_if_filename(init_file) as f:
if hasattr(f, 'name') and f.name.endswith('.lock'):
# Initialize the environment from a lockfile
self._read_manifest(default_manifest_yaml)
self._read_lockfile(f)
self._set_user_specs_from_lockfile()
self.yaml = _read_yaml(default_manifest_yaml)
else:
# Initialize the environment from a spack.yaml file
self._read_manifest(f)
else:
# read lockfile, if it exists
if os.path.exists(self.lock_path):
with open(self.lock_path) as f:
self._read_lockfile(f)
if os.path.exists(self.manifest_path):
# read the spack.yaml file, if exists
default_manifest = not os.path.exists(self.manifest_path)
if default_manifest:
self._read_manifest(default_manifest_yaml)
else:
with open(self.manifest_path) as f:
self._read_manifest(f)
elif self.concretized_user_specs:
# if not, take user specs from the lockfile
if os.path.exists(self.lock_path):
with open(self.lock_path) as f:
self._read_lockfile(f)
if default_manifest:
self._set_user_specs_from_lockfile()
self.yaml = _read_yaml(default_manifest_yaml)
else:
# if there's no manifest or lockfile, use the default
self._read_manifest(default_manifest_yaml)
if with_view is False:
self._view_path = None
elif isinstance(with_view, six.string_types):
self._view_path = with_view
# If with_view is None, then defer to the view settings determined by
# the manifest file
def _read_manifest(self, f):
"""Read manifest file and set up user specs."""
@ -378,6 +439,17 @@ def _read_manifest(self, f):
if spec_list:
self.user_specs = [Spec(s) for s in spec_list if s]
enable_view = config_dict(self.yaml).get('view')
# enable_view can be true/false, a string, or None (if the manifest did
# not specify it)
if enable_view is True or enable_view is None:
self._view_path = self.default_view_path
elif isinstance(enable_view, six.string_types):
self._view_path = enable_view
else:
# enable_view is False
self._view_path = None
def _set_user_specs_from_lockfile(self):
"""Copy user_specs from a read-in lockfile."""
self.user_specs = [Spec(s) for s in self.concretized_user_specs]
@ -436,6 +508,10 @@ def repos_path(self):
def log_path(self):
return os.path.join(self.path, env_subdir_name, 'logs')
@property
def default_view_path(self):
return os.path.join(self.env_subdir_path, 'view')
@property
def repo(self):
if self._repo is None:
@ -619,7 +695,97 @@ def install(self, user_spec, concrete_spec=None, **install_args):
concrete = spec.concretized()
self._add_concrete_spec(spec, concrete)
concrete.package.do_install(**install_args)
self._install(concrete, **install_args)
def _install(self, spec, **install_args):
spec.package.do_install(**install_args)
# Make sure log directory exists
log_path = self.log_path
fs.mkdirp(log_path)
with fs.working_dir(self.path):
# Link the resulting log file into logs dir
build_log_link = os.path.join(
log_path, '%s-%s.log' % (spec.name, spec.dag_hash(7)))
if os.path.lexists(build_log_link):
os.remove(build_log_link)
os.symlink(spec.package.build_log_path, build_log_link)
def view(self):
if not self._view_path:
raise SpackEnvironmentError(
"{0} does not have a view enabled".format(self.name))
return YamlFilesystemView(
self._view_path, spack.store.layout, ignore_conflicts=True)
def update_view(self, view_path):
if self._view_path and self._view_path != view_path:
shutil.rmtree(self._view_path)
self._view_path = view_path
def regenerate_view(self):
if not self._view_path:
tty.debug("Skip view update, this environment does not"
" maintain a view")
return
specs_for_view = []
for spec in self._get_environment_specs():
# The view does not store build deps, so if we want it to
# recognize environment specs (which do store build deps), then
# they need to be stripped
specs_for_view.append(spack.spec.Spec.from_dict(
spec.to_dict(all_deps=False)
))
installed_specs_for_view = set(s for s in specs_for_view
if s.package.installed)
view = self.view()
view.clean()
specs_in_view = set(view.get_all_specs())
tty.msg("Updating view at {0}".format(self._view_path))
rm_specs = specs_in_view - installed_specs_for_view
view.remove_specs(*rm_specs, with_dependents=False)
add_specs = installed_specs_for_view - specs_in_view
view.add_specs(*add_specs, with_dependencies=False)
def _shell_vars(self):
updates = [
('PATH', ['bin']),
('MANPATH', ['man', 'share/man']),
('ACLOCAL_PATH', ['share/aclocal']),
('LD_LIBRARY_PATH', ['lib', 'lib64']),
('LIBRARY_PATH', ['lib', 'lib64']),
('CPATH', ['include']),
('PKG_CONFIG_PATH', ['lib/pkgconfig', 'lib64/pkgconfig']),
('CMAKE_PREFIX_PATH', ['']),
]
path_updates = list()
for var, subdirs in updates:
paths = filter(lambda x: os.path.exists(x),
list(os.path.join(self._view_path, x)
for x in subdirs))
path_updates.append((var, paths))
return path_updates
def add_view_to_shell(self, shell):
env_mod = EnvironmentModifications()
for var, paths in self._shell_vars():
for path in paths:
env_mod.prepend_path(var, path)
return env_mod.shell_modifications(shell)
def rm_view_from_shell(self, shell):
env_mod = EnvironmentModifications()
for var, paths in self._shell_vars():
for path in paths:
env_mod.remove_path(var, path)
return env_mod.shell_modifications(shell)
def _add_concrete_spec(self, spec, concrete, new=True):
"""Called when a new concretized spec is added to the environment.
@ -648,11 +814,6 @@ def _add_concrete_spec(self, spec, concrete, new=True):
def install_all(self, args=None):
"""Install all concretized specs in an environment."""
# Make sure log directory exists
log_path = self.log_path
fs.mkdirp(log_path)
for concretized_hash in self.concretized_order:
spec = self.specs_by_hash[concretized_hash]
@ -662,17 +823,18 @@ def install_all(self, args=None):
if args:
spack.cmd.install.update_kwargs_from_args(args, kwargs)
with fs.working_dir(self.path):
spec.package.do_install(**kwargs)
self._install(spec, **kwargs)
if not spec.external:
# Link the resulting log file into logs dir
build_log_link = os.path.join(
log_path, '%s-%s.log' % (spec.name, spec.dag_hash(7)))
if os.path.exists(build_log_link):
self.log_path, '%s-%s.log' % (spec.name, spec.dag_hash(7)))
if os.path.lexists(build_log_link):
os.remove(build_log_link)
os.symlink(spec.package.build_log_path, build_log_link)
self.regenerate_view()
def all_specs_by_hash(self):
"""Map of hashes to spec for all specs in this environment."""
hashes = {}
@ -857,13 +1019,26 @@ def write(self):
self._repo = None
# put the new user specs in the YAML
yaml_spec_list = config_dict(self.yaml).setdefault('specs', [])
yaml_dict = config_dict(self.yaml)
yaml_spec_list = yaml_dict.setdefault('specs', [])
yaml_spec_list[:] = [str(s) for s in self.user_specs]
if self._view_path == self.default_view_path:
view = True
elif self._view_path:
view = self._view_path
else:
view = False
config_dict(self.yaml)['view'] = view
# if all that worked, write out the manifest file at the top level
with fs.write_tmp_and_move(self.manifest_path) as f:
_write_yaml(self.yaml, f)
# TODO: for operations that just add to the env (install etc.) this
# could just call update_view
self.regenerate_view()
def __enter__(self):
self._previous_active = _active_environment
activate(self)

View file

@ -14,7 +14,8 @@
from llnl.util import tty
from llnl.util.lang import match_predicate, index_by
from llnl.util.tty.color import colorize
from llnl.util.filesystem import mkdirp
from llnl.util.filesystem import (
mkdirp, remove_dead_links, remove_empty_directories)
import spack.util.spack_yaml as s_yaml
@ -407,7 +408,7 @@ def remove_specs(self, *specs, **kwargs):
set(map(remove_extension, extensions))
set(map(self.remove_standalone, standalones))
self.purge_empty_directories()
self._purge_empty_directories()
def remove_extension(self, spec, with_dependents=True):
"""
@ -575,18 +576,15 @@ def print_status(self, *specs, **kwargs):
else:
tty.warn(self._croot + "No packages found.")
def purge_empty_directories(self):
"""
Ascend up from the leaves accessible from `path`
and remove empty directories.
"""
for dirpath, subdirs, files in os.walk(self._root, topdown=False):
for sd in subdirs:
sdp = os.path.join(dirpath, sd)
try:
os.rmdir(sdp)
except OSError:
pass
def _purge_empty_directories(self):
remove_empty_directories(self._root)
def _purge_broken_links(self):
remove_dead_links(self._root)
def clean(self):
self._purge_broken_links()
self._purge_empty_directories()
def unlink_meta_folder(self, spec):
path = self.get_path_meta_folder(spec)

View file

@ -47,6 +47,9 @@
{'type': 'object'},
]
}
},
'view': {
'type': ['boolean', 'string']
}
}
)

View file

@ -603,3 +603,179 @@ def test_uninstall_removes_from_env(mock_stage, mock_fetch, install_mockery):
assert not test.specs_by_hash
assert not test.concretized_order
assert not test.user_specs
def test_env_updates_view_install(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
add('mpileaks')
install('--fake')
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
def test_env_without_view_install(
tmpdir, mock_stage, mock_fetch, install_mockery
):
# Test enabling a view after installing specs
env('create', '--without-view', 'test')
test_env = ev.read('test')
with pytest.raises(spack.environment.SpackEnvironmentError):
test_env.view()
view_dir = tmpdir.mkdir('view')
with ev.read('test'):
add('mpileaks')
install('--fake')
env('view', 'enable', str(view_dir))
# After enabling the view, the specs should be linked into the environment
# view dir
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
def test_env_config_view_default(
tmpdir, mock_stage, mock_fetch, install_mockery
):
# This config doesn't mention whether a view is enabled
test_config = """\
env:
specs:
- mpileaks
"""
_env_create('test', StringIO(test_config))
with ev.read('test'):
install('--fake')
e = ev.read('test')
# Try retrieving the view object
view = e.view()
assert view.get_spec('mpileaks')
def test_env_updates_view_install_package(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
def test_env_updates_view_add_concretize(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
add('mpileaks')
concretize()
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
def test_env_updates_view_uninstall(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
with ev.read('test'):
uninstall('-ay')
assert (not os.path.exists(str(view_dir.join('.spack'))) or
os.listdir(str(view_dir.join('.spack'))) == ['projections.yaml'])
def test_env_updates_view_uninstall_referenced_elsewhere(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
add('mpileaks')
concretize()
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
with ev.read('test'):
uninstall('-ay')
assert (not os.path.exists(str(view_dir.join('.spack'))) or
os.listdir(str(view_dir.join('.spack'))) == ['projections.yaml'])
def test_env_updates_view_remove_concretize(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
add('mpileaks')
concretize()
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
with ev.read('test'):
remove('mpileaks')
concretize()
assert (not os.path.exists(str(view_dir.join('.spack'))) or
os.listdir(str(view_dir.join('.spack'))) == ['projections.yaml'])
def test_env_updates_view_force_remove(
tmpdir, mock_stage, mock_fetch, install_mockery
):
view_dir = tmpdir.mkdir('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
assert os.path.exists(str(view_dir.join('.spack/mpileaks')))
# Check that dependencies got in too
assert os.path.exists(str(view_dir.join('.spack/libdwarf')))
with ev.read('test'):
remove('-f', 'mpileaks')
assert (not os.path.exists(str(view_dir.join('.spack'))) or
os.listdir(str(view_dir.join('.spack'))) == ['projections.yaml'])
def test_env_activate_view_fails(
tmpdir, mock_stage, mock_fetch, install_mockery
):
"""Sanity check on env activate to make sure it requires shell support"""
out = env('activate', 'test')
assert "To initialize spack's shell commands:" in out

View file

@ -25,6 +25,18 @@
system_paths
_shell_set_strings = {
'sh': 'export {0}={1};\n',
'csh': 'setenv {0} {1};\n',
}
_shell_unset_strings = {
'sh': 'unset {0};\n',
'csh': 'unsetenv {0};\n',
}
def is_system_path(path):
"""Predicate that given a path returns True if it is a system path,
False otherwise.
@ -138,62 +150,62 @@ def update_args(self, **kwargs):
class SetEnv(NameValueModifier):
def execute(self):
os.environ[self.name] = str(self.value)
def execute(self, env):
env[self.name] = str(self.value)
class AppendFlagsEnv(NameValueModifier):
def execute(self):
if self.name in os.environ and os.environ[self.name]:
os.environ[self.name] += self.separator + str(self.value)
def execute(self, env):
if self.name in env and env[self.name]:
env[self.name] += self.separator + str(self.value)
else:
os.environ[self.name] = str(self.value)
env[self.name] = str(self.value)
class UnsetEnv(NameModifier):
def execute(self):
def execute(self, env):
# Avoid throwing if the variable was not set
os.environ.pop(self.name, None)
env.pop(self.name, None)
class SetPath(NameValueModifier):
def execute(self):
def execute(self, env):
string_path = concatenate_paths(self.value, separator=self.separator)
os.environ[self.name] = string_path
env[self.name] = string_path
class AppendPath(NameValueModifier):
def execute(self):
environment_value = os.environ.get(self.name, '')
def execute(self, env):
environment_value = env.get(self.name, '')
directories = environment_value.split(
self.separator) if environment_value else []
directories.append(os.path.normpath(self.value))
os.environ[self.name] = self.separator.join(directories)
env[self.name] = self.separator.join(directories)
class PrependPath(NameValueModifier):
def execute(self):
environment_value = os.environ.get(self.name, '')
def execute(self, env):
environment_value = env.get(self.name, '')
directories = environment_value.split(
self.separator) if environment_value else []
directories = [os.path.normpath(self.value)] + directories
os.environ[self.name] = self.separator.join(directories)
env[self.name] = self.separator.join(directories)
class RemovePath(NameValueModifier):
def execute(self):
environment_value = os.environ.get(self.name, '')
def execute(self, env):
environment_value = env.get(self.name, '')
directories = environment_value.split(
self.separator) if environment_value else []
directories = [os.path.normpath(x) for x in directories
if x != os.path.normpath(self.value)]
os.environ[self.name] = self.separator.join(directories)
env[self.name] = self.separator.join(directories)
class EnvironmentModifications(object):
@ -361,7 +373,28 @@ def apply_modifications(self):
# Apply modifications one variable at a time
for name, actions in sorted(modifications.items()):
for x in actions:
x.execute()
x.execute(os.environ)
def shell_modifications(self, shell='sh'):
"""Return shell code to apply the modifications and clears the list."""
modifications = self.group_by_name()
new_env = os.environ.copy()
for name, actions in sorted(modifications.items()):
for x in actions:
x.execute(new_env)
cmds = ''
for name in set(new_env) & set(os.environ):
new = new_env.get(name, None)
old = os.environ.get(name, None)
if new != old:
if new is None:
cmds += _shell_unset_strings[shell].format(name)
else:
cmds += _shell_set_strings[shell].format(name,
new_env[name])
return cmds
@staticmethod
def from_sourcing_file(filename, *args, **kwargs):