mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-14 09:11:54 +00:00
fix(cli): set PYTHON env for node-gyp native builds on NixOS (#40690)
* fix(cli): set PYTHON env for node-gyp native builds on NixOS
node-gyp (triggered by node-pty during npm ci) looks for python3 on
PATH, which fails on NixOS because python3 lives in the nix store and
is not on the system PATH.
Add _nixos_build_env() — a two-tier helper that detects NixOS and:
1. Fast path: hermes venv python3 (~0s)
2. Fallback: nix-shell which python3 (~2-5s)
Wire it into _run_npm_install_deterministic() via a new env= parameter,
then pass it through cmd_gui() and _update_node_dependencies().
Non-NixOS systems: _nixos_build_env() returns None, behavior unchanged.
* fix(cli): merge _nixos_build_env() with os.environ, fix NixOS detection, add explicit return None
- Critical fix: both Tier 1 (venv) and Tier 2 (nix-shell) now return
{**os.environ, "PYTHON": ...} instead of {"PYTHON": ...} — subprocess.run
with env= replaces the entire environment, so the old code wiped PATH
and broke npm/node on NixOS entirely.
- Uses re.search(r"^ID=nixos$", ...) for anchored NixOS detection instead
of unanchored substring match (could match ID_LIKE=...nixos).
- Removes redundant Path.exists() guard before read_text(); just catches
OSError (one filesystem read instead of two).
- Adds explicit return None at end of function for type-hint consistency.
This commit is contained in:
parent
7a5827c8b0
commit
15c99b437f
2 changed files with 63 additions and 2 deletions
|
|
@ -7044,12 +7044,66 @@ def _run_with_idle_timeout(
|
|||
return subprocess.CompletedProcess(cmd, rc, stdout=combined, stderr="")
|
||||
|
||||
|
||||
def _nixos_build_env() -> dict[str, str] | None:
|
||||
"""Return extra env vars for native module builds on NixOS.
|
||||
|
||||
On NixOS, python3 is typically not on the system PATH (it lives in
|
||||
the Nix store and only enters PATH inside a nix-shell or when
|
||||
explicitly installed as a system package). node-gyp uses Python to
|
||||
compile native addons like ``node-pty`` and its ``find-python.js``
|
||||
does a bare ``PATH`` lookup — which fails on NixOS.
|
||||
|
||||
Two-tier resolution:
|
||||
1. Fast path — the hermes venv's python3 (present in managed installs)
|
||||
2. Fallback — resolves the absolute python3 path via ``nix-shell``
|
||||
|
||||
Returns an env dict suitable for ``subprocess.run(env=...)`` or
|
||||
``None`` when we are not on NixOS or python3 is already on PATH.
|
||||
"""
|
||||
import re
|
||||
|
||||
try:
|
||||
os_release = Path("/etc/os-release").read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return None
|
||||
if not re.search(r"^ID=nixos$", os_release, re.M):
|
||||
return None
|
||||
|
||||
# python3 already on PATH — nothing to do
|
||||
if shutil.which("python3"):
|
||||
return None
|
||||
|
||||
# Tier 1: fast path — hermes venv python3, no nix-shell overhead
|
||||
for venv_name in ("venv", ".venv"):
|
||||
venv_python = PROJECT_ROOT / venv_name / "bin" / "python3"
|
||||
if venv_python.exists():
|
||||
return {**os.environ, "PYTHON": str(venv_python)}
|
||||
|
||||
# Tier 2: nix-shell fallback — resolves the absolute python3 path once.
|
||||
# Slower (~2–5 s for the nix-shell eval) but always works, even without
|
||||
# a hermes venv (pip / non-managed / bare-git installs). The resolved
|
||||
# path is a self-contained Nix store binary (all deps via RPATH) so it
|
||||
# stays valid even after the nix-shell exits.
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nix-shell", "-p", "python3", "--run", "which python3"],
|
||||
capture_output=True, text=True, check=False, timeout=15,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
python3_path = result.stdout.strip()
|
||||
if python3_path and Path(python3_path).exists():
|
||||
return {**os.environ, "PYTHON": python3_path}
|
||||
except Exception:
|
||||
pass # nix-shell not available — caller will get None
|
||||
|
||||
return None
|
||||
def _run_npm_install_deterministic(
|
||||
npm: str,
|
||||
cwd: Path,
|
||||
*,
|
||||
extra_args: tuple[str, ...] = (),
|
||||
capture_output: bool = True,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run a deterministic npm install that does not mutate ``package-lock.json``.
|
||||
|
||||
|
|
@ -7066,6 +7120,7 @@ def _run_npm_install_deterministic(
|
|||
ci_result = subprocess.run(
|
||||
ci_cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
|
|
@ -7080,6 +7135,7 @@ def _run_npm_install_deterministic(
|
|||
return subprocess.run(
|
||||
install_cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
|
|
@ -7620,7 +7676,8 @@ def cmd_gui(args: argparse.Namespace):
|
|||
print(f"✓ Desktop {build_label} is up to date (content stamp matches)")
|
||||
else:
|
||||
print("→ Installing desktop workspace dependencies...")
|
||||
install_result = _run_npm_install_deterministic(npm, PROJECT_ROOT, capture_output=False)
|
||||
nixos_env = _nixos_build_env()
|
||||
install_result = _run_npm_install_deterministic(npm, PROJECT_ROOT, capture_output=False, env=nixos_env)
|
||||
if install_result.returncode != 0:
|
||||
print("✗ Desktop dependency install failed")
|
||||
print(f" Run manually: cd {PROJECT_ROOT} && npm ci")
|
||||
|
|
@ -9619,6 +9676,8 @@ def _update_node_dependencies() -> None:
|
|||
print("→ Updating Node.js dependencies...")
|
||||
extra_args = ["--no-fund", "--no-audit", "--progress=false"]
|
||||
|
||||
nixos_env = _nixos_build_env()
|
||||
|
||||
# Step 1: root install (no workspace recursion).
|
||||
root_args = [*extra_args, "--workspaces=false"]
|
||||
root_result = _run_npm_install_deterministic(
|
||||
|
|
@ -9626,6 +9685,7 @@ def _update_node_dependencies() -> None:
|
|||
PROJECT_ROOT,
|
||||
extra_args=tuple(root_args),
|
||||
capture_output=False,
|
||||
env=nixos_env,
|
||||
)
|
||||
if root_result.returncode != 0:
|
||||
print(" ⚠ npm install failed in repo root")
|
||||
|
|
@ -9642,6 +9702,7 @@ def _update_node_dependencies() -> None:
|
|||
PROJECT_ROOT,
|
||||
extra_args=tuple(ws_args),
|
||||
capture_output=False,
|
||||
env=nixos_env,
|
||||
)
|
||||
if ws_result.returncode == 0:
|
||||
print(" ✓ repo root + ui-tui, web workspaces (desktop skipped)")
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ def test_gui_installs_packages_and_launches_desktop_app(tmp_path, monkeypatch):
|
|||
cli_main.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 0
|
||||
mock_install.assert_called_once_with("/usr/bin/npm", root, capture_output=False)
|
||||
mock_install.assert_called_once_with("/usr/bin/npm", root, capture_output=False, env=None)
|
||||
assert mock_run.call_args_list[0].args[0] == ["/usr/bin/npm", "run", "pack"]
|
||||
assert mock_run.call_args_list[0].kwargs["cwd"] == desktop_dir
|
||||
assert mock_run.call_args_list[1].args[0] == [str(packaged_exe)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue