environment-views: fix bug where missing recipe/repo breaks env commands (#17608)

* environment-views: fix bug where missing recipe/repo breaks env commands

When a recipe or a repo has been removed from Spack and an environment
is active, it causes the view activation to crash Spack before any
commands can be executed. Further, the error message it not at all clear
in explaining the issue.

This forces view regeneration to always start from scratch to avoid the
missing package recipes, and defaults add_view=False in main for views activated
by the `spack -e` option.

* add messages to env status and deactivate

Warn users that a view may be corrupt when deactivating an environment
or checking its status while active. Updated message for activate.

* tests for view checking

Co-authored-by: Gregory Becker <becker33@llnl.gov>
This commit is contained in:
robo-wylder 2020-07-23 11:00:58 -07:00 committed by GitHub
parent ae82650174
commit 3c145b42bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 19 deletions

View file

@ -351,6 +351,9 @@ def env_status(args):
% (ev.manifest_name, env.path)) % (ev.manifest_name, env.path))
else: else:
tty.msg('In environment %s' % env.name) tty.msg('In environment %s' % env.name)
# Check if environment views can be safely activated
env.check_views()
else: else:
tty.msg('No active environment') tty.msg('No active environment')

View file

@ -175,9 +175,20 @@ def activate(
# MANPATH, PYTHONPATH, etc. All variables that end in PATH (case-sensitive) # MANPATH, PYTHONPATH, etc. All variables that end in PATH (case-sensitive)
# become PATH variables. # become PATH variables.
# #
try:
if add_view and default_view_name in env.views: if add_view and default_view_name in env.views:
with spack.store.db.read_transaction(): with spack.store.db.read_transaction():
cmds += env.add_default_view_to_shell(shell) cmds += env.add_default_view_to_shell(shell)
except (spack.repo.UnknownPackageError,
spack.repo.UnknownNamespaceError) as e:
tty.error(e)
tty.die(
'Environment view is broken due to a missing package or repo.\n',
' To activate without views enabled, activate with:\n',
' spack env activate -V {0}\n'.format(env.name),
' To remove it and resolve the issue, '
'force concretize with the command:\n',
' spack -e {0} concretize --force'.format(env.name))
return cmds return cmds
@ -230,9 +241,15 @@ def deactivate(shell='sh'):
cmds += ' unset SPACK_OLD_PS1; export SPACK_OLD_PS1;\n' cmds += ' unset SPACK_OLD_PS1; export SPACK_OLD_PS1;\n'
cmds += 'fi;\n' cmds += 'fi;\n'
try:
if default_view_name in _active_environment.views: if default_view_name in _active_environment.views:
with spack.store.db.read_transaction(): with spack.store.db.read_transaction():
cmds += _active_environment.rm_default_view_from_shell(shell) cmds += _active_environment.rm_default_view_from_shell(shell)
except (spack.repo.UnknownPackageError,
spack.repo.UnknownNamespaceError) as e:
tty.warn(e)
tty.warn('Could not fully deactivate view due to missing package '
'or repo, shell environment may be corrupt.')
tty.debug("Deactivated environmennt '%s'" % _active_environment.name) tty.debug("Deactivated environmennt '%s'" % _active_environment.name)
_active_environment = None _active_environment = None
@ -527,6 +544,12 @@ def regenerate(self, all_specs, roots):
installed_specs_for_view = set( installed_specs_for_view = set(
s for s in specs_for_view if s in self and s.package.installed) s for s in specs_for_view if s in self and s.package.installed)
# To ensure there are no conflicts with packages being installed
# that cannot be resolved or have repos that have been removed
# we always regenerate the view from scratch. We must first make
# sure the root directory exists for the very first time though.
fs.mkdirp(self.root)
with fs.replace_directory_transaction(self.root):
view = self.view() view = self.view()
view.clean() view.clean()
@ -1111,6 +1134,24 @@ 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 check_views(self):
"""Checks if the environments default view can be activated."""
try:
# This is effectively a no-op, but it touches all packages in the
# default view if they are installed.
for view_name, view in self.views.items():
for _, spec in self.concretized_specs():
if spec in view and spec.package.installed:
tty.debug(
'Spec %s in view %s' % (spec.name, view_name))
except (spack.repo.UnknownPackageError,
spack.repo.UnknownNamespaceError) as e:
tty.warn(e)
tty.warn(
'Environment %s includes out of date packages or repos. '
'Loading the environment view will require reconcretization.'
% self.name)
def _env_modifications_for_default_view(self, reverse=False): def _env_modifications_for_default_view(self, reverse=False):
all_mods = spack.util.environment.EnvironmentModifications() all_mods = spack.util.environment.EnvironmentModifications()

View file

@ -710,7 +710,7 @@ def main(argv=None):
if not args.no_env: if not args.no_env:
env = ev.find_environment(args) env = ev.find_environment(args)
if env: if env:
ev.activate(env, args.use_env_repo) ev.activate(env, args.use_env_repo, add_view=False)
if args.print_shell_vars: if args.print_shell_vars:
print_setup_info(*args.print_shell_vars.split(',')) print_setup_info(*args.print_shell_vars.split(','))

View file

@ -16,7 +16,7 @@
from spack.cmd.env import _env_create from spack.cmd.env import _env_create
from spack.spec import Spec from spack.spec import Spec
from spack.main import SpackCommand from spack.main import SpackCommand, SpackCommandError
from spack.stage import stage_prefix from spack.stage import stage_prefix
from spack.util.mock_package import MockPackageMultiRepo from spack.util.mock_package import MockPackageMultiRepo
@ -284,6 +284,45 @@ def test_environment_status(capsys, tmpdir):
assert 'in current directory' in env('status') assert 'in current directory' in env('status')
def test_env_status_broken_view(
mutable_mock_env_path, mock_archive, mock_fetch, mock_packages,
install_mockery
):
with ev.create('test'):
install('trivial-install-test-package')
# switch to a new repo that doesn't include the installed package
# test that Spack detects the missing package and warns the user
new_repo = MockPackageMultiRepo()
with spack.repo.swap(new_repo):
output = env('status')
assert 'In environment test' in output
assert 'Environment test includes out of date' in output
# Test that the warning goes away when it's fixed
output = env('status')
assert 'In environment test' in output
assert 'Environment test includes out of date' not in output
def test_env_activate_broken_view(
mutable_mock_env_path, mock_archive, mock_fetch, mock_packages,
install_mockery
):
with ev.create('test'):
install('trivial-install-test-package')
# switch to a new repo that doesn't include the installed package
# test that Spack detects the missing package and fails gracefully
new_repo = MockPackageMultiRepo()
with spack.repo.swap(new_repo):
with pytest.raises(SpackCommandError):
env('activate', '--sh', 'test')
# test replacing repo fixes it
env('activate', '--sh', 'test')
def test_to_lockfile_dict(): def test_to_lockfile_dict():
e = ev.create('test') e = ev.create('test')
e.add('mpileaks') e.add('mpileaks')

View file

@ -77,6 +77,8 @@ def __init__(self):
def get(self, spec): def get(self, spec):
if not isinstance(spec, spack.spec.Spec): if not isinstance(spec, spack.spec.Spec):
spec = Spec(spec) spec = Spec(spec)
if spec.name not in self.spec_to_pkg:
raise spack.repo.UnknownPackageError(spec.fullname)
return self.spec_to_pkg[spec.name] return self.spec_to_pkg[spec.name]
def get_pkg_class(self, name): def get_pkg_class(self, name):