env activation: use package defined env setup methods (#13249)

This PR ensures that environment activation sets all environment variables set by the equivalent `module load` operations, except that the spec prefixes are "rebased" to the view associated with the environment.

Currently, Spack blindly adds paths relative to the environment view root to the user environment on activation. Issue #12731 points out ways in which this behavior is insufficient.

This PR changes that behavior to use the `setup_run_environment` logic for each package to augment the prefix inspections (as in Spack's modulefile generation logic) to ensure that all necessary variables are set to make use of the packages in the environment.

See #12731 for details on the previous problems in behavior.

This PR also updates the `ViewDescriptor` object in `spack.environment` to have a `__contains__` method. This allows for checks like `if spec in self.default_view`. The `__contains__` operator for `ViewDescriptor` objects checks whether the spec satisfies the filters of the View descriptor, not whether the spec is already linked into the underlying `FilesystemView` object.
This commit is contained in:
Greg Becker 2019-10-22 23:27:40 -07:00 committed by Todd Gamblin
parent b14f18acda
commit 95a48b27ec
6 changed files with 190 additions and 45 deletions

View file

@ -28,9 +28,11 @@
import spack.spec import spack.spec
import spack.util.spack_json as sjson import spack.util.spack_json as sjson
import spack.config import spack.config
import spack.build_environment as build_env
from spack.util.prefix import Prefix
from spack.filesystem_view import YamlFilesystemView from spack.filesystem_view import YamlFilesystemView
from spack.util.environment import EnvironmentModifications import spack.util.environment
import spack.architecture as architecture import spack.architecture as architecture
from spack.spec import Spec from spack.spec import Spec
from spack.spec_list import SpecList, InvalidSpecConstraintError from spack.spec_list import SpecList, InvalidSpecConstraintError
@ -468,6 +470,23 @@ def view(self):
ignore_conflicts=True, ignore_conflicts=True,
projections=self.projections) projections=self.projections)
def __contains__(self, spec):
"""Is the spec described by the view descriptor
Note: This does not claim the spec is already linked in the view.
It merely checks that the spec is selected if a select operation is
specified and is not excluded if an exclude operator is specified.
"""
if self.select:
if not self.select_fn(spec):
return False
if self.exclude:
if not self.exclude_fn(spec):
return False
return True
def regenerate(self, all_specs, roots): def regenerate(self, all_specs, roots):
specs_for_view = [] specs_for_view = []
specs = all_specs if self.link == 'all' else roots specs = all_specs if self.link == 'all' else roots
@ -478,14 +497,8 @@ def regenerate(self, all_specs, roots):
if spec.concrete: # Do not link unconcretized roots if spec.concrete: # Do not link unconcretized roots
specs_for_view.append(spec.copy(deps=('link', 'run'))) specs_for_view.append(spec.copy(deps=('link', 'run')))
if self.select:
specs_for_view = list(filter(self.select_fn, specs_for_view))
if self.exclude:
specs_for_view = list(filter(self.exclude_fn, specs_for_view))
installed_specs_for_view = set(s for s in specs_for_view installed_specs_for_view = set(s for s in specs_for_view
if s.package.installed) if s in self and s.package.installed)
view = self.view() view = self.view()
@ -1009,38 +1022,76 @@ def regenerate_views(self):
for view in self.views.values(): for view in self.views.values():
view.regenerate(specs, self.roots()) view.regenerate(specs, self.roots())
def _shell_vars(self): prefix_inspections = {
updates = [ 'bin': ['PATH'],
('PATH', ['bin']), 'lib': ['LD_LIBRARY_PATH', 'LIBRARY_PATH', 'DYLD_LIBRARY_PATH'],
('MANPATH', ['man', 'share/man']), 'lib64': ['LD_LIBRARY_PATH', 'LIBRARY_PATH', 'DYLD_LIBRARY_PATH'],
('ACLOCAL_PATH', ['share/aclocal']), 'man': ['MANPATH'],
('LD_LIBRARY_PATH', ['lib', 'lib64']), 'share/man': ['MANPATH'],
('LIBRARY_PATH', ['lib', 'lib64']), 'share/aclocal': ['ACLOCAL_PATH'],
('CPATH', ['include']), 'include': ['CPATH'],
('PKG_CONFIG_PATH', ['lib/pkgconfig', 'lib64/pkgconfig', 'lib/pkgconfig': ['PKG_CONFIG_PATH'],
'share/pkgconfig']), 'lib64/pkgconfig': ['PKG_CONFIG_PATH'],
('CMAKE_PREFIX_PATH', ['']), '': ['CMAKE_PREFIX_PATH']
] }
path_updates = list() def environment_modifications_for_spec(self, spec, view=None):
if default_view_name in self.views: """List of environment modifications to be processed."""
for var, dirs in updates: spec = spec.copy()
paths = [os.path.join(self.default_view.root, x) for x in dirs] if view:
path_updates.append((var, paths)) spec.prefix = Prefix(view.view().get_projection_for_spec(spec))
return path_updates
# 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): def add_default_view_to_shell(self, shell):
env_mod = EnvironmentModifications() env_mod = spack.util.environment.EnvironmentModifications()
for var, paths in self._shell_vars():
for path in paths: if default_view_name not in self.views:
env_mod.prepend_path(var, path) # No default view to add to shell
return env_mod.shell_modifications(shell)
for _, spec in self.concretized_specs():
if spec in self.default_view:
env_mod.extend(self.environment_modifications_for_spec(
spec, self.default_view))
# deduplicate paths from specs mapped to the same location
for env_var in env_mod.group_by_name():
env_mod.prune_duplicate_paths(env_var)
return env_mod.shell_modifications(shell) return env_mod.shell_modifications(shell)
def rm_default_view_from_shell(self, shell): def rm_default_view_from_shell(self, shell):
env_mod = EnvironmentModifications() env_mod = spack.util.environment.EnvironmentModifications()
for var, paths in self._shell_vars():
for path in paths: if default_view_name not in self.views:
env_mod.remove_path(var, path) # No default view to add to shell
return env_mod.shell_modifications(shell)
for _, spec in self.concretized_specs():
if spec in self.default_view:
env_mod.extend(
self.environment_modifications_for_spec(
spec, self.default_view).reversed())
return env_mod.shell_modifications(shell) return env_mod.shell_modifications(shell)
def _add_concrete_spec(self, spec, concrete, new=True): def _add_concrete_spec(self, spec, concrete, new=True):

