diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst index 3ad83c57bda5ca8cb08c52838961b379b2668978..8342ab4489ac95468855d0de6c5b55fc9417bb38 100644 --- a/lib/spack/docs/basic_usage.rst +++ b/lib/spack/docs/basic_usage.rst @@ -1317,10 +1317,9 @@ directly when you run ``python``: Using Extensions ^^^^^^^^^^^^^^^^ -There are three ways to get ``numpy`` working in Python. The first is -to use :ref:`shell-support`. You can simply ``load`` the -module for the extension, and it will be added to the ``PYTHONPATH`` -in your current shell: +There are four ways to get ``numpy`` working in Python. The first is +to use :ref:`shell-support`. You can simply ``load`` the extension, +and it will be added to the ``PYTHONPATH`` in your current shell: .. code-block:: console @@ -1330,11 +1329,29 @@ in your current shell: Now ``import numpy`` will succeed for as long as you keep your current session open. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Loading Extensions via Modules +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Instead of using Spack's environment modification capabilities through +the ``spack load`` command, you can load numpy through your +environment modules (using ``environment-modules`` or ``lmod``). This +will also add the extension to the ``PYTHONPATH`` in your current +shell. + +.. code-block:: console + + $ module load <name of numpy module> + +If you do not know the name of the specific numpy module you wish to +load, you can use the ``spack module tcl|lmod loads`` command to get +the name of the module from the Spack spec. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Activating Extensions in a View ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The second way to use extensions is to create a view, which merges the +Another way to use extensions is to create a view, which merges the python installation along with the extensions into a single prefix. See :ref:`filesystem-views` for a more in-depth description of views and :ref:`cmd-spack-view` for usage of the ``spack view`` command. diff --git a/lib/spack/docs/module_file_support.rst b/lib/spack/docs/module_file_support.rst index ba964c2d61c455227a564fce6eebbed4f95bc6e8..aa7eb576531e5e461687292256d37187c7e21aaa 100644 --- a/lib/spack/docs/module_file_support.rst +++ b/lib/spack/docs/module_file_support.rst @@ -119,7 +119,7 @@ For example this will add the ``mpich`` package built with ``gcc`` to your path: # ... wait for install ... - $ spack load mpich %gcc@4.4.7 # modules + $ spack load mpich %gcc@4.4.7 $ which mpicc ~/spack/opt/linux-debian7-x86_64/gcc@4.4.7/mpich@3.0.4/bin/mpicc @@ -129,27 +129,29 @@ want to use a package, you can type unload or unuse similarly: .. code-block:: console - $ spack unload mpich %gcc@4.4.7 # modules + $ spack unload mpich %gcc@4.4.7 .. note:: - The ``load`` and ``unload`` subcommands are - only available if you have enabled Spack's shell support *and* you - have environment-modules installed on your machine. + The ``load`` and ``unload`` subcommands are only available if you + have enabled Spack's shell support. These command DO NOT use the + underlying Spack-generated module files. -^^^^^^^^^^^^^^^^^^^^^^ -Ambiguous module names -^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^ +Ambiguous specs +^^^^^^^^^^^^^^^ -If a spec used with load/unload or use/unuse is ambiguous (i.e. more -than one installed package matches it), then Spack will warn you: +If a spec used with load/unload or is ambiguous (i.e. more than one +installed package matches it), then Spack will warn you: .. code-block:: console $ spack load libelf - ==> Error: Multiple matches for spec libelf. Choose one: - libelf@0.8.13%gcc@4.4.7 arch=linux-debian7-x86_64 - libelf@0.8.13%intel@15.0.0 arch=linux-debian7-x86_64 + ==> Error: libelf matches multiple packages. + Matching packages: + libelf@0.8.13%gcc@4.4.7 arch=linux-debian7-x86_64 + libelf@0.8.13%intel@15.0.0 arch=linux-debian7-x86_64 + Use a more specific spec You can either type the ``spack load`` command again with a fully qualified argument, or you can add just enough extra constraints to @@ -171,8 +173,15 @@ To identify just the one built with the Intel compiler. ``spack module tcl loads`` ^^^^^^^^^^^^^^^^^^^^^^^^^^ -In some cases, it is desirable to load not just a module, but also all -the modules it depends on. This is not required for most modules +In some cases, it is desirable to use a Spack-generated module, rather +than relying on Spack's built-in user-environment modification +capabilities. To translate a spec into a module name, use ``spack +module tcl loads`` or ``spack module lmod loads`` depending on the +module system desired. + + +To load not just a module, but also all the modules it depends on, use +the ``--dependencies`` option. This is not required for most modules because Spack builds binaries with RPATH support. However, not all packages use RPATH to find their dependencies: this can be true in particular for Python extensions, which are currently *not* built with diff --git a/lib/spack/docs/workflows.rst b/lib/spack/docs/workflows.rst index 73f4655d1eca67eb9f7dc82e20f0534030bb244b..914f84041ba58f640ae8ea7d2617609b392f0103 100644 --- a/lib/spack/docs/workflows.rst +++ b/lib/spack/docs/workflows.rst @@ -253,14 +253,14 @@ However, other more powerful methods are generally preferred for user environments. -^^^^^^^^^^^^^^^^^^^^^^^ -Spack-Generated Modules -^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using ``spack load`` to Manage the User Environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Suppose that Spack has been used to install a set of command-line programs, which users now wish to use. One can in principle put a number of ``spack load`` commands into ``.bashrc``, for example, to -load a set of Spack-generated modules: +load a set of Spack packages: .. code-block:: sh @@ -273,7 +273,7 @@ load a set of Spack-generated modules: Although simple load scripts like this are useful in many cases, they have some drawbacks: -1. The set of modules loaded by them will in general not be +1. The set of packages loaded by them will in general not be consistent. They are a decent way to load commands to be called from command shells. See below for better ways to assemble a consistent set of packages for building application programs. @@ -285,19 +285,24 @@ have some drawbacks: other hand, are not very smart: if the user-supplied spec matches more than one installed package, then ``spack module tcl loads`` will fail. This may change in the future. For now, the workaround is to - be more specific on any ``spack module tcl loads`` lines that fail. + be more specific on any ``spack load`` commands that fail. """""""""""""""""""""" Generated Load Scripts """""""""""""""""""""" -Another problem with using `spack load` is, it is slow; a typical user -environment could take several seconds to load, and would not be -appropriate to put into ``.bashrc`` directly. It is preferable to use -a series of ``spack module tcl loads`` commands to pre-compute which -modules to load. These can be put in a script that is run whenever -installed Spack packages change. For example: +Another problem with using `spack load` is, it can be slow; a typical +user environment could take several seconds to load, and would not be +appropriate to put into ``.bashrc`` directly. This is because it +requires the full start-up overhead of python/Spack for each command. +In some circumstances it is preferable to use a series of ``spack +module tcl loads`` (or ``spack module lmod loads``) commands to +pre-compute which modules to load. This will generate the modulenames +to load the packages using environment modules, rather than Spack's +built-in support for environment modifications. These can be put in a +script that is run whenever installed Spack packages change. For +example: .. code-block:: sh @@ -634,7 +639,7 @@ Global Activations Python (and similar systems) packages directly or creating a view. If extensions are globally activated, then ``spack load python`` will also load all the extensions activated for the given ``python``. -This reduces the need for users to load a large number of modules. +This reduces the need for users to load a large number of packages. However, Spack global activations have two potential drawbacks: @@ -1254,7 +1259,7 @@ In order to build and run the image, execute: RUN spack install tar \ && spack clean -a - # need the modules already during image build? + # need the executables from a package already during image build? #RUN /bin/bash -l -c ' \ # spack load tar \ # && which tar' diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index ef620c112b15d4da5b7b548685e4520e74dd62a9..2a75a87b5492fe552cf416f4a24adc2b3103f658 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -190,6 +190,20 @@ def disambiguate_spec(spec, env, local=False, installed=True): database query. See ``spack.database.Database._query`` for details. """ hashes = env.all_hashes() if env else None + return disambiguate_spec_from_hashes(spec, hashes, local, installed) + + +def disambiguate_spec_from_hashes(spec, hashes, local=False, installed=True): + """Given a spec and a list of hashes, get concrete spec the spec refers to. + + Arguments: + spec (spack.spec.Spec): a spec to disambiguate + hashes (iterable): a set of hashes of specs among which to disambiguate + local (boolean, default False): do not search chained spack instances + installed (boolean or any, or spack.database.InstallStatus or iterable + of spack.database.InstallStatus): install status argument passed to + database query. See ``spack.database.Database._query`` for details. + """ if local: matching_specs = spack.store.db.query_local(spec, hashes=hashes, installed=installed) diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index b337cd07386c61dc6fe707bf2cd2ca07e245fef2..fa36e1cd26708f9022c7d68dfd5a0bba08c9c26a 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -4,7 +4,9 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) from __future__ import print_function + import copy +import os import llnl.util.tty as tty import llnl.util.tty.color as color @@ -14,6 +16,7 @@ import spack.repo import spack.cmd as cmd import spack.cmd.common.arguments as arguments +import spack.user_environment as uenv from spack.util.string import plural from spack.database import InstallStatuses @@ -81,6 +84,9 @@ def setup_parser(subparser): action='store_true', dest='variants', help='show variants in output (can be long)') + subparser.add_argument( + '--loaded', action='store_true', + help='show only packages loaded in the user environment') subparser.add_argument('-M', '--only-missing', action='store_true', dest='only_missing', @@ -220,6 +226,10 @@ def find(parser, args): packages_with_tags = spack.repo.path.packages_with_tags(*args.tags) results = [x for x in results if x.name in packages_with_tags] + if args.loaded: + hashes = os.environ.get(uenv.spack_loaded_hashes_var, '').split(':') + results = [x for x in results if x.dag_hash() in hashes] + # Display the result if args.json: cmd.display_specs_as_json(results, deps=args.deps) diff --git a/lib/spack/spack/cmd/load.py b/lib/spack/spack/cmd/load.py index 9c48fe802ae2071cfbfda8cc7e670486dec8205b..80c7263a7af38ab0d15ff22c4f322a1e81050251 100644 --- a/lib/spack/spack/cmd/load.py +++ b/lib/spack/spack/cmd/load.py @@ -3,10 +3,18 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -from spack.cmd.common import print_module_placeholder_help, arguments +import sys -description = "add package to environment using `module load`" -section = "modules" +import llnl.util.tty as tty + +import spack.cmd +import spack.cmd.common.arguments as arguments +import spack.environment as ev +import spack.util.environment +import spack.user_environment as uenv + +description = "add package to the user environment" +section = "user environment" level = "short" @@ -14,8 +22,56 @@ def setup_parser(subparser): """Parser is only constructed so that this prints a nice help message with -h. """ arguments.add_common_arguments( - subparser, ['recurse_dependencies', 'installed_spec']) + subparser, ['recurse_dependencies', 'installed_specs']) + + shells = subparser.add_mutually_exclusive_group() + shells.add_argument( + '--sh', action='store_const', dest='shell', const='sh', + help="print sh commands to load the package") + shells.add_argument( + '--csh', action='store_const', dest='shell', const='csh', + help="print csh commands to load the package") + + subparser.add_argument( + '--only', + default='package,dependencies', + dest='things_to_load', + choices=['package', 'dependencies'], + help="""select whether to load the package and its dependencies +the default is to load the package and all dependencies +alternatively one can decide to load only the package or only +the dependencies""" + ) def load(parser, args): - print_module_placeholder_help() + env = ev.get_env(args, 'load') + specs = [spack.cmd.disambiguate_spec(spec, env) + for spec in spack.cmd.parse_specs(args.specs)] + + if not args.shell: + msg = [ + "This command works best with Spack's shell support", + "" + ] + spack.cmd.common.shell_init_instructions + [ + 'Or, if you want to use `spack load` without initializing', + 'shell support, you can run one of these:', + '', + ' eval `spack load --sh %s` # for bash/sh' % args.specs, + ' eval `spack load --csh %s` # for csh/tcsh' % args.specs, + ] + tty.msg(*msg) + return 1 + + if 'dependencies' in args.things_to_load: + include_roots = 'package' in args.things_to_load + specs = [dep for spec in specs + for dep in spec.traverse(root=include_roots, order='post')] + + env_mod = spack.util.environment.EnvironmentModifications() + for spec in specs: + env_mod.extend(uenv.environment_modifications_for_spec(spec)) + env_mod.prepend_path(uenv.spack_loaded_hashes_var, spec.dag_hash()) + cmds = env_mod.shell_modifications(args.shell) + + sys.stdout.write(cmds) diff --git a/lib/spack/spack/cmd/module.py b/lib/spack/spack/cmd/module.py index 19a2eacce19804ea8242e86f920d3903fd8be399..f86f3e5f25f54bdb48a28889502c90c5a8a4e1bc 100644 --- a/lib/spack/spack/cmd/module.py +++ b/lib/spack/spack/cmd/module.py @@ -11,7 +11,7 @@ import spack.cmd.modules.tcl description = "manipulate module files" -section = "modules" +section = "user environment" level = "short" diff --git a/lib/spack/spack/cmd/unload.py b/lib/spack/spack/cmd/unload.py index 92a25478b6f6796441861554cd7ed8f8d6e936de..d19a33102f9fc138deac6df7f57b837e49428139 100644 --- a/lib/spack/spack/cmd/unload.py +++ b/lib/spack/spack/cmd/unload.py @@ -3,18 +3,71 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -from spack.cmd.common import print_module_placeholder_help, arguments +import sys +import os -description = "remove package from environment using `module unload`" -section = "modules" +import llnl.util.tty as tty + +import spack.cmd +import spack.cmd.common.arguments as arguments +import spack.util.environment +import spack.user_environment as uenv +import spack.error + +description = "remove package from the user environment" +section = "user environment" level = "short" def setup_parser(subparser): """Parser is only constructed so that this prints a nice help message with -h. """ - arguments.add_common_arguments(subparser, ['installed_spec']) + arguments.add_common_arguments(subparser, ['installed_specs']) + + shells = subparser.add_mutually_exclusive_group() + shells.add_argument( + '--sh', action='store_const', dest='shell', const='sh', + help="print sh commands to activate the environment") + shells.add_argument( + '--csh', action='store_const', dest='shell', const='csh', + help="print csh commands to activate the environment") + + subparser.add_argument('-a', '--all', action='store_true', + help='unload all loaded Spack packages.') def unload(parser, args): - print_module_placeholder_help() + """Unload spack packages from the user environment.""" + if args.specs and args.all: + raise spack.error.SpackError("Cannot specify specs on command line" + " when unloading all specs with '--all'") + + hashes = os.environ.get(uenv.spack_loaded_hashes_var, '').split(':') + if args.specs: + specs = [spack.cmd.disambiguate_spec_from_hashes(spec, hashes) + for spec in spack.cmd.parse_specs(args.specs)] + else: + specs = spack.store.db.query(hashes=hashes) + + if not args.shell: + msg = [ + "This command works best with Spack's shell support", + "" + ] + spack.cmd.common.shell_init_instructions + [ + 'Or, if you want to use `spack unload` without initializing', + 'shell support, you can run one of these:', + '', + ' eval `spack unload --sh %s` # for bash/sh' % args.specs, + ' eval `spack unload --csh %s` # for csh/tcsh' % args.specs, + ] + tty.msg(*msg) + return 1 + + env_mod = spack.util.environment.EnvironmentModifications() + for spec in specs: + env_mod.extend( + uenv.environment_modifications_for_spec(spec).reversed()) + env_mod.remove_path(uenv.spack_loaded_hashes_var, spec.dag_hash()) + cmds = env_mod.shell_modifications(args.shell) + + sys.stdout.write(cmds) diff --git a/lib/spack/spack/environment.py b/lib/spack/spack/environment.py index 341d62fecbc4068148201381febc834c64a4a78d..351120b127a6632695fd3a4edf3c19884ccd6e6c 100644 --- a/lib/spack/spack/environment.py +++ b/lib/spack/spack/environment.py @@ -29,9 +29,7 @@ import spack.util.spack_json as sjson import spack.util.spack_yaml as syaml import spack.config -import spack.build_environment as build_env - -from spack.util.prefix import Prefix +import spack.user_environment as uenv from spack.filesystem_view import YamlFilesystemView import spack.util.environment import spack.architecture as architecture @@ -1070,62 +1068,6 @@ def regenerate_views(self): for view in self.views.values(): view.regenerate(specs, self.roots()) - prefix_inspections = { - 'bin': ['PATH'], - 'lib': ['LD_LIBRARY_PATH', 'LIBRARY_PATH', 'DYLD_LIBRARY_PATH'], - 'lib64': ['LD_LIBRARY_PATH', 'LIBRARY_PATH', 'DYLD_LIBRARY_PATH'], - 'man': ['MANPATH'], - 'share/man': ['MANPATH'], - 'share/aclocal': ['ACLOCAL_PATH'], - 'include': ['CPATH'], - 'lib/pkgconfig': ['PKG_CONFIG_PATH'], - 'lib64/pkgconfig': ['PKG_CONFIG_PATH'], - '': ['CMAKE_PREFIX_PATH'] - } - - def unconditional_environment_modifications(self, view): - """List of environment (shell) modifications to be processed for view. - - This list does not depend on the specs in this environment""" - env = spack.util.environment.EnvironmentModifications() - - for subdir, vars in self.prefix_inspections.items(): - full_subdir = os.path.join(view.root, subdir) - for var in vars: - env.prepend_path(var, full_subdir) - - return env - - def environment_modifications_for_spec(self, spec, view=None): - """List of environment (shell) modifications to be processed for spec. - - This list is specific to the location of the spec or its projection in - the view.""" - spec = spec.copy() - if view: - spec.prefix = Prefix(view.view().get_projection_for_spec(spec)) - - # generic environment modifications determined by inspecting the spec - # prefix - env = spack.util.environment.inspect_path( - spec.prefix, - self.prefix_inspections, - exclude=spack.util.environment.is_system_path - ) - - # Let the extendee/dependency modify their extensions/dependents - # before asking for package-specific modifications - env.extend( - build_env.modifications_from_dependencies( - spec, context='run' - ) - ) - # Package specific modifications - build_env.set_module_variables_for_package(spec.package) - spec.package.setup_run_environment(env) - - return env - def add_default_view_to_shell(self, shell): env_mod = spack.util.environment.EnvironmentModifications() @@ -1133,12 +1075,12 @@ def add_default_view_to_shell(self, shell): # No default view to add to shell return env_mod.shell_modifications(shell) - env_mod.extend(self.unconditional_environment_modifications( + env_mod.extend(uenv.unconditional_environment_modifications( self.default_view)) for _, spec in self.concretized_specs(): if spec in self.default_view and spec.package.installed: - env_mod.extend(self.environment_modifications_for_spec( + env_mod.extend(uenv.environment_modifications_for_spec( spec, self.default_view)) # deduplicate paths from specs mapped to the same location @@ -1154,13 +1096,13 @@ def rm_default_view_from_shell(self, shell): # No default view to add to shell return env_mod.shell_modifications(shell) - env_mod.extend(self.unconditional_environment_modifications( + env_mod.extend(uenv.unconditional_environment_modifications( self.default_view).reversed()) for _, spec in self.concretized_specs(): if spec in self.default_view and spec.package.installed: env_mod.extend( - self.environment_modifications_for_spec( + uenv.environment_modifications_for_spec( spec, self.default_view).reversed()) return env_mod.shell_modifications(shell) diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py index 2a6b774536e5ef3a35aa411670863ee24b932021..4ce4ae331e0350dc5d72c65fb9c577e984c727ee 100644 --- a/lib/spack/spack/main.py +++ b/lib/spack/spack/main.py @@ -645,6 +645,17 @@ def main(argv=None): parser.add_argument('command', nargs=argparse.REMAINDER) args, unknown = parser.parse_known_args(argv) + # Recover stored LD_LIBRARY_PATH variables from spack shell function + # This is necessary because MacOS System Integrity Protection clears + # (DY?)LD_LIBRARY_PATH variables on process start. + # Spack clears these variables before building and installing packages, + # but needs to know the prior state for commands like `spack load` and + # `spack env activate that modify the user environment. + for var in ('LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH'): + stored_var_name = 'SPACK_%s' % var + if stored_var_name in os.environ: + os.environ[var] = os.environ[stored_var_name] + # activate an environment if one was specified on the command line if not args.no_env: env = ev.find_environment(args) diff --git a/lib/spack/spack/test/cmd/find.py b/lib/spack/spack/test/cmd/find.py index 560d8169f5d33496e33f7ec9ac6f0d0b2162a59f..8516569592dea4f93b4a027a4536f30be352cb1c 100644 --- a/lib/spack/spack/test/cmd/find.py +++ b/lib/spack/spack/test/cmd/find.py @@ -5,10 +5,12 @@ import argparse import json +import os import pytest import spack.cmd as cmd import spack.cmd.find +import spack.user_environment as uenv from spack.main import SpackCommand from spack.spec import Spec from spack.util.pattern import Bunch @@ -318,3 +320,14 @@ def test_find_prefix_in_env(mutable_mock_env_path, install_mockery, mock_fetch, find('-l') find('-L') # Would throw error on regression + + +def test_find_loaded(database, working_env): + output = find('--loaded', '--group') + assert output == '' # 0 packages installed printed separately + + os.environ[uenv.spack_loaded_hashes_var] = ':'.join( + [x.dag_hash() for x in spack.store.db.query()]) + output = find('--loaded') + expected = find() + assert output == expected diff --git a/lib/spack/spack/test/cmd/load.py b/lib/spack/spack/test/cmd/load.py new file mode 100644 index 0000000000000000000000000000000000000000..a10b99d45b416dca8913197cff475dcaa8b8b098 --- /dev/null +++ b/lib/spack/spack/test/cmd/load.py @@ -0,0 +1,125 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) +import os +from spack.main import SpackCommand +import spack.spec +import spack.user_environment as uenv + +load = SpackCommand('load') +unload = SpackCommand('unload') +install = SpackCommand('install') +location = SpackCommand('location') + + +def test_load(install_mockery, mock_fetch, mock_archive, mock_packages): + """Test that the commands generated by load add the specified prefix + inspections. Also test that Spack records loaded specs by hash in the + user environment. + + CMAKE_PREFIX_PATH is the only prefix inspection guaranteed for fake + packages, since it keys on the prefix instead of a subdir.""" + install('mpileaks') + mpileaks_spec = spack.spec.Spec('mpileaks').concretized() + + sh_out = load('--sh', '--only', 'package', 'mpileaks') + csh_out = load('--csh', '--only', 'package', 'mpileaks') + + # Test prefix inspections + sh_out_test = 'export CMAKE_PREFIX_PATH=%s' % mpileaks_spec.prefix + csh_out_test = 'setenv CMAKE_PREFIX_PATH %s' % mpileaks_spec.prefix + assert sh_out_test in sh_out + assert csh_out_test in csh_out + + # Test hashes recorded properly + hash_test_replacements = (uenv.spack_loaded_hashes_var, + mpileaks_spec.dag_hash()) + sh_hash_test = 'export %s=%s' % hash_test_replacements + csh_hash_test = 'setenv %s %s' % hash_test_replacements + assert sh_hash_test in sh_out + assert csh_hash_test in csh_out + + +def test_load_recursive(install_mockery, mock_fetch, mock_archive, + mock_packages): + """Test that the '-r' option to the load command prepends dependency prefix + inspections in post-order""" + install('mpileaks') + mpileaks_spec = spack.spec.Spec('mpileaks').concretized() + + sh_out = load('--sh', 'mpileaks') + csh_out = load('--csh', 'mpileaks') + + # Test prefix inspections + prefix_test_replacement = ':'.join(reversed( + [s.prefix for s in mpileaks_spec.traverse(order='post')])) + + sh_prefix_test = 'export CMAKE_PREFIX_PATH=%s' % prefix_test_replacement + csh_prefix_test = 'setenv CMAKE_PREFIX_PATH %s' % prefix_test_replacement + assert sh_prefix_test in sh_out + assert csh_prefix_test in csh_out + + # Test spack records loaded hashes properly + hash_test_replacement = (uenv.spack_loaded_hashes_var, ':'.join(reversed( + [s.dag_hash() for s in mpileaks_spec.traverse(order='post')]))) + sh_hash_test = 'export %s=%s' % hash_test_replacement + csh_hash_test = 'setenv %s %s' % hash_test_replacement + assert sh_hash_test in sh_out + assert csh_hash_test in csh_out + + +def test_load_includes_run_env(install_mockery, mock_fetch, mock_archive, + mock_packages): + """Tests that environment changes from the package's + `setup_run_environment` method are added to the user environment in + addition to the prefix inspections""" + install('mpileaks') + + sh_out = load('--sh', 'mpileaks') + csh_out = load('--csh', 'mpileaks') + + assert 'export FOOBAR=mpileaks' in sh_out + assert 'setenv FOOBAR mpileaks' in csh_out + + +def test_load_fails_no_shell(install_mockery, mock_fetch, mock_archive, + mock_packages): + """Test that spack load prints an error message without a shell.""" + install('mpileaks') + + out = load('mpileaks', fail_on_error=False) + assert "To initialize spack's shell commands" in out + + +def test_unload(install_mockery, mock_fetch, mock_archive, mock_packages, + working_env): + """Tests that any variables set in the user environment are undone by the + unload command""" + install('mpileaks') + mpileaks_spec = spack.spec.Spec('mpileaks').concretized() + + # Set so unload has something to do + os.environ['FOOBAR'] = 'mpileaks' + os.environ[uenv.spack_loaded_hashes_var] = '%s:%s' % ( + mpileaks_spec.dag_hash(), 'garbage') + + sh_out = unload('--sh', 'mpileaks') + csh_out = unload('--csh', 'mpileaks') + + assert 'unset FOOBAR' in sh_out + assert 'unsetenv FOOBAR' in csh_out + + assert 'export %s=garbage' % uenv.spack_loaded_hashes_var in sh_out + assert 'setenv %s garbage' % uenv.spack_loaded_hashes_var in csh_out + + +def test_unload_fails_no_shell(install_mockery, mock_fetch, mock_archive, + mock_packages, working_env): + """Test that spack unload prints an error message without a shell.""" + install('mpileaks') + mpileaks_spec = spack.spec.Spec('mpileaks').concretized() + os.environ[uenv.spack_loaded_hashes_var] = mpileaks_spec.dag_hash() + + out = unload('mpileaks', fail_on_error=False) + assert "To initialize spack's shell commands" in out diff --git a/lib/spack/spack/user_environment.py b/lib/spack/spack/user_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..4c9fdc3d67fda37d8e7f8c35df33d6174b3093f9 --- /dev/null +++ b/lib/spack/spack/user_environment.py @@ -0,0 +1,91 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) +import sys +import os + +import spack.util.prefix as prefix +import spack.util.environment as environment +import spack.build_environment as build_env + +#: Environment variable name Spack uses to track individually loaded packages +spack_loaded_hashes_var = 'SPACK_LOADED_HASHES' + + +def prefix_inspections(platform): + """Get list of prefix inspections for platform + + Arguments: + platform (string): the name of the platform to consider. The platform + determines what environment variables Spack will use for some + inspections. + + Returns: + A dictionary mapping subdirectory names to lists of environment + variables to modify with that directory if it exists. + """ + inspections = { + 'bin': ['PATH'], + 'lib': ['LD_LIBRARY_PATH', 'LIBRARY_PATH'], + 'lib64': ['LD_LIBRARY_PATH', 'LIBRARY_PATH'], + 'man': ['MANPATH'], + 'share/man': ['MANPATH'], + 'share/aclocal': ['ACLOCAL_PATH'], + 'include': ['CPATH'], + 'lib/pkgconfig': ['PKG_CONFIG_PATH'], + 'lib64/pkgconfig': ['PKG_CONFIG_PATH'], + '': ['CMAKE_PREFIX_PATH'] + } + + if platform == 'darwin': + for subdir in ('lib', 'lib64'): + inspections[subdir].append('DYLD_LIBRARY_PATH') + + return inspections + + +def unconditional_environment_modifications(view): + """List of environment (shell) modifications to be processed for view. + + This list does not depend on the specs in this environment""" + env = environment.EnvironmentModifications() + + for subdir, vars in prefix_inspections(sys.platform).items(): + full_subdir = os.path.join(view.root, subdir) + for var in vars: + env.prepend_path(var, full_subdir) + + return env + + +def environment_modifications_for_spec(spec, view=None): + """List of environment (shell) modifications to be processed for spec. + + This list is specific to the location of the spec or its projection in + the view.""" + spec = spec.copy() + if view: + spec.prefix = prefix.Prefix(view.view().get_projection_for_spec(spec)) + + # generic environment modifications determined by inspecting the spec + # prefix + env = environment.inspect_path( + spec.prefix, + prefix_inspections(spec.platform), + exclude=environment.is_system_path + ) + + # Let the extendee/dependency modify their extensions/dependents + # before asking for package-specific modifications + env.extend( + build_env.modifications_from_dependencies( + spec, context='run' + ) + ) + + # Package specific modifications + build_env.set_module_variables_for_package(spec.package) + spec.package.setup_run_environment(env) + + return env diff --git a/share/spack/csh/spack.csh b/share/spack/csh/spack.csh index fe2c5a1f08cfb8385dd96680ea406544d71476dc..b4d963ae101c3b4dd426184474528fc289e63aed 100644 --- a/share/spack/csh/spack.csh +++ b/share/spack/csh/spack.csh @@ -27,6 +27,16 @@ # avoids the need to come up with a user-friendly naming scheme for # spack module files. ######################################################################## +# Store LD_LIBRARY_PATH variables from spack shell function +# This is necessary because MacOS System Integrity Protection clears +# (DY?)LD_LIBRARY_PATH variables on process start. +if ( ${?LD_LIBRARY_PATH} ) then + setenv SPACK_LD_LIBRARY_PATH $LD_LIBRARY_PATH +endif +if ( ${?DYLD_LIBRARY_PATH} ) then + setenv SPACK_DYLD_LIBRARY_PATH $DYLD_LIBRARY_PATH +endif + # accumulate initial flags for main spack command set _sp_flags = "" while ( $#_sp_args > 0 ) @@ -47,8 +57,7 @@ set _sp_spec="" [ $#_sp_args -gt 0 ] && set _sp_subcommand = ($_sp_args[1]) [ $#_sp_args -gt 1 ] && set _sp_spec = ($_sp_args[2-]) -# Figure out what type of module we're running here. -set _sp_modtype = "" +# Run subcommand switch ($_sp_subcommand) case cd: shift _sp_args # get rid of 'cd' @@ -106,35 +115,17 @@ case env: endif case load: case unload: - set _sp_module_args="""" - if ( "$_sp_spec" =~ "-*" ) then - set _sp_module_args = $_sp_spec[1] - shift _sp_spec - set _sp_spec = ($_sp_spec) + # Space in `-h` portion is important for differentiating -h option + # from variants that begin with "h" or packages with "-h" in name + if ( "$_sp_spec" =~ "*--sh*" || "$_sp_spec" =~ "*--csh*" || \ + " $_sp_spec" =~ "* -h*" || "$_sp_spec" =~ "*--help*") then + # IF a shell is given, print shell output + \spack $_sp_flags $_sp_subcommand $_sp_spec + else + # otherwise eval with csh + eval `\spack $_sp_flags $_sp_subcommand --csh $_sp_spec || \ + echo "exit 1"` endif - - # Here the user has run load or unload with a spec. Find a matching - # spec using 'spack module find', then use the appropriate module - # tool's commands to add/remove the result from the environment. - switch ($_sp_subcommand) - case "load": - # _sp_module_args may be "-r" for recursive spec retrieval - set _sp_full_spec = ( "`\spack $_sp_flags module tcl find $_sp_module_args $_sp_spec`" ) - if ( "$_sp_module_args" == "-r" ) then - # module load can handle the list of modules to load and "-r" is not a valid option - set _sp_module_args = "" - endif - if ( $? == 0 ) then - module load $_sp_module_args $_sp_full_spec - endif - breaksw - case "unload": - set _sp_full_spec = ( "`\spack $_sp_flags module tcl find $_sp_spec`" ) - if ( $? == 0 ) then - module unload $_sp_module_args $_sp_full_spec - endif - breaksw - endsw breaksw default: @@ -143,6 +134,5 @@ default: endsw _sp_end: -unset _sp_args _sp_full_spec _sp_modtype _sp_module_args -unset _sp_sh_cmd _sp_spec _sp_subcommand _sp_flags +unset _sp_args _sp_full_spec _sp_sh_cmd _sp_spec _sp_subcommand _sp_flags unset _sp_arg _sp_env_arg diff --git a/share/spack/qa/setup-env-test.sh b/share/spack/qa/setup-env-test.sh index 66284d1a96c749e8cac197b749477251dd825ea5..da4fb9657d141648a0f9a482bc93213319b805a4 100755 --- a/share/spack/qa/setup-env-test.sh +++ b/share/spack/qa/setup-env-test.sh @@ -104,20 +104,25 @@ contains "usage: spack module " spack -m module --help contains "usage: spack module " spack -m module title 'Testing `spack load`' -contains "module load $b_module" spack -m load b +contains "export LD_LIBRARY_PATH=$(spack -m location -i b)/lib" spack -m load --only package --sh b +succeeds spack -m load b fails spack -m load -l -contains "module load -l --arg $b_module" spack -m load -l --arg b -contains "module load $b_module $a_module" spack -m load -r a -contains "module load $b_module $a_module" spack -m load --dependencies a +# test a variable MacOS clears and one it doesn't for recursive loads +contains "export LD_LIBRARY_PATH=$(spack -m location -i a)/lib:$(spack -m location -i b)/lib" spack -m load --sh a +contains "export LIBRARY_PATH=$(spack -m location -i a)/lib:$(spack -m location -i b)/lib" spack -m load --sh a +succeeds spack -m load --only dependencies a +succeeds spack -m load --only package a fails spack -m load d contains "usage: spack load " spack -m load -h contains "usage: spack load " spack -m load -h d contains "usage: spack load " spack -m load --help title 'Testing `spack unload`' -contains "module unload $b_module" spack -m unload b +spack -m load b a # setup +succeeds spack -m unload b +succeeds spack -m unload --all +spack -m unload --all # cleanup fails spack -m unload -l -contains "module unload -l --arg $b_module" spack -m unload -l --arg b fails spack -m unload d contains "usage: spack unload " spack -m unload -h contains "usage: spack unload " spack -m unload -h d diff --git a/share/spack/setup-env.sh b/share/spack/setup-env.sh index 23a29ce6a26c81176b0156d872e402e421a7df7a..207e9c4a801a2b2ce3562b6934cc9d5fee827a53 100755 --- a/share/spack/setup-env.sh +++ b/share/spack/setup-env.sh @@ -40,6 +40,16 @@ ######################################################################## spack() { + # Store LD_LIBRARY_PATH variables from spack shell function + # This is necessary because MacOS System Integrity Protection clears + # (DY?)LD_LIBRARY_PATH variables on process start. + if [ -n "${LD_LIBRARY_PATH-}" ]; then + export SPACK_LD_LIBRARY_PATH=$LD_LIBRARY_PATH + fi + if [ -n "${DYLD_LIBRARY_PATH-}" ]; then + export SPACK_DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH + fi + # Zsh does not do word splitting by default, this enables it for this # function only if [ -n "${ZSH_VERSION:-}" ]; then @@ -141,41 +151,22 @@ spack() { return ;; "load"|"unload") - # Shift any other args for use off before parsing spec. - _sp_subcommand_args="" - _sp_module_args="" - while [ "${1#-}" != "${1}" ]; do - if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - command spack $_sp_flags $_sp_subcommand $_sp_subcommand_args "$@" - return - elif [ "$1" = "-r" ] || [ "$1" = "--dependencies" ]; then - _sp_subcommand_args="$_sp_subcommand_args $1" - else - _sp_module_args="$_sp_module_args $1" - fi - shift - done - - # Here the user has run use or unuse with a spec. Find a matching - # spec using 'spack module find', then use the appropriate module - # tool's commands to add/remove the result from the environment. - # If spack module command comes back with an error, do nothing. - case $_sp_subcommand in - "load") - if _sp_full_spec=$(command spack $_sp_flags module tcl find $_sp_subcommand_args "$@"); then - module load $_sp_module_args $_sp_full_spec - else - $(exit 1) - fi - ;; - "unload") - if _sp_full_spec=$(command spack $_sp_flags module tcl find $_sp_subcommand_args "$@"); then - module unload $_sp_module_args $_sp_full_spec - else - $(exit 1) - fi - ;; - esac + # get --sh, --csh, --help, or -h arguments + # space is important for -h case to differentiate between `-h` + # argument and specs with "-h" in package name or variant settings + _a=" $@" + if [ "${_a#* --sh}" != "$_a" ] || \ + [ "${_a#* --csh}" != "$_a" ] || \ + [ "${_a#* -h}" != "$_a" ] || \ + [ "${_a#* --help}" != "$_a" ]; + then + # just execute the command if --sh or --csh are provided + # or if the -h or --help arguments are provided + command spack $_sp_flags $_sp_subcommand "$@" + else + eval $(command spack $_sp_flags $_sp_subcommand --sh "$@" || \ + echo "return 1") # return 1 if spack command fails + fi ;; *) command spack $_sp_flags $_sp_subcommand "$@" diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash index 0284e81113167d67d5692cca57f209fbcc6f99c1..79dcf8e5590a253cff86eb9fad824990913fbf05 100755 --- a/share/spack/spack-completion.bash +++ b/share/spack/spack-completion.bash @@ -817,7 +817,7 @@ _spack_fetch() { _spack_find() { if $list_options then - SPACK_COMPREPLY="-h --help --format --json -d --deps -p --paths --groups --no-groups -l --long -L --very-long -t --tags -c --show-concretized -f --show-flags --show-full-compiler -x --explicit -X --implicit -u --unknown -m --missing -v --variants -M --only-missing --deprecated --only-deprecated -N --namespace --start-date --end-date" + SPACK_COMPREPLY="-h --help --format --json -d --deps -p --paths --groups --no-groups -l --long -L --very-long -t --tags -c --show-concretized -f --show-flags --show-full-compiler -x --explicit -X --implicit -u --unknown -m --missing -v --variants --loaded -M --only-missing --deprecated --only-deprecated -N --namespace --start-date --end-date" else _installed_packages fi @@ -972,7 +972,7 @@ _spack_list() { _spack_load() { if $list_options then - SPACK_COMPREPLY="-h --help -r --dependencies" + SPACK_COMPREPLY="-h --help -r --dependencies --sh --csh --only" else _installed_packages fi @@ -1420,7 +1420,7 @@ _spack_uninstall() { _spack_unload() { if $list_options then - SPACK_COMPREPLY="-h --help" + SPACK_COMPREPLY="-h --help --sh --csh -a --all" else _installed_packages fi