The CI cache barely ever restored anything useful, for four independent reasons found in live run logs and the fork's cache store: - `uv cache prune --ci` (setup-pm/prune) discards downloaded wheels before the save, so the snapshot carried only source-built wheels — the next run "restored" it and still cold-downloaded everything. Upstream main's logs showed the end state: a 209KB stub cache exact-hitting forever. - Tool-only jobs (icons-freshness etc.) auto-saved a 1.5KB empty uv cache under the production exact key; caches are immutable, so the stub won forever and blocked real saves. - The npm cache key had no restore-keys, so one lockfile bump missed the exact key and every npm job went cold. - `2efa4ff94f` deleted the electron-builder toolchain save but left the assemble job restoring `eb2-` — a fossil nobody regenerates; assembly cold-downloads winCodeSign/ATS/dotnet on every release. Changes: - pm/cache_lock.py: move prune_uv_cache_to_lock out of scripts/bundles/native.py (which re-exports it); the lock-exactness contract now serves both the bundle ship gate and CI caches. - pm.build_env learns --exact-lock --lock-source: prune cache entries the project uv.lock cannot resolve, keeping lock-required downloaded wheels (unlike --ci). Refuses --ci/--prune-cache combinations. - setup-pm: python-cache auto-save now requires extras (no stubs from tool-only jobs); key drops the prune flag and bumps to v3 — pruned and unpruned saves share one namespace since both are lock-exact; pre-save pruning switched from --ci to --exact-lock; npm cache gains a lockfile-agnostic restore prefix. - save-pm-cache: same exact-lock prune before explicit saves. - desktop-bundled-release: build legs (cache-mode: write) restore+save the electron-builder toolchain under eb3- keyed on the locked builder version; assemble restores the same namespace; the dead default-cache resolution step is removed (assembly resolves no electron artifacts). - cleanup_pm_toolchain_caches.py: match v2 and v3 smoke keys. Validation: tests/scripts/test_bundle_native.py 11/11 (incl. both lock-prune gates), tests/pm failures identical before/after the diff, tests/scripts/test_bundle_payload.py 5/5, tests/ci cleanup 3/3, tests-js setup-pm-post 1/1 and the three setup-pm-cache contract tests updated and green (6 failures in that file pre-date this diff and are drift between the workflow and its stale assertions); pm.build_env --exact-lock E2E against a real uv cache copy pruned 3 stale entries and kept the rest.
60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
"""Exact-to-lock pruning for uv caches.
|
|
|
|
The lock is the contract for anything downstream consumes wholesale: a
|
|
shipped bundle payload and a rolling CI cache snapshot both must not carry
|
|
wheels the project's uv.lock cannot resolve. Rolling caches accumulate
|
|
entries for superseded pins (prefix restores across dependency changes);
|
|
pruning to the lock keeps every consumer exact without discarding wheels
|
|
the current lock still needs (unlike ``uv cache prune --ci``).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
|
|
def lock_package_names(source_repo: Path) -> set[str]:
|
|
"""Dist names the lock can resolve; the exactness contract for a cache."""
|
|
import tomllib
|
|
|
|
data = tomllib.loads((source_repo / "uv.lock").read_text(encoding="utf-8"))
|
|
return {entry["name"].lower().replace("_", "-") for entry in data["package"]}
|
|
|
|
|
|
def prune_uv_cache_to_lock(cache: Path, source_repo: Path) -> int:
|
|
"""Delete cache entries for dists the lock cannot resolve.
|
|
|
|
Unidentifiable buckets (no dist-info) survive — pruning fails open for
|
|
unknown layouts, never for identifiable stale pins. Returns the pruned
|
|
entry count for the build log.
|
|
"""
|
|
keep = lock_package_names(source_repo)
|
|
dist_info = re.compile(r"([A-Za-z0-9_.]+?)-\d[^-]*\.dist-info")
|
|
|
|
def dist_name(bucket: Path) -> str | None:
|
|
for marker_file in bucket.glob("*.dist-info"):
|
|
match = dist_info.match(marker_file.name)
|
|
if match:
|
|
return match.group(1).lower().replace("_", "-")
|
|
return None
|
|
|
|
pruned = 0
|
|
archive = cache / "archive-v0"
|
|
if archive.is_dir():
|
|
for bucket in archive.iterdir():
|
|
if not bucket.is_dir():
|
|
continue
|
|
name = dist_name(bucket)
|
|
if name is not None and name not in keep:
|
|
shutil.rmtree(bucket, ignore_errors=True)
|
|
pruned += 1
|
|
for family_dir in (*cache.glob("wheels-v*/pypi"), *cache.glob("sdists-v*/pypi")):
|
|
if not family_dir.is_dir():
|
|
continue
|
|
for entry in family_dir.iterdir():
|
|
if entry.is_dir() and entry.name.lower().replace("_", "-") not in keep:
|
|
shutil.rmtree(entry, ignore_errors=True)
|
|
pruned += 1
|
|
return pruned
|