refactor(setup): route dependency installs through the canonical uv→pip→ensurepip ladder

Replace the hand-rolled ensurepip bootstrap (and five other one-off
pip-install code paths) with hermes_cli.tools_config._pip_install, which
prefers the bundled uv (fast, needs no pip in the venv), falls back to
python -m pip, and bootstraps pip via ensurepip only when missing.

Sites unified:
- hermes_cli/setup.py: _install_neutts_deps, _install_kittentts_deps,
  modal SDK install, daytona SDK install
- hermes_cli/memory_setup.py: memory-plugin pip deps (previously dead-ended
  when uv AND pip binaries were both absent)
- hermes_cli/dingtalk_auth.py: qrcode auto-install (previously invoked
  'python -m uv' which is not how uv ships)
- agent/lsp/install.py: --target LSP server installs
- plugins/google_meet/cli.py, plugins/platforms/matrix/adapter.py,
  plugins/platforms/google_chat/oauth.py, plugins/memory/honcho/cli.py

Tests updated to assert the ladder behavior (uv-first, pip fallback,
ensurepip bootstrap) instead of the removed bespoke branches.
This commit is contained in:
teknium1 2026-07-07 02:25:24 -07:00 committed by Teknium
parent 569b78c1f9
commit ba865e4038
9 changed files with 107 additions and 165 deletions

View file

