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.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 # 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 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 - 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 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 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) 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 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) 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 @@ {'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_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):