Files
PaperClipAI/packages/paperclip-runner/scripts/materialize-hermes.py

78 lines
4.1 KiB
Python

"""Build a native closure after provision-hermes verifies source and resolves uv.lock.
This runs only during explicit provisioning, never in an agent's execution.
"""
import hashlib
import json
import os
import shutil
import sys
import sysconfig
import subprocess
from pathlib import Path
source, destination, provider = map(Path, sys.argv[1:])
if destination.exists():
raise ValueError("Hermes provisioning destination must be new")
destination.mkdir(parents=True, mode=0o700)
def copy_tree(origin, target, excluded=()):
def ignore(directory, names):
return [name for name in names if name in excluded or name == "__pycache__"
or name.endswith((".pyc", ".pyo"))]
shutil.copytree(origin, target, symlinks=False, ignore=ignore, dirs_exist_ok=True)
copy_tree(Path(sys.base_prefix), destination / "python", ("include", "share"))
# uv rewrites sysconfig to its installation home. That path differs between
# local consumers, root image builds and the Daytona runtime user. Normalize
# this build metadata so the verified closure is relocatable and reproducible.
prefixes = {sys.base_prefix, str(Path(sys.base_prefix).resolve()), sysconfig.get_config_var("prefix")}
for metadata in (destination / "python/lib/python3.12").glob("_sysconfigdata_*.py"):
text = metadata.read_text()
for prefix in sorted((p for p in prefixes if isinstance(p, str)), key=len, reverse=True):
text = text.replace(prefix, "/paperclip-hermes/python")
metadata.write_text(text)
if sys.platform == "darwin":
# uv also rewrites the Mach-O install name. Restore a relative identity and
# a deterministic ad-hoc signature after changing its load command.
library = destination / "python/lib/libpython3.12.dylib"
if not shutil.which("install_name_tool") or not shutil.which("codesign"):
raise ValueError("Hermes provisioning requires macOS Command Line Tools (install_name_tool and codesign)")
subprocess.run(["install_name_tool", "-id", "@rpath/libpython3.12.dylib", str(library)], check=True)
subprocess.run(["codesign", "--force", "--sign", "-", "--identifier", "paperclip.hermes.libpython3.12", "--timestamp=none", str(library)], check=True)
site = destination / "python/lib/python3.12/site-packages"
site.mkdir(parents=True, exist_ok=True)
# Do not retain editable-install locators, virtualenv hooks, or provenance files
# containing the build machine's absolute paths. Hermes source is loaded below.
for item in (source / ".venv/lib/python3.12/site-packages").iterdir():
if item.name.startswith(("__editable__", "_virtualenv", "hermes_agent-")) or item.name == "__pycache__":
continue
if item.suffix == ".pth":
raise ValueError("Unreviewed Python site hook in Hermes dependencies")
if item.is_dir():
copy_tree(item, site / item.name)
elif item.suffix not in (".pyc", ".pyo"):
shutil.copy2(item, site / item.name)
copy_tree(source, destination / "app", (".venv", ".git", ".github", "tests", "website", "docs", ".pytest_cache"))
for name in ("bridge.py", "policy.py", "tool_process.py", "entry.py", "version.json"):
shutil.copy2(provider / name, destination / name)
# Installed RECORD and direct_url metadata can contain mutable build locations.
for item in destination.rglob("*.dist-info/direct_url.json"):
item.unlink()
for item in destination.rglob("*.dist-info/RECORD"):
item.unlink()
entries = []
for item in sorted(destination.rglob("*")):
if not item.is_file():
continue
data = item.read_bytes()
entries.append({"path": item.relative_to(destination).as_posix(), "sha256": hashlib.sha256(data).hexdigest(),
"size": len(data), "executable": bool(item.stat().st_mode & 0o111)})
entries.sort(key=lambda entry: entry["path"].encode("utf-16-be"))
serialized = json.dumps(entries, separators=(",", ":"), ensure_ascii=False).encode()
digest = hashlib.sha256(serialized).hexdigest()
(destination / "manifest.json").write_text(json.dumps({"entries": entries}, separators=(",", ":")))
print(json.dumps({"closureSha256": digest, "files": len(entries), "bytes": sum(e["size"] for e in entries)}))