"""Bundled-payload CLI entry wrapper — the distlib launcher's zip overlay. scripts/build/launchers.py reads this file, substitutes the four HERMES_* placeholders (see the constants below), and hands the result to a distlib ScriptMaker as the script text. On win32 the minted artifact is a real PE: the distlib launcher stub + a shebang whose interpreter is the ```` placeholder form (resolved at runtime relative to the launcher's own directory) + this script packed as __main__.py in a zip overlay. The The wrapper replaces everything the old rust shim + its sidecar did (plan: pm-clean): * resolve the launcher's own directory from sys.argv[0] (the distlib launcher puts its exe path there — the shebang's ```` placeholder resolves the interpreter the same way), * put the payload repo snapshot FIRST and the venv site-packages second on sys.path (same order, same reason as the old shim: the repo's hermes_cli wins over anything stale in site-packages — the sealed payload's venv has no working editable install, its pointer names the BUILD machine). The site entry goes through ``site.addsitedir()`` — the only mechanism that runs ``.pth`` files — because pywin32.pth is what puts win32\\lib on sys.path and therefore what makes ``import pywintypes`` resolve on Windows bundles (portalocker's Win32Locker → concurrent-log-handler → hermes_logging.py's files), * drop inherited PYTHONPATH / PYTHONHOME so foreign installs can never shadow the bundle, * default sys.pycache_prefix to the user-level cache (%LOCALAPPDATA% \\hermes\\pycache on win32, ~/.cache/hermes-pycache elsewhere) when the user has not set one — the sealed payload must never see __pycache__ writes (signature-breaking on mac, read-only mount on AppImage/MSIX). The whole file is import-safe so tests can drive configure()/main() directly (tests/scripts/test_desktop_cli_wrapper.py). """ import importlib import os import site import sys HERMES_ENTRY_MODULE = "__HERMES_ENTRY_MODULE__" HERMES_ENTRY_FUNC = "__HERMES_ENTRY_FUNC__" HERMES_REPO_REL = "__HERMES_REPO_REL__" HERMES_SITE_REL = "__HERMES_SITE_REL__" def _join_rel(here, rel): """Join a forward-slash relative path (the minted-in convention, same as the payload manifest) onto a host dir without os.sep assumptions.""" return os.path.join(here, *rel.split("/")) def launcher_dir(argv0): """The directory of the launcher itself, from sys.argv[0]. abspath because cwd is meaningless for a GUI/alias launch.""" return os.path.dirname(os.path.abspath(argv0)) def payload_sys_paths(here): """Import roots for the sealed payload: repo first (wins), then the venv's dependency tree — mirroring the old shim's PYTHONPATH order.""" return [_join_rel(here, HERMES_REPO_REL), _join_rel(here, HERMES_SITE_REL)] def default_pycache_dir(environ): """User-level bytecode cache. The sealed payload is read-only at runtime (or signature-sealed, on mac), so __pycache__ writes must land outside it. Only consulted when the user has not set their own PYTHONPYCACHEPREFIX.""" if sys.platform == "win32": base = environ.get("LOCALAPPDATA") return os.path.join(base, "hermes", "pycache") if base else None base = environ.get("HOME") return os.path.join(base, ".cache", "hermes-pycache") if base else None def configure(here, environ=None): """Point THIS process (the minted launcher's python) at the payload. Returns the sys.path entries prepended, for tests.""" environ = os.environ if environ is None else environ # Same hygiene as the old rust shim: an inherited PYTHONPATH could # shadow bundled modules with foreign ones; PYTHONHOME would repoint # the stdlib entirely. environ.pop("PYTHONPATH", None) environ.pop("PYTHONHOME", None) repo_entry, site_entry = payload_sys_paths(here) # Repo snapshot first — its hermes_cli wins over anything stale in # site-packages (the sealed payload has no working editable install). sys.path.insert(0, repo_entry) # addsitedir(), not a raw append: only it processes the venv's .pth # files. pywin32.pth is load-bearing on Windows — it puts win32\lib # on sys.path, which is what makes `import pywintypes` resolve, and # without that portalocker's Win32Locker dies and # concurrent-log-handler silently drops every file-log record (the # same trap gateway/run.py's MCP venv bootstrap works around). Keep # site-packages directly after the repo, ahead of .pth-added dirs. site.addsitedir(site_entry) if site_entry in sys.path: sys.path.remove(site_entry) sys.path.insert(1 if sys.path and sys.path[0] == repo_entry else 0, site_entry) if not environ.get("PYTHONPYCACHEPREFIX"): # A baked payload reads its own source-adjacent __pycache__ dirs # (Python's default lookup). sys.pycache_prefix must stay UNSET here: # the prefix relocates reads as well as writes, so it would hide the # baked bytecode and re-pay the cold-compile stall this marker exists # to remove. Without the marker (old payloads), the user-level # redirect below keeps bytecode writes out of the sealed tree. if not os.path.exists(os.path.join(os.path.dirname(repo_entry), ".hermes-baked-pycache")): default = default_pycache_dir(environ) if default: environ["PYTHONPYCACHEPREFIX"] = default # The env var is only read at interpreter startup; we ARE at # startup, but setting it in os.environ cannot retro-activate # it — sys.pycache_prefix is the live switch. sys.pycache_prefix = default return [repo_entry, site_entry] def main(argv=None): argv = list(sys.argv if argv is None else argv) here = launcher_dir(argv[0] if argv else sys.argv[0]) configure(here) importlib.import_module("hermes_bootstrap") module = importlib.import_module(HERMES_ENTRY_MODULE) target = getattr(module, HERMES_ENTRY_FUNC) # main() reads sys.argv; hand it the real argv (argv[0] stays the # launcher path, exactly like a console-script entry point). return target() if __name__ == "__main__": sys.exit(main())