Skip to content

Commit 9d7dd26

Browse files
committed
fix some issues writing the constraints file during bootstrap
We're seeing some cases downstream where fromager fails with an exception because it tries to remove something from the unresolved dependency list when the item isn't in the list. This seems to be caused by having multiple candidate versions for a package where both versions could be used by all consumers. This change resolves the issue by stopping the resolution loop when we find a version of a package that can work for all consumers. The loop now iterates from the newest version to the oldest, to ensure we choose the newest package that works. There is also some error handling in case this fix is not sufficient to eliminate the original ValueError. I also noticed that a log message that we supposed to print debug info about the candidate versions was using the wrong list variable so it was always printing the same values, regardless of the package. I cleaned that up and added additional debug logging.
1 parent 8ade718 commit 9d7dd26

File tree

2 files changed

+84
-9
lines changed

2 files changed

+84
-9
lines changed

src/fromager/commands/bootstrap.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def write_constraints_file(
219219
# Make copy of the original list and loop over unresolved dependencies
220220
for dep_name, nodes in unresolved_dependencies[:]:
221221
# Track which versions can be used by which parent requirement.
222-
usable_versions: dict[str, list[Version]] = {}
222+
usable_versions: dict[Version, list[Version]] = {}
223223
# Track how many total users of a requirement (by name) there are so we
224224
# can tell later if any version can be used by all of them.
225225
user_counter = 0
@@ -245,7 +245,7 @@ def write_constraints_file(
245245
for matching_version in parent_edge.req.specifier.filter(
246246
dep_versions
247247
):
248-
usable_versions.setdefault(str(matching_version), []).append(
248+
usable_versions.setdefault(matching_version, []).append(
249249
parent_edge.destination_node.version
250250
)
251251
user_counter += 1
@@ -254,18 +254,34 @@ def write_constraints_file(
254254
# and output that if we find it. Otherwise, include a warning and report
255255
# all versions so a human reading the file can make their own decision
256256
# about how to resolve the conflict.
257-
for v, users in usable_versions.items():
258-
if len(users) == user_counter:
259-
version_strs = [str(v) for v in sorted(versions)]
257+
for v, users in reversed(sorted(usable_versions.items())):
258+
if len(users) != user_counter:
260259
logger.debug(
261-
"%s: selecting %s from multiple candidates %s",
260+
"%s: version %s is useable by %d of %d consumers, skipping it",
262261
dep_name,
263262
v,
264-
version_strs,
263+
len(users),
264+
user_counter,
265265
)
266-
resolved[dep_name] = Version(v)
267-
resolved_something = True
266+
continue
267+
version_strs = [str(v) for v in reversed(sorted(dep_versions))]
268+
logger.debug(
269+
"%s: selecting %s from multiple candidates %s",
270+
dep_name,
271+
v,
272+
version_strs,
273+
)
274+
resolved[dep_name] = v
275+
resolved_something = True
276+
try:
268277
unresolved_dependencies.remove((dep_name, nodes))
278+
except ValueError:
279+
logger.debug(
280+
"%s: %s not in unresolved dependencies list, ignoring",
281+
dep_name,
282+
(dep_name, nodes),
283+
)
284+
break
269285

270286
# Write resolved versions to constraints file
271287
for dep_name, resolved_version in sorted(resolved.items()):

tests/test_bootstrap.py

+59
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,62 @@ def test_write_constraints_file_duplicates():
261261
d==1.0
262262
""").lstrip()
263263
assert expected == buffer.getvalue()
264+
265+
266+
def test_write_constraints_file_multiples():
267+
buffer = io.StringIO()
268+
raw_graph = {
269+
"": {
270+
"download_url": "",
271+
"pre_built": False,
272+
"version": "0",
273+
"canonicalized_name": "",
274+
"edges": [
275+
{
276+
"key": "docling==2.7.0",
277+
"req_type": "toplevel",
278+
"req": "docling==2.7.0",
279+
},
280+
{
281+
"key": "deepsearch-glm==0.26.1",
282+
"req_type": "toplevel",
283+
"req": "deepsearch-glm==0.26.1",
284+
},
285+
],
286+
},
287+
"deepsearch-glm==0.26.1": {
288+
"download_url": "",
289+
"pre_built": False,
290+
"version": "0.26.1",
291+
"canonicalized_name": "deepsearch-glm",
292+
"edges": [],
293+
},
294+
"deepsearch-glm==0.26.2": {
295+
"download_url": "",
296+
"pre_built": False,
297+
"version": "0.26.2",
298+
"canonicalized_name": "deepsearch-glm",
299+
"edges": [],
300+
},
301+
"docling==2.7.0": {
302+
"download_url": "",
303+
"pre_built": False,
304+
"version": "2.7.0",
305+
"canonicalized_name": "docling",
306+
"edges": [
307+
{
308+
"key": "deepsearch-glm==0.26.2",
309+
"req_type": "install",
310+
"req": "deepsearch-glm<0.27.0,>=0.26.1",
311+
},
312+
],
313+
},
314+
}
315+
graph = dependency_graph.DependencyGraph.from_dict(raw_graph)
316+
assert bootstrap.write_constraints_file(graph, buffer)
317+
expected = textwrap.dedent("""
318+
# NOTE: fromager selected deepsearch-glm==0.26.2 from: ['0.26.1', '0.26.2']
319+
deepsearch-glm==0.26.2
320+
docling==2.7.0
321+
""").lstrip()
322+
assert expected == buffer.getvalue()

0 commit comments

Comments
 (0)