Source code for spack.cmd.uninstall

# Copyright 2013-2023 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)

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

import spack.cmd
import spack.cmd.common.arguments as arguments
import spack.environment as ev
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

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.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.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: confirm_removal(uninstall_list) # 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 confirm_removal(specs: List[spack.spec.Spec]): """Display the list of specs to be removed and ask for confirmation. Args: specs: specs to be removed """ 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) if not answer: tty.msg("Aborting uninstallation") sys.exit(0)
[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)