Skip to content

Commit 8a076c6

Browse files
committed
feat: get install and build dependencies
Extend `DependencyNode` to get all install dependencies and build requirements. The new method return unique dependencies by recursively walking the dependency graph. The build requirements include all recursive installation requirements of build requirements. Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent 64aee9d commit 8a076c6

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

src/fromager/dependency_graph.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,60 @@ def construct_root_node(cls) -> DependencyNode:
111111
Version("0"),
112112
)
113113

114+
def iter_build_requirements(self) -> typing.Iterable[DependencyNode]:
115+
"""Get all unique, recursive build requirements
116+
117+
Yield all direct and indirect requirements to build the dependency.
118+
Includes direct build dependencies and their recursive **install**
119+
requirements.
120+
121+
The result is equivalent to the set of ``[build-system].requires``
122+
plus all ``Requires-Dist`` of build system requirements -- all
123+
packages in the build environment.
124+
"""
125+
visited: set[str] = set()
126+
# The outer loop iterates over all children and picks
127+
# direct build requirements. For each build requirement, it traverses
128+
# all children and recursively get their install requirements
129+
# (depth first).
130+
for edge in self.children:
131+
if edge.key in visited:
132+
# optimization: don't traverse visited nodes
133+
continue
134+
if not edge.req_type.is_build_requirement:
135+
# not a build requirement
136+
continue
137+
visited.add(edge.key)
138+
# it's a new ``[build-system].requires``.
139+
yield edge.destination_node
140+
# recursively get install dependencies of this build dep (depth first).
141+
for install_edge in self._traverse_install_requirements(
142+
edge.destination_node.children, visited
143+
):
144+
yield install_edge.destination_node
145+
146+
def iter_install_requirements(self) -> typing.Iterable[DependencyNode]:
147+
"""Get all unique, recursive install requirements"""
148+
visited: set[str] = set()
149+
for edge in self._traverse_install_requirements(self.children, visited):
150+
yield edge.destination_node
151+
152+
def _traverse_install_requirements(
153+
self,
154+
start_edges: list[DependencyEdge],
155+
visited: set[str],
156+
) -> typing.Iterable[DependencyEdge]:
157+
for edge in start_edges:
158+
if edge.key in visited:
159+
continue
160+
if not edge.req_type.is_install_requirement:
161+
continue
162+
visited.add(edge.destination_node.key)
163+
yield edge
164+
yield from self._traverse_install_requirements(
165+
edge.destination_node.children, visited
166+
)
167+
114168

115169
@dataclasses.dataclass(frozen=True, order=True, slots=True)
116170
class DependencyEdge:

tests/test_dependency_graph.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import dataclasses
2+
import graphlib
23

34
import pytest
5+
from packaging.requirements import Requirement
46
from packaging.utils import canonicalize_name
57
from packaging.version import Version
68

79
from fromager.dependency_graph import DependencyNode
10+
from fromager.requirements_file import RequirementType
811

912

