spack uninstall: reduce verbosity with named environments (#34001)
This commit is contained in:
parent
bf71b78094
commit
35e1dc8eba
@ -6,6 +6,7 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from llnl.util import tty
|
||||
from llnl.util.tty.colify import colify
|
||||
@ -16,6 +17,7 @@
|
||||
import spack.error
|
||||
import spack.package_base
|
||||
import spack.repo
|
||||
import spack.spec
|
||||
import spack.store
|
||||
import spack.traverse as traverse
|
||||
from spack.database import InstallStatuses
|
||||
@ -78,15 +80,19 @@ def setup_parser(subparser):
|
||||
)
|
||||
|
||||
|
||||
def find_matching_specs(env, specs, allow_multiple_matches=False, force=False, origin=None):
|
||||
def find_matching_specs(
|
||||
env: Optional[ev.Environment],
|
||||
specs: List[spack.spec.Spec],
|
||||
allow_multiple_matches: bool = False,
|
||||
origin=None,
|
||||
) -> List[spack.spec.Spec]:
|
||||
"""Returns a list of specs matching the not necessarily
|
||||
concretized specs given from cli
|
||||
|
||||
Args:
|
||||
env (spack.environment.Environment): active environment, or ``None``
|
||||
if there is not one
|
||||
specs (list): list of specs to be matched against installed packages
|
||||
allow_multiple_matches (bool): if True multiple matches are admitted
|
||||
env: optional active environment
|
||||
specs: list of specs to be matched against installed packages
|
||||
allow_multiple_matches: if True multiple matches are admitted
|
||||
|
||||
Return:
|
||||
list: list of specs
|
||||
@ -128,91 +134,52 @@ def find_matching_specs(env, specs, allow_multiple_matches=False, force=False, o
|
||||
return specs_from_cli
|
||||
|
||||
|
||||
def installed_runtime_dependents(specs, env):
|
||||
"""Map each spec to a list of its installed dependents.
|
||||
def installed_dependents(specs: List[spack.spec.Spec]) -> List[spack.spec.Spec]:
|
||||
# Note: the combination of arguments (in particular order=breadth
|
||||
# and root=False) ensures dependents and matching_specs are non-overlapping;
|
||||
# In the extreme case of "spack uninstall --all" we get the entire database as
|
||||
# input; in that case we return an empty list.
|
||||
|
||||
Args:
|
||||
specs (list): list of Specs
|
||||
env (spack.environment.Environment or None): the active environment, or None
|
||||
def is_installed(spec):
|
||||
record = spack.store.db.query_local_by_spec_hash(spec.dag_hash())
|
||||
return record and record.installed
|
||||
|
||||
Returns:
|
||||
tuple: two mappings: one from specs to their dependent installs in the
|
||||
active environment, and one from specs to dependent installs outside of
|
||||
the active environment.
|
||||
specs = traverse.traverse_nodes(
|
||||
specs,
|
||||
root=False,
|
||||
order="breadth",
|
||||
cover="nodes",
|
||||
deptype=("link", "run"),
|
||||
direction="parents",
|
||||
key=lambda s: s.dag_hash(),
|
||||
)
|
||||
|
||||
Every installed dependent spec is listed once.
|
||||
|
||||
If there is not current active environment, the first mapping will be
|
||||
empty.
|
||||
"""
|
||||
active_dpts = {}
|
||||
outside_dpts = {}
|
||||
|
||||
env_hashes = set(env.all_hashes()) if env else set()
|
||||
|
||||
# Ensure we stop traversal at input specs.
|
||||
visited = set(s.dag_hash() for s in specs)
|
||||
|
||||
for spec in specs:
|
||||
for dpt in traverse.traverse_nodes(
|
||||
spec.dependents(deptype=("link", "run")),
|
||||
direction="parents",
|
||||
visited=visited,
|
||||
deptype=("link", "run"),
|
||||
root=True,
|
||||
key=lambda s: s.dag_hash(),
|
||||
):
|
||||
hash = dpt.dag_hash()
|
||||
# Ensure that all the specs we get are installed
|
||||
record = spack.store.db.query_local_by_spec_hash(hash)
|
||||
if record is None or not record.installed:
|
||||
continue
|
||||
if hash in env_hashes:
|
||||
active_dpts.setdefault(spec, set()).add(dpt)
|
||||
else:
|
||||
outside_dpts.setdefault(spec, set()).add(dpt)
|
||||
|
||||
return active_dpts, outside_dpts
|
||||
return [spec for spec in specs if is_installed(spec)]
|
||||
|
||||
|
||||
def dependent_environments(specs):
|
||||
"""Map each spec to environments that depend on it.
|
||||
def dependent_environments(
|
||||
specs: List[spack.spec.Spec], current_env: Optional[ev.Environment] = None
|
||||
) -> Dict[ev.Environment, List[spack.spec.Spec]]:
|
||||
# For each tracked environment, get the specs we would uninstall from it.
|
||||
# Don't instantiate current environment twice.
|
||||
env_names = ev.all_environment_names()
|
||||
if current_env:
|
||||
env_names = (name for name in env_names if name != current_env.name)
|
||||
|
||||
Args:
|
||||
specs (list): list of Specs
|
||||
# Mapping from Environment -> non-zero list of specs contained in it.
|
||||
other_envs_to_specs: Dict[ev.Environment, List[spack.spec.Spec]] = {}
|
||||
for other_env in (ev.Environment(ev.root(name)) for name in env_names):
|
||||
specs_in_other_env = all_specs_in_env(other_env, specs)
|
||||
if specs_in_other_env:
|
||||
other_envs_to_specs[other_env] = specs_in_other_env
|
||||
|
||||
Returns:
|
||||
dict: mapping from spec to lists of dependent Environments
|
||||
"""
|
||||
dependents = {}
|
||||
for env in ev.all_environments():
|
||||
hashes = set(env.all_hashes())
|
||||
for spec in specs:
|
||||
if spec.dag_hash() in hashes:
|
||||
dependents.setdefault(spec, []).append(env)
|
||||
return dependents
|
||||
return other_envs_to_specs
|
||||
|
||||
|
||||
def inactive_dependent_environments(spec_envs):
|
||||
"""Strip the active environment from a dependent map.
|
||||
|
||||
Take the output of ``dependent_environment()`` and remove the active
|
||||
environment from all mappings. Remove any specs in the map that now
|
||||
have no dependent environments. Return the result.
|
||||
|
||||
Args:
|
||||
spec_envs (dict): mapping from spec to lists of dependent Environments
|
||||
|
||||
Returns:
|
||||
dict: mapping from spec to lists of *inactive* dependent Environments
|
||||
"""
|
||||
spec_inactive_envs = {}
|
||||
for spec, de_list in spec_envs.items():
|
||||
inactive = [de for de in de_list if not de.active]
|
||||
if inactive:
|
||||
spec_inactive_envs[spec] = inactive
|
||||
|
||||
return spec_inactive_envs
|
||||
def all_specs_in_env(env: ev.Environment, specs: List[spack.spec.Spec]) -> List[spack.spec.Spec]:
|
||||
"""Given a list of specs, return those that are in the env"""
|
||||
hashes = set(env.all_hashes())
|
||||
return [s for s in specs if s.dag_hash() in hashes]
|
||||
|
||||
|
||||
def _remove_from_env(spec, env):
|
||||
@ -225,7 +192,7 @@ def _remove_from_env(spec, env):
|
||||
pass # ignore non-root specs
|
||||
|
||||
|
||||
def do_uninstall(specs, force=False):
|
||||
def do_uninstall(specs: List[spack.spec.Spec], force: bool = False):
|
||||
# TODO: get rid of the call-sites that use this function,
|
||||
# so that we don't have to do a dance of list -> set -> list -> set
|
||||
hashes_to_remove = set(s.dag_hash() for s in specs)
|
||||
@ -237,102 +204,56 @@ def do_uninstall(specs, force=False):
|
||||
spack.package_base.PackageBase.uninstall_by_spec(s, force=force)
|
||||
|
||||
|
||||
def get_uninstall_list(args, specs, env):
|
||||
"""Returns uninstall_list and remove_list: these may overlap (some things
|
||||
def get_uninstall_list(args, specs: List[spack.spec.Spec], env: Optional[ev.Environment]):
|
||||
"""Returns unordered uninstall_list and remove_list: these may overlap (some things
|
||||
may be both uninstalled and removed from the current environment).
|
||||
|
||||
It is assumed we are in an environment if --remove is specified (this
|
||||
method raises an exception otherwise).
|
||||
|
||||
uninstall_list is topologically sorted: dependents come before
|
||||
dependencies (so if a user uninstalls specs in the order provided,
|
||||
the dependents will always be uninstalled first).
|
||||
"""
|
||||
method raises an exception otherwise)."""
|
||||
if args.remove and not env:
|
||||
raise ValueError("Can only use --remove when in an environment")
|
||||
|
||||
# Gets the list of installed specs that match the ones given via cli
|
||||
# args.all takes care of the case where '-a' is given in the cli
|
||||
base_uninstall_specs = set(find_matching_specs(env, specs, args.all, args.force))
|
||||
matching_specs = find_matching_specs(env, specs, args.all)
|
||||
dependent_specs = installed_dependents(matching_specs)
|
||||
all_uninstall_specs = matching_specs + dependent_specs if args.dependents else matching_specs
|
||||
other_dependent_envs = dependent_environments(all_uninstall_specs, current_env=env)
|
||||
|
||||
active_dpts, outside_dpts = installed_runtime_dependents(base_uninstall_specs, env)
|
||||
# It will be useful to track the unified set of specs with dependents, as
|
||||
# well as to separately track specs in the current env with dependents
|
||||
spec_to_dpts = {}
|
||||
for spec, dpts in active_dpts.items():
|
||||
spec_to_dpts[spec] = list(dpts)
|
||||
for spec, dpts in outside_dpts.items():
|
||||
if spec in spec_to_dpts:
|
||||
spec_to_dpts[spec].extend(dpts)
|
||||
else:
|
||||
spec_to_dpts[spec] = list(dpts)
|
||||
# There are dependents and we didn't ask to remove dependents
|
||||
dangling_dependents = dependent_specs and not args.dependents
|
||||
|
||||
all_uninstall_specs = set(base_uninstall_specs)
|
||||
if args.dependents:
|
||||
for spec, lst in active_dpts.items():
|
||||
all_uninstall_specs.update(lst)
|
||||
for spec, lst in outside_dpts.items():
|
||||
all_uninstall_specs.update(lst)
|
||||
# An environment different than the current env depends on
|
||||
# one or more of the list of all specs to be uninstalled.
|
||||
dangling_environments = not args.remove and other_dependent_envs
|
||||
|
||||
# For each spec that we intend to uninstall, this tracks the set of
|
||||
# environments outside the current active environment which depend on the
|
||||
# spec. There may be environments not managed directly with Spack: such
|
||||
# environments would not be included here.
|
||||
spec_to_other_envs = inactive_dependent_environments(
|
||||
dependent_environments(all_uninstall_specs)
|
||||
)
|
||||
|
||||
has_error = not args.force and (
|
||||
# There are dependents in the current env and we didn't ask to remove
|
||||
# dependents
|
||||
(spec_to_dpts and not args.dependents)
|
||||
# An environment different than the current env (if any) depends on
|
||||
# one or more of the specs to be uninstalled. There may also be
|
||||
# packages in those envs which depend on the base set of packages
|
||||
# to uninstall, but this covers that scenario.
|
||||
or (not args.remove and spec_to_other_envs)
|
||||
)
|
||||
has_error = not args.force and (dangling_dependents or dangling_environments)
|
||||
|
||||
if has_error:
|
||||
# say why each problem spec is needed
|
||||
specs = set(spec_to_dpts)
|
||||
specs.update(set(spec_to_other_envs)) # environments depend on this
|
||||
|
||||
for i, spec in enumerate(sorted(specs)):
|
||||
# space out blocks of reasons
|
||||
if i > 0:
|
||||
print()
|
||||
|
||||
spec_format = "{name}{@version}{%compiler}{/hash:7}"
|
||||
tty.info("Will not uninstall %s" % spec.cformat(spec_format), format="*r")
|
||||
|
||||
dependents = spec_to_dpts.get(spec)
|
||||
if dependents and not args.dependents:
|
||||
print("The following packages depend on it:")
|
||||
spack.cmd.display_specs(dependents, **display_args)
|
||||
|
||||
envs = spec_to_other_envs.get(spec)
|
||||
if envs:
|
||||
if env:
|
||||
env_context_qualifier = " other"
|
||||
else:
|
||||
env_context_qualifier = ""
|
||||
print("It is used by the following{0} environments:".format(env_context_qualifier))
|
||||
colify([e.name for e in envs], indent=4)
|
||||
|
||||
msgs = []
|
||||
if spec_to_dpts and not args.dependents:
|
||||
tty.info("Refusing to uninstall the following specs")
|
||||
spack.cmd.display_specs(matching_specs, **display_args)
|
||||
if dangling_dependents:
|
||||
print()
|
||||
tty.info("The following dependents are still installed:")
|
||||
spack.cmd.display_specs(dependent_specs, **display_args)
|
||||
msgs.append("use `spack uninstall --dependents` to remove dependents too")
|
||||
if spec_to_other_envs:
|
||||
msgs.append("use `spack env remove` to remove from environments")
|
||||
if dangling_environments:
|
||||
print()
|
||||
tty.info("The following environments still reference these specs:")
|
||||
colify([e.name for e in other_dependent_envs.keys()], indent=4)
|
||||
msgs.append("use `spack env remove` to remove environments")
|
||||
msgs.append("use `spack uninstall --force` to override")
|
||||
print()
|
||||
tty.die("There are still dependents.", *msgs)
|
||||
|
||||
# If we are in an environment, this will track specs in this environment
|
||||
# which should only be removed from the environment rather than uninstalled
|
||||
remove_only = set()
|
||||
remove_only = []
|
||||
if args.remove and not args.force:
|
||||
remove_only.update(spec_to_other_envs)
|
||||
for specs_in_other_env in other_dependent_envs.values():
|
||||
remove_only.extend(specs_in_other_env)
|
||||
|
||||
if remove_only:
|
||||
tty.info(
|
||||
"The following specs will be removed but not uninstalled because"
|
||||
@ -344,23 +265,9 @@ def get_uninstall_list(args, specs, env):
|
||||
# Compute the set of specs that should be removed from the current env.
|
||||
# This may overlap (some specs may be uninstalled and also removed from
|
||||
# the current environment).
|
||||
if args.remove:
|
||||
remove_specs = set(base_uninstall_specs)
|
||||
if args.dependents:
|
||||
# Any spec matched from the cli, or dependent of, should be removed
|
||||
# from the environment
|
||||
for spec, lst in active_dpts.items():
|
||||
remove_specs.update(lst)
|
||||
else:
|
||||
remove_specs = set()
|
||||
remove_specs = all_specs_in_env(env, all_uninstall_specs) if env and args.remove else []
|
||||
|
||||
all_uninstall_specs -= remove_only
|
||||
# Inefficient topological sort: uninstall dependents before dependencies
|
||||
all_uninstall_specs = sorted(
|
||||
all_uninstall_specs, key=lambda x: sum(1 for i in x.traverse()), reverse=True
|
||||
)
|
||||
|
||||
return list(all_uninstall_specs), list(remove_specs)
|
||||
return list(set(all_uninstall_specs) - set(remove_only)), remove_specs
|
||||
|
||||
|
||||
def uninstall_specs(args, specs):
|
||||
@ -387,13 +294,13 @@ def uninstall_specs(args, specs):
|
||||
env.regenerate_views()
|
||||
|
||||
|
||||
def confirm_removal(specs):
|
||||
def confirm_removal(specs: List[spack.spec.Spec]):
|
||||
"""Display the list of specs to be removed and ask for confirmation.
|
||||
|
||||
Args:
|
||||
specs (list): specs to be removed
|
||||
specs: specs to be removed
|
||||
"""
|
||||
tty.msg("The following packages will be uninstalled:\n")
|
||||
tty.msg("The following {} packages will be uninstalled:\n".format(len(specs)))
|
||||
spack.cmd.display_specs(specs, **display_args)
|
||||
print("")
|
||||
answer = tty.get_yes_or_no("Do you want to proceed?", default=False)
|
||||
|
@ -229,7 +229,7 @@ def deactivate():
|
||||
_active_environment = None
|
||||
|
||||
|
||||
def active_environment():
|
||||
def active_environment() -> Optional["Environment"]:
|
||||
"""Returns the active environment when there is any"""
|
||||
return _active_environment
|
||||
|
||||
|
@ -402,8 +402,9 @@ def parse(self, initial_spec: spack.spec.Spec) -> spack.spec.Spec:
|
||||
elif not self.has_hash and self.ctx.accept(TokenType.DAG_HASH):
|
||||
dag_hash = self.ctx.current_token.value[1:]
|
||||
matches = []
|
||||
if spack.environment.active_environment():
|
||||
matches = spack.environment.active_environment().get_by_hash(dag_hash)
|
||||
active_env = spack.environment.active_environment()
|
||||
if active_env:
|
||||
matches = active_env.get_by_hash(dag_hash)
|
||||
if not matches:
|
||||
matches = spack.store.db.get_by_hash(dag_hash)
|
||||
if not matches:
|
||||
|
@ -1049,7 +1049,7 @@ def test_env_blocks_uninstall(mock_stage, mock_fetch, install_mockery):
|
||||
|
||||
out = uninstall("mpileaks", fail_on_error=False)
|
||||
assert uninstall.returncode == 1
|
||||
assert "used by the following environments" in out
|
||||
assert "The following environments still reference these specs" in out
|
||||
|
||||
|
||||
def test_roots_display_with_variants():
|
||||
|
@ -3,7 +3,6 @@
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
@ -58,14 +57,11 @@ def test_correct_installed_dependents(mutable_database):
|
||||
callpath.package.do_uninstall(force=True)
|
||||
|
||||
# Retrieve all dependent hashes
|
||||
inside_dpts, outside_dpts = spack.cmd.uninstall.installed_runtime_dependents(
|
||||
dependencies, None
|
||||
)
|
||||
dependent_hashes = [s.dag_hash() for s in itertools.chain(*outside_dpts.values())]
|
||||
set_dependent_hashes = set(dependent_hashes)
|
||||
dependents = spack.cmd.uninstall.installed_dependents(dependencies)
|
||||
assert dependents
|
||||
|
||||
# We dont have an env, so this should be empty.
|
||||
assert not inside_dpts
|
||||
dependent_hashes = [s.dag_hash() for s in dependents]
|
||||
set_dependent_hashes = set(dependent_hashes)
|
||||
|
||||
# Assert uniqueness
|
||||
assert len(dependent_hashes) == len(set_dependent_hashes)
|
||||
|
Loading…
Reference in New Issue
Block a user