env: move env uninstall into spack uninstall

- uninstall now:
  - restricts its spec search to the current environment
  - removes uninstalled specs from the current environment
  - reports envs that still need specs you're trying to uninstall

- removed spack env uninstall command
- updated tests
This commit is contained in:
Todd Gamblin 2018-10-29 02:40:23 -07:00
parent 7136274f4b
commit 26a55ff749
6 changed files with 190 additions and 71 deletions

View File

@ -7,6 +7,7 @@
import os
import re
import sys
import llnl.util.tty as tty
from llnl.util.lang import attr_setdefault, index_by
@ -207,6 +208,7 @@ def display_specs(specs, args=None, **kwargs):
namespace (bool): Print namespaces along with names
show_flags (bool): Show compiler flags with specs
variants (bool): Show variants with specs
indent (int): indent each line this much
"""
def get_arg(name, default=None):
@ -225,6 +227,9 @@ def get_arg(name, default=None):
full_compiler = get_arg('show_full_compiler', False)
variants = get_arg('variants', False)
indent = get_arg('indent', 0)
ispace = indent * ' '
hlen = 7
if get_arg('very_long', False):
hashes = True
@ -255,6 +260,7 @@ def get_arg(name, default=None):
# If they don't have a compiler / architecture attached to them,
# then skip the header
if architecture is not None or compiler is not None:
sys.stdout.write(ispace)
tty.hline(colorize(header), char='-')
specs = index[(architecture, compiler)]
@ -269,7 +275,7 @@ def get_arg(name, default=None):
for abbrv, spec in zip(abbreviated, specs):
prefix = gray_hash(spec, hlen) if hashes else ''
print(prefix + (format % (abbrv, spec.prefix)))
print(ispace + prefix + (format % (abbrv, spec.prefix)))
elif mode == 'deps':
for spec in specs:
@ -290,13 +296,13 @@ def fmt(s):
return string
colify(fmt(s) for s in specs)
colify((fmt(s) for s in specs), indent=indent)
# Print one entry per line if including flags
else:
for spec in specs:
# Print the hash if necessary
hsh = gray_hash(spec, hlen) + ' ' if hashes else ''
print(hsh + spec.cformat(format_string) + '\n')
print(ispace + hsh + spec.cformat(format_string))
else:
raise ValueError(

View File

@ -56,7 +56,7 @@ def _specs(self, **kwargs):
# only its installed packages.
env = spack.environment.active
if env:
kwargs['hashes'] = set(env.specs_by_hash.keys())
kwargs['hashes'] = set(env.all_hashes())
# return everything for an empty query.
if not qspecs:

View File

@ -34,7 +34,6 @@
['list', 'ls'],
['status', 'st'],
'loads',
'uninstall',
]
@ -234,7 +233,7 @@ def env_remove(args):
for env_name in args.env:
env = ev.disambiguate(env_name)
if ev.active and ev.active.path == env.path:
if env.active:
tty.die("Environment %s can't be removed while activated.")
env.destroy()
@ -250,7 +249,7 @@ def env_list_setup_parser(subparser):
def env_list(args):
names = ev.list_environments()
names = ev.all_environment_names()
color_names = []
for name in names:
@ -268,21 +267,6 @@ def env_list(args):
colify(color_names, indent=4)
# REMOVE
# env uninstall
#
def env_uninstall_setup_parser(subparser):
"""uninstall packages from an environment"""
subparser.add_argument(
'env', nargs='?', help='uninstall all packages in this environment')
spack.cmd.uninstall.add_common_arguments(subparser)
def env_uninstall(args):
env = ev.get_env(args, 'env uninstall')
env.uninstall(args)
#
# env status
#
@ -308,6 +292,7 @@ def env_status(args):
hashlen=None if args.very_long else 7,
install_status=True)
#
# env loads
#

View File

@ -8,12 +8,14 @@
import argparse
import spack.cmd
import spack.environment as ev
import spack.package
import spack.cmd.common.arguments as arguments
import spack.repo
import spack.store
from llnl.util import tty
from llnl.util.tty.colify import colify
description = "remove installed packages"
section = "build"
@ -28,14 +30,16 @@
display_args = {
'long': True,
'show_flags': True,
'variants': True
'variants': True,
'indent': 4,
}
def add_common_arguments(subparser):
subparser.add_argument(
'-f', '--force', action='store_true', dest='force',
help="remove regardless of whether other packages depend on this one")
help="remove regardless of whether other packages or environments "
"depend on this one")
arguments.add_common_arguments(
subparser, ['recurse_dependents', 'yes_to_all'])
@ -44,10 +48,12 @@ def setup_parser(subparser):
add_common_arguments(subparser)
subparser.add_argument(
'-a', '--all', action='store_true', dest='all',
help="USE CAREFULLY. remove ALL installed packages that match each "
"supplied spec. i.e., if you `uninstall --all libelf`,"
" ALL versions of `libelf` are uninstalled. if no spec is "
"supplied all installed software will be uninstalled.")
help="USE CAREFULLY. Remove ALL installed packages that match each "
"supplied spec. i.e., if you `uninstall --all libelf`,"
" ALL versions of `libelf` are uninstalled. If no spec is "
"supplied, all installed packages will be uninstalled. "
"If used in an environment, all packages in the environment "
"will be uninstalled.")
subparser.add_argument(
'packages',
@ -66,11 +72,13 @@ def find_matching_specs(specs, allow_multiple_matches=False, force=False):
Return:
list of specs
"""
hashes = ev.active.all_hashes() if ev.active else None
# List of specs that match expressions given via command line
specs_from_cli = []
has_errors = False
for spec in specs:
matching = spack.store.db.query(spec)
matching = spack.store.db.query(spec, hashes=hashes)
# For each spec provided, make sure it refers to only one package.
# Fail and ask user to be unambiguous if it doesn't
if not allow_multiple_matches and len(matching) > 1:
@ -82,11 +90,14 @@ def find_matching_specs(specs, allow_multiple_matches=False, force=False):
# No installed package matches the query
if len(matching) == 0 and spec is not any:
tty.error('{0} does not match any installed packages.'.format(
spec))
has_errors = True
if ev.active:
pkg_type = "packages in environment '%s'" % ev.active.name
else:
pkg_type = 'installed packages'
tty.die('{0} does not match any {1}.'.format(spec, pkg_type))
specs_from_cli.extend(matching)
if has_errors:
tty.die(error_message)
@ -94,14 +105,14 @@ def find_matching_specs(specs, allow_multiple_matches=False, force=False):
def installed_dependents(specs):
"""Returns a dictionary that maps a spec with a list of its
installed dependents
"""Map each spec to a list of its installed dependents.
Args:
specs: list of specs to be checked for dependents
specs (list): list of Specs
Returns:
dictionary of installed dependents
(dict): mapping from spec to lists of Environments
"""
dependents = {}
for item in specs:
@ -114,6 +125,28 @@ def installed_dependents(specs):
return dependents
def dependent_environments(specs):
"""Map each spec to environments that depend on it.
This excludes the active environment, because we allow uninstalling
from the active environment.
Args:
specs (list): list of Specs
Returns:
(dict): mapping from spec to lists of Environments
"""
dependents = {}
for env in ev.all_environments():
if not env.active:
hashes = set([s.dag_hash() for s in env.all_specs()])
for spec in specs:
if spec.dag_hash() in hashes:
dependents.setdefault(spec, []).append(env)
return dependents
def do_uninstall(specs, force):
"""
Uninstalls all the specs in a list.
@ -132,6 +165,12 @@ def do_uninstall(specs, force):
# want to uninstall.
spack.package.Package.uninstall_by_spec(item, force=True)
if ev.active:
try:
ev.active.remove(item, force=True)
except ev.EnvError:
pass # ignore errors from specs that are not roots
# Sort packages to be uninstalled by the number of installed dependents
# This ensures we do things in the right order
def num_installed_deps(pkg):
@ -143,6 +182,10 @@ def num_installed_deps(pkg):
for item in packages:
item.do_uninstall(force=force)
# write any changes made to the active environment
if ev.active:
ev.active.write()
def get_uninstall_list(args, specs):
# Gets the list of installed specs that match the ones give via cli
@ -150,25 +193,54 @@ def get_uninstall_list(args, specs):
uninstall_list = find_matching_specs(specs, args.all, args.force)
# Takes care of '-R'
dependent_list = installed_dependents(uninstall_list)
spec_dependents = installed_dependents(uninstall_list)
spec_envs = dependent_environments(uninstall_list)
# Process spec_dependents and update uninstall_list
has_error = not args.force and (
(spec_dependents and not args.dependents) or
spec_envs)
# say why each problem spec is needed
if has_error:
specs = set(list(spec_dependents.keys()) + list(spec_envs.keys()))
for i, spec in enumerate(sorted(specs)):
# space out blocks of reasons
if i > 0:
print()
tty.info("Will not uninstall %s" % spec.cformat("$_$@$%@$/"),
format='*r')
dependents = spec_dependents.get(spec)
if dependents:
print('The following packages depend on it:')
spack.cmd.display_specs(dependents, **display_args)
envs = spec_envs.get(spec)
if envs:
print('It is used by the following environments:')
colify([e.name for e in envs], indent=4)
msgs = []
if spec_dependents:
msgs.append(
'use `spack uninstall --dependents` to uninstall dependents '
'as well.')
if spec_envs:
msgs.append(
'use `spack env remove` to remove environments, or '
'`spack remove` to remove specs from environments.')
if ev.active:
msgs.append('consider using `spack remove` to remove the spec '
'from this environment')
print()
tty.die('There are still dependents.', *msgs)
# Process dependent_list and update uninstall_list
has_error = False
if dependent_list and not args.dependents and not args.force:
for spec, lst in dependent_list.items():
tty.error("Will not uninstall %s" % spec.cformat("$_$@$%@$/"))
print('')
print('The following packages depend on it:')
spack.cmd.display_specs(lst, **display_args)
print('')
has_error = True
elif args.dependents:
for key, lst in dependent_list.items():
for spec, lst in spec_dependents.items():
uninstall_list.extend(lst)
uninstall_list = list(set(uninstall_list))
if has_error:
tty.die('Use `spack uninstall --dependents` '
'to uninstall these dependencies as well.')
return uninstall_list
@ -196,5 +268,6 @@ def uninstall(parser, args):
tty.die('uninstall requires at least one package argument.',
' Use `spack uninstall --all` to uninstall ALL packages.')
# [any] here handles the --all case by forcing all specs to be returned
uninstall_specs(
args, spack.cmd.parse_specs(args.packages) if args.packages else [any])

