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:
parent
ae82650174
commit
3c145b42bc
5 changed files with 104 additions and 19 deletions
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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(','))
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue