Files
hermes-agent/pm/_uv.py
Teknium eb699a158c fix(pm): MSIX plugin installs build on a writable copy of the bundled Python (#135236)
Enabling a plugin with Python dependencies, and `hermes pm install`, failed
in the packaged Windows app: uv lock/sync/pip check exit 101 with "Unable to
create process using ...\venv\Scripts\python.exe".

uv queries a venv by running its Scripts\python.exe. That redirector lives
outside the package and CreateProcess's the interpreter named by
pyvenv.cfg `home`. 6a031bf1b3 moved generations onto the packaged
interpreter, so `home` names a file under WindowsApps that a process without
package identity may not start (exit 101 is the redirector's
RC_CREATE_PROCESS). Restore the verified writable copy of the pinned Python
in the user store as the build interpreter for sealed Windows payloads.

venv_command (also from 6a031bf1b3) stays: Hermes still enters those venvs
on the packaged interpreter, so its own children keep package access to the
bundled git, rg and PM worker.
2026-10-08 20:20:55 -07:00

65 lines
3.0 KiB
Python

"""Pinned tool acquisition for PM's private Python engine; never worker API."""
from __future__ import annotations
from pathlib import Path
from pm import paths
from pm.filesystem import long_root, native
from pm.lock import Facts
from pm.package import InstallError
from pm.registry import get_package
from pm.store import Store, current_target
def _toolchain(*, realize: bool = True, explicit: bool = False) -> tuple[Path, Path] | None:
"""Resolve uv and Python without host discovery or recursive worker dispatch.
A read-only probe never installs. A sealed Windows payload builds on a verified writable
copy of its Python: uv starts a venv's ``Scripts\\python.exe`` to query it, and that
redirector runs outside the package, so its target must too. A redirector naming the
packaged interpreter dies with exit 101 (#135236). Hermes itself still enters those venvs
on the packaged interpreter (``pm.environments.venv_command``).
"""
from pm.install import (
_install, _installed_location, _lockfile, _refuse_lazy,
ensure, lazy_installs_allowed, sealed,
)
if realize:
ensure("uv", explicit=explicit)
lockfile = _lockfile()
target = current_target()
binaries = {}
for name in ("uv", "python"):
package = get_package(name)
location = _installed_location(package, lockfile, target)
if location is None:
if not realize:
return None
raise InstallError(name, "pinned tool is unavailable", "run `hermes pm install`")
if name == "python" and target.startswith("win32") and sealed():
# Spelled like Store spells its root, so the comparison and the copy's file calls agree.
writable = long_root(paths.writable_store_root())
if location[1].root != writable:
copied = _installed_location(package, lockfile, target, verify=explicit, roots=(writable,))
if copied is None:
if not realize:
return None
if not explicit and not lazy_installs_allowed():
raise _refuse_lazy(name, "a writable Python is required for bundled builds")
copy_store = Store(writable)
copy_facts = Facts(writable / "facts.json")
_install(package, lockfile, copy_facts, copy_store, target, copy_from=location)
copied = copy_facts, copy_store
location = copied
facts, store = location
binary = package.binary(store.entry(facts.get(name)["entry"]), target)
if binary is None or not binary.is_file():
if not realize:
return None
raise InstallError(name, "installed binary is missing", "run `hermes pm install`")
binaries[name] = binary
# Callers execute and compare these outside PM's own file calls: the ordinary spelling, not the store's.
return Path(native(binaries["uv"])), Path(native(binaries["python"]))