# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import sys
from typing import Dict, List, Optional
from llnl.util import tty
from llnl.util.tty.colify import colify
import spack.cmd
import spack.cmd.common.confirmation as confirmation
import spack.environment as ev
import spack.package_base
import spack.spec
import spack.store
import spack.traverse as traverse
from spack.cmd.common import arguments
from spack.database import InstallStatuses
description = "remove installed packages"
section = "build"
level = "short"
error_message = """You can either:
a) use a more specific spec, or
b) specify the spec by its hash (e.g. `spack uninstall /hash`), or
c) use `spack uninstall --all` to uninstall ALL matching specs.
"""
# Arguments for display_specs when we find ambiguity
display_args = {"long": True, "show_flags": False, "variants": False, "indent": 4}
[docs]
def setup_parser(subparser):
epilog_msg = (
"Specs to be uninstalled are specified using the spec syntax"
" (`spack help --spec`) and can be identified by their "
"hashes. To remove packages that are needed only at build "
"time and were not explicitly installed see `spack gc -h`."
"\n\nWhen using the --all option ALL packages matching the "
"supplied specs will be uninstalled. For instance, "
"`spack uninstall --all libelf` uninstalls all the versions "
"of `libelf` currently present in Spack's store. 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.epilog = epilog_msg
subparser.add_argument(
"-f",
"--force",
action="store_true",
dest="force",
help="remove regardless of whether other packages or environments depend on this one",
)
subparser.add_argument(
"--remove",
action="store_true",
dest="remove",
help="if in an environment, then the spec should also be removed from "
"the environment description",
)
arguments.add_common_arguments(
subparser, ["recurse_dependents", "yes_to_all", "installed_specs"]
)
subparser.add_argument(
"-a",
"--all",
action="store_true",
dest="all",
help="remove ALL installed packages that match each supplied spec",
)
subparser.add_argument(
"--origin", dest="origin", help="only remove DB records with the specified origin"
)
[docs]
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: 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
"""
# constrain uninstall resolution to current environment if one is active
hashes = env.all_hashes() if env else None
# List of specs that match expressions given via command line
specs_from_cli = []
has_errors = False
for spec in specs:
install_query = [InstallStatuses.INSTALLED, InstallStatuses.DEPRECATED]
matching = spack.store.STORE.db.query_local(
spec, hashes=hashes, installed=install_query, origin=origin
)
# 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:
tty.error("{0} matches multiple packages:".format(spec))
sys.stderr.write("\n")
spack.cmd.display_specs(matching, output=sys.stderr, **display_args)
sys.stderr.write("\n")
sys.stderr.flush()
has_errors = True
# No installed package matches the query
if len(matching) == 0 and spec is not any:
if env:
pkg_type = "packages in environment '%s'" % env.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)
return specs_from_cli
[docs]
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.
def is_installed(spec):
record = spack.store.STORE.db.query_local_by_spec_hash(spec.dag_hash())
return record and record.installed
specs = traverse.traverse_nodes(
specs,
root=False,
order="breadth",
cover="nodes",
deptype=("link", "run"),
direction="parents",
key=lambda s: s.dag_hash(),
)
return [spec for spec in specs if is_installed(spec)]
[docs]
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)
# 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
return other_envs_to_specs
[docs]
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):
"""Remove a spec from an environment if it is a root."""
try:
# try removing the spec from the current active
# environment. this will fail if the spec is not a root
env.remove(spec, force=True)
except ev.SpackEnvironmentError:
pass # ignore non-root specs
[docs]
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)
for s in traverse.traverse_nodes(
specs, order="topo", direction="children", root=True, cover="nodes", deptype="all"
):
if s.dag_hash() in hashes_to_remove:
spack.package_base.PackageBase.uninstall_by_spec(s, force=force)
[docs]
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)."""
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
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)
# There are dependents and we didn't ask to remove dependents
dangling_dependents = dependent_specs and not args.dependents
# 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
has_error = not args.force and (dangling_dependents or dangling_environments)
if has_error:
msgs = []
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 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 = []
if args.remove and not args.force:
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"
" they are also used by another environment: {speclist}".format(
speclist=", ".join(x.name for x in remove_only)
)
)
# 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).
remove_specs = all_specs_in_env(env, all_uninstall_specs) if env and args.remove else []
return list(set(all_uninstall_specs) - set(remove_only)), remove_specs
[docs]
def uninstall_specs(args, specs):
env = ev.active_environment()
uninstall_list, remove_list = get_uninstall_list(args, specs, env)
if not uninstall_list:
tty.warn("There are no package to uninstall.")
return
if not args.yes_to_all:
confirmation.confirm_action(uninstall_list, "uninstalled", "uninstall")
# Uninstall everything on the list
do_uninstall(uninstall_list, args.force)
if env:
with env.write_transaction():
for spec in remove_list:
_remove_from_env(spec, env)
env.write()
env.regenerate_views()
[docs]
def uninstall(parser, args):
if not args.specs and not args.all:
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
specs = spack.cmd.parse_specs(args.specs) if args.specs else [any]
uninstall_specs(args, specs)