View file

@ -1577,8 +1577,10 @@ def test_stack_view_activate_from_default(tmpdir, mock_fetch, mock_packages,
install() install()
shell = env('activate', '--sh', 'test') shell = env('activate', '--sh', 'test')
assert 'PATH' in shell assert 'PATH' in shell
assert os.path.join(viewdir, 'bin') in shell assert os.path.join(viewdir, 'bin') in shell
assert 'FOOBAR=mpileaks' in shell
def test_stack_view_no_activate_without_default(tmpdir, mock_fetch, def test_stack_view_no_activate_without_default(tmpdir, mock_fetch,

View file

@ -172,15 +172,16 @@ def test_prs_update_old_api():
] ]
failing = [] failing = []
for file in changed_package_files: for file in changed_package_files:
if 'builtin.mock' not in file: # don't restrict packages for tests
name = os.path.basename(os.path.dirname(file)) name = os.path.basename(os.path.dirname(file))
pkg = spack.repo.get(name) pkg = spack.repo.get(name)
# Check for old APIs
failed = (hasattr(pkg, 'setup_environment') or failed = (hasattr(pkg, 'setup_environment') or
hasattr(pkg, 'setup_dependent_environment')) hasattr(pkg, 'setup_dependent_environment'))
if failed: if failed:
failing.append(pkg) failing.append(name)
msg = 'there are {0} packages still using old APIs in this PR [{1}]' msg = 'there are {0} packages still using old APIs in this PR [{1}]'
assert not failing, msg.format( assert not failing, msg.format(
len(failing), ','.join(x.name for x in failing) len(failing), ','.join(failing)
) )

View file

@ -103,3 +103,34 @@ def test_dump_environment(prepare_environment_for_tests, tmpdir):
with open(dumpfile_path, 'r') as dumpfile: with open(dumpfile_path, 'r') as dumpfile:
assert('TEST_ENV_VAR={0}; export TEST_ENV_VAR\n'.format(test_paths) assert('TEST_ENV_VAR={0}; export TEST_ENV_VAR\n'.format(test_paths)
in list(dumpfile)) in list(dumpfile))
def test_reverse_environment_modifications(working_env):
start_env = {
'PREPEND_PATH': '/path/to/prepend/to',
'APPEND_PATH': '/path/to/append/to',
'UNSET': 'var_to_unset',
'APPEND_FLAGS': 'flags to append to',
}
to_reverse = envutil.EnvironmentModifications()
to_reverse.prepend_path('PREPEND_PATH', '/new/path/prepended')
to_reverse.append_path('APPEND_PATH', '/new/path/appended')
to_reverse.set_path('SET_PATH', ['/one/set/path', '/two/set/path'])
to_reverse.set('SET', 'a var')
to_reverse.unset('UNSET')
to_reverse.append_flags('APPEND_FLAGS', 'more_flags')
reversal = to_reverse.reversed()
os.environ = start_env.copy()
print(os.environ)
to_reverse.apply_modifications()
print(os.environ)
reversal.apply_modifications()
print(os.environ)
start_env.pop('UNSET')
assert os.environ == start_env

View file

@ -226,6 +226,16 @@ def execute(self, env):
env.pop(self.name, None) env.pop(self.name, None)
class RemoveFlagsEnv(NameValueModifier):
def execute(self, env):
environment_value = env.get(self.name, '')
flags = environment_value.split(
self.separator) if environment_value else []
flags = [f for f in flags if f != self.value]
env[self.name] = self.separator.join(flags)
class SetPath(NameValueModifier): class SetPath(NameValueModifier):
def execute(self, env): def execute(self, env):
@ -372,6 +382,21 @@ def unset(self, name, **kwargs):
item = UnsetEnv(name, **kwargs) item = UnsetEnv(name, **kwargs)
self.env_modifications.append(item) self.env_modifications.append(item)
def remove_flags(self, name, value, sep=' ', **kwargs):
"""
Stores in the current object a request to remove flags from an
env variable
Args:
name: name of the environment variable to be removed from
value: value to remove to the environment variable
sep: separator to assume for environment variable
"""
kwargs.update(self._get_outside_caller_attributes())
kwargs.update({'separator': sep})
item = RemoveFlagsEnv(name, value, **kwargs)
self.env_modifications.append(item)
def set_path(self, name, elements, **kwargs): def set_path(self, name, elements, **kwargs):
"""Stores a request to set a path generated from a list. """Stores a request to set a path generated from a list.
@ -467,6 +492,40 @@ def clear(self):
""" """
self.env_modifications = [] self.env_modifications = []
def reversed(self):
"""
Returns the EnvironmentModifications object that will reverse self
Only creates reversals for additions to the environment, as reversing
``unset`` and ``remove_path`` modifications is impossible.
Reversable operations are set(), prepend_path(), append_path(),
set_path(), and append_flags().
"""
rev = EnvironmentModifications()
for envmod in reversed(self.env_modifications):
if type(envmod) == SetEnv:
tty.warn("Reversing `Set` environment operation may lose "
"original value")
rev.unset(envmod.name)
elif type(envmod) == AppendPath:
rev.remove_path(envmod.name, envmod.value)
elif type(envmod) == PrependPath:
rev.remove_path(envmod.name, envmod.value)
elif type(envmod) == SetPath:
tty.warn("Reversing `SetPath` environment operation may lose "
"original value")
rev.unset(envmod.name)
elif type(envmod) == AppendFlagsEnv:
rev.remove_flags(envmod.name, envmod.value)
else:
# This is an un-reversable operation
tty.warn("Skipping reversal of unreversable operation"
"%s %s" % (type(envmod), envmod.name))
return rev
def apply_modifications(self): def apply_modifications(self):
"""Applies the modifications and clears the list.""" """Applies the modifications and clears the list."""
modifications = self.group_by_name() modifications = self.group_by_name()
@ -485,7 +544,8 @@ def shell_modifications(self, shell='sh'):
x.execute(new_env) x.execute(new_env)
cmds = '' cmds = ''
for name in set(new_env) & set(os.environ):
for name in set(new_env) | set(os.environ):
new = new_env.get(name, None) new = new_env.get(name, None)
old = os.environ.get(name, None) old = os.environ.get(name, None)
if new != old: if new != old:

View file

@ -43,7 +43,7 @@ def flip(self):
def do_not_execute(self): def do_not_execute(self):
self.did_something = True self.did_something = True
def setup_environment(self, spack_env, run_env): def setup_build_environment(self, spack_env):
spack_cc # Ensure spack module-scope variable is avaiabl spack_cc # Ensure spack module-scope variable is avaiabl
check(from_cmake == "from_cmake", check(from_cmake == "from_cmake",
"setup_environment couldn't read global set by cmake.") "setup_environment couldn't read global set by cmake.")
@ -52,7 +52,7 @@ def setup_environment(self, spack_env, run_env):
"link arg on dependency spec not readable from " "link arg on dependency spec not readable from "
"setup_environment.") "setup_environment.")
def setup_dependent_environment(self, spack_env, run_env, dspec): def setup_dependent_build_environment(self, spack_env, dspec):
spack_cc # Ensure spack module-scope variable is avaiable spack_cc # Ensure spack module-scope variable is avaiable
check(from_cmake == "from_cmake", check(from_cmake == "from_cmake",
"setup_dependent_environment couldn't read global set by cmake.") "setup_dependent_environment couldn't read global set by cmake.")