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

View File

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

View File

@ -34,7 +34,6 @@
['list', 'ls'], ['list', 'ls'],
['status', 'st'], ['status', 'st'],
'loads', 'loads',
'uninstall',
] ]
@ -234,7 +233,7 @@ def env_remove(args):
for env_name in args.env: for env_name in args.env:
env = ev.disambiguate(env_name) 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.") tty.die("Environment %s can't be removed while activated.")
env.destroy() env.destroy()
@ -250,7 +249,7 @@ def env_list_setup_parser(subparser):
def env_list(args): def env_list(args):
names = ev.list_environments() names = ev.all_environment_names()
color_names = [] color_names = []
for name in names: for name in names:
@ -268,21 +267,6 @@ def env_list(args):
colify(color_names, indent=4) 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 # env status
# #
@ -308,6 +292,7 @@ def env_status(args):
hashlen=None if args.very_long else 7, hashlen=None if args.very_long else 7,
install_status=True) install_status=True)
# #
# env loads # env loads
# #

View File

@ -8,12 +8,14 @@
import argparse import argparse
import spack.cmd import spack.cmd
import spack.environment as ev
import spack.package import spack.package
import spack.cmd.common.arguments as arguments import spack.cmd.common.arguments as arguments
import spack.repo import spack.repo
import spack.store import spack.store
from llnl.util import tty from llnl.util import tty
from llnl.util.tty.colify import colify
description = "remove installed packages" description = "remove installed packages"
section = "build" section = "build"
@ -28,14 +30,16 @@
display_args = { display_args = {
'long': True, 'long': True,
'show_flags': True, 'show_flags': True,
'variants': True 'variants': True,
'indent': 4,
} }
def add_common_arguments(subparser): def add_common_arguments(subparser):
subparser.add_argument( subparser.add_argument(
'-f', '--force', action='store_true', dest='force', '-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( arguments.add_common_arguments(
subparser, ['recurse_dependents', 'yes_to_all']) subparser, ['recurse_dependents', 'yes_to_all'])
@ -44,10 +48,12 @@ def setup_parser(subparser):
add_common_arguments(subparser) add_common_arguments(subparser)
subparser.add_argument( subparser.add_argument(
'-a', '--all', action='store_true', dest='all', '-a', '--all', action='store_true', dest='all',
help="USE CAREFULLY. remove ALL installed packages that match each " help="USE CAREFULLY. Remove ALL installed packages that match each "
"supplied spec. i.e., if you `uninstall --all libelf`," "supplied spec. i.e., if you `uninstall --all libelf`,"
" ALL versions of `libelf` are uninstalled. if no spec is " " ALL versions of `libelf` are uninstalled. If no spec is "
"supplied all installed software will be uninstalled.") "supplied, all installed packages will be uninstalled. "
"If used in an environment, all packages in the environment "
"will be uninstalled.")
subparser.add_argument( subparser.add_argument(
'packages', 'packages',
@ -66,11 +72,13 @@ def find_matching_specs(specs, allow_multiple_matches=False, force=False):
Return: Return:
list of specs list of specs
""" """
hashes = ev.active.all_hashes() if ev.active else None
# List of specs that match expressions given via command line # List of specs that match expressions given via command line
specs_from_cli = [] specs_from_cli = []
has_errors = False has_errors = False
for spec in specs: 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. # For each spec provided, make sure it refers to only one package.
# Fail and ask user to be unambiguous if it doesn't # Fail and ask user to be unambiguous if it doesn't
if not allow_multiple_matches and len(matching) > 1: 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 # No installed package matches the query
if len(matching) == 0 and spec is not any: if len(matching) == 0 and spec is not any:
tty.error('{0} does not match any installed packages.'.format( if ev.active:
spec)) pkg_type = "packages in environment '%s'" % ev.active.name
has_errors = True else:
pkg_type = 'installed packages'
tty.die('{0} does not match any {1}.'.format(spec, pkg_type))
specs_from_cli.extend(matching) specs_from_cli.extend(matching)
if has_errors: if has_errors:
tty.die(error_message) tty.die(error_message)
@ -94,14 +105,14 @@ def find_matching_specs(specs, allow_multiple_matches=False, force=False):
def installed_dependents(specs): def installed_dependents(specs):
"""Returns a dictionary that maps a spec with a list of its """Map each spec to a list of its installed dependents.
installed dependents
Args: Args:
specs: list of specs to be checked for dependents specs (list): list of Specs
Returns: Returns:
dictionary of installed dependents (dict): mapping from spec to lists of Environments
""" """
dependents = {} dependents = {}
for item in specs: for item in specs:
@ -114,6 +125,28 @@ def installed_dependents(specs):
return dependents 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): def do_uninstall(specs, force):
""" """
Uninstalls all the specs in a list. Uninstalls all the specs in a list.
@ -132,6 +165,12 @@ def do_uninstall(specs, force):
# want to uninstall. # want to uninstall.
spack.package.Package.uninstall_by_spec(item, force=True) 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 # Sort packages to be uninstalled by the number of installed dependents
# This ensures we do things in the right order # This ensures we do things in the right order
def num_installed_deps(pkg): def num_installed_deps(pkg):
@ -143,6 +182,10 @@ def num_installed_deps(pkg):
for item in packages: for item in packages:
item.do_uninstall(force=force) 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): def get_uninstall_list(args, specs):
# Gets the list of installed specs that match the ones give via cli # 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) uninstall_list = find_matching_specs(specs, args.all, args.force)
# Takes care of '-R' # Takes care of '-R'
dependent_list = installed_dependents(uninstall_list) spec_dependents = installed_dependents(uninstall_list)
spec_envs = dependent_environments(uninstall_list)
# Process dependent_list and update uninstall_list # Process spec_dependents and update uninstall_list
has_error = False has_error = not args.force and (
if dependent_list and not args.dependents and not args.force: (spec_dependents and not args.dependents) or
for spec, lst in dependent_list.items(): spec_envs)
tty.error("Will not uninstall %s" % spec.cformat("$_$@$%@$/"))
print('') # 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:') print('The following packages depend on it:')
spack.cmd.display_specs(lst, **display_args) spack.cmd.display_specs(dependents, **display_args)
print('')
has_error = True 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)
elif args.dependents: elif args.dependents:
for key, lst in dependent_list.items(): for spec, lst in spec_dependents.items():
uninstall_list.extend(lst) uninstall_list.extend(lst)
uninstall_list = list(set(uninstall_list)) uninstall_list = list(set(uninstall_list))
if has_error:
tty.die('Use `spack uninstall --dependents` '
'to uninstall these dependencies as well.')
return uninstall_list return uninstall_list
@ -196,5 +268,6 @@ def uninstall(parser, args):
tty.die('uninstall requires at least one package argument.', tty.die('uninstall requires at least one package argument.',
' Use `spack uninstall --all` to uninstall ALL packages.') ' Use `spack uninstall --all` to uninstall ALL packages.')
# [any] here handles the --all case by forcing all specs to be returned
uninstall_specs( uninstall_specs(
args, spack.cmd.parse_specs(args.packages) if args.packages else [any]) 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] return yaml_data[key]
def list_environments(): def all_environment_names():
"""List the names of environments that currently exist.""" """List the names of environments that currently exist."""
# just return empty if the env path does not exist. A read-only # just return empty if the env path does not exist. A read-only
# operation like list should not try to create a directory. # operation like list should not try to create a directory.
@ -258,6 +258,12 @@ def list_environments():
return names return names
def all_environments():
"""Generator for all named Environments."""
for name in all_environment_names():
yield read(name)
def validate(data, filename=None): def validate(data, filename=None):
global _validator global _validator
if _validator is None: if _validator is None:
@ -362,6 +368,11 @@ def name(self):
else: else:
return self.path return self.path
@property
def active(self):
"""True if this environment is currently active."""
return active and self.path == active.path
@property @property
def manifest_path(self): def manifest_path(self):
"""Path to spack.yaml file in this environment.""" """Path to spack.yaml file in this environment."""
@ -480,8 +491,7 @@ def remove(self, query_spec, force=False):
specs_hashes = zip( specs_hashes = zip(
self.concretized_user_specs, self.concretized_order) self.concretized_user_specs, self.concretized_order)
matches = [ matches = [
s for s, h in specs_hashes s for s, h in specs_hashes if query_spec.dag_hash() == h]
if s.satisfies(query_spec) or query_spec.dag_hash() == h]
if not matches: if not matches:
raise EnvError("Not found: {0}".format(query_spec)) raise EnvError("Not found: {0}".format(query_spec))
@ -615,12 +625,6 @@ def install_all(self, args=None):
os.remove(build_log_link) os.remove(build_log_link)
os.symlink(spec.package.build_log_path, 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): def status(self, stream, **kwargs):
"""List the specs in an environment.""" """List the specs in an environment."""
tty.msg('In environment %s' % self.name) 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] current = [(s, c) for s, c in concretized if s in self.user_specs]
def write_kind(s): 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): 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: if added:
write_kind('added:') write_kind('added:')
@ -661,21 +665,42 @@ def write_user_spec(s, c):
write_user_spec(s, 'r') write_user_spec(s, 'r')
stream.write(c.tree(**kwargs)) 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): def _get_environment_specs(self, recurse_dependencies=True):
"""Returns the specs of all the packages in an environment. """Returns the specs of all the packages in an environment.
If these specs appear under different user_specs, only one copy 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 = {} package_to_spec = {}
spec_list = list() spec_list = list()
for spec_hash in self.concretized_order: for spec_hash in self.concretized_order:
spec = self.specs_by_hash[spec_hash] spec = self.specs_by_hash[spec_hash]
specs = spec.traverse(deptype=('link', 'run')) \ specs = (spec.traverse(deptype=('link', 'run'))
if recurse_dependencies else (spec,) if recurse_dependencies else (spec,))
for dep in specs: for dep in specs:
if dep.name in package_to_spec: prior = package_to_spec.get(dep.name)
tty.warn("{0} takes priority over {1}" if prior and prior != dep:
tty.debug("{0} takes priority over {1}"
.format(package_to_spec[dep.name].format(), .format(package_to_spec[dep.name].format(),
dep.format())) dep.format()))
else: else:

View File

@ -27,6 +27,7 @@
remove = SpackCommand('remove') remove = SpackCommand('remove')
concretize = SpackCommand('concretize') concretize = SpackCommand('concretize')
stage = SpackCommand('stage') stage = SpackCommand('stage')
uninstall = SpackCommand('uninstall')
def test_add(): 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 # these have an optional env arg and raise errors via tty.die
with pytest.raises(spack.main.SpackCommandError): with pytest.raises(spack.main.SpackCommandError):
env('loads') env('loads')
with pytest.raises(spack.main.SpackCommandError):
env('uninstall')
# This should NOT raise an error with no environment # This should NOT raise an error with no environment
# it just tells the user there isn't an environment # it just tells the user there isn't an environment
env('status') 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