From 29416e2146bcb2d39afa8d6f8fcb652f2e81f1dd Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 9 Oct 2026 01:36:35 -0400 Subject: [PATCH] fix(pm): re-seed the uv cache when an update ships a new lock uv_cache_dir() wrote a bare ".seeded" marker after the first seed and never compared against the payload again, so an updated install never received the new payload's wheels. Enabling a plugin re-resolves without --offline, so uv fetched the new pins from PyPI instead; for cryptography 50.0.1 on Windows ARM64, which has no wheel on PyPI, that is an sdist Rust build. The marker now records the running uv.lock's digest, so the first call under a new lock merges the new payload into the existing cache. --- pm/packages.py | 21 +++++++++++++++------ tests/pm/test_uv_cache_seed.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pm/packages.py b/pm/packages.py index 84c32814b1..e2a72875ba 100644 --- a/pm/packages.py +++ b/pm/packages.py @@ -320,24 +320,33 @@ def uv_cache_dir() -> Path: """The hermes-owned uv cache: machine-scoped and shared (keyed by content — two profiles reuse one cache), anchored to the DEFAULT hermes root like partials_root(). A bundle ships a seeded copy at - the payload root (uv-cache/); the first call on a sealed install - copies it out to the writable machine cache (the read-only payload + the payload root (uv-cache/); the first call under each uv.lock + merges it into the writable machine cache (the read-only payload can't serve uv's working cache), and a warm `uv sync --offline` from it is near-free (probed: 0.4s vs 1.2s cold) — the blow-away- on-update contract depends on it. uv's default cache location is per-user/platform-opinionated and never used by pm.""" from hermes_constants import get_default_hermes_root + from pm.paths import repo_root, store_root + machine_cache = get_default_hermes_root() / "cache" / "uv" marker = machine_cache / ".seeded" - if not marker.is_file(): + # The marker names the lock whose payload was merged. An update ships a new payload cache for + # a new lock; a bare "done" marker skipped it forever, so plugin rebuilds fetched the new pins + # from the network (and built sdists where the index has no wheel for the target). + lock = repo_root() / "uv.lock" + seed = _uv_lock_digest(lock).hex() if lock.is_file() else "1" + try: + current = marker.read_text(encoding="utf-8-sig").strip() == seed + except OSError: + current = False + if not current: # Seed from a shipped bundle cache when present (payload root = # store_root().parent on a sealed install). Record completion ONLY after a clean copy: a # partial seed that marked itself done would never be retried, and every later offline # sync that needs the missing entries fails closed. try: - from pm.paths import store_root - # uv's cache trees run deep; both roots get the long spelling so # the copy is not cut at MAX_PATH. The returned path stays ordinary. payload_cache = long_root(store_root().parent / "uv-cache") @@ -359,7 +368,7 @@ def uv_cache_dir() -> Path: else: try: marker.parent.mkdir(parents=True, exist_ok=True) - marker.write_text("1", encoding="utf-8") + marker.write_text(seed, encoding="utf-8") except OSError: pass return machine_cache diff --git a/tests/pm/test_uv_cache_seed.py b/tests/pm/test_uv_cache_seed.py index 8fd7214ab6..60102c8a2e 100644 --- a/tests/pm/test_uv_cache_seed.py +++ b/tests/pm/test_uv_cache_seed.py @@ -30,6 +30,25 @@ def payload_cache(tmp_path, monkeypatch): return get_default_hermes_root() / "cache" / "uv" +def test_update_merges_the_new_payload_into_an_existing_seed(payload_cache, tmp_path, monkeypatch): + """An update ships a payload cache for a new lock; its wheels must reach the machine cache. + + Regression: ``.seeded`` meant "done forever", so an updated install never received the new + pins' wheels and plugin rebuilds fetched them from the index. + """ + code = tmp_path / "code" + code.mkdir() + (code / "uv.lock").write_text("version = 1 # old\n", encoding="utf-8") + monkeypatch.setattr(paths, "repo_root", lambda: code) + packages.uv_cache_dir() + + added = paths.store_root().parent / "uv-cache" / "archive-v0" / "bucket" / "new.py" + added.write_text("new pin\n", encoding="utf-8") + (code / "uv.lock").write_text("version = 1 # new\n", encoding="utf-8") + packages.uv_cache_dir() + assert (payload_cache / "archive-v0" / "bucket" / "new.py").read_text(encoding="utf-8-sig") == "new pin\n" + + def test_partial_seed_is_retried_on_the_next_install(payload_cache, monkeypatch): real_copytree = shutil.copytree attempts: list[int] = []