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
def installed_dependents(specs, env):
def installed_runtime_dependents(specs, env):
"""Map each spec to a list of its installed dependents.
Args:
@ -160,10 +160,10 @@ def installed_dependents(specs, env):
for spec in specs:
for dpt in traverse.traverse_nodes(
spec.dependents(deptype="all"),
spec.dependents(deptype=("link", "run")),
direction="parents",
visited=visited,
deptype="all",
deptype=("link", "run"),
root=True,
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
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
# well as to separately track specs in the current env with dependents
spec_to_dpts = {}

View File

@ -50,15 +50,17 @@ def test_correct_installed_dependents(mutable_database):
callpath = spack.store.db.query_local("callpath")[0]
# Ensure it still has dependents and dependencies
dependents = callpath.dependents(deptype="all")
dependencies = callpath.dependencies(deptype="all")
dependents = callpath.dependents(deptype=("run", "link"))
dependencies = callpath.dependencies(deptype=("run", "link"))
assert dependents and dependencies
# Uninstall it, so it's missing.
callpath.package.do_uninstall(force=True)
# 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())]
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
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")
@ -230,16 +232,16 @@ def environment_setup(
TestUninstallFromEnv.env("create", "e1")
e1 = spack.environment.read("e1")
with e1:
TestUninstallFromEnv.add("dt-diamond-left")
TestUninstallFromEnv.add("dt-diamond-bottom")
TestUninstallFromEnv.add("diamond-link-left")
TestUninstallFromEnv.add("diamond-link-bottom")
TestUninstallFromEnv.concretize()
install("--fake")
TestUninstallFromEnv.env("create", "e2")
e2 = spack.environment.read("e2")
with e2:
TestUninstallFromEnv.add("dt-diamond-right")
TestUninstallFromEnv.add("dt-diamond-bottom")
TestUninstallFromEnv.add("diamond-link-right")
TestUninstallFromEnv.add("diamond-link-bottom")
TestUninstallFromEnv.concretize()
install("--fake")
@ -251,47 +253,47 @@ def test_basic_env_sanity(self, environment_setup):
assert concretized_spec.package.installed
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
both e1 and e2.
"""
e1 = spack.environment.read("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
# --remove was not specified
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():
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.
e2 = spack.environment.read("e2")
with e2:
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():
assert not concretized_spec.package.installed
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);
dt-diamond-left is also uninstalled (since only e1 needs it) but
dt-diamond-bottom is not uninstalled (since e2 needs it).
diamond-link-left is also uninstalled (since only e1 needs it) but
diamond-link-bottom is not uninstalled (since e2 needs it).
"""
e1 = spack.environment.read("e1")
with e1:
dtdiamondleft = next(
concrete
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 not list(e1.roots())
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")
with e2:
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():
assert concretized_spec.package.installed
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
e2).
"""
e1 = spack.environment.read("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 "use `spack env remove`" in output
# The environment should be unchanged and nothing should have been
# uninstalled
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():
assert concretized_spec.package.installed
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.
All packages will also be uninstalled from e2, but the roots will
remain unchanged.
@ -336,53 +338,53 @@ def test_uninstall_force_and_remove_dependency_shared_between_envs(self, environ
dtdiamondleft = next(
concrete
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 dtdiamondleft.package.installed
e2 = spack.environment.read("e2")
with e2:
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():
assert not concretized_spec.package.installed
def test_uninstall_keep_dependents_dependency_shared_between_envs(self, environment_setup):
"""If you "spack uninstall -f --remove dt-diamond-bottom" from
e1, then dt-diamond-bottom should be uninstalled, which leaves
"""If you "spack uninstall -f --remove diamond-link-bottom" from
e1, then diamond-link-bottom should be uninstalled, which leaves
"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")
with e1:
dtdiamondleft = next(
concrete
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")
# dt-diamond-bottom was removed from the list of roots (note that
# it would still be installed since dt-diamond-left depends on it)
assert set(x.name for x in e1.roots()) == set(["dt-diamond-left"])
uninstall("-f", "-y", "--remove", "diamond-link-bottom")
# diamond-link-bottom was removed from the list of roots (note that
# it would still be installed since diamond-link-left depends on it)
assert set(x.name for x in e1.roots()) == set(["diamond-link-left"])
assert dtdiamondleft.package.installed
e2 = spack.environment.read("e2")
with e2:
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(
concrete
for (_, concrete) in e2.concretized_specs()
if concrete.name == "dt-diamond-right"
if concrete.name == "diamond-link-right"
)
assert dtdiamondright.package.installed
dtdiamondbottom = next(
concrete
for (_, concrete) in e2.concretized_specs()
if concrete.name == "dt-diamond-bottom"
if concrete.name == "diamond-link-bottom"
)
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")