diff options
author | Harmen Stoppels <me@harmenstoppels.nl> | 2024-11-28 17:48:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-28 17:48:48 +0100 |
commit | e37e53cfe8d2fc9640872ceccd78a8edc5227201 (patch) | |
tree | 6d083869fc4d67da63e1f54c3434c57aac14579e | |
parent | cf31d20d4c5834db261e76b4e5e8734679b0a55f (diff) | |
download | spack-e37e53cfe8d2fc9640872ceccd78a8edc5227201.tar.gz spack-e37e53cfe8d2fc9640872ceccd78a8edc5227201.tar.bz2 spack-e37e53cfe8d2fc9640872ceccd78a8edc5227201.tar.xz spack-e37e53cfe8d2fc9640872ceccd78a8edc5227201.zip |
traverse: add MixedDepthVisitor, use in cmake (#47750)
This visitor accepts the sub-dag of all nodes and unique edges that have
deptype X directly from given roots, or deptype Y transitively for any
of the roots.
-rw-r--r-- | lib/spack/spack/build_systems/cmake.py | 22 | ||||
-rw-r--r-- | lib/spack/spack/test/traverse.py | 63 | ||||
-rw-r--r-- | lib/spack/spack/traverse.py | 70 |
3 files changed, 136 insertions, 19 deletions
diff --git a/lib/spack/spack/build_systems/cmake.py b/lib/spack/spack/build_systems/cmake.py index dd1261c811..010d427633 100644 --- a/lib/spack/spack/build_systems/cmake.py +++ b/lib/spack/spack/build_systems/cmake.py @@ -9,7 +9,7 @@ import platform import re import sys from itertools import chain -from typing import Any, List, Optional, Set, Tuple +from typing import Any, List, Optional, Tuple import llnl.util.filesystem as fs from llnl.util.lang import stable_partition @@ -21,6 +21,7 @@ import spack.package_base import spack.phase_callbacks import spack.spec import spack.util.prefix +from spack import traverse from spack.directives import build_system, conflicts, depends_on, variant from spack.multimethod import when from spack.util.environment import filter_system_paths @@ -166,15 +167,18 @@ def generator(*names: str, default: Optional[str] = None) -> None: def get_cmake_prefix_path(pkg: spack.package_base.PackageBase) -> List[str]: """Obtain the CMAKE_PREFIX_PATH entries for a package, based on the cmake_prefix_path package attribute of direct build/test and transitive link dependencies.""" - # Add direct build/test deps - selected: Set[str] = {s.dag_hash() for s in pkg.spec.dependencies(deptype=dt.BUILD | dt.TEST)} - # Add transitive link deps - selected.update(s.dag_hash() for s in pkg.spec.traverse(root=False, deptype=dt.LINK)) - # Separate out externals so they do not shadow Spack prefixes - externals, spack_built = stable_partition( - (s for s in pkg.spec.traverse(root=False, order="topo") if s.dag_hash() in selected), - lambda x: x.external, + edges = traverse.traverse_topo_edges_generator( + traverse.with_artificial_edges([pkg.spec]), + visitor=traverse.MixedDepthVisitor( + direct=dt.BUILD | dt.TEST, transitive=dt.LINK, key=traverse.by_dag_hash + ), + key=traverse.by_dag_hash, + root=False, + all_edges=False, # cover all nodes, not all edges ) + ordered_specs = [edge.spec for edge in edges] + # Separate out externals so they do not shadow Spack prefixes + externals, spack_built = stable_partition((s for s in ordered_specs), lambda x: x.external) return filter_system_paths( path for spec in chain(spack_built, externals) for path in spec.package.cmake_prefix_paths diff --git a/lib/spack/spack/test/traverse.py b/lib/spack/spack/test/traverse.py index 62ce24d366..79aae6eb55 100644 --- a/lib/spack/spack/test/traverse.py +++ b/lib/spack/spack/test/traverse.py @@ -20,9 +20,8 @@ def create_dag(nodes, edges): """ specs = {name: Spec(name) for name in nodes} for parent, child, deptypes in edges: - specs[parent].add_dependency_edge( - specs[child], depflag=dt.canonicalize(deptypes), virtuals=() - ) + depflag = deptypes if isinstance(deptypes, dt.DepFlag) else dt.canonicalize(deptypes) + specs[parent].add_dependency_edge(specs[child], depflag=depflag, virtuals=()) return specs @@ -454,3 +453,61 @@ def test_topo_is_bfs_for_trees(cover): assert list(traverse.traverse_nodes([binary_tree["A"]], order="topo", cover=cover)) == list( traverse.traverse_nodes([binary_tree["A"]], order="breadth", cover=cover) ) + + +@pytest.mark.parametrize("roots", [["A"], ["A", "B"], ["B", "A"], ["A", "B", "A"]]) +@pytest.mark.parametrize("order", ["breadth", "post", "pre"]) +@pytest.mark.parametrize("include_root", [True, False]) +def test_mixed_depth_visitor(roots, order, include_root): + """Test that the MixedDepthVisitor lists unique edges that are reachable either directly from + roots through build type edges, or transitively through link type edges. The tests ensures that + unique edges are listed exactly once.""" + my_graph = create_dag( + nodes=["A", "B", "C", "D", "E", "F", "G", "H", "I"], + edges=( + ("A", "B", dt.LINK | dt.RUN), + ("A", "C", dt.BUILD), + ("A", "D", dt.BUILD | dt.RUN), + ("A", "H", dt.LINK), + ("A", "I", dt.RUN), + ("B", "D", dt.BUILD | dt.LINK), + ("C", "E", dt.BUILD | dt.LINK | dt.RUN), + ("D", "F", dt.LINK), + ("D", "G", dt.BUILD | dt.RUN), + ("H", "B", dt.LINK), + ), + ) + starting_points = traverse.with_artificial_edges([my_graph[root] for root in roots]) + visitor = traverse.MixedDepthVisitor(direct=dt.BUILD, transitive=dt.LINK) + + if order == "pre": + edges = traverse.traverse_depth_first_edges_generator( + starting_points, visitor, post_order=False, root=include_root + ) + elif order == "post": + edges = traverse.traverse_depth_first_edges_generator( + starting_points, visitor, post_order=True, root=include_root + ) + elif order == "breadth": + edges = traverse.traverse_breadth_first_edges_generator( + starting_points, visitor, root=include_root + ) + + artificial_edges = [(None, root) for root in roots] if include_root else [] + simple_edges = [ + (None if edge.parent is None else edge.parent.name, edge.spec.name) for edge in edges + ] + + # make sure that every edge is listed exactly once and that the right edges are listed + assert len(simple_edges) == len(set(simple_edges)) + assert set(simple_edges) == { + # the roots + *artificial_edges, + ("A", "B"), + ("A", "C"), + ("A", "D"), + ("A", "H"), + ("B", "D"), + ("D", "F"), + ("H", "B"), + } diff --git a/lib/spack/spack/traverse.py b/lib/spack/spack/traverse.py index 158f4d892e..880d7f71fa 100644 --- a/lib/spack/spack/traverse.py +++ b/lib/spack/spack/traverse.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) from collections import defaultdict -from typing import NamedTuple, Union +from typing import Any, Callable, List, NamedTuple, Set, Union import spack.deptypes as dt import spack.spec @@ -115,6 +115,64 @@ class CoverEdgesVisitor: return self.visitor.neighbors(item) +class MixedDepthVisitor: + """Visits all unique edges of the sub-DAG induced by direct dependencies of type ``direct`` + and transitive dependencies of type ``transitive``. An example use for this is traversing build + type dependencies non-recursively, and link dependencies recursively.""" + + def __init__( + self, + *, + direct: dt.DepFlag, + transitive: dt.DepFlag, + key: Callable[["spack.spec.Spec"], Any] = id, + ) -> None: + self.direct_type = direct + self.transitive_type = transitive + self.key = key + self.seen: Set[Any] = set() + self.seen_roots: Set[Any] = set() + + def accept(self, item: EdgeAndDepth) -> bool: + # Do not accept duplicate root nodes. This only happens if the user starts iterating from + # multiple roots and lists one of the roots multiple times. + if item.edge.parent is None: + node_id = self.key(item.edge.spec) + if node_id in self.seen_roots: + return False + self.seen_roots.add(node_id) + return True + + def neighbors(self, item: EdgeAndDepth) -> List[EdgeAndDepth]: + # If we're here through an artificial source node, it's a root, and we return all + # direct_type and transitive_type edges. If we're here through a transitive_type edge, we + # return all transitive_type edges. To avoid returning the same edge twice: + # 1. If we had already encountered the current node through a transitive_type edge, we + # don't need to return transitive_type edges again. + # 2. If we encounter the current node through a direct_type edge, and we had already seen + # it through a transitive_type edge, only return the non-transitive_type, direct_type + # edges. + node_id = self.key(item.edge.spec) + seen = node_id in self.seen + is_root = item.edge.parent is None + follow_transitive = is_root or bool(item.edge.depflag & self.transitive_type) + follow = self.direct_type if is_root else dt.NONE + + if follow_transitive and not seen: + follow |= self.transitive_type + self.seen.add(node_id) + elif follow == dt.NONE: + return [] + + edges = item.edge.spec.edges_to_dependencies(depflag=follow) + + # filter direct_type edges already followed before becuase they were also transitive_type. + if seen: + edges = [edge for edge in edges if not edge.depflag & self.transitive_type] + + return sort_edges(edges) + + def get_visitor_from_args( cover, direction, depflag: Union[dt.DepFlag, dt.DepTypes], key=id, visited=None, visitor=None ): @@ -342,9 +400,7 @@ def traverse_topo_edges_generator(edges, visitor, key=id, root=True, all_edges=F # maps parent identifier to a list of edges, where None is a special identifier # for the artificial root/source. node_to_edges = defaultdict(list) - for edge in traverse_breadth_first_edges_generator( - edges, CoverEdgesVisitor(visitor, key=key), root=True, depth=False - ): + for edge in traverse_breadth_first_edges_generator(edges, visitor, root=True, depth=False): in_edge_count[key(edge.spec)] += 1 parent_id = key(edge.parent) if edge.parent is not None else None node_to_edges[parent_id].append(edge) @@ -422,9 +478,9 @@ def traverse_edges( elif order not in ("post", "pre", "breadth"): raise ValueError(f"Unknown order {order}") - # In topo traversal we need to construct a sub-DAG including all edges even if we are yielding - # a subset of them, hence "paths". - _cover = "paths" if order == "topo" else cover + # In topo traversal we need to construct a sub-DAG including all unique edges even if we are + # yielding a subset of them, hence "edges". + _cover = "edges" if order == "topo" else cover visitor = get_visitor_from_args(_cover, direction, deptype, key, visited) root_edges = with_artificial_edges(specs) |