Files
hermes-agent/scripts/bundles/bytecode.py
ethernet f6713f2762 perf(bundles): bake payload bytecode; ship only what the sealed runtime reads
First launch of a bundled payload paid a cold-compile stall: the launcher
redirects bytecode writes to a user-level cache (signature-breaking on
macOS, read-only mount on AppImage/MSIX), so every import compiled from
source. Now staging bakes the cache into the payload:

- compileall with the payload's OWN staged 3.14 interpreter, unchecked-
  hash pycs: repack mtimes cannot invalidate them, a stale source can
  never trigger a rewrite, and read-only pycs mean the macOS signature
  never observes a change. Dirs stay writable — in-place rebuilds
  rmtree the tree; asserted coverage plus unchecked-hash means no
  cache-miss write can target them.
- coverage is the perf contract: the bake FAILS if any parseable module
  lacks a pyc (empirically 0 unparseable files ship, so compileall is
  strict). Probe suite: py_compile/cache_from_source, PEP 552 flags,
  multi-root read, stale-source no-rewrite, read-only cache-dir import.
- launcher: the baked marker makes configure() leave sys.pycache_prefix
  UNSET — the prefix relocates reads too and would hide the baked pycs.
  Payload modules read their source-adjacent cache (Python's default
  multi-root lookup); plugin/user modules keep caching beside their own
  sources under HERMES_HOME. Unmarked payloads keep the old redirect.
- snapshot(): the sealed payload ships without tests/website/evals/
  .github/nix/docker/tests-js (~69MB, 46% of tracked bytes) and without
  apps/ui-tui/web/scripts — CI prebuilds those products, and
  is_bundled_payload routes sealed updates to the channel updater, so
  the rebuild graph never runs in a bundle (linux_desktop_entry degrades
  to the themed icon). Frontend product staging keeps the full tree.
- test_bundle_native now stages the FULL relocatable toolchain (a bare
  interpreter ELF falls back to its compile-time /install prefix and
  cannot create a venv), and runs on the real 3.14 for the first time
  this campaign — the whole battery had been running 3.12 against the
  3.14-pinned lock.
2026-09-14 19:26:04 -04:00

113 lines
5.1 KiB
Python

"""Bake the payload's bytecode cache at staging time.
A sealed payload ships no __pycache__: the runtime redirects bytecode
writes to a user-level cache (signature-breaking on macOS, read-only
mount on AppImage/MSIX), so every fresh install pays a cold-compile
stall on first launch. Baking turns that into a warm read:
* compileall runs with the payload's OWN staged interpreter (a pyc
minted by any other interpreter is silently ignored — the magic tag
is part of the cache filename);
* ``--invalidation-mode unchecked-hash``: the pyc is trusted without
source validation and never rewritten, so staging-repack mtimes
cannot invalidate it and a stale source cannot trigger a rewrite
into the sealed tree;
* the baked pycs are chmodded read-only BEFORE packaging, so no
incidental write can touch them; the dirs stay writable because
in-place rebuilds rmtree the tree, and asserted coverage means no
cache-miss write can target them;
* the runtime's sys.pycache_prefix redirect is dropped when the baked
marker is present (the prefix relocates READS too — see
scripts/build/launcher_wrapper.py), so imports read the source-
adjacent baked pycs, which is Python's default multi-root lookup:
payload modules read theirs; plugin/user modules keep caching beside
their own sources under HERMES_HOME.
Coverage is the performance contract: every parseable module under
the baked roots must have a pyc. Unparseable fixtures (deliberately
invalid test data) are counted and skipped, not failed.
"""
import os
import py_compile
import subprocess
from pathlib import Path
MARKER = ".hermes-baked-pycache"
def _import_roots(root: Path) -> list[Path]:
"""The payload's import roots, in launcher order (repo, venv site, PM)."""
roots = [root / "hermes-agent"]
for source_root in roots + [root / "venv", root / "pm-runtime"]:
if not source_root.is_dir():
raise FileNotFoundError(f"missing import root: {source_root}")
sites = sorted((root / "venv").glob("lib/python*/site-packages")) + \
sorted((root / "venv").glob("Lib/site-packages"))
if len(sites) != 1:
raise ValueError(f"expected exactly one staged venv site-packages, found {len(sites)}")
roots.append(sites[0])
pm_sites = sorted((root / "pm-runtime").glob("lib/python*/site-packages")) + \
sorted((root / "pm-runtime").glob("Lib/site-packages"))
if len(pm_sites) != 1:
raise ValueError(f"expected exactly one PM runtime site-packages, found {len(pm_sites)}")
roots.append(pm_sites[0])
return roots
def _uncovered(root: Path) -> tuple[list[Path], int]:
"""Parseable .py files without a pyc, and unparseable fixtures skipped."""
from importlib.util import cache_from_source
missing: list[Path] = []
unparseable = 0
for path in root.rglob("*.py"):
if "__pycache__" in path.parts:
continue
if Path(cache_from_source(path)).exists():
continue
try:
compile(path.read_bytes(), str(path), "exec")
except SyntaxError:
# Deliberately invalid test fixtures ship in the snapshot; they
# can never be imported, so no pyc is expected.
unparseable += 1
else:
missing.append(path)
return missing, unparseable
def bake_bytecode(root: Path, python: Path) -> dict:
"""Compile every payload module with the staged interpreter and seal the
caches read-only. ``root`` is the payload root; ``python`` MUST be the
payload's own staged interpreter binary."""
root = Path(root).resolve()
python = Path(python).resolve()
if not python.is_file():
raise FileNotFoundError(f"staged interpreter is missing: {python}")
total = 0
for source_root in _import_roots(root):
subprocess.run(
[str(python), "-I", "-m", "compileall", "-q",
"--invalidation-mode", "unchecked-hash", str(source_root)],
check=True, timeout=30 * 60, stdin=subprocess.DEVNULL)
missing, unparseable = _uncovered(source_root)
if missing:
sample = ", ".join(str(m.relative_to(root)) for m in missing[:5])
raise ValueError(f"{len(missing)} payload modules have no bytecode pyc "
f"({sample}…): a fresh install would cold-compile them "
"every launch")
total += sum(1 for _ in source_root.rglob("__pycache__/*.pyc"))
# Pycs are read-only BEFORE packaging so a stale-source rewrite (which
# unchecked-hash never triggers anyway) or any incidental write can never
# touch them and the macOS signature never observes a pyc change. The
# __pycache__ DIRS stay writable: in-place payload rebuilds rmtree the
# tree (native._prepare_native), and complete asserted coverage plus
# unchecked-hash means no cache-miss write can ever target these dirs.
for source_root in _import_roots(root):
for cache in source_root.rglob("__pycache__"):
for pyc in cache.iterdir():
if pyc.is_file():
pyc.chmod(0o444)
(root / MARKER).write_text("unchecked-hash\n", encoding="utf-8")
return {"modules": total, "marker": str(root / MARKER)}