Compare commits

...

4 Commits

Author SHA1 Message Date
teknium1
581bfc0549 chore(release): map islam666 for salvaged PR #39749 2026-06-07 06:39:35 -07:00
teknium1
7bcdab1b61 test(update): cover _resolve_venv_dir resolution order
Hardening on top of salvaged #39749 (subagent flagged the venv fix shipped
with no regression test): cover sys.prefix, VIRTUAL_ENV, .venv-before-venv
fallback, and the None case.
2026-06-07 06:39:35 -07:00
islam666
d2f690bc0e fix(update): copy os.environ fallback to avoid mutating process environment 2026-06-07 06:38:41 -07:00
islam666
c7f85e334e fix(update): resolve venv from active interpreter instead of hardcoded path
On uv-based installs, the active virtualenv is often at PROJECT_ROOT/.venv
(not PROJECT_ROOT/venv). Several code paths in main.py hardcoded
PROJECT_ROOT/"venv", causing 'hermes update' to install dependencies into
a wrong, orphan virtualenv that the running CLI never uses.

Changes:
- Add _resolve_venv_dir() helper that detects the active venv from
  sys.prefix, VIRTUAL_ENV env var, or falls back to .venv/ then venv/
- Use _resolve_venv_dir() in both update code paths (zip and git)
  instead of hardcoded PROJECT_ROOT/"venv"
- Update _venv_scripts_dir() to use the same helper

Fixes #39714
2026-06-07 06:38:33 -07:00
3 changed files with 91 additions and 4 deletions

View File

@@ -8157,7 +8157,8 @@ def _update_via_zip(args):
if not uv_bin:
uv_bin = _ensure_uv_for_termux(pip_cmd)
if uv_bin:
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
venv_dir = _resolve_venv_dir()
uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_dir)} if venv_dir else {**os.environ}
if _is_termux_env(uv_env):
uv_env.pop("PYTHONPATH", None)
uv_env.pop("PYTHONHOME", None)
@@ -8792,10 +8793,39 @@ def _is_windows() -> bool:
return sys.platform == "win32"
def _resolve_venv_dir() -> Path | None:
"""Resolve the active virtualenv directory.
Prefers the running interpreter's venv (sys.prefix), then VIRTUAL_ENV env var,
then falls back to common directory names under PROJECT_ROOT (.venv before venv).
Returns None when no virtualenv can be found.
"""
# If we're running inside a virtualenv, sys.prefix points to it.
if sys.prefix != sys.base_prefix:
venv = Path(sys.prefix)
if venv.is_dir():
return venv
# uv and some other tools set VIRTUAL_ENV without changing sys.prefix.
_virtual_env = os.environ.get("VIRTUAL_ENV")
if _virtual_env:
venv = Path(_virtual_env)
if venv.is_dir():
return venv
# Fallback: check common virtualenv directory names under the project root.
for candidate in (".venv", "venv"):
venv = PROJECT_ROOT / candidate
if venv.is_dir():
return venv
return None
def _venv_scripts_dir() -> Path | None:
"""Return the venv Scripts directory if we're running inside the project venv."""
venv_dir = PROJECT_ROOT / "venv"
if not venv_dir.is_dir():
venv_dir = _resolve_venv_dir()
if venv_dir is None:
return None
scripts = venv_dir / ("Scripts" if _is_windows() else "bin")
return scripts if scripts.is_dir() else None
@@ -10686,7 +10716,8 @@ def _cmd_update_impl(args, gateway_mode: bool):
install_group = "all"
if uv_bin:
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
venv_dir = _resolve_venv_dir()
uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_dir)} if venv_dir else {**os.environ}
if _is_termux_env(uv_env):
uv_env.pop("PYTHONPATH", None)
uv_env.pop("PYTHONHOME", None)

View File

@@ -58,6 +58,7 @@ AUTHOR_MAP = {
"129007007+HeLLGURD@users.noreply.github.com": "HeLLGURD",
"290859878+synapsesx@users.noreply.github.com": "synapsesx",
"dirtyren@users.noreply.github.com": "dirtyren",
"islam666@users.noreply.github.com": "islam666",
"zhaolei.vc@bytedance.com": "zhaoleibd",
"jeffrobodie@gmail.com": "jeffrobodie-glitch",
"kyssta-exe@users.noreply.github.com": "kyssta-exe",

View File

@@ -0,0 +1,55 @@
"""Regression tests for _resolve_venv_dir (salvaged PR #39749).
`hermes update` previously hardcoded `PROJECT_ROOT / "venv"`, which orphaned
deps into a stray `venv/` on uv-style installs whose real env is `.venv/`.
`_resolve_venv_dir()` now prefers the active interpreter's venv, then
`VIRTUAL_ENV`, then a `.venv`-before-`venv` directory fallback.
"""
import os
import sys
from pathlib import Path
import pytest
from hermes_cli import main as hermes_main
def test_resolves_active_interpreter_venv(monkeypatch, tmp_path):
"""When running inside a venv, sys.prefix wins."""
venv = tmp_path / "active-venv"
venv.mkdir()
monkeypatch.setattr(sys, "prefix", str(venv))
monkeypatch.setattr(sys, "base_prefix", str(tmp_path / "system"))
assert hermes_main._resolve_venv_dir() == venv
def test_falls_back_to_virtual_env_var(monkeypatch, tmp_path):
"""uv sets VIRTUAL_ENV without changing sys.prefix; honor it."""
venv = tmp_path / "uv-venv"
venv.mkdir()
# Not inside a venv per sys.prefix.
monkeypatch.setattr(sys, "prefix", str(tmp_path / "system"))
monkeypatch.setattr(sys, "base_prefix", str(tmp_path / "system"))
monkeypatch.setenv("VIRTUAL_ENV", str(venv))
assert hermes_main._resolve_venv_dir() == venv
def test_prefers_dot_venv_over_venv_in_fallback(monkeypatch, tmp_path):
"""With no active venv and no VIRTUAL_ENV, .venv beats venv."""
(tmp_path / ".venv").mkdir()
(tmp_path / "venv").mkdir()
monkeypatch.setattr(sys, "prefix", str(tmp_path / "system"))
monkeypatch.setattr(sys, "base_prefix", str(tmp_path / "system"))
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.setattr(hermes_main, "PROJECT_ROOT", tmp_path)
assert hermes_main._resolve_venv_dir() == tmp_path / ".venv"
def test_returns_none_when_no_venv(monkeypatch, tmp_path):
"""No active venv, no VIRTUAL_ENV, no fallback dirs -> None."""
monkeypatch.setattr(sys, "prefix", str(tmp_path / "system"))
monkeypatch.setattr(sys, "base_prefix", str(tmp_path / "system"))
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.setattr(hermes_main, "PROJECT_ROOT", tmp_path / "empty")
assert hermes_main._resolve_venv_dir() is None