1013
def mknode(name: str, version: str = "1.0", **kwargs) -> DependencyNode:
@@ -59,3 +62,113 @@ def test_dependencynode_dataclass():
5962
assert root.canonicalized_name == ""
6063
assert root.version == Version("0.0")
6164
assert root.key == ""
65+
66+
67+
def test_iter_requirements() -> None:
68+
a = mknode("a")
69+
# install requirements of a
70+
b = mknode("b")
71+
# build requirement of a
72+
c = mknode("c")
73+
# build requirement of c
74+
d = mknode("d")
75+
# install requirement of b and c
76+
e = mknode("e")
77+
# build requirement of a and c
78+
f = mknode("f")
79+
80+
a.add_child(b, Requirement(b.canonicalized_name), RequirementType.INSTALL)
81+
a.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
82+
a.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_SYSTEM)
83+
a.add_child(f, Requirement(c.canonicalized_name), RequirementType.BUILD_SYSTEM)
84+
b.add_child(e, Requirement(b.canonicalized_name), RequirementType.INSTALL)
85+
c.add_child(d, Requirement(d.canonicalized_name), RequirementType.BUILD_SYSTEM)
86+
c.add_child(e, Requirement(e.canonicalized_name), RequirementType.INSTALL)
87+
c.add_child(f, Requirement(f.canonicalized_name), RequirementType.BUILD_BACKEND)
88+
89+
assert sorted(a.iter_install_requirements()) == [b, e]
90+
assert sorted(a.iter_build_requirements()) == [c, e, f]
91+
assert sorted(b.iter_install_requirements()) == [e]
92+
assert sorted(b.iter_build_requirements()) == []
93+
assert sorted(c.iter_install_requirements()) == [e]
94+
assert sorted(c.iter_build_requirements()) == [d, f]
95+
96+
build_graph = get_build_graph(a, b, c, d, e, f)
97+
assert build_graph == [
98+
# no build requirements, B and E can be built in parallel, as
99+
# B just has an install requirement on E.
100+
["b", "d", "e", "f"],
101+
# C needs D, F to build.
102+
["c"],
103+
# A needs C, E, F.
104+
["a"],
105+
]
106+
107+
108+
def get_build_graph(*nodes: DependencyNode) -> list[list[str]]:
109+
topo: graphlib.TopologicalSorter[str] = graphlib.TopologicalSorter()
110+
for node in nodes:
111+
build_deps = [n.canonicalized_name for n in node.iter_build_requirements()]
112+
topo.add(node.canonicalized_name, *build_deps)
113+
topo.prepare()
114+
steps: list[list[str]] = []
115+
while topo.is_active():
116+
ready = topo.get_ready()
117+
steps.append(sorted(ready))
118+
topo.done(*ready)
119+
return steps
120+
121+
122+
def test_pr759_discussion() -> None:
123+
a = mknode("a")
124+
b = mknode("b")
125+
c = mknode("c")
126+
d = mknode("d")
127+
# A needs B to build.
128+
a.add_child(b, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
129+
# B needs C to build.
130+
b.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
131+
# B needs D to install.
132+
b.add_child(d, Requirement(c.canonicalized_name), RequirementType.INSTALL)
133+
134+
assert sorted(a.iter_build_requirements()) == [b, d]
135+
assert sorted(b.iter_build_requirements()) == [c]
136+
assert sorted(c.iter_build_requirements()) == []
137+
assert sorted(d.iter_build_requirements()) == []
138+
139+
build_graph = get_build_graph(a, b, c, d)
140+
assert build_graph == [["c", "d"], ["b"], ["a"]]
141+
142+
# add more nodes
143+
e = mknode("e")
144+
f = mknode("f")
145+
# D needs E to install.
146+
d.add_child(e, Requirement(c.canonicalized_name), RequirementType.INSTALL)
147+
# E needs F to build.
148+
e.add_child(f, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
149+
150+
# build requirements
151+
assert sorted(a.iter_build_requirements()) == [b, d, e]
152+
assert sorted(b.iter_build_requirements()) == [c]
153+
assert sorted(c.iter_build_requirements()) == []
154+
assert sorted(d.iter_build_requirements()) == []
155+
assert sorted(e.iter_build_requirements()) == [f]
156+
157+
build_graph = get_build_graph(a, b, c, d, e, f)
158+
assert build_graph == [
159+
# D, C, F don't have build requirements
160+
["c", "d", "f"],
161+
# B needs C, E needs F
162+
["b", "e"],
163+
# A needs B, D, E
164+
["a"],
165+
]
166+
167+
# install requirements
168+
assert sorted(a.iter_install_requirements()) == []
169+
# E is an indirect install dependency
170+
assert sorted(b.iter_install_requirements()) == [d, e]
171+
assert sorted(c.iter_install_requirements()) == []
172+
assert sorted(d.iter_install_requirements()) == [e]
173+
assert sorted(e.iter_install_requirements()) == []
174+
assert sorted(f.iter_install_requirements()) == []

0 commit comments

Comments
 (0)