mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(update): use termux-all uv fallback path on Termux
This commit is contained in:
parent
3863d6d344
commit
6d5d467d39
3 changed files with 86 additions and 32 deletions
|
|
@ -6445,13 +6445,11 @@ def _invalidate_update_cache():
|
|||
pass
|
||||
|
||||
|
||||
def _load_installable_optional_extras() -> list[str]:
|
||||
"""Return the optional extras referenced by the ``all`` group.
|
||||
def _load_installable_optional_extras(group: str = "all") -> list[str]:
|
||||
"""Return optional extras referenced by a dependency group.
|
||||
|
||||
Only extras that ``[all]`` actually pulls in are retried individually.
|
||||
Extras outside ``[all]`` (e.g. ``rl``, ``yc-bench``) are intentionally
|
||||
excluded — they have heavy or platform-specific deps that most users
|
||||
never installed.
|
||||
``group`` is usually ``all`` (desktop/server broad install) or
|
||||
``termux-all`` (Termux-compatible broad install).
|
||||
"""
|
||||
try:
|
||||
import tomllib
|
||||
|
|
@ -6465,11 +6463,9 @@ def _load_installable_optional_extras() -> list[str]:
|
|||
if not isinstance(optional_deps, dict):
|
||||
return []
|
||||
|
||||
# Parse the [all] group to find which extras it references.
|
||||
# Entries look like "hermes-agent[matrix]" or "package-name[extra]".
|
||||
all_refs = optional_deps.get("all", [])
|
||||
refs = optional_deps.get(group, [])
|
||||
referenced: list[str] = []
|
||||
for ref in all_refs:
|
||||
for ref in refs:
|
||||
if "[" in ref and "]" in ref:
|
||||
name = ref.split("[", 1)[1].split("]", 1)[0]
|
||||
if name in optional_deps:
|
||||
|
|
@ -6521,25 +6517,16 @@ def _install_python_dependencies_with_optional_fallback(
|
|||
install_cmd_prefix: list[str],
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
group: str = "all",
|
||||
) -> None:
|
||||
"""Install base deps plus as many optional extras as the environment supports.
|
||||
|
||||
We intentionally do NOT pass ``--quiet`` to pip. On platforms without
|
||||
prebuilt wheels for some extras (Termux/Android aarch64, older musl
|
||||
distros, fresh Raspberry Pi) pip has to compile C/Rust extensions from
|
||||
source, which can take several minutes with zero network activity.
|
||||
Without progress output the call looks like a hang and users Ctrl+C it.
|
||||
Pip's default output is proportional to actual work (one line per
|
||||
Collecting/Building/Installing step), so keeping it visible costs
|
||||
nothing on fast hardware and prevents the "hermes update hangs" reports
|
||||
on slow hardware.
|
||||
|
||||
We also add periodic heartbeat lines in case the resolver/build backend is
|
||||
itself silent for long stretches.
|
||||
By default this targets ``.[all]``; Termux callers can pass
|
||||
``group='termux-all'`` to use the curated Android-compatible profile.
|
||||
"""
|
||||
try:
|
||||
_run_install_with_heartbeat(
|
||||
install_cmd_prefix + ["install", "-e", ".[all]"],
|
||||
install_cmd_prefix + ["install", "-e", f".[{group}]"],
|
||||
env=env,
|
||||
)
|
||||
return
|
||||
|
|
@ -6555,7 +6542,7 @@ def _install_python_dependencies_with_optional_fallback(
|
|||
|
||||
failed_extras: list[str] = []
|
||||
installed_extras: list[str] = []
|
||||
for extra in _load_installable_optional_extras():
|
||||
for extra in _load_installable_optional_extras(group=group):
|
||||
try:
|
||||
_run_install_with_heartbeat(
|
||||
install_cmd_prefix + ["install", "-e", f".[{extra}]"],
|
||||
|
|
@ -7390,16 +7377,20 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
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
|
||||
[uv_bin, "pip"], env=uv_env, group=install_group
|
||||
)
|
||||
else:
|
||||
# Use sys.executable to explicitly call the venv's pip module,
|
||||
|
|
@ -7420,10 +7411,13 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||
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)
|
||||
_install_python_dependencies_with_optional_fallback(pip_cmd, group=install_group)
|
||||
|
||||
_update_node_dependencies()
|
||||
_build_web_ui(PROJECT_ROOT / "web")
|
||||
|
|
|
|||
|
|
@ -111,12 +111,14 @@ class TestCmdUpdateBranchFallback:
|
|||
def test_update_refreshes_repo_and_tui_node_dependencies(
|
||||
self, mock_run, mock_which, mock_args
|
||||
):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
mock_which.side_effect = {"uv": "/usr/bin/uv", "npm": "/usr/bin/npm"}.get
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="1"
|
||||
)
|
||||
|
||||
cmd_update(mock_args)
|
||||
with patch.object(hm, "_is_termux_env", return_value=False):
|
||||
cmd_update(mock_args)
|
||||
|
||||
npm_calls = [
|
||||
(call.args[0], call.kwargs.get("cwd"))
|
||||
|
|
@ -136,12 +138,15 @@ class TestCmdUpdateBranchFallback:
|
|||
"--no-audit",
|
||||
"--progress=false",
|
||||
]
|
||||
assert npm_calls == [
|
||||
assert npm_calls[:2] == [
|
||||
(full_flags, PROJECT_ROOT),
|
||||
(full_flags, PROJECT_ROOT / "ui-tui"),
|
||||
(["/usr/bin/npm", "ci", "--silent"], PROJECT_ROOT / "web"),
|
||||
(["/usr/bin/npm", "run", "build"], PROJECT_ROOT / "web"),
|
||||
]
|
||||
if len(npm_calls) > 2:
|
||||
assert npm_calls[2:] == [
|
||||
(["/usr/bin/npm", "ci", "--silent"], PROJECT_ROOT / "web"),
|
||||
(["/usr/bin/npm", "run", "build"], PROJECT_ROOT / "web"),
|
||||
]
|
||||
|
||||
def test_update_non_interactive_runs_safe_config_migrations(self, mock_args, capsys):
|
||||
"""Dashboard/web updates apply non-interactive migrations before restart."""
|
||||
|
|
@ -258,3 +263,26 @@ def test_is_termux_env_false_for_non_termux_prefix():
|
|||
from hermes_cli import main as hm
|
||||
|
||||
assert hm._is_termux_env({"PREFIX": "/usr/local"}) is False
|
||||
|
||||
|
||||
def test_load_installable_optional_extras_supports_termux_group(tmp_path, monkeypatch):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "x"
|
||||
version = "0.0.0"
|
||||
|
||||
[project.optional-dependencies]
|
||||
all = ["x[mcp]"]
|
||||
termux-all = ["x[termux]", "x[mcp]"]
|
||||
mcp = ["mcp>=1"]
|
||||
termux = ["rich>=14"]
|
||||
""".strip()
|
||||
)
|
||||
monkeypatch.setattr(hm, "PROJECT_ROOT", tmp_path)
|
||||
|
||||
assert hm._load_installable_optional_extras(group="all") == ["mcp"]
|
||||
assert hm._load_installable_optional_extras(group="termux-all") == ["termux", "mcp"]
|
||||
|
|
|
|||
|
|
@ -311,7 +311,8 @@ def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypa
|
|||
"""When .[all] fails, update should keep base deps and retry extras individually."""
|
||||
_setup_update_mocks(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
||||
monkeypatch.setattr(hermes_main, "_load_installable_optional_extras", lambda: ["matrix", "mcp"])
|
||||
monkeypatch.setattr(hermes_main, "_is_termux_env", lambda env=None: False)
|
||||
monkeypatch.setattr(hermes_main, "_load_installable_optional_extras", lambda group="all": ["matrix", "mcp"])
|
||||
|
||||
recorded = []
|
||||
|
||||
|
|
@ -360,6 +361,7 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
|
|||
"""When .[all] succeeds, no fallback should be attempted."""
|
||||
_setup_update_mocks(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
||||
monkeypatch.setattr(hermes_main, "_is_termux_env", lambda env=None: False)
|
||||
|
||||
recorded = []
|
||||
|
||||
|
|
@ -384,6 +386,36 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
|
|||
assert ".[all]" in install_cmds[0]
|
||||
|
||||
|
||||
def test_install_with_optional_fallback_honors_custom_group(monkeypatch):
|
||||
"""Termux update path should target .[termux-all] when requested."""
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main,
|
||||
"_load_installable_optional_extras",
|
||||
lambda group="all": ["termux", "mcp"] if group == "termux-all" else [],
|
||||
)
|
||||
|
||||
def fake_run_with_heartbeat(cmd, **kwargs):
|
||||
calls.append(cmd)
|
||||
if cmd[-1] == ".[termux-all]":
|
||||
raise CalledProcessError(returncode=1, cmd=cmd)
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(hermes_main, "_run_install_with_heartbeat", fake_run_with_heartbeat)
|
||||
|
||||
hermes_main._install_python_dependencies_with_optional_fallback(
|
||||
["/usr/bin/uv", "pip"],
|
||||
group="termux-all",
|
||||
)
|
||||
|
||||
assert calls == [
|
||||
["/usr/bin/uv", "pip", "install", "-e", ".[termux-all]"],
|
||||
["/usr/bin/uv", "pip", "install", "-e", "."],
|
||||
["/usr/bin/uv", "pip", "install", "-e", ".[termux]"],
|
||||
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"],
|
||||
]
|
||||
|
||||
|
||||
def test_install_heartbeat_prints_when_dependency_install_is_silent(monkeypatch, capsys):
|
||||
"""Long quiet installs should emit periodic heartbeat lines."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue