spack uninstall: follow run/link edges on --dependents (#34058)

`spack gc` removes build deps of explicitly installed specs, but somehow
if you take one of the specs that `spack gc` would remove, and feed it
to `spack uninstall /<hash>` by hash, it complains about all the
dependents that still rely on it.

This resolves the inconsistency by only following run/link type deps in
spack uninstall.

That way you can finally do `spack uninstall cmake` without having to
remove all packages built with cmake.
This commit is contained in:
Harmen Stoppels 2023-02-16 14:26:30 +01:00 committed by GitHub
parent 50691ccdd9
commit 09eb86e077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 42 deletions

View File

@ -133,7 +133,7 @@ def find_matching_specs(env, specs, allow_multiple_matches=False, force=False, o
return specs_from_cli return specs_from_cli
def installed_dependents(specs, env): def installed_runtime_dependents(specs, env):
"""Map each spec to a list of its installed dependents. """Map each spec to a list of its installed dependents.
Args: Args:
@ -160,10 +160,10 @@ def installed_dependents(specs, env):
for spec in specs: for spec in specs:
for dpt in traverse.traverse_nodes( for dpt in traverse.traverse_nodes(
spec.dependents(deptype="all"), spec.dependents(deptype=("link", "run")),
direction="parents", direction="parents",
visited=visited, visited=visited,
deptype="all", deptype=("link", "run"),
root=True, root=True,
key=lambda s: s.dag_hash(), key=lambda s: s.dag_hash(),
): ):
@ -265,7 +265,7 @@ def get_uninstall_list(args, specs, env):
# args.all takes care of the case where '-a' is given in the 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)) base_uninstall_specs = set(find_matching_specs(env, specs, args.all, args.force))
active_dpts, outside_dpts = installed_dependents(base_uninstall_specs, 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 # 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 # well as to separately track specs in the current env with dependents
spec_to_dpts = {} spec_to_dpts = {}

View File

@ -50,15 +50,17 @@ def test_correct_installed_dependents(mutable_database):
callpath = spack.store.db.query_local("callpath")[0] callpath = spack.store.db.query_local("callpath")[0]
# Ensure it still has dependents and dependencies # Ensure it still has dependents and dependencies
dependents = callpath.dependents(deptype="all") dependents = callpath.dependents(deptype=("run", "link"))
dependencies = callpath.dependencies(deptype="all") dependencies = callpath.dependencies(deptype=("run", "link"))
assert dependents and dependencies assert dependents and dependencies
# Uninstall it, so it's missing. # Uninstall it, so it's missing.
callpath.package.do_uninstall(force=True) callpath.package.do_uninstall(force=True)
# Retrieve all dependent hashes # Retrieve all dependent hashes
inside_dpts, outside_dpts = spack.cmd.uninstall.installed_dependents(dependencies, None) 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())] dependent_hashes = [s.dag_hash() for s in itertools.chain(*outside_dpts.values())]
set_dependent_hashes = set(dependent_hashes) set_dependent_hashes = set(dependent_hashes)
@ -213,9 +215,9 @@ class TestUninstallFromEnv(object):
"""Tests an installation with two environments e1 and e2, which each have """Tests an installation with two environments e1 and e2, which each have
shared package installations: shared package installations:
e1 has dt-diamond-left -> dt-diamond-bottom e1 has diamond-link-left -> diamond-link-bottom
e2 has dt-diamond-right -> dt-diamond-bottom e2 has diamond-link-right -> diamond-link-bottom
""" """
env = SpackCommand("env") env = SpackCommand("env")
@ -230,16 +232,16 @@ def environment_setup(
TestUninstallFromEnv.env("create", "e1") TestUninstallFromEnv.env("create", "e1")
e1 = spack.environment.read("e1") e1 = spack.environment.read("e1")
with e1: with e1:
TestUninstallFromEnv.add("dt-diamond-left") TestUninstallFromEnv.add("diamond-link-left")
TestUninstallFromEnv.add("dt-diamond-bottom") TestUninstallFromEnv.add("diamond-link-bottom")
TestUninstallFromEnv.concretize() TestUninstallFromEnv.concretize()
install("--fake") install("--fake")
TestUninstallFromEnv.env("create", "e2") TestUninstallFromEnv.env("create", "e2")
e2 = spack.environment.read("e2") e2 = spack.environment.read("e2")
with e2: with e2:
TestUninstallFromEnv.add("dt-diamond-right") TestUninstallFromEnv.add("diamond-link-right")
TestUninstallFromEnv.add("dt-diamond-bottom") TestUninstallFromEnv.add("diamond-link-bottom")
TestUninstallFromEnv.concretize() TestUninstallFromEnv.concretize()
install("--fake") install("--fake")
@ -251,47 +253,47 @@ def test_basic_env_sanity(self, environment_setup):
assert concretized_spec.package.installed assert concretized_spec.package.installed
def test_uninstall_force_dependency_shared_between_envs(self, environment_setup): def test_uninstall_force_dependency_shared_between_envs(self, environment_setup):
"""If you "spack uninstall -f --dependents dt-diamond-bottom" from """If you "spack uninstall -f --dependents diamond-link-bottom" from
e1, then all packages should be uninstalled (but not removed) from e1, then all packages should be uninstalled (but not removed) from
both e1 and e2. both e1 and e2.
""" """
e1 = spack.environment.read("e1") e1 = spack.environment.read("e1")
with e1: with e1:
uninstall("-f", "-y", "--dependents", "dt-diamond-bottom") uninstall("-f", "-y", "--dependents", "diamond-link-bottom")
# The specs should still be in the environment, since # The specs should still be in the environment, since
# --remove was not specified # --remove was not specified
assert set(root.name for (root, _) in e1.concretized_specs()) == set( assert set(root.name for (root, _) in e1.concretized_specs()) == set(
["dt-diamond-left", "dt-diamond-bottom"] ["diamond-link-left", "diamond-link-bottom"]
) )
for _, concretized_spec in e1.concretized_specs(): for _, concretized_spec in e1.concretized_specs():
assert not concretized_spec.package.installed assert not concretized_spec.package.installed
# Everything in e2 depended on dt-diamond-bottom, so should also # Everything in e2 depended on diamond-link-bottom, so should also
# have been uninstalled. The roots should be unchanged though. # have been uninstalled. The roots should be unchanged though.
e2 = spack.environment.read("e2") e2 = spack.environment.read("e2")
with e2: with e2:
assert set(root.name for (root, _) in e2.concretized_specs()) == set( assert set(root.name for (root, _) in e2.concretized_specs()) == set(
["dt-diamond-right", "dt-diamond-bottom"] ["diamond-link-right", "diamond-link-bottom"]
) )
for _, concretized_spec in e2.concretized_specs(): for _, concretized_spec in e2.concretized_specs():
assert not concretized_spec.package.installed assert not concretized_spec.package.installed
def test_uninstall_remove_dependency_shared_between_envs(self, environment_setup): def test_uninstall_remove_dependency_shared_between_envs(self, environment_setup):
"""If you "spack uninstall --dependents --remove dt-diamond-bottom" from """If you "spack uninstall --dependents --remove diamond-link-bottom" from
e1, then all packages are removed from e1 (it is now empty); e1, then all packages are removed from e1 (it is now empty);
dt-diamond-left is also uninstalled (since only e1 needs it) but diamond-link-left is also uninstalled (since only e1 needs it) but
dt-diamond-bottom is not uninstalled (since e2 needs it). diamond-link-bottom is not uninstalled (since e2 needs it).
""" """
e1 = spack.environment.read("e1") e1 = spack.environment.read("e1")
with e1: with e1:
dtdiamondleft = next( dtdiamondleft = next(
concrete concrete
for (_, concrete) in e1.concretized_specs() for (_, concrete) in e1.concretized_specs()
if concrete.name == "dt-diamond-left" if concrete.name == "diamond-link-left"
) )
output = uninstall("-y", "--dependents", "--remove", "dt-diamond-bottom") output = uninstall("-y", "--dependents", "--remove", "diamond-link-bottom")
assert "The following specs will be removed but not uninstalled" in output assert "The following specs will be removed but not uninstalled" in output
assert not list(e1.roots()) assert not list(e1.roots())
assert not dtdiamondleft.package.installed assert not dtdiamondleft.package.installed
@ -301,32 +303,32 @@ def test_uninstall_remove_dependency_shared_between_envs(self, environment_setup
e2 = spack.environment.read("e2") e2 = spack.environment.read("e2")
with e2: with e2:
assert set(root.name for (root, _) in e2.concretized_specs()) == set( assert set(root.name for (root, _) in e2.concretized_specs()) == set(
["dt-diamond-right", "dt-diamond-bottom"] ["diamond-link-right", "diamond-link-bottom"]
) )
for _, concretized_spec in e2.concretized_specs(): for _, concretized_spec in e2.concretized_specs():
assert concretized_spec.package.installed assert concretized_spec.package.installed
def test_uninstall_dependency_shared_between_envs_fail(self, environment_setup): def test_uninstall_dependency_shared_between_envs_fail(self, environment_setup):
"""If you "spack uninstall --dependents dt-diamond-bottom" from """If you "spack uninstall --dependents diamond-link-bottom" from
e1 (without --remove or -f), then this should fail (this is needed by e1 (without --remove or -f), then this should fail (this is needed by
e2). e2).
""" """
e1 = spack.environment.read("e1") e1 = spack.environment.read("e1")
with e1: with e1:
output = uninstall("-y", "--dependents", "dt-diamond-bottom", fail_on_error=False) output = uninstall("-y", "--dependents", "diamond-link-bottom", fail_on_error=False)
assert "There are still dependents." in output assert "There are still dependents." in output
assert "use `spack env remove`" in output assert "use `spack env remove`" in output
# The environment should be unchanged and nothing should have been # The environment should be unchanged and nothing should have been
# uninstalled # uninstalled
assert set(root.name for (root, _) in e1.concretized_specs()) == set( assert set(root.name for (root, _) in e1.concretized_specs()) == set(
["dt-diamond-left", "dt-diamond-bottom"] ["diamond-link-left", "diamond-link-bottom"]
) )
for _, concretized_spec in e1.concretized_specs(): for _, concretized_spec in e1.concretized_specs():
assert concretized_spec.package.installed assert concretized_spec.package.installed
def test_uninstall_force_and_remove_dependency_shared_between_envs(self, environment_setup): def test_uninstall_force_and_remove_dependency_shared_between_envs(self, environment_setup):
"""If you "spack uninstall -f --dependents --remove dt-diamond-bottom" from """If you "spack uninstall -f --dependents --remove diamond-link-bottom" from
e1, then all packages should be uninstalled and removed from e1. e1, then all packages should be uninstalled and removed from e1.
All packages will also be uninstalled from e2, but the roots will All packages will also be uninstalled from e2, but the roots will
remain unchanged. remain unchanged.
@ -336,53 +338,53 @@ def test_uninstall_force_and_remove_dependency_shared_between_envs(self, environ
dtdiamondleft = next( dtdiamondleft = next(
concrete concrete
for (_, concrete) in e1.concretized_specs() for (_, concrete) in e1.concretized_specs()
if concrete.name == "dt-diamond-left" if concrete.name == "diamond-link-left"
) )
uninstall("-f", "-y", "--dependents", "--remove", "dt-diamond-bottom") uninstall("-f", "-y", "--dependents", "--remove", "diamond-link-bottom")
assert not list(e1.roots()) assert not list(e1.roots())
assert not dtdiamondleft.package.installed assert not dtdiamondleft.package.installed
e2 = spack.environment.read("e2") e2 = spack.environment.read("e2")
with e2: with e2:
assert set(root.name for (root, _) in e2.concretized_specs()) == set( assert set(root.name for (root, _) in e2.concretized_specs()) == set(
["dt-diamond-right", "dt-diamond-bottom"] ["diamond-link-right", "diamond-link-bottom"]
) )
for _, concretized_spec in e2.concretized_specs(): for _, concretized_spec in e2.concretized_specs():
assert not concretized_spec.package.installed assert not concretized_spec.package.installed
def test_uninstall_keep_dependents_dependency_shared_between_envs(self, environment_setup): def test_uninstall_keep_dependents_dependency_shared_between_envs(self, environment_setup):
"""If you "spack uninstall -f --remove dt-diamond-bottom" from """If you "spack uninstall -f --remove diamond-link-bottom" from
e1, then dt-diamond-bottom should be uninstalled, which leaves e1, then diamond-link-bottom should be uninstalled, which leaves
"dangling" references in both environments, since "dangling" references in both environments, since
dt-diamond-left and dt-diamond-right both need it. diamond-link-left and diamond-link-right both need it.
""" """
e1 = spack.environment.read("e1") e1 = spack.environment.read("e1")
with e1: with e1:
dtdiamondleft = next( dtdiamondleft = next(
concrete concrete
for (_, concrete) in e1.concretized_specs() for (_, concrete) in e1.concretized_specs()
if concrete.name == "dt-diamond-left" if concrete.name == "diamond-link-left"
) )
uninstall("-f", "-y", "--remove", "dt-diamond-bottom") uninstall("-f", "-y", "--remove", "diamond-link-bottom")
# dt-diamond-bottom was removed from the list of roots (note that # diamond-link-bottom was removed from the list of roots (note that
# it would still be installed since dt-diamond-left depends on it) # it would still be installed since diamond-link-left depends on it)
assert set(x.name for x in e1.roots()) == set(["dt-diamond-left"]) assert set(x.name for x in e1.roots()) == set(["diamond-link-left"])
assert dtdiamondleft.package.installed assert dtdiamondleft.package.installed
e2 = spack.environment.read("e2") e2 = spack.environment.read("e2")
with e2: with e2:
assert set(root.name for (root, _) in e2.concretized_specs()) == set( assert set(root.name for (root, _) in e2.concretized_specs()) == set(
["dt-diamond-right", "dt-diamond-bottom"] ["diamond-link-right", "diamond-link-bottom"]
) )
dtdiamondright = next( dtdiamondright = next(
concrete concrete
for (_, concrete) in e2.concretized_specs() for (_, concrete) in e2.concretized_specs()
if concrete.name == "dt-diamond-right" if concrete.name == "diamond-link-right"
) )
assert dtdiamondright.package.installed assert dtdiamondright.package.installed
dtdiamondbottom = next( dtdiamondbottom = next(
concrete concrete
for (_, concrete) in e2.concretized_specs() for (_, concrete) in e2.concretized_specs()
if concrete.name == "dt-diamond-bottom" if concrete.name == "diamond-link-bottom"
) )
assert not dtdiamondbottom.package.installed assert not dtdiamondbottom.package.installed

View File

@ -0,0 +1,15 @@
# Copyright 2013-2022 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 spack.package import *
class DiamondLinkBottom(Package):
"""Part of diamond-link-{top,left,right,bottom} group"""
homepage = "http://www.example.com"
url = "http://www.example.com/diamond-link-bottom-1.0.tar.gz"
version("1.0", "0123456789abcdef0123456789abcdef")

View File

@ -0,0 +1,17 @@
# Copyright 2013-2022 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 spack.package import *
class DiamondLinkLeft(Package):
"""Part of diamond-link-{top,left,right,bottom} group"""
homepage = "http://www.example.com"
url = "http://www.example.com/diamond-link-left-1.0.tar.gz"
version("1.0", "0123456789abcdef0123456789abcdef")
depends_on("diamond-link-bottom", type="link")

View File

@ -0,0 +1,17 @@
# Copyright 2013-2022 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 spack.package import *
class DiamondLinkRight(Package):
"""Part of diamond-link-{top,left,right,bottom} group"""
homepage = "http://www.example.com"
url = "http://www.example.com/diamond-link-right-1.0.tar.gz"
version("1.0", "0123456789abcdef0123456789abcdef")
depends_on("diamond-link-bottom", type="link")

View File

@ -0,0 +1,18 @@
# Copyright 2013-2022 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 spack.package import *
class DiamondLinkTop(Package):
"""Part of diamond-link-{top,left,right,bottom} group"""
homepage = "http://www.example.com"
url = "http://www.example.com/diamond-link-top-1.0.tar.gz"
version("1.0", "0123456789abcdef0123456789abcdef")
depends_on("diamond-link-left", type="link")
depends_on("diamond-link-right", type="link")