Source code for spack.cmd.uninstall

# 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)