Files
hermes-agent/scripts/build/mint_launchers.py

112 lines
4.8 KiB
Python

r"""Mint the bundled payload's win32 CLI launchers with distlib.
Run by scripts/bundles/desktop.py (step 5b) with the payload's OWN
store python as the minting interpreter — the store python is the same
distribution the launchers will execute, and its architecture is by
construction the target architecture, which is exactly what distlib's
_get_launcher needs (it picks a 32/64-bit, -arm-suffixed launcher stub by
the MINTING interpreter's platform; cross-arch minting with a mismatched
host python produces launchers that cannot run).
distlib availability: the payload python ships pip (python-build-standalone
includes it), and pip vendors distlib at pip._vendor.distlib — so the mint
works with NO extra dependency. A top-level distlib install is preferred
when present. (Self-contained .\_pth interpreters were REJECTED as an
alternative approach: they break uv venv materialization.)
Layout facts arrive via environment from scripts/build/launchers.py:
HERMES_MINT_BIN_DIR absolute output dir (agent-payload/bin)
HERMES_MINT_SPECS JSON list of {"name": exe stem, "module": dotted
module, "func": entry function} — mirrors
[project.scripts] in pyproject.toml
HERMES_MINT_WRAPPER path of the RENDERED launcher-wrapper.py for THIS
entry (substitution is scripts/build/launchers.py's job,
one implementation, one test)
HERMES_MINT_PYTHON bin-relative path of the store python, BACKslashes,
e.g. <launcher_dir>\..\tools\<entry>\python.exe —
baked into the shebang; the literal <launcher_dir>
prefix is resolved by the launcher at run time
relative to its own directory (relocatability is
live-proved: the placeholder survives minting)
Testable standalone: tests/scripts/test_mint_launchers.py drives main()
against a temp tree with a stub module and RUNS the minted exe.
"""
from __future__ import annotations
import json
import os
try: # a real distlib install when available
from distlib.scripts import ScriptMaker
except ImportError: # the payload python's guaranteed fallback: pip's vendor
from pip._vendor.distlib.scripts import ScriptMaker
def make_maker(bin_dir, shebang_python, wrapper_text):
class MintScriptMaker(ScriptMaker):
# The wrapper text is fully pre-rendered, so ignore the entry the
# spec carried — the name is all we need from it.
def __init__(self, wrapper, **kwargs):
target_dir = kwargs.pop("target_dir")
super().__init__(None, target_dir, **kwargs)
self._wrapper_text = wrapper
def _get_script_text(self, entry):
return self._wrapper_text
# The shebang the launcher stub parses: #! + this string. The
# <launcher_dir> literal is replaced by the launcher with its own
# directory at run time — that IS the self-relative mechanism.
maker = MintScriptMaker(
wrapper_text,
target_dir=bin_dir,
add_launchers=True,
dry_run=False,
)
maker.executable = shebang_python
return maker
def mint_one(bin_dir, shebang_python, wrapper_text, spec):
"""Mint one launcher exe; returns its absolute path. distlib also
writes a python-versioned twin (hermes-3.14.exe) — the payload ships
exactly one name per entry, so the twin is removed."""
maker = make_maker(bin_dir, shebang_python, wrapper_text)
filenames = maker.make(f"{spec['name']} = {spec['module']}:{spec['func']}")
plain = os.path.join(bin_dir, f"{spec['name']}.exe")
if os.path.abspath(plain) not in (os.path.abspath(f) for f in filenames):
raise SystemExit(f"distlib did not write {plain}: {filenames}")
for extra in filenames:
if os.path.abspath(extra) != os.path.abspath(plain):
os.remove(extra)
return plain
def main(environ=None):
environ = os.environ if environ is None else environ
bin_dir = environ["HERMES_MINT_BIN_DIR"]
specs = json.loads(environ["HERMES_MINT_SPECS"])
shebang_python = environ["HERMES_MINT_PYTHON"]
if not os.path.isabs(bin_dir):
raise SystemExit("HERMES_MINT_BIN_DIR must be absolute")
if not shebang_python.startswith("<launcher_dir>\\..\\"):
raise SystemExit(
"HERMES_MINT_PYTHON must be <launcher_dir>\\..\\payload-relative "
f"(backslashes), got: {shebang_python}"
)
os.makedirs(bin_dir, exist_ok=True)
minted = []
for spec in specs:
# utf-8-sig: tolerate a BOM the JS renderer might prepend.
with open(environ["HERMES_MINT_WRAPPER"], encoding="utf-8-sig") as f:
wrapper_text = f.read()
minted.append(mint_one(bin_dir, shebang_python, wrapper_text, spec))
return minted
if __name__ == "__main__":
print("\n".join(main()))