Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 91 additions & 41 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6700,6 +6700,49 @@ def _cleanup_quarantined_exes(scripts_dir: Path | None = None) -> None:
pass


def _rename_active_windows_exe() -> "Path | None":
"""On Windows, rename hermes.exe to hermes.exe.old before uv/pip install.

Windows holds a file lock on any running .exe, causing uv pip install -e .
to fail with "Access is denied (os error 5)" when it tries to overwrite
the active hermes.exe in venv/Scripts/.

Renaming releases the write lock while keeping the process alive — Windows
allows renaming an open executable in place. The installer then writes a
fresh hermes.exe, and the stale .old file is cleaned up afterwards.

Returns the Path of the renamed .old file, or None if not applicable.
"""
if sys.platform != "win32":
return None
exe_path = PROJECT_ROOT / "venv" / "Scripts" / "hermes.exe"
if not exe_path.exists():
return None
old_path = exe_path.with_suffix(".exe.old")
try:
if old_path.exists():
old_path.unlink(missing_ok=True)
exe_path.rename(old_path)
return old_path
except OSError as exc:
logger.debug("Windows exe rename skipped: %s", exc)
return None


def _cleanup_windows_exe_old(old_path: "Path | None") -> None:
"""Remove the .old backup created by _rename_active_windows_exe()."""
if old_path is None:
return
try:
old_path.unlink(missing_ok=True)
except OSError as exc:
logger.debug(
"Could not remove %s (will be cleaned up on next update): %s",
old_path,
exc,
)


def _install_python_dependencies_with_optional_fallback(
install_cmd_prefix: list[str],
*,
Expand Down Expand Up @@ -7578,49 +7621,56 @@ def _cmd_update_impl(args, gateway_mode: bool):
# breaks on this machine, keep base deps and reinstall the remaining extras
# individually so update does not silently strip working capabilities.
print("→ Updating Python dependencies...")
pip_cmd = [sys.executable, "-m", "pip"]
uv_bin = shutil.which("uv") or _ensure_uv_for_termux(pip_cmd)
install_group = "all"

if uv_bin:
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
if _is_termux_env(uv_env):
uv_env.pop("PYTHONPATH", None)
uv_env.pop("PYTHONHOME", None)
install_group = "termux-all"
print(" → Termux detected: using uv + curated termux-all optional profile...")
if _is_termux_env(uv_env) and _is_android_python():
print(" → Termux/Android detected: prebuilding psutil with Linux source path compatibility...")
_install_psutil_android_compat([uv_bin, "pip"], env=uv_env)
_install_python_dependencies_with_optional_fallback(
[uv_bin, "pip"], env=uv_env, group=install_group
)
else:
# Use sys.executable to explicitly call the venv's pip module,
# avoiding PEP 668 'externally-managed-environment' errors on Debian/Ubuntu.
# Some environments lose pip inside the venv; bootstrap it back with
# ensurepip before trying the editable install.
# On Windows, rename hermes.exe before install to release the
# file lock that would cause "Access is denied (os error 5)".
_windows_exe_old = _rename_active_windows_exe()
try:
pip_cmd = [sys.executable, "-m", "pip"]
try:
subprocess.run(
pip_cmd + ["--version"],
cwd=PROJECT_ROOT,
check=True,
capture_output=True,
)
except subprocess.CalledProcessError:
subprocess.run(
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
cwd=PROJECT_ROOT,
check=True,
uv_bin = shutil.which("uv") or _ensure_uv_for_termux(pip_cmd)
install_group = "all"

if uv_bin:
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
if _is_termux_env(uv_env):
uv_env.pop("PYTHONPATH", None)
uv_env.pop("PYTHONHOME", None)
install_group = "termux-all"
print(" → Termux detected: using uv + curated termux-all optional profile...")
if _is_termux_env(uv_env) and _is_android_python():
print(" → Termux/Android detected: prebuilding psutil with Linux source path compatibility...")
_install_psutil_android_compat([uv_bin, "pip"], env=uv_env)
_install_python_dependencies_with_optional_fallback(
[uv_bin, "pip"], env=uv_env, group=install_group
)
if _is_termux_env():
install_group = "termux-all"
print(" → Termux detected: using curated termux-all optional profile...")
if _is_termux_env() and _is_android_python():
print(" → Termux/Android detected: prebuilding psutil with Linux source path compatibility...")
_install_psutil_android_compat(pip_cmd)
_install_python_dependencies_with_optional_fallback(pip_cmd, group=install_group)
else:
# Use sys.executable to explicitly call the venv's pip module,
# avoiding PEP 668 'externally-managed-environment' errors on Debian/Ubuntu.
# Some environments lose pip inside the venv; bootstrap it back with
# ensurepip before trying the editable install.
pip_cmd = [sys.executable, "-m", "pip"]
try:
subprocess.run(
pip_cmd + ["--version"],
cwd=PROJECT_ROOT,
check=True,
capture_output=True,
)
except subprocess.CalledProcessError:
subprocess.run(
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
cwd=PROJECT_ROOT,
check=True,
)
if _is_termux_env():
install_group = "termux-all"
print(" → Termux detected: using curated termux-all optional profile...")
if _is_termux_env() and _is_android_python():
print(" → Termux/Android detected: prebuilding psutil with Linux source path compatibility...")
_install_psutil_android_compat(pip_cmd)
_install_python_dependencies_with_optional_fallback(pip_cmd, group=install_group)

finally:
_cleanup_windows_exe_old(_windows_exe_old)

_update_node_dependencies()
_build_web_ui(PROJECT_ROOT / "web")
Expand Down
Loading