Files
hermes-agent/scripts/build/python_env.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

82 lines
3.4 KiB
Python

"""Native/Docker dependency preparation over PM's explicit uv mechanics.
Unlike live PM, a build never discovers plugins or publishes a selection.
The source is the prepared application tree; editable installs keep pointing
at that tree. Nix and Termux retain their own dependency providers.
"""
from __future__ import annotations
import argparse
from collections.abc import Mapping, Sequence
import os
from pathlib import Path
import shutil
import subprocess
import sys
from pm.environment import PythonEnvironment, prune_site_pth
from pm.package import InstallError
def build_python_environment(
*, source: Path, python: Path, uv: Path, out: Path,
env: Mapping[str, str], cache: Path | None = None,
extras: Sequence[str] = (), all_extras: bool = False,
no_install_project: bool = False, offline: bool = False,
) -> Path:
"""Build a fresh environment and return its Python executable after validation.
``out`` must not exist. Failure removes only this invocation's destination;
a pre-existing environment is never deleted or used as a successful result.
The cache (default: ``out.parent / ".uv-cache"``) remains available to the
native offline-cache packager. ``no_install_project`` prepares only the
locked dependencies, before an application source layer is available.
"""
source, out = source.absolute(), out.absolute()
cache = out.parent / ".uv-cache" if cache is None else cache.absolute()
environment = PythonEnvironment(
uv=uv.absolute(), python=python.absolute(), destination=out,
cache=cache.absolute(), env=env, offline=offline, output=sys.stderr,
)
out.mkdir(parents=True)
try:
environment.create()
environment.sync(source, extras=extras, all_extras=all_extras,
no_install_project=no_install_project)
environment.check()
# The sealed payload must never process uv's venv-marker or
# editable-install .pth files (see pm.environment.prune_site_pth):
# the launcher addsitedirs the venv, and those two would repoint
# sys.prefix / shadow the repo snapshot with build-machine paths.
prune_site_pth(out)
except BaseException:
shutil.rmtree(out, ignore_errors=True)
raise
return environment.executable
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True, type=Path)
parser.add_argument("--python", required=True, type=Path)
parser.add_argument("--uv", required=True, type=Path)
parser.add_argument("--out", required=True, type=Path)
parser.add_argument("--cache", type=Path, help="uv cache; defaults to OUT's sibling .uv-cache")
selection = parser.add_mutually_exclusive_group()
selection.add_argument("--extra", dest="extras", action="append", default=[])
selection.add_argument("--all-extras", action="store_true")
parser.add_argument("--no-install-project", action="store_true")
parser.add_argument("--offline", action="store_true")
args = parser.parse_args(argv)
try:
executable = build_python_environment(**vars(args), env=dict(os.environ))
except (InstallError, OSError, ValueError, subprocess.TimeoutExpired) as exc:
print(f"python environment: {exc}", file=sys.stderr)
return 1
print(executable)
return 0
if __name__ == "__main__":
raise SystemExit(main())