Files
hermes-agent/pm/native_build.py
ethernet a564b9dc17 feat(pm): contain child output interactively, stream it in CI
uv, npm ci, icon generation and the desktop build printed hundreds of
lines on every interactive install, update and `hermes desktop`.

pm/progress.py holds one policy. CI (CI / GITHUB_ACTIONS) or
HERMES_VERBOSE=1 streams child output unchanged. Otherwise each child is
one "→ label…" line, rewritten in place with its latest output on a
terminal, or just the start and finish lines when piped. A failure
always prints the last 80 lines. HERMES_VERBOSE=0 forces containment.

Applied to PM's uv runs (no uv --verbose outside CI; quick steps stay
silent unless they fail), the source build scripts (node-deps, TUI,
icons, web; npm warn lines stay out of the live line), the desktop
build and package steps, and the Windows ARM64 vcpkg/OpenSSL provider.
The policy reads the parent's environment, so the CI=1 the builders
force on their node children does not switch it to verbose.
2026-09-24 14:23:48 -04:00

67 lines
3.1 KiB
Python

"""Native compiler environment for dependency builds from a source checkout.
Windows ARM64 has no wheel for parts of the locked closure (cryptography), so
every sync there compiles from sdists and needs MSVC, Clang, Rust and static
OpenSSL. PM owns the sync, so PM prepares that environment. Otherwise only
callers that remembered to (source activation) could build, and
install.ps1, `hermes update` and repair failed in openssl-sys.
"""
from __future__ import annotations
from collections.abc import Mapping
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
from pm.progress import run_contained
_PROVIDER = Path("scripts/build/windows-deps.ps1")
def prepare_windows_environment(*, source: Path, state: Path, env: Mapping[str, str]) -> dict[str, str]:
"""The distribution adapter decides whether this target needs ARM64 tools."""
shell = shutil.which("powershell", path=env.get("PATH")) or shutil.which("pwsh", path=env.get("PATH"))
if shell is None:
raise FileNotFoundError("PowerShell is required to prepare Windows ARM64 build dependencies")
state.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="environment-", dir=state) as scratch:
output = Path(scratch) / "environment.json"
# A cold vcpkg clone and OpenSSL build print thousands of lines; the
# user needs the step and its failure, not the patch log.
run_contained(
[shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
str(source / _PROVIDER), "-StateRoot", str(state),
"-EnvironmentFile", str(output)],
"Preparing Windows ARM64 build tools", indent=" ",
cwd=source, env=dict(env), stdin=subprocess.DEVNULL,
)
prepared = json.loads(output.read_text(encoding="utf-8-sig"))
if not isinstance(prepared, dict) or any(not isinstance(k, str) or not isinstance(v, str) for k, v in prepared.items()):
raise ValueError("Windows build dependency provider returned an invalid environment")
return prepared
def source_build_environment(source: Path) -> dict[str, str] | None:
"""The environment a dependency build of ``source`` needs on this host.
None means the ambient environment suffices. Only a checkout carries the
provider; a payload's dependencies are prebuilt, so it needs no compiler.
The state root is the store's parent, the one source setup has always
used, so an existing vcpkg/OpenSSL build is reused rather than repeated.
"""
from pm.paths import store_root
from pm.store import current_target
if current_target() != "win32-arm64" or not (source / _PROVIDER).is_file():
return None
from pm.index_config import bridged_index_settings
prepared = prepare_windows_environment(source=source, state=store_root().parent, env=os.environ)
# managed_environment translates pip's index knobs only for the ambient
# environment; this one replaces it, so mirrors must ride along.
prepared.update(bridged_index_settings(os.environ))
return prepared