Skip to content

Commit 474a5b5

Browse files
committed
graph: various minor fixes
Apply fixes requested and suggested during review.
1 parent 2c26c6a commit 474a5b5

File tree

4 files changed

+94
-44
lines changed

4 files changed

+94
-44
lines changed

lib/rift/Controller.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,8 +1107,8 @@ def get_packages_to_build(config, staff, modules, args):
11071107

11081108
def result_position(new_build_requirements):
11091109
"""
1110-
Return the first index in result of packages in provided build
1111-
requirements list.
1110+
Return the index of the first package in result that appear as a build
1111+
requirement in new_build_requirements
11121112
"""
11131113
for build_requirement in new_build_requirements:
11141114
for index, package in enumerate(result):

lib/rift/graph.py

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (C) 2024 CEA
2+
# Copyright (C) 2025 CEA
33
#
44
# This file is part of Rift project.
55
#
@@ -54,6 +54,16 @@ def __init__(self, package):
5454
self.subpackages = spec.provides
5555
# Parse buildrequires string in spec file to discard explicit versions
5656
# enforcement.
57+
#
58+
# Note this is currently done this way for the sake of simplicity,
59+
# despite the value that could be provided by these version constraints.
60+
# It could notably be interesting to extract lesser version constraints
61+
# when a dependency is updated to a greater version.
62+
#
63+
# Currently, the automatic rebuilds of recursive reverse dependencies
64+
# eventually fail at some point because of invalid versioning in this
65+
# case but it could be nice to fail faster by detecting mismatching
66+
# versions before the actual builds.
5767
self.build_requires = [
5868
value.group(1)
5969
for value
@@ -99,24 +109,13 @@ def dump(self):
99109
"""Dump graph in its current state with logging message."""
100110
for node in self.nodes:
101111
logging.info("→ %s", node.package.name)
112+
logging.info(" provides: %s", str(node.subpackages))
102113
logging.info(" requires: %s", node.build_requires)
103-
logging.info(" subpackages: %s", str(node.subpackages))
104114
logging.info(
105-
" rdeps: %s", str([rdep.package.name for rdep in node.rdeps])
115+
" is required by: %s",
116+
str([rdep.package.name for rdep in node.rdeps])
106117
)
107118

108-
def solve(self, package):
109-
"""
110-
Return list of recursive build requirements for the provided package.
111-
"""
112-
self.path = [] # Start with empty path
113-
for node in self.nodes:
114-
if node.package.name == package.name:
115-
return self._solve(node, "User request")
116-
117-
# Package not found in graph, return empty list.
118-
return []
119-
120119
def _dep_index(self, new, result):
121120
"""
122121
The new and results arguments are list of build requirements. The result
@@ -148,7 +147,7 @@ def _dep_index(self, new, result):
148147
def _solve(self, node, reason, depth=0):
149148
"""
150149
Return list of recursive build requirements for the provided package
151-
dependency node. The reason argument is a string to textually justify
150+
dependency node. The "reason" argument is a string to textually justify
152151
the build requirement of the given node. The depth argument is used to
153152
track the depth of recursive path in the dependency graph.
154153
"""
@@ -176,18 +175,19 @@ def _solve(self, node, reason, depth=0):
176175
if rdep.package.depends is not None:
177176
reason = f"depends on {node.package.name}"
178177
else:
179-
reason = "build requires on " + ", ".join(
178+
reason = "build depends on " + ", ".join(
180179
node.required_subpackages(rdep)
181180
)
182181
# If reverse dependency has already been processed in the processing
183182
# path to the current node, add it to resulting list and stop
184183
# processing to avoid endless loop.
185184
if rdep in self.path[0:depth]:
186185
logging.debug(
187-
"%s ⥀ Loop detected on node %s at depth %d",
186+
"%s ⥀ Loop detected on node %s at depth %d: %s",
188187
' '*depth,
189188
rdep.package.name,
190-
depth
189+
depth,
190+
'→'.join(node.package.name for node in self.path + [rdep]),
191191
)
192192
result.append(BuildRequirement(rdep.package, [reason]))
193193
continue
@@ -196,7 +196,7 @@ def _solve(self, node, reason, depth=0):
196196
' '*depth,
197197
rdep.package.name
198198
)
199-
# Iterate over all recursively solve build requirements for this
199+
# Iterate over all recursively solved build requirements for this
200200
# reverse dependency.
201201
build_requirements = self._solve(rdep, reason, depth+1)
202202
for idx, build_requirement in enumerate(build_requirements):
@@ -218,6 +218,28 @@ def _solve(self, node, reason, depth=0):
218218
result.insert(position, build_requirement)
219219
return result
220220

221+
def solve(self, package):
222+
"""
223+
Return list of recursive build requirements for the provided package.
224+
"""
225+
self.path = [] # Start with empty path
226+
for node in self.nodes:
227+
if node.package.name == package.name:
228+
return self._solve(node, "User request")
229+
230+
# Package not found in graph, return empty list.
231+
return []
232+
233+
def _insert(self, package):
234+
"""Insert package in the graph."""
235+
node = PackageDependencyNode(package)
236+
for _node in self.nodes:
237+
if _node.depends_on(node):
238+
node.rdeps.append(_node)
239+
if node.depends_on(_node):
240+
_node.rdeps.append(node)
241+
self.nodes.append(node)
242+
221243
def build(self, packages):
222244
"""Build graph with the provided packages."""
223245
tic = time.perf_counter()
@@ -227,24 +249,14 @@ def build(self, packages):
227249
try:
228250
package.load()
229251
except FileNotFoundError as err:
230-
logging.warning("Skipping package %s unable to load: %s",
252+
logging.warning("Skipping package '%s' unable to load: %s",
231253
package.name, err)
232254
continue
233255
self._insert(package)
234256
toc = time.perf_counter()
235257
logging.debug("Graph built in %0.4f seconds", toc - tic)
236258
logging.debug("Graph size: %d", len(self.nodes))
237259

238-
def _insert(self, package):
239-
"""Insert package in the graph."""
240-
node = PackageDependencyNode(package)
241-
for _node in self.nodes:
242-
if _node.depends_on(node):
243-
node.rdeps.append(_node)
244-
if node.depends_on(_node):
245-
_node.rdeps.append(node)
246-
self.nodes.append(node)
247-
248260
@classmethod
249261
def from_project(cls, config, staff, modules):
250262
"""

tests/Controller.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,45 @@ def test_get_packages_to_build_package_order(self):
11261126
[pkg.name for pkg in pkgs], ['libtwo', 'libone', 'my-software']
11271127
)
11281128

