view: fix issue with non-contributing specs (#34661)

Specs that did not contribute any files to an env view caused a problem
where zip(specs, files grouped by prefix) got "out of sync", causing the
wrong merge map to be passed to a package's `add_files_to_view`, which
specifically caused an issue where *sometimes* bin/python ended up as a
symlink instead of a copy.

One such example is kokkos + kokkos-nvcc-wrapper, as the latter package
only provides the file bin/nvcc_wrapper, which is also added to view by
kokkos, causing kokkos-nvcc-wrapper to contribute 0 files.

The test feels a bit contrived, but it captures the problem... pkg a is
added first and has 0 files to contribute, pkg b adds a single file, and
we check if pkg b receives a merge map (and a does not).
This commit is contained in:
Harmen Stoppels 2023-02-16 19:36:22 +01:00 committed by GitHub
parent 69369429b6
commit 68b711c1ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 13 deletions

View file

@ -75,7 +75,7 @@ def __init__(self, ignore=None):
# so that we have a fast lookup and can run mkdir in order. # so that we have a fast lookup and can run mkdir in order.
self.directories = OrderedDict() self.directories = OrderedDict()
# Files to link. Maps dst_rel to (src_rel, src_root) # Files to link. Maps dst_rel to (src_root, src_rel)
self.files = OrderedDict() self.files = OrderedDict()
def before_visit_dir(self, root, rel_path, depth): def before_visit_dir(self, root, rel_path, depth):

View file

@ -686,22 +686,34 @@ def skip_list(file):
for dst in visitor.directories: for dst in visitor.directories:
os.mkdir(os.path.join(self._root, dst)) os.mkdir(os.path.join(self._root, dst))
# Then group the files to be linked by spec... # Link the files using a "merge map": full src => full dst
# For compatibility, we have to create a merge_map dict mapping merge_map_per_prefix = self._source_merge_visitor_to_merge_map(visitor)
# full_src => full_dst for spec in specs:
files_per_spec = itertools.groupby(visitor.files.items(), key=lambda item: item[1][0]) merge_map = merge_map_per_prefix.get(spec.package.view_source(), None)
if not merge_map:
for (spec, (src_root, rel_paths)) in zip(specs, files_per_spec): # Not every spec may have files to contribute.
merge_map = dict() continue
for dst_rel, (_, src_rel) in rel_paths:
full_src = os.path.join(src_root, src_rel)
full_dst = os.path.join(self._root, dst_rel)
merge_map[full_src] = full_dst
spec.package.add_files_to_view(self, merge_map, skip_if_exists=False) spec.package.add_files_to_view(self, merge_map, skip_if_exists=False)
# Finally create the metadata dirs. # Finally create the metadata dirs.
self.link_metadata(specs) self.link_metadata(specs)
def _source_merge_visitor_to_merge_map(self, visitor: SourceMergeVisitor):
# For compatibility with add_files_to_view, we have to create a
# merge_map of the form join(src_root, src_rel) => join(dst_root, dst_rel),
# but our visitor.files format is dst_rel => (src_root, src_rel).
# We exploit that visitor.files is an ordered dict, and files per source
# prefix are contiguous.
source_root = lambda item: item[1][0]
per_source = itertools.groupby(visitor.files.items(), key=source_root)
return {
src_root: {
os.path.join(src_root, src_rel): os.path.join(self._root, dst_rel)
for dst_rel, (_, src_rel) in group
}
for src_root, group in per_source
}
def link_metadata(self, specs): def link_metadata(self, specs):
metadata_visitor = SourceMergeVisitor() metadata_visitor = SourceMergeVisitor()

View file

@ -3,12 +3,13 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import sys import sys
import pytest import pytest
from spack.directory_layout import DirectoryLayout from spack.directory_layout import DirectoryLayout
from spack.filesystem_view import YamlFilesystemView from spack.filesystem_view import SimpleFilesystemView, YamlFilesystemView
from spack.spec import Spec from spack.spec import Spec
@ -23,3 +24,45 @@ def test_remove_extensions_ordered(install_mockery, mock_fetch, tmpdir):
e1 = e2["extension1"] e1 = e2["extension1"]
view.remove_specs(e1, e2) view.remove_specs(e1, e2)
@pytest.mark.regression("32456")
def test_view_with_spec_not_contributing_files(mock_packages, tmpdir):
view_dir = os.path.join(str(tmpdir), "view")
os.mkdir(view_dir)
layout = DirectoryLayout(view_dir)
view = SimpleFilesystemView(view_dir, layout)
a = Spec("a")
b = Spec("b")
a.prefix = os.path.join(tmpdir, "a")
b.prefix = os.path.join(tmpdir, "b")
a._mark_concrete()
b._mark_concrete()
# Create directory structure for a and b, and view
os.makedirs(a.prefix.subdir)
os.makedirs(b.prefix.subdir)
os.makedirs(os.path.join(a.prefix, ".spack"))
os.makedirs(os.path.join(b.prefix, ".spack"))
# Add files to b's prefix, but not to a's
with open(b.prefix.file, "w") as f:
f.write("file 1")
with open(b.prefix.subdir.file, "w") as f:
f.write("file 2")
# In previous versions of Spack we incorrectly called add_files_to_view
# with b's merge map. It shouldn't be called at all, since a has no
# files to add to the view.
def pkg_a_add_files_to_view(view, merge_map, skip_if_exists=True):
assert False, "There shouldn't be files to add"
a.package.add_files_to_view = pkg_a_add_files_to_view
# Create view and see if files are linked.
view.add_specs(a, b)
assert os.path.lexists(os.path.join(view_dir, "file"))
assert os.path.lexists(os.path.join(view_dir, "subdir", "file"))