Files
hermes-agent/scripts/build/launchers.py
ethernet f67a3b59db fix(bundle): process the venv's .pth files in payload launchers
The minted launchers wired venv site-packages onto sys.path with a raw
insert (win32 wrapper) / PYTHONPATH (posix), neither of which runs .pth
files. pywin32.pth is load-bearing on Windows: it puts win32\lib on
sys.path, which is what makes 'import pywintypes' resolve — without it
portalocker's Win32Locker dies and concurrent-log-handler silently drops
every file-log record on Windows bundles.

* launcher_wrapper.py: site.addsitedir() for the site entry (repo first,
  site directly after, .pth dirs last)
* launchers.py posix: same via HERMES_SITE env in the -c bootstrap
* pm/environment.py: prune_site_pth() drops _virtualenv.pth and the
  __editable__ pointer (build-machine path) that must never run in a
  sealed payload
* python_env.py: run the prune after every environment build
2026-09-11 16:45:28 -04:00

105 lines
4.7 KiB
Python

"""Shared payload launcher rendering; all paths are supplied by the assembler."""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
import subprocess
import tempfile
def render_wrapper(entry: str, repo: str, site: str) -> str:
module, func = entry.split(":", 1)
text = (Path(__file__).with_name("launcher_wrapper.py")).read_text(encoding="utf-8-sig")
for key, value in {"ENTRY_MODULE": module, "ENTRY_FUNC": func, "REPO_REL": repo, "SITE_REL": site}.items():
if '"' in value or "\n" in value or "__" in value:
raise ValueError(f"invalid launcher value: {key}")
text = text.replace(f"__HERMES_{key}__", value)
unresolved = re.search(r"__HERMES_\w+?__", text)
if unresolved:
raise ValueError(f"unresolved launcher placeholder: {unresolved.group()}")
return text
def posix_launcher(name: str, entry: str, *, python: str, repo: str, site: str, target: str) -> str:
import shlex
def shell_path(value: str) -> str:
return shlex.quote(value) if Path(value).is_absolute() else '"$root"/' + shlex.quote(value)
module, func = entry.split(":", 1)
bionic = target.endswith("-bionic")
header = "#!/data/data/com.termux/files/usr/bin/sh" if bionic else "#!/usr/bin/env bash"
extra = ""
if bionic:
extra = '''PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
export PREFIX
export LD_LIBRARY_PATH="$root/tools/python/data/data/com.termux/files/usr/lib:$root/tools/node/data/data/com.termux/files/usr/lib:$root/tools/ffmpeg/data/data/com.termux/files/usr/lib:$root/runtime-libs/lib:$PREFIX/lib"
export HERMES_PYTHON_SRC_ROOT="$REPO"
export HERMES_PYTHON="$PYTHON"
export HERMES_NODE="$root/tools/node/data/data/com.termux/files/usr/bin/node"
export HERMES_RUNTIME_DIR="$root/tools"
export PATH="$root/tools/npm/bin:$root/tools/node/data/data/com.termux/files/usr/bin:$root/tools/ffmpeg/data/data/com.termux/files/usr/bin:$root/tools/ripgrep:$PATH"
'''
code = (
f"import os, site, sys; sys.argv[0]={name!r}; "
"site.addsitedir(os.environ['HERMES_SITE']); "
f"from {module} import {func}; sys.exit({func}())"
)
return f'''{header}
set -eu
self="$0"
while [ -L "$self" ]; do
target="$(readlink "$self")"
case "$target" in
/*) self="$target" ;;
*) self="$(dirname "$self")/$target" ;;
esac
done
root="$(cd "$(dirname "$self")/.." && pwd)"
PYTHON={shell_path(python)}
REPO={shell_path(repo)}
SITE={shell_path(site)}
[ -x "$PYTHON" ] || {{ printf '%s\\n' 'Bundled interpreter missing; reinstall Hermes.' >&2; exit 2; }}
unset PYTHONPATH PYTHONHOME
export PYTHONPATH="$REPO:$SITE"
# PYTHONPATH cannot process .pth files (only site.addsitedir() can), and
# the venv's .pth files are load-bearing (pywin32.pth -> win32\\lib ->
# `import pywintypes` on Windows bundles; the win32 wrapper mirrors this
# in launcher_wrapper.py). The -c bootstrap below runs addsitedir() on it.
export HERMES_SITE="$SITE"
export PYTHONPYCACHEPREFIX="${{PYTHONPYCACHEPREFIX:-${{XDG_CACHE_HOME:-$HOME/.cache}}/hermes-pycache}}"
{extra}exec "$PYTHON" -P -c {shlex.quote(code)} "$@"
'''
def write_launchers(root: Path, entries: dict[str, str], *, python: str,
repo: str, site: str, target: str, bin_dir: str = "bin",
run=subprocess.run) -> dict[str, str]:
"""Mint only declared commands; the caller owns completion publication."""
bindir = root / bin_dir
bindir.mkdir(parents=True, exist_ok=True)
windows = target.startswith("win32")
for name, entry in entries.items():
if windows:
with tempfile.TemporaryDirectory(prefix="hermes-mint-") as temp:
wrapper = Path(temp) / "wrapper.py"
wrapper.write_text(render_wrapper(entry, f"../{repo}", f"../{site}"), encoding="utf-8")
module, func = entry.split(":", 1)
env = {**os.environ, "HERMES_MINT_BIN_DIR": str(bindir),
"HERMES_MINT_SPECS": json.dumps([{"name": name, "module": module, "func": func}]),
"HERMES_MINT_WRAPPER": str(wrapper),
"HERMES_MINT_PYTHON": "<launcher_dir>\\..\\" + python.replace("/", "\\")}
run([str(root / python), str(Path(__file__).with_name("mint_launchers.py"))], env=env, check=True)
else:
script = posix_launcher(name, entry, python=python, repo=repo, site=site, target=target)
output = bindir / name
output.write_text(script, encoding="utf-8")
output.chmod(0o755)
commands = {name: f"{bin_dir}/{name}{'.exe' if windows else ''}" for name in entries}
for command in commands.values():
if not (root / command).is_file():
raise FileNotFoundError(f"payload launcher missing: {command}")
return commands