summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPeter Scheibel <scheibel1@llnl.gov>2019-04-10 16:00:12 -0700
committerTodd Gamblin <tgamblin@llnl.gov>2019-04-10 16:00:12 -0700
commitea1de6b941c1eb9eec70f970cc363c6e35d08edf (patch)
treea3033d81ec9c99a816a0c4f450da44782efa24b7 /lib
parent8f1ebfc73cb4926c71f87df6f10cf82814c95821 (diff)
downloadspack-ea1de6b941c1eb9eec70f970cc363c6e35d08edf.tar.gz
spack-ea1de6b941c1eb9eec70f970cc363c6e35d08edf.tar.bz2
spack-ea1de6b941c1eb9eec70f970cc363c6e35d08edf.tar.xz
spack-ea1de6b941c1eb9eec70f970cc363c6e35d08edf.zip
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.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/llnl/util/filesystem.py25
-rw-r--r--lib/spack/spack/cmd/env.py133
-rw-r--r--lib/spack/spack/environment.py247
-rw-r--r--lib/spack/spack/filesystem_view.py26
-rw-r--r--lib/spack/spack/schema/env.py3
-rw-r--r--lib/spack/spack/test/cmd/env.py176
-rw-r--r--lib/spack/spack/util/environment.py73
7 files changed, 569 insertions, 114 deletions
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index 00e4dc5f37..f5017f5236 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -703,15 +703,32 @@ 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)
- remove_if_dead_link(path)
+ for dirpath, subdirs, files in os.walk(root, topdown=False):
+ for f in files:
+ path = join_path(dirpath, f)
+ remove_if_dead_link(path)
def remove_if_dead_link(path):
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
index e8ac8a5c86..1b85849e8f 100644
--- a/lib/spack/spack/cmd/env.py
+++ b/lib/spack/spack/cmd/env.py
@@ -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.cmd.common.arguments as arguments
import spack.environment as ev
import spack.util.string as string
+
description = "manage virtual environments"
section = "environments"
level = "short"
@@ -34,6 +36,7 @@ subcommands = [
['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
#
diff --git a/lib/spack/spack/environment.py b/lib/spack/spack/environment.py
index e9e3328bf6..68639a9deb 100644
--- a/lib/spack/spack/environment.py
+++ b/lib/spack/spack/environment.py
@@ -9,9 +9,11 @@ import sys
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.spec
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 @@ spack:
# 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)
+
+ 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():
+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 @@ class Environment(object):
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
+ 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)
+
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()
- if os.path.exists(self.manifest_path):
- # read the spack.yaml file, if exists
- with open(self.manifest_path) as f:
- self._read_manifest(f)
-
- elif self.concretized_user_specs:
- # if not, take user specs from the lockfile
- 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 @@ class Environment(object):
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]
@@ -437,6 +509,10 @@ class Environment(object):
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:
self._repo = make_repo_path(self.repos_path)
@@ -619,7 +695,97 @@ class Environment(object):
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 @@ class Environment(object):
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 @@ class Environment(object):
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 @@ class Environment(object):
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)
diff --git a/lib/spack/spack/filesystem_view.py b/lib/spack/spack/filesystem_view.py
index ed69f53df6..abb6eb4c24 100644
--- a/lib/spack/spack/filesystem_view.py
+++ b/lib/spack/spack/filesystem_view.py
@@ -14,7 +14,8 @@ from llnl.util.link_tree import LinkTree, MergeConflictError
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 @@ class YamlFilesystemView(FilesystemView):
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 @@ class YamlFilesystemView(FilesystemView):
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)
diff --git a/lib/spack/spack/schema/env.py b/lib/spack/spack/schema/env.py
index f65ffc987a..9fbc59219c 100644
--- a/lib/spack/spack/schema/env.py
+++ b/lib/spack/spack/schema/env.py
@@ -47,6 +47,9 @@ schema = {
{'type': 'object'},
]
}
+ },
+ 'view': {
+ 'type': ['boolean', 'string']
}
}
)
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
index 2a0e8a85bc..0da9377196 100644
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -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
diff --git a/lib/spack/spack/util/environment.py b/lib/spack/spack/util/environment.py
index 1666f4711e..4296e2cbee 100644
--- a/lib/spack/spack/util/environment.py
+++ b/lib/spack/spack/util/environment.py
@@ -25,6 +25,18 @@ system_dirs = [os.path.join(p, s) for s in suffixes for p in system_paths] + \
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 @@ class NameValueModifier(object):
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 @@ class EnvironmentModifications(object):
# 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):