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:
		| @@ -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 = {} | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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") | ||||||
| @@ -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") | ||||||
| @@ -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") | ||||||
| @@ -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") | ||||||
		Reference in New Issue
	
	Block a user
	 Harmen Stoppels
					Harmen Stoppels