Repository: spack/spack

Issue:
views: specs were not being associated with the correct file group
This fixes a bug which affects all packages added to views when those packages have specialized view logic (i.e. any `Package` with a specialized implementation of `add_files_to_view`, which includes all `py-*` packages). There was an incorrect mapping which meant that specs were not grouped correctly with the prefix and set-of-files associated with that spec, namely the line:

```
for (spec, (src_root, rel_paths)) in zip(specs, files_per_spec):
```

(in particular because the order of `files_per_spec` does not relate to `specs` so `zip`ping them effectively randomizes the file paths associated with each spec)


Discussion:

Resolving patch:
diff --git a/lib/spack/llnl/util/link_tree.py b/lib/spack/llnl/util/link_tree.py
--- a/lib/spack/llnl/util/link_tree.py
+++ b/lib/spack/llnl/util/link_tree.py
@@ -75,7 +75,7 @@ def __init__(self, ignore=None):
         # so that we have a fast lookup and can run mkdir in order.
         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()
 
     def before_visit_dir(self, root, rel_path, depth):
diff --git a/lib/spack/spack/filesystem_view.py b/lib/spack/spack/filesystem_view.py
--- a/lib/spack/spack/filesystem_view.py
+++ b/lib/spack/spack/filesystem_view.py
@@ -686,22 +686,34 @@ def skip_list(file):
         for dst in visitor.directories:
             os.mkdir(os.path.join(self._root, dst))
 
-        # Then group the files to be linked by spec...
-        # For compatibility, we have to create a merge_map dict mapping
-        # full_src => full_dst
-        files_per_spec = itertools.groupby(visitor.files.items(), key=lambda item: item[1][0])
-
-        for (spec, (src_root, rel_paths)) in zip(specs, files_per_spec):
-            merge_map = dict()
-            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
+        # Link the files using a "merge map": full src => full dst
+        merge_map_per_prefix = self._source_merge_visitor_to_merge_map(visitor)
+        for spec in specs:
+            merge_map = merge_map_per_prefix.get(spec.package.view_source(), None)
+            if not merge_map:
+                # Not every spec may have files to contribute.
+                continue
             spec.package.add_files_to_view(self, merge_map, skip_if_exists=False)
 
         # Finally create the metadata dirs.
         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):
         metadata_visitor = SourceMergeVisitor()
 


Test patch:
diff --git a/lib/spack/spack/test/views.py b/lib/spack/spack/test/views.py
--- a/lib/spack/spack/test/views.py
+++ b/lib/spack/spack/test/views.py
@@ -3,12 +3,13 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
+import os
 import sys
 
 import pytest
 
 from spack.directory_layout import DirectoryLayout
-from spack.filesystem_view import YamlFilesystemView
+from spack.filesystem_view import SimpleFilesystemView, YamlFilesystemView
 from spack.spec import Spec
 
 
@@ -23,3 +24,45 @@ def test_remove_extensions_ordered(install_mockery, mock_fetch, tmpdir):
 
     e1 = e2["extension1"]
     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"))