feat(pm): ship the uv cache + mutable-venv bootstrap seed for sealed installs

The blow-the-venv-on-update contract needs rebuilds to be cheap, and
sealed installs need a writable venv at all (settled 2026-09-02 plan,
task 7):

- uv_cache_dir(): hermes-owned machine cache at
  <default hermes root>/cache/uv — content-addressed, shared across
  profiles. uv_env() ALWAYS pins UV_CACHE_DIR there (ambient UV_*
  stripped), so the cache that ships is the cache that gets used.
  First call on a sealed install seeds it from the payload's shipped
  uv-cache/ (read-only payload can't serve uv's working cache); the
  .seeded marker makes it once-only and non-clobbering.
- pm bundle stages the warmed cache into the payload after the venv
  sync (uv-cache/ beside manifest.json) — warm 'uv sync --offline'
  rebuilds probed at 0.4s vs 1.2s cold.
- Venv.venv_dir(): sealed installs resolve the MUTABLE venv to the
  machine hermes root (<root>/venv), not the read-only payload;
  dev/source installs keep the repo-local venv unchanged.
- Venv.seed_mutable_venv(): the bootstrap seed — lazy-off installs
  copy the payload's shipped venv out as the starting point; lazy-on
  installs skip the copy (first sync builds fresh from the shipped
  cache). adopt() triggers it (KeyError-guarded, failure reported
  never fatal — a cold sync still converges).

tests/pm/test_uv_cache.py: 6 tests (env pinning + ambient strip,
payload seed + marker once-only, cold machine, sealed venv_dir, seed
copy idempotence, lazy-on skip). tests/pm: 175 passed, 0 failed.
This commit is contained in:
ethernet
2026-09-02 20:00:34 -04:00
parent 64a6564084
commit ba39525c39
4 changed files with 247 additions and 1 deletions

View File

@@ -623,6 +623,24 @@ def cmd_bundle(args) -> int:
write_features(features, out)
print(f"✓ enabled-features.json ({len(features)} extras recorded)")
# Ship the uv cache: the staged venv sync just warmed the hermes-owned
# cache with every wheel this payload needs. Copying it in makes a
# mutable-venv rebuild from the bundle near-free (`uv sync --offline`
# from a warm cache probed at 0.4s vs 1.2s cold) — the blow-away-on-
# update contract depends on it.
from pm.packages import uv_cache_dir as bundle_uv_cache_dir
payload_cache = out / "uv-cache"
if payload_cache.exists():
shutil.rmtree(payload_cache, ignore_errors=True)
src_cache = bundle_uv_cache_dir()
if src_cache.is_dir():
print(f" uv-cache: copying {src_cache} → payload...", flush=True)
shutil.copytree(src_cache, payload_cache)
print("✓ uv-cache (staged — warm rebuilds for the mutable venv)")
else:
print(" uv-cache: none warm (first bundle on this machine?)")
bad = _arch_guard(store_dir)
for line in bad:
print(f"✗ arch: {line}")

View File

@@ -407,6 +407,21 @@ def adopt() -> bool:
marker.write_text("", encoding="utf-8")
except OSError:
pass
# Bootstrap the mutable venv (sealed installs): lazy-off copies the
# shipped venv out as the seed; lazy-on builds fresh from the shipped
# uv cache on first sync. Failure is reported, never fatal — a cold
# sync still converges.
try:
venv_package = get_package("venv")
except KeyError:
venv_package = None
seed = getattr(venv_package, "seed_mutable_venv", None) if venv_package else None
if seed is not None:
reason = seed()
if reason:
LOG.info("pm adopt: mutable venv seed skipped: %s", reason)
return True

View File