1129+
def test_get_packages_to_build_cyclic_deps(self):
1130+
""" Test get_packages_to_build() returns correctly ordered list of reverse dependencies. """
1131+
self.make_pkg(
1132+
name='libone',
1133+
metadata={
1134+
'depends': 'libtwo'
1135+
}
1136+
)
1137+
self.make_pkg(
1138+
name='libtwo',
1139+
metadata={
1140+
'depends': 'libthree'
1141+
}
1142+
)
1143+
self.make_pkg(
1144+
name='libthree',
1145+
metadata={
1146+
'depends': 'libone'
1147+
}
1148+
)
1149+
# Enable tracking, disable --skip-deps
1150+
self.config.set('dependency_tracking', True)
1151+
args = Mock()
1152+
args.skip_deps = False
1153+
args.packages = ['libone']
1154+
with self.assertLogs(level="DEBUG") as cm:
1155+
pkgs = get_packages_to_build(
1156+
self.config, self.staff, self.modules, args
1157+
)
1158+
# Package libone must be present after libtwo and my-software must be
1159+
# present after both libtwo and libone in the order list of build
1160+
# requirements.
1161+
self.assertCountEqual(
1162+
[pkg.name for pkg in pkgs], ['libthree', 'libtwo', 'libone']
1163+
)
1164+
self.assertIn(
1165+
'DEBUG:root: ⥀ Loop detected on node libone at depth 2: libone→libthree→libtwo→libone',
1166+
cm.output
1167+
)
11291168

11301169
class ControllerArgumentsTest(RiftTestCase):
11311170
""" Arguments parsing tests for Controller module"""

tests/graph.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (C) 2020 CEA
2+
# Copyright (C) 2025 CEA
33
#
44
import os
55
import shutil
@@ -47,8 +47,8 @@ def test_packages_unable_load(self):
4747
# Check warning message has been emitted
4848
self.assertEqual(
4949
cm.output,
50-
[ "WARNING:root:Skipping package failed unable to load: [Errno 2] "
51-
"No such file or directory: "
50+
[ "WARNING:root:Skipping package 'failed' unable to load: [Errno 2]"
51+
" No such file or directory: "
5252
f"'{self.projdir}/packages/failed/info.yaml'" ]
5353
)
5454
# Check success package is successfully loaded anyway.
@@ -59,7 +59,6 @@ def test_dump(self):
5959
""" Test graph dump """
6060
pkg_name = 'fake'
6161
self.make_pkg(name=pkg_name)
62-
package = Package(pkg_name, self.config, self.staff, self.modules)
6362
graph = PackagesDependencyGraph.from_project(
6463
self.config,
6564
self.staff,
@@ -71,9 +70,9 @@ def test_dump(self):
7170
cm.output,
7271
[
7372
'INFO:root:→ fake',
73+
"INFO:root: provides: ['fake', 'fake-provide']",
7474
"INFO:root: requires: ['br-package']",
75-
"INFO:root: subpackages: ['fake', 'fake-provide']",
76-
'INFO:root: rdeps: []'
75+
'INFO:root: is required by: []'
7776
]
7877
)
7978

@@ -215,7 +214,7 @@ def load_graph():
215214
self.assertEqual(build_requirements[1].package.name, 'my-software')
216215
self.assertEqual(
217216
build_requirements[1].reasons,
218-
["build requires on libone-devel"]
217+
["build depends on libone-devel"]
219218
)
220219

221220
# Rebuild of libtwo triggers rebuild of libone and my-software because
@@ -231,14 +230,14 @@ def load_graph():
231230
self.assertEqual(build_requirements[1].package.name, 'libone')
232231
self.assertEqual(
233232
build_requirements[1].reasons,
234-
["build requires on libtwo-devel"]
233+
["build depends on libtwo-devel"]
235234
)
236235
self.assertEqual(build_requirements[2].package.name, 'my-software')
237236
self.assertCountEqual(
238237
build_requirements[2].reasons,
239238
[
240-
"build requires on libone-devel",
241-
"build requires on libtwo-devel"
239+
"build depends on libone-devel",
240+
"build depends on libtwo-devel"
242241
]
243242
)
244243

@@ -304,7 +303,7 @@ def load_graph():
304303
self.assertEqual(build_requirements[1].package.name, 'my-software')
305304
self.assertEqual(
306305
build_requirements[1].reasons,
307-
["build requires on libone-provide"]
306+
["build depends on libone-provide"]
308307
)
309308

310309
def test_loop(self):

0 commit comments

Comments
 (0)