summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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):