@@ -235,6 +235,51 @@ def _uv_lock_digest(path: Path) -> bytes:
_uv_lock_digest_cache: dict[Path, tuple] = {}
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
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
machine_cache = get_default_hermes_root() / "cache" / "uv"
marker = machine_cache / ".seeded"
if not marker.is_file():
# Seed from a shipped bundle cache when present (payload root =
# store_root().parent on a sealed install).
try:
from pm.paths import store_root
payload_cache = store_root().parent / "uv-cache"
if payload_cache.is_dir():
machine_cache.mkdir(parents=True, exist_ok=True)
import shutil as _shutil
for entry in payload_cache.iterdir():
if entry.name == ".seeded":
continue
dest = machine_cache / entry.name
if not dest.exists():
(
_shutil.copytree(entry, dest)
if entry.is_dir()
else _shutil.copy2(entry, dest)
)
except OSError:
pass # seeding is best-effort; a cold sync still works
try:
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text("1", encoding="utf-8")
except OSError:
pass
return machine_cache
def uv_env(base_env: Optional[dict] = None) -> dict[str, str]:
"""Sanitized env for pm's internal uv invocations: user-level UV
overrides and active-venv leakage must not steer which interpreter or
@@ -243,7 +288,9 @@ def uv_env(base_env: Optional[dict] = None) -> dict[str, str]:
must not reach the subprocess either. User config discovery is
redirected to an empty dir; HOME is left alone so caches, credentials,
and git keep working (the #82446 isolation contract, which lived in
the installers' run_locked_uv_sync() before the sync moved into pm)."""
the installers' run_locked_uv_sync() before the sync moved into pm).
UV_CACHE_DIR always points at the hermes-owned cache (shipped with
bundles, shared machine-wide) — never the user's ambient cache."""
env = dict(os.environ if base_env is None else base_env)
for key in list(env):
if key.startswith("UV_") or key in (
@@ -258,6 +305,7 @@ def uv_env(base_env: Optional[dict] = None) -> dict[str, str]:
env["XDG_CONFIG_HOME"] = str(isolated)
env["XDG_CONFIG_DIRS"] = str(isolated)
env["UV_NO_CONFIG"] = "1"
env["UV_CACHE_DIR"] = str(uv_cache_dir())
return env
@@ -290,11 +338,48 @@ class Venv(StatePackage):
return repo_root()
def venv_dir(self) -> Path:
# Sealed installs are read-only: the MUTABLE venv lives in the
# writable machine hermes root (get_default_hermes_root()/venv —
# per-install, beside the uv cache), seeded on first adopt by
# copying the payload's shipped venv out. Dev/source installs
# keep the repo-local venv (project_venv_dir) unchanged.
from pm.ensure import sealed
if sealed():
from hermes_constants import get_default_hermes_root
return get_default_hermes_root() / "venv"
from hermes_constants import project_venv_dir
found = project_venv_dir(self.project_root())
return found if found else self.project_root() / "venv"
def seed_mutable_venv(self) -> Optional[str]:
"""Bootstrap the mutable venv from the shipped payload venv
(lazy-off installs). Returns None on success, else why not."""
import shutil
from pm.ensure import lazy_installs_allowed, sealed
from pm.paths import store_root
if not sealed():
return None # dev installs use the repo venv directly
dest = self.venv_dir()
if dest.exists():
return None # already bootstrapped
if lazy_installs_allowed():
# Lazy installs ON: build fresh from the shipped uv cache —
# no seed copy needed (the sync is near-free from the cache).
return None
payload_venv = store_root().parent / "venv"
if not payload_venv.is_dir():
return "payload ships no venv to seed from"
try:
shutil.copytree(payload_venv, dest)
except OSError as exc:
return f"seed copy failed: {exc}"
return None
def expected_stamp(self, extras: list[str]) -> str:
import hashlib
import sys

128
tests/pm/test_uv_cache.py Normal file
View File

@@ -0,0 +1,128 @@
"""pm uv cache: hermes-owned UV_CACHE_DIR + sealed-venv bootstrap seed.
The cache ships with bundles and is copied out to the writable machine
cache on first use; every pm-internal uv invocation pins UV_CACHE_DIR.
The mutable venv on sealed installs lives in the machine hermes root,
seeded from the payload's shipped venv when lazy installs are off.
"""
from __future__ import annotations
from pathlib import Path
import pytest
import pm.packages as pkgs
def test_uv_env_pins_cache_dir(monkeypatch, tmp_path):
monkeypatch.setattr(pkgs, "uv_cache_dir", lambda: tmp_path / "c")
env = pkgs.uv_env({"UV_CACHE_DIR": "/ambient/user/cache", "PATH": "x"})
assert env["UV_CACHE_DIR"] == str(tmp_path / "c")
# ambient UV_ vars are stripped, not inherited
assert "UV_PROJECT_ENVIRONMENT" not in env
def test_uv_cache_dir_seeds_from_payload(monkeypatch, tmp_path):
home = tmp_path / "home"
home.mkdir()
payload = tmp_path / "payload"
(payload / "uv-cache" / "wheels-v5").mkdir(parents=True)
(payload / "uv-cache" / "wheels-v5" / "some.pkg").write_text("x", encoding="utf-8")
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: home)
import pm.paths as paths_mod
monkeypatch.setattr(paths_mod, "store_root", lambda: payload / "tools")
machine = pkgs.uv_cache_dir()
assert machine == home / "cache" / "uv"
# seed copied out
assert (machine / "wheels-v5" / "some.pkg").read_text(encoding="utf-8") == "x"
# seeded marker written → second call doesn't re-copy
assert (machine / ".seeded").is_file()
(payload / "uv-cache" / "wheels-v5" / "some.pkg").write_text("changed", encoding="utf-8")
pkgs.uv_cache_dir()
assert (machine / "wheels-v5" / "some.pkg").read_text(encoding="utf-8") == "x"
def test_uv_cache_dir_cold_machine_no_payload(monkeypatch, tmp_path):
home = tmp_path / "home"
home.mkdir()
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: home)
import pm.paths as paths_mod
monkeypatch.setattr(paths_mod, "store_root", lambda: tmp_path / "nowhere" / "tools")
machine = pkgs.uv_cache_dir()
assert machine == home / "cache" / "uv"
assert (machine / ".seeded").is_file()
def test_venv_dir_sealed_goes_to_machine_root(monkeypatch, tmp_path):
home = tmp_path / "home"
home.mkdir()
import sys
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: home)
ensure_mod = sys.modules["pm.ensure"]
monkeypatch.setattr(ensure_mod, "sealed", lambda: True)
venv = pkgs.Venv()
assert venv.venv_dir() == home / "venv"
def test_seed_mutable_venv_copies_payload_venv(monkeypatch, tmp_path):
home = tmp_path / "home"
home.mkdir()
payload = tmp_path / "payload"
(payload / "venv" / "Scripts").mkdir(parents=True)
(payload / "venv" / "Scripts" / "python.exe").write_text("bin", encoding="utf-8")
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: home)
import sys
ensure_mod = sys.modules["pm.ensure"]
import pm.paths as paths_mod
monkeypatch.setattr(ensure_mod, "sealed", lambda: True)
monkeypatch.setattr(ensure_mod, "lazy_installs_allowed", lambda: False)
monkeypatch.setattr(paths_mod, "store_root", lambda: payload / "tools")
venv = pkgs.Venv()
reason = venv.seed_mutable_venv()
assert reason is None
seeded = home / "venv"
assert (seeded / "Scripts" / "python.exe").read_text(encoding="utf-8") == "bin"
# idempotent: second call is a no-op
assert venv.seed_mutable_venv() is None
def test_seed_mutable_venv_lazy_on_skips_copy(monkeypatch, tmp_path):
home = tmp_path / "home"
home.mkdir()
payload = tmp_path / "payload"
(payload / "venv").mkdir(parents=True)
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: home)
import sys
ensure_mod = sys.modules["pm.ensure"]
import pm.paths as paths_mod
monkeypatch.setattr(ensure_mod, "sealed", lambda: True)
monkeypatch.setattr(ensure_mod, "lazy_installs_allowed", lambda: True)
monkeypatch.setattr(paths_mod, "store_root", lambda: payload / "tools")
venv = pkgs.Venv()
assert venv.seed_mutable_venv() is None
assert not (home / "venv").exists() # builds fresh later, no seed copy