Skip to content

Commit 44daa62

Browse files
Xaenaltclaudecursoragent
committed
feat(cache): add unified CacheManager subsystem
Introduce a centralized CacheManager that coordinates all cache operations behind a layered lookup strategy with local and remote PEP 503 backends, hierarchical collection routing, short-circuit optimization in the bootstrapper, and dedicated CLI commands. Key components: - WheelCacheKey for content-addressed artifact identity - CacheBackend protocol with LocalDirectoryBackend and RemotePEP503Backend - CacheCollection for named groups of backends searched in priority order - StoreRouter for variant-aware package routing - CacheManager orchestrating hierarchical lookup with fallback - CLI commands: cache list, stats, verify, invalidate, gc Security: - SHA256 streaming verification for remote downloads - Filename sanitization against path traversal - Atomic file operations (tempfile + rename) - Symlink protection in scan/gc - HTTP without SHA256 rejected unless --cache-allow-insecure Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Sean Pryor <spryor@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Sean Pryor <spryor@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2e7c9cd commit 44daa62

11 files changed

Lines changed: 3251 additions & 7 deletions

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ canonicalize = "fromager.commands.canonicalize:canonicalize"
128128
download-sequence = "fromager.commands.download_sequence:download_sequence"
129129
wheel-server = "fromager.commands.server:wheel_server"
130130
lint-requirements = "fromager.commands.lint_requirements:lint_requirements"
131+
cache = "fromager.commands.cache_cmd:cache"
131132

132133
[tool.coverage.run]
133134
branch = true

src/fromager/bootstrapper.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,23 +325,52 @@ def _download_wheel_from_cache(
325325
return None, None
326326

327327

328+
def _find_cached_wheel_via_manager(
329+
ctx: context.WorkContext,
330+
req: Requirement,
331+
resolved_version: Version,
332+
) -> tuple[pathlib.Path | None, pathlib.Path | None]:
333+
"""Cache lookup using the CacheManager (thread-safe, no Bootstrapper state).
334+
335+
Delegates to ``ctx.cache.lookup_wheel()`` for a unified hierarchical
336+
lookup across collections.
337+
338+
Returns:
339+
Tuple of (cached_wheel_filename, parent_dir_as_sentinel).
340+
Both None if no cache hit. The second value is non-None on hit
341+
to signal the caller that a cached wheel was found.
342+
"""
343+
assert ctx.cache is not None
344+
pbi = ctx.package_build_info(req)
345+
build_tag = pbi.build_tag(resolved_version)
346+
347+
result = ctx.cache.lookup_wheel(req, resolved_version, build_tag)
348+
if not result.hit:
349+
return None, None
350+
351+
assert result.path is not None
352+
return result.path, result.path.parent
353+
354+
328355
def _find_cached_wheel(
329356
ctx: context.WorkContext,
330357
cache_wheel_server_url: str | None,
331358
req: Requirement,
332359
resolved_version: Version,
333360
) -> tuple[pathlib.Path | None, pathlib.Path | None]:
334-
"""Look for cached wheel in 3 locations (thread-safe, no Bootstrapper state).
361+
"""Look for cached wheel (thread-safe, no Bootstrapper state).
335362
336-
Checks for cached wheels in order:
337-
1. wheels_build directory (previously built)
338-
2. wheels_downloads directory (previously downloaded)
339-
3. Cache server (remote cache)
363+
When a CacheManager is configured on the context, delegates to it
364+
for a unified hierarchical lookup across collections. Otherwise
365+
falls back to the legacy per-directory search.
340366
341367
Returns:
342368
Tuple of (cached_wheel_filename, unpacked_cached_wheel).
343369
Both None if no cache hit.
344370
"""
371+
if ctx.cache is not None:
372+
return _find_cached_wheel_via_manager(ctx, req, resolved_version)
373+
345374
cached_wheel, unpacked = _look_for_existing_wheel(
346375
ctx, req, resolved_version, ctx.wheels_build
347376
)
@@ -1753,6 +1782,33 @@ def _phase_prepare_source(self, item: WorkItem) -> list[WorkItem]:
17531782
sdist_root_dir = prepared.sdist_root_dir
17541783
item.cached_wheel_filename = prepared.cached_wheel_filename
17551784

1785+
# Short-circuit: when CacheManager provides a hit, skip directly to
1786+
# PROCESS_INSTALL_DEPS -- no source download, no build env, no build
1787+
# deps resolution needed. Install deps are extracted from the wheel.
1788+
if prepared.cached_wheel_filename and self.ctx.cache is not None:
1789+
pbi = self.ctx.package_build_info(item.req)
1790+
build_tag = pbi.build_tag(item.resolved_version)
1791+
self.ctx.cache.store_wheel(
1792+
item.req,
1793+
item.resolved_version,
1794+
build_tag,
1795+
prepared.cached_wheel_filename,
1796+
)
1797+
server.update_wheel_mirror(self.ctx)
1798+
unpack_dir = _create_unpack_dir(
1799+
self.ctx.work_dir, item.req, item.resolved_version
1800+
)
1801+
item.build_result = SourceBuildResult(
1802+
wheel_filename=prepared.cached_wheel_filename,
1803+
sdist_filename=None,
1804+
unpack_dir=unpack_dir,
1805+
sdist_root_dir=None,
1806+
build_env=None,
1807+
source_type=SourceType.CACHED,
1808+
)
1809+
item.phase = BootstrapPhase.PROCESS_INSTALL_DEPS
1810+
return [item]
1811+
17561812
assert sdist_root_dir is not None
17571813

17581814
if sdist_root_dir.parent.parent != self.ctx.work_dir:
@@ -1990,6 +2046,20 @@ def _phase_build(self, item: WorkItem) -> list[WorkItem]:
19902046
cached_wheel_filename=item.cached_wheel_filename,
19912047
)
19922048

2049+
# Route newly built wheels to the appropriate collection directory.
2050+
# This copies the wheel into the routed collection's storage while
2051+
# keeping the original in downloads/ for the internal wheel server.
2052+
if (
2053+
wheel_filename is not None
2054+
and self.ctx.cache is not None
2055+
and not item.cached_wheel_filename
2056+
):
2057+
pbi = self.ctx.package_build_info(item.req)
2058+
build_tag = pbi.build_tag(item.resolved_version)
2059+
self.ctx.cache.store_wheel(
2060+
item.req, item.resolved_version, build_tag, wheel_filename
2061+
)
2062+
19932063
source_type = sources.get_source_type(self.ctx, item.req)
19942064

19952065
item.build_result = SourceBuildResult(

0 commit comments

Comments
 (0)