Files
hermes-agent/tests/scripts/test_baked_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
4.5 KiB
Python

"""Baked bytecode: staging contract and launcher marker behavior."""
import os
import sys
from pathlib import Path
import pytest
from scripts.bundles.bytecode import MARKER, bake_bytecode
def _make_payload(root: Path) -> Path:
(root / "hermes-agent" / "pkg").mkdir(parents=True)
(root / "hermes-agent" / "pkg" / "__init__.py").write_text("")
(root / "hermes-agent" / "pkg" / "mod.py").write_text("X = 1\n")
site = root / "venv" / f"lib/python{sys.version_info.major}.{sys.version_info.minor}" / "site-packages"
site.mkdir(parents=True)
(site / "dep.py").write_text("Y = 2\n")
pm = root / "pm-runtime" / f"lib/python{sys.version_info.major}.{sys.version_info.minor}" / "site-packages"
pm.mkdir(parents=True)
(pm / "pmdep.py").write_text("Z = 3\n")
return root
def test_bake_produces_readonly_unchecked_hash_pycs(tmp_path):
root = _make_payload(tmp_path)
result = bake_bytecode(root, Path(sys.executable))
assert (root / MARKER).read_text().strip() == "unchecked-hash"
pyc = next((root / "hermes-agent" / "pkg" / "__pycache__").glob("mod*.pyc"))
# PEP 552 header, little-endian flags at bytes 4..8: value 1 =
# hash-based (bit0) and unchecked (bit1 clear). Timestamp pycs would be 0;
# checked-hash would be 3.
assert pyc.read_bytes()[4:8] == b"\x01\x00\x00\x00"
# read-only before packaging: a cache-miss write cannot land
assert not os.access(pyc, os.W_OK) or pyc.stat().st_mode & 0o222 == 0
assert result["modules"] >= 3
def test_bake_fails_closed_on_uncompilable_module(tmp_path):
"""Strict compileall: an unparseable module fails the whole bake instead of
silently shipping cold-compile-every-launch bytecode."""
import subprocess as _sp
root = _make_payload(tmp_path)
(root / "hermes-agent" / "pkg" / "broken.py").write_text("this is (( not python\n")
with pytest.raises(_sp.CalledProcessError):
bake_bytecode(root, Path(sys.executable))
def test_uncovered_reports_parseable_module_without_pyc(tmp_path):
"""The coverage gate: a parseable module lacking bytecode is reported
before it can ship as a cold-compile stall."""
from importlib.util import cache_from_source
root = _make_payload(tmp_path)
bake_bytecode(root, Path(sys.executable))
from scripts.bundles.bytecode import _uncovered
missing, unparseable = _uncovered(root / "hermes-agent")
assert missing == [] and unparseable == 0
pyc = Path(cache_from_source(root / "hermes-agent" / "pkg" / "mod.py"))
pyc.chmod(0o644) # pycs are sealed read-only; dirs stay writable
pyc.unlink()
missing, _ = _uncovered(root / "hermes-agent")
assert [m.name for m in missing] == ["mod.py"]
def test_bake_rejects_missing_import_root(tmp_path):
(tmp_path / "venv").mkdir()
with pytest.raises(FileNotFoundError):
bake_bytecode(tmp_path, Path(sys.executable))
def _load_wrapper():
"""Import the wrapper with placeholders substituted, like the build does."""
from scripts.build.launchers import render_wrapper
text = render_wrapper("stubmod.entry:main", "../hermes-agent", "../venv/Lib/site-packages")
namespace: dict = {"__name__": "launcher_wrapper_under_test"}
wrapper = Path("scripts/build/launcher_wrapper.py")
exec(compile(text, str(wrapper), "exec"), namespace) # noqa: S102 - test fixture
return namespace
def test_launcher_skips_user_cache_redirect_when_marker_present(tmp_path):
ns = _load_wrapper()
payload = _make_payload(tmp_path)
(payload / MARKER).write_text("unchecked-hash\n")
here = payload / "bin"
here.mkdir()
environ = {"HOME": str(tmp_path / "userhome")}
original = sys.pycache_prefix
try:
ns["configure"](str(here), environ=environ)
# Baked payload: no prefix redirect — imports read the baked
# source-adjacent dirs (Python's default lookup), and the prefix
# would relocate those READS away from the payload.
assert "PYTHONPYCACHEPREFIX" not in environ
finally:
sys.pycache_prefix = original
def test_launcher_keeps_user_cache_redirect_without_marker(tmp_path):
ns = _load_wrapper()
payload = _make_payload(tmp_path)
here = payload / "bin"
here.mkdir()
environ = {"HOME": str(tmp_path / "userhome")}
original = sys.pycache_prefix
try:
ns["configure"](str(here), environ=environ)
assert environ["PYTHONPYCACHEPREFIX"] == str(
tmp_path / "userhome" / ".cache" / "hermes-pycache")
finally:
sys.pycache_prefix = original