@ -348,17 +348,15 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:
pip_target.mkdir(parents=True, exist_ok=True)
try:
logger.info("[install] pip install --target %s %s", pip_target, pkg)
proc = subprocess.run(
[sys.executable, "-m", "pip", "install", "--target", str(pip_target), "--quiet", pkg],
check=False,
capture_output=True,
text=True,
from hermes_cli.tools_config import _pip_install
proc = _pip_install(
["--target", str(pip_target), "--quiet", pkg],
timeout=300,
stdin=subprocess.DEVNULL,
)
if proc.returncode != 0:
logger.warning(
"[install] pip install failed for %s: %s", pkg, proc.stderr.strip()[:500]
"[install] pip install failed for %s: %s", pkg, (proc.stderr or "").strip()[:500]
)
return None
except (subprocess.TimeoutExpired, OSError) as e:

View file

@ -163,17 +163,15 @@ def _ensure_qrcode_installed() -> bool:
import subprocess
# Try uv first (Hermes convention), then pip
for cmd in (
[sys.executable, "-m", "uv", "pip", "install", "qrcode"],
[sys.executable, "-m", "pip", "install", "-q", "qrcode"],
):
try:
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
from hermes_cli.tools_config import _pip_install
try:
result = _pip_install(["-q", "qrcode"], timeout=120)
if result.returncode == 0:
import qrcode # noqa: F401,F811
return True
except (subprocess.CalledProcessError, ImportError, FileNotFoundError):
continue
except (subprocess.SubprocessError, ImportError, OSError):
pass
return False

View file

@ -122,36 +122,19 @@ def _install_dependencies(provider_name: str) -> None:
print(f"\n Installing dependencies: {', '.join(missing)}")
import shutil
uv_path = shutil.which("uv")
if uv_path:
install_cmd = [uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing
manual_cmd = f"uv pip install --python {sys.executable} {' '.join(missing)}"
else:
pip_cmd = shutil.which("pip3") or shutil.which("pip")
if not pip_cmd:
print(" ⚠ uv not found — cannot install dependencies")
print(" Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh")
print(" Then re-run: hermes memory setup")
return
print(" ⚠ uv not found. Falling back to standard pip...")
install_cmd = [sys.executable, "-m", "pip", "install", "--quiet"] + missing
manual_cmd = f"{sys.executable} -m pip install {' '.join(missing)}"
from hermes_cli.tools_config import _pip_install
manual_cmd = f"uv pip install {' '.join(missing)}"
try:
subprocess.run(
install_cmd,
check=True, timeout=120,
capture_output=True,
)
print(f" ✓ Installed {', '.join(missing)}")
except subprocess.CalledProcessError as e:
print(f" ⚠ Failed to install {', '.join(missing)}")
stderr = (e.stderr or b"").decode()[:200]
if stderr:
print(f" {stderr}")
print(f" Run manually: {manual_cmd}")
result = _pip_install(["--quiet"] + missing, timeout=120)
if result.returncode == 0:
print(f" ✓ Installed {', '.join(missing)}")
else:
print(f" ⚠ Failed to install {', '.join(missing)}")
stderr = (result.stderr or "")[:200]
if stderr:
print(f" {stderr}")
print(f" Run manually: {manual_cmd}")
except Exception as e:
print(f" ⚠ Install failed: {e}")
print(f" Run manually: {manual_cmd}")

View file

@ -829,29 +829,23 @@ def _install_neutts_deps() -> bool:
print_info("This will also download the TTS model (~300MB) on first use.")
print()
# Ensure pip is available — some venvs are created without pip (e.g. Ubuntu 25.10)
try:
import importlib
if importlib.util.find_spec("pip") is None:
print_info("pip not found in venv — bootstrapping with ensurepip...")
subprocess.run([sys.executable, "-m", "ensurepip", "--upgrade"], check=True, timeout=30)
print_success("pip bootstrapped successfully")
except Exception as e:
print_warning(f"Could not bootstrap pip: {e}")
print_info("Try: python -m ensurepip --upgrade")
return False
# Route through the canonical uv → pip → ensurepip ladder so pip-less
# venvs (Ubuntu 25.10 `python -m venv`, `uv venv`) work out of the box.
from hermes_cli.tools_config import _pip_install
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "-U", "neutts[all]", "--quiet"],
check=True, timeout=300,
)
result = _pip_install(["-U", "neutts[all]", "--quiet"], timeout=300)
except Exception as e:
print_error(f"Failed to install neutts: {e}")
print_info("Try manually: uv pip install -U 'neutts[all]'")
return False
if result.returncode == 0:
print_success("neutts installed successfully")
return True
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
print_error(f"Failed to install neutts: {e}")
print_info("Try manually: python -m pip install -U neutts[all]")
return False
err = (result.stderr or "").strip()
print_error(f"Failed to install neutts: {err[:300] if err else 'install failed'}")
print_info("Try manually: uv pip install -U 'neutts[all]'")
return False
def _install_kittentts_deps() -> bool:
@ -866,17 +860,22 @@ def _install_kittentts_deps() -> bool:
print()
print_info("Installing kittentts Python package (~25-80MB model downloaded on first use)...")
print()
from hermes_cli.tools_config import _pip_install
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "-U", wheel_url, "soundfile", "--quiet"],
check=True, timeout=300,
)
result = _pip_install(["-U", wheel_url, "soundfile", "--quiet"], timeout=300)
except Exception as e:
print_error(f"Failed to install kittentts: {e}")
print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
return False
if result.returncode == 0:
print_success("kittentts installed successfully")
return True
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
print_error(f"Failed to install kittentts: {e}")
print_info(f"Try manually: python -m pip install -U '{wheel_url}' soundfile")
return False
err = (result.stderr or "").strip()
print_error(f"Failed to install kittentts: {err[:300] if err else 'install failed'}")
print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
return False
def _xai_oauth_logged_in_for_setup() -> bool:
@ -1315,32 +1314,13 @@ def setup_terminal_backend(config: dict):
__import__("modal")
except ImportError:
print_info("Installing modal SDK...")
import subprocess
from hermes_cli.tools_config import _pip_install
uv_bin = shutil.which("uv")
if uv_bin:
result = subprocess.run(
[
uv_bin,
"pip",
"install",
"--python",
sys.executable,
"modal",
],
capture_output=True,
text=True,
)
else:
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "modal"],
capture_output=True,
text=True,
)
result = _pip_install(["modal"])
if result.returncode == 0:
print_success("modal SDK installed")
else:
print_warning("Install failed — run manually: pip install modal")
print_warning("Install failed — run manually: uv pip install modal")
# Modal token
print()
@ -1375,25 +1355,13 @@ def setup_terminal_backend(config: dict):
__import__("daytona")
except ImportError:
print_info("Installing daytona SDK...")
import subprocess
from hermes_cli.tools_config import _pip_install
uv_bin = shutil.which("uv")
if uv_bin:
result = subprocess.run(
[uv_bin, "pip", "install", "--python", sys.executable, "daytona"],
capture_output=True,
text=True,
)
else:
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "daytona"],
capture_output=True,
text=True,
)
result = _pip_install(["daytona"])
if result.returncode == 0:
print_success("daytona SDK installed")
else:
print_warning("Install failed — run manually: pip install daytona")
print_warning("Install failed — run manually: uv pip install daytona")
if result.stderr:
print_info(f" Error: {result.stderr.strip().splitlines()[-1]}")

