Skip to content

Commit 2bf71ed

Browse files
authored
Merge pull request #1148 from rd4398/filter-already-built
feat(bootstrapper): skip already-cached versions in multiple version mode
2 parents 2132ef3 + 5dc4d7b commit 2bf71ed

2 files changed

Lines changed: 102 additions & 3 deletions

File tree

src/fromager/bootstrapper.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,11 +1299,13 @@ def _phase_resolve(self, item: WorkItem) -> list[WorkItem]:
12991299
"""RESOLVE phase: resolve versions and expand into START-phase items.
13001300
13011301
Centralizes version resolution so all dependencies are expanded
1302-
uniformly. Future filtering (e.g. versions already on disk) and
1303-
parallelization can be added here in one place.
1302+
uniformly. In multiple_versions mode, filters out versions whose
1303+
wheels are already cached to avoid redundant builds and
1304+
transitive dependency processing.
13041305
13051306
Returns:
1306-
One START-phase item per resolved version.
1307+
One START-phase item per resolved version that needs building.
1308+
Empty list if all versions are already cached.
13071309
"""
13081310
resolved_versions = self.resolve_versions(
13091311
item.req,
@@ -1315,6 +1317,27 @@ def _phase_resolve(self, item: WorkItem) -> list[WorkItem]:
13151317

13161318
if self.multiple_versions:
13171319
logger.info(f"resolved {len(resolved_versions)} version(s) for {item.req}")
1320+
filtered: list[tuple[str, Version]] = []
1321+
for source_url, version in resolved_versions:
1322+
cached_wheel, _ = self._find_cached_wheel(item.req, version)
1323+
if cached_wheel:
1324+
logger.info(
1325+
f"{item.req.name}=={version}: wheel already cached "
1326+
f"at {cached_wheel.name}, skipping"
1327+
)
1328+
else:
1329+
filtered.append((source_url, version))
1330+
if not filtered:
1331+
# Always process the highest version (first in
1332+
# resolved_versions) so new transitive dependencies
1333+
# are discovered even when every wheel is cached.
1334+
logger.info(
1335+
f"all versions of {item.req.name} already cached, "
1336+
f"keeping highest version {resolved_versions[0][1]} "
1337+
f"for dependency discovery"
1338+
)
1339+
filtered.append(resolved_versions[0])
1340+
resolved_versions = filtered
13181341

13191342
# Build list so highest version ends up on top of the stack
13201343
# (last element after extend) and is processed first.

tests/test_bootstrapper_iterative.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,82 @@ def test_preserves_why_snapshot(self, tmp_context: WorkContext) -> None:
277277

278278
assert result[0].why_snapshot == snapshot
279279

280+
def test_filters_cached_versions_in_multiple_versions_mode(
281+
self, tmp_context: WorkContext
282+
) -> None:
283+
"""Cached versions are filtered out before creating START items."""
284+
bt = bootstrapper.Bootstrapper(tmp_context, multiple_versions=True)
285+
item = _make_resolve_item()
286+
287+
def mock_cache(req: Requirement, version: Version) -> tuple:
288+
if str(version) == "2.0":
289+
return (tmp_context.work_dir / "pkg-2.0-py3-none-any.whl", None)
290+
return (None, None)
291+
292+
with (
293+
patch.object(
294+
bt,
295+
"resolve_versions",
296+
return_value=[
297+
("url-3.0", Version("3.0")),
298+
("url-2.0", Version("2.0")),
299+
("url-1.0", Version("1.0")),
300+
],
301+
),
302+
patch.object(bt, "_find_cached_wheel", side_effect=mock_cache),
303+
):
304+
result = bt._phase_resolve(item)
305+
306+
assert len(result) == 2
307+
versions = {str(it.resolved_version) for it in result}
308+
assert versions == {"1.0", "3.0"}
309+
310+
def test_all_cached_keeps_highest_version(self, tmp_context: WorkContext) -> None:
311+
"""If all versions are cached, keeps the highest for dependency discovery."""
312+
bt = bootstrapper.Bootstrapper(tmp_context, multiple_versions=True)
313+
item = _make_resolve_item()
314+
315+
with (
316+
patch.object(
317+
bt,
318+
"resolve_versions",
319+
return_value=[
320+
("url-3.0", Version("3.0")),
321+
("url-2.0", Version("2.0")),
322+
("url-1.0", Version("1.0")),
323+
],
324+
),
325+
patch.object(
326+
bt,
327+
"_find_cached_wheel",
328+
return_value=(tmp_context.work_dir / "cached.whl", None),
329+
),
330+
):
331+
result = bt._phase_resolve(item)
332+
333+
assert len(result) == 1
334+
assert result[0].resolved_version == Version("3.0")
335+
336+
def test_no_filtering_in_single_version_mode(
337+
self, tmp_context: WorkContext
338+
) -> None:
339+
"""Cache filtering does not apply in single version mode."""
340+
bt = bootstrapper.Bootstrapper(tmp_context, multiple_versions=False)
341+
item = _make_resolve_item()
342+
343+
with (
344+
patch.object(
345+
bt,
346+
"resolve_versions",
347+
return_value=[("url-1.0", Version("1.0"))],
348+
),
349+
patch.object(bt, "_find_cached_wheel") as mock_cache,
350+
):
351+
result = bt._phase_resolve(item)
352+
353+
assert len(result) == 1
354+
mock_cache.assert_not_called()
355+
280356

281357
class TestPhaseStart:
282358
def test_new_item_advances_to_prepare_source(

0 commit comments

Comments
 (0)