View File

@ -242,7 +242,7 @@ def config_dict(yaml_data):
return yaml_data[key]
def list_environments():
def all_environment_names():
"""List the names of environments that currently exist."""
# just return empty if the env path does not exist. A read-only
# operation like list should not try to create a directory.
@ -258,6 +258,12 @@ def list_environments():
return names
def all_environments():
"""Generator for all named Environments."""
for name in all_environment_names():
yield read(name)
def validate(data, filename=None):
global _validator
if _validator is None:
@ -362,6 +368,11 @@ def name(self):
else:
return self.path
@property
def active(self):
"""True if this environment is currently active."""
return active and self.path == active.path
@property
def manifest_path(self):
"""Path to spack.yaml file in this environment."""
@ -480,8 +491,7 @@ def remove(self, query_spec, force=False):
specs_hashes = zip(
self.concretized_user_specs, self.concretized_order)
matches = [
s for s, h in specs_hashes
if s.satisfies(query_spec) or query_spec.dag_hash() == h]
s for s, h in specs_hashes if query_spec.dag_hash() == h]
if not matches:
raise EnvError("Not found: {0}".format(query_spec))
@ -615,12 +625,6 @@ def install_all(self, args=None):
os.remove(build_log_link)
os.symlink(spec.package.build_log_path, build_log_link)
def uninstall(self, args):
"""Uninstall all the specs in an Environment."""
specs = self._get_environment_specs(recurse_dependencies=True)
args.all = False
spack.cmd.uninstall.uninstall_specs(args, specs)
def status(self, stream, **kwargs):
"""List the specs in an environment."""
tty.msg('In environment %s' % self.name)
@ -635,10 +639,10 @@ def status(self, stream, **kwargs):
current = [(s, c) for s, c in concretized if s in self.user_specs]
def write_kind(s):
color.cwrite('@c{%s}\n' % str(s), stream)
color.cwrite('@c{%s}\n' % color.cescape(s), stream)
def write_user_spec(s, c):
color.cwrite('@%s{----} %s\n' % (c, str(s)), stream)
color.cwrite('@%s{----} %s\n' % (c, color.cescape(s)), stream)
if added:
write_kind('added:')
@ -661,23 +665,44 @@ def write_user_spec(s, c):
write_user_spec(s, 'r')
stream.write(c.tree(**kwargs))
def all_specs_by_hash(self):
"""Map of hashes to spec for all specs in this environment."""
hashes = {}
for h in self.concretized_order:
specs = self.specs_by_hash[h].traverse(deptype=('link', 'run'))
for spec in specs:
hashes[spec.dag_hash()] = spec
return hashes
def all_specs(self):
"""Return all specs, even those a user spec would shadow."""
return sorted(self.all_specs_by_hash().values())
def all_hashes(self):
"""Return all specs, even those a user spec would shadow."""
return list(self.all_specs_by_hash().keys())
def _get_environment_specs(self, recurse_dependencies=True):
"""Returns the specs of all the packages in an environment.
If these specs appear under different user_specs, only one copy
is added to the list returned."""
is added to the list returned.
"""
package_to_spec = {}
spec_list = list()
for spec_hash in self.concretized_order:
spec = self.specs_by_hash[spec_hash]
specs = spec.traverse(deptype=('link', 'run')) \
if recurse_dependencies else (spec,)
specs = (spec.traverse(deptype=('link', 'run'))
if recurse_dependencies else (spec,))
for dep in specs:
if dep.name in package_to_spec:
tty.warn("{0} takes priority over {1}"
.format(package_to_spec[dep.name].format(),
dep.format()))
prior = package_to_spec.get(dep.name)
if prior and prior != dep:
tty.debug("{0} takes priority over {1}"
.format(package_to_spec[dep.name].format(),
dep.format()))
else:
package_to_spec[dep.name] = dep
spec_list.append(dep)

View File

@ -27,6 +27,7 @@
remove = SpackCommand('remove')
concretize = SpackCommand('concretize')
stage = SpackCommand('stage')
uninstall = SpackCommand('uninstall')
def test_add():
@ -541,9 +542,38 @@ def test_env_commands_die_with_no_env_arg():
# these have an optional env arg and raise errors via tty.die
with pytest.raises(spack.main.SpackCommandError):
env('loads')
with pytest.raises(spack.main.SpackCommandError):
env('uninstall')
# This should NOT raise an error with no environment
# it just tells the user there isn't an environment
env('status')
def test_env_blocks_uninstall(mock_stage, mock_fetch, install_mockery):
env('create', 'test')
with ev.read('test'):
add('mpileaks')
install('--fake')
out = uninstall('mpileaks', fail_on_error=False)
assert uninstall.returncode == 1
assert 'used by the following environments' in out
def test_uninstall_removes_from_env(mock_stage, mock_fetch, install_mockery):
env('create', 'test')
with ev.read('test'):
add('mpileaks')
add('libelf')
install('--fake')
test = ev.read('test')
assert any(s.name == 'mpileaks' for s in test.specs_by_hash.values())
assert any(s.name == 'libelf' for s in test.specs_by_hash.values())
with ev.read('test'):
uninstall('-ya')
test = ev.read('test')
assert not test.specs_by_hash
assert not test.concretized_order
assert not test.user_specs