Remove extensions from view in the correct order (#12961)
When removing packages from a view, extensions were being deactivated in an arbitrary order. Extensions must be deactivated in preorder traversal (dependents before dependencies), so when this order was violated the view update would fail. This commit ensures that views deactivate extensions based on a preorder traversal and adds a test for it.
This commit is contained in:
parent
4a84155caa
commit
6b3e173331
2 changed files with 37 additions and 15 deletions
|
@ -399,23 +399,31 @@ def remove_specs(self, *specs, **kwargs):
|
||||||
"The following packages will be unusable: %s"
|
"The following packages will be unusable: %s"
|
||||||
% ", ".join((s.name for s in dependents)))
|
% ", ".join((s.name for s in dependents)))
|
||||||
|
|
||||||
extensions = set(filter(lambda s: s.package.is_extension,
|
# Determine the order that packages should be removed from the view;
|
||||||
to_deactivate))
|
# dependents come before their dependencies.
|
||||||
standalones = to_deactivate - extensions
|
to_deactivate_sorted = list()
|
||||||
|
depmap = dict()
|
||||||
|
for spec in to_deactivate:
|
||||||
|
depmap[spec] = set(d for d in spec.traverse(root=False)
|
||||||
|
if d in to_deactivate)
|
||||||
|
|
||||||
# Please note that a traversal of the DAG in post-order and then
|
while depmap:
|
||||||
# forcibly removing each package should remove the need to specify
|
for spec in [s for s, d in depmap.items() if not d]:
|
||||||
# with_dependents for deactivating extensions/allow removal without
|
to_deactivate_sorted.append(spec)
|
||||||
# additional checks (force=True). If removal performance becomes
|
for s in depmap.keys():
|
||||||
# unbearable for whatever reason, this should be the first point of
|
depmap[s].discard(spec)
|
||||||
# attack.
|
depmap.pop(spec)
|
||||||
#
|
to_deactivate_sorted.reverse()
|
||||||
# see: https://github.com/spack/spack/pull/3227#discussion_r117147475
|
|
||||||
remove_extension = ft.partial(self.remove_extension,
|
|
||||||
with_dependents=with_dependents)
|
|
||||||
|
|
||||||
set(map(remove_extension, extensions))
|
# Ensure that the sorted list contains all the packages
|
||||||
set(map(self.remove_standalone, standalones))
|
assert set(to_deactivate_sorted) == to_deactivate
|
||||||
|
|
||||||
|
# Remove the packages from the view
|
||||||
|
for spec in to_deactivate_sorted:
|
||||||
|
if spec.package.is_extension:
|
||||||
|
self.remove_extension(spec, with_dependents=with_dependents)
|
||||||
|
else:
|
||||||
|
self.remove_standalone(spec)
|
||||||
|
|
||||||
self._purge_empty_directories()
|
self._purge_empty_directories()
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from spack.spec import Spec
|
from spack.spec import Spec
|
||||||
|
from spack.directory_layout import YamlDirectoryLayout
|
||||||
|
from spack.filesystem_view import YamlFilesystemView
|
||||||
|
|
||||||
|
|
||||||
def test_global_activation(install_mockery, mock_fetch):
|
def test_global_activation(install_mockery, mock_fetch):
|
||||||
|
@ -27,3 +29,15 @@ def test_global_activation(install_mockery, mock_fetch):
|
||||||
extendee_spec.prefix, '.spack', 'extensions.yaml')
|
extendee_spec.prefix, '.spack', 'extensions.yaml')
|
||||||
assert (view.extensions_layout.extension_file_path(extendee_spec) ==
|
assert (view.extensions_layout.extension_file_path(extendee_spec) ==
|
||||||
expected_path)
|
expected_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_extensions_ordered(install_mockery, mock_fetch, tmpdir):
|
||||||
|
view_dir = str(tmpdir.join('view'))
|
||||||
|
layout = YamlDirectoryLayout(view_dir)
|
||||||
|
view = YamlFilesystemView(view_dir, layout)
|
||||||
|
e2 = Spec('extension2').concretized()
|
||||||
|
e2.package.do_install()
|
||||||
|
view.add_specs(e2)
|
||||||
|
|
||||||
|
e1 = e2['extension1']
|
||||||
|
view.remove_specs(e1, e2)
|
||||||
|
|
Loading…
Reference in a new issue