View file

@ -250,10 +250,9 @@ def _cmd_install(*, realtime: bool, assume_yes: bool) -> int:
pip_pkgs = ["playwright", "websockets"]
print(f"\n[1/3] pip install: {' '.join(pip_pkgs)}")
try:
res = _sp.run(
[sys.executable, "-m", "pip", "install", "--upgrade", *pip_pkgs],
check=False,
)
from hermes_cli.tools_config import _pip_install
res = _pip_install(["--upgrade", *pip_pkgs], capture_output=False)
if res.returncode != 0:
print(" pip install failed")
return 1

View file

@ -515,20 +515,16 @@ def _ensure_sdk_installed() -> bool:
print(" Skipping install. Run: pip install 'honcho-ai>=2.0.1'\n")
return False
import subprocess
print(" Installing honcho-ai...", flush=True)
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "honcho-ai>=2.0.1"],
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
from hermes_cli.tools_config import _pip_install
result = _pip_install(["honcho-ai>=2.0.1"])
if result.returncode == 0:
print(" Installed.\n")
return True
else:
print(f" Install failed:\n{result.stderr.strip()}")
print(" Run manually: pip install 'honcho-ai>=2.0.1'\n")
print(f" Install failed:\n{(result.stderr or '').strip()}")
print(" Run manually: uv pip install 'honcho-ai>=2.0.1'\n")
return False

View file

@ -379,13 +379,14 @@ def install_deps() -> bool:
print("Installing Google Chat OAuth dependencies...")
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--quiet"] + _REQUIRED_PACKAGES,
stdout=subprocess.DEVNULL,
)
from hermes_cli.tools_config import _pip_install
result = _pip_install(["--quiet"] + _REQUIRED_PACKAGES)
if result.returncode != 0:
raise RuntimeError((result.stderr or "install failed").strip()[:300])
print("Dependencies installed.")
return True
except subprocess.CalledProcessError as exc:
except Exception as exc:
print(f"ERROR: Failed to install dependencies: {exc}")
print("Or install via the optional extra:")
print(" pip install 'hermes-agent[google_chat]'")

View file

@ -4497,23 +4497,14 @@ def interactive_setup() -> None:
__import__("mautrix")
except ImportError:
print_info(f"Installing {matrix_pkg}...")
import subprocess
uv_bin = shutil.which("uv")
if uv_bin:
result = subprocess.run(
[uv_bin, "pip", "install", "--python", _sys.executable, matrix_pkg],
capture_output=True, text=True,
)
else:
result = subprocess.run(
[_sys.executable, "-m", "pip", "install", matrix_pkg],
capture_output=True, text=True,
)
from hermes_cli.tools_config import _pip_install
result = _pip_install([matrix_pkg])
if result.returncode == 0:
print_success(f"{matrix_pkg} installed")
else:
print_warning(
f"Install failed — run manually: pip install "
f"Install failed — run manually: uv pip install "
f"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks"
)

View file

@ -51,47 +51,55 @@ class TestMemorySetupProviderRouting:
class TestInstallDependenciesRunner:
"""`_install_dependencies` must install via `uv` when present and fall back
to standard `pip` when `uv` is unavailable (e.g. slim containers / CI images
that don't ship uv) instead of dead-ending with "cannot install"."""
"""`_install_dependencies` must route through the canonical
``_pip_install`` ladder (uv pip ensurepip): uv when present, standard
pip when uv is unavailable, and an ensurepip bootstrap for pip-less venvs
instead of dead-ending with "cannot install"."""
def _run_with_missing_dep(self, tmp_path, which_side_effect):
def _run_with_missing_dep(self, tmp_path, which_side_effect, run_behavior=None):
"""Drive _install_dependencies for a plugin that declares one missing
pip dep, capturing the subprocess.run argv (or None if never called)."""
pip dep, capturing every subprocess.run argv issued by the ladder."""
import sys
(tmp_path / "plugin.yaml").write_text(
"pip_dependencies:\n - definitely-not-installed-xyz\n", encoding="utf-8"
)
captured = {}
calls = []
def fake_run(cmd, **kw):
captured["cmd"] = cmd
return SimpleNamespace()
calls.append(cmd)
if run_behavior:
return run_behavior(cmd)
return SimpleNamespace(returncode=0, stdout="", stderr="")
with patch("plugins.memory.find_provider_dir", return_value=tmp_path), \
patch("shutil.which", side_effect=which_side_effect), \
patch("subprocess.run", fake_run):
patch("hermes_cli.tools_config.shutil.which", side_effect=which_side_effect), \
patch("hermes_cli.tools_config.subprocess.run", fake_run):
memory_setup._install_dependencies("x")
return captured.get("cmd"), sys.executable
return calls, sys.executable
def test_uses_uv_when_available(self, tmp_path):
cmd, _ = self._run_with_missing_dep(
calls, _ = self._run_with_missing_dep(
tmp_path, lambda b: "/usr/bin/uv" if b == "uv" else None
)
assert cmd is not None
assert cmd[:3] == ["/usr/bin/uv", "pip", "install"]
assert calls
assert calls[0][:3] == ["/usr/bin/uv", "pip", "install"]
def test_falls_back_to_pip_when_uv_missing(self, tmp_path, capsys):
"""The salvaged behavior (#5954): no uv but pip present -> python -m pip."""
cmd, py = self._run_with_missing_dep(
tmp_path, lambda b: "/usr/bin/pip3" if b == "pip3" else None
)
assert cmd is not None
assert cmd[:4] == [py, "-m", "pip", "install"]
assert "Falling back to standard pip" in capsys.readouterr().out
def test_falls_back_to_pip_when_uv_missing(self, tmp_path):
"""No uv but pip importable -> python -m pip install."""
calls, py = self._run_with_missing_dep(tmp_path, lambda b: None)
assert calls
# Ladder probes pip first, then installs with it.
assert calls[0][:3] == [py, "-m", "pip"]
assert calls[-1][:4] == [py, "-m", "pip", "install"]
def test_aborts_when_neither_uv_nor_pip(self, tmp_path, capsys):
cmd, _ = self._run_with_missing_dep(tmp_path, lambda b: None)
assert cmd is None # no install attempted
assert "cannot install dependencies" in capsys.readouterr().out
def test_bootstraps_pip_via_ensurepip_when_missing(self, tmp_path):
"""Neither uv nor pip -> ensurepip bootstrap, then pip install."""
def behavior(cmd):
if cmd[-1] == "--version":
return SimpleNamespace(returncode=1, stdout="", stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
calls, py = self._run_with_missing_dep(tmp_path, lambda b: None, behavior)
assert any("ensurepip" in c for c in calls)
assert calls[-1][:4] == [py, "-m", "pip", "install"]