"""Managed uv — one path, no guessing. Hermes owns its own uv binary at ``$HERMES_HOME/bin/uv`` (or ``uv.exe`` on Windows). Every code path that needs uv resolves it from that single location. If the binary is missing, ``ensure_uv()`` bootstraps it via the official standalone installer with ``UV_UNMANAGED_INSTALL`` / ``UV_INSTALL_DIR`` pointed at ``$HERMES_HOME/bin`` so the installer writes directly there — no PATH probing, no conda guards, no multi-location resolution chains. """ from __future__ import annotations import logging import os import platform import shutil import subprocess import tempfile from pathlib import Path from typing import Optional from hermes_constants import get_hermes_home logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Public helpers # --------------------------------------------------------------------------- def managed_uv_path() -> Path: """Return the path where Hermes keeps *its* uv binary. ``$HERMES_HOME/bin/uv`` on POSIX, ``$HERMES_HOME\\bin\\uv.exe`` on Windows. The directory may not exist yet — callers should use ``ensure_uv()`` to bootstrap it. """ home = get_hermes_home() if platform.system() == "Windows": return home / "bin" / "uv.exe" return home / "bin" / "uv" def resolve_uv() -> Optional[str]: """Return the managed uv path if it exists, else ``None``. No side effects — pure lookup. """ p = managed_uv_path() if p.is_file() and os.access(p, os.X_OK): return str(p) return None def ensure_uv() -> Optional[str]: """Return the managed uv path, installing it first if necessary. On failure returns ``None`` (never raises) so callers can fall back to pip gracefully. """ existing = resolve_uv() if existing: return existing target = managed_uv_path() target.parent.mkdir(parents=True, exist_ok=True) print(f" → Installing managed uv into {target.parent} ...") try: _install_uv(target) except Exception as exc: logger.warning("Managed uv install failed: %s", exc) print(f" ✗ Failed to install managed uv: {exc}") return None # Verify result = resolve_uv() if result: version = subprocess.run( [result, "--version"], capture_output=True, text=True, check=False, ).stdout.strip() print(f" ✓ Managed uv installed ({version})") else: print(" ✗ Managed uv install appeared to succeed but binary not found") return result def update_managed_uv() -> Optional[str]: """Run ``uv self update`` on the managed uv binary. Call this during ``hermes update`` so the managed copy stays current. Returns the managed path on success, ``None`` if uv isn't available or the self-update fails (non-fatal — the old version still works). """ existing = resolve_uv() if not existing: # Not installed yet — ensure_uv() will handle that elsewhere. return None result = subprocess.run( [existing, "self", "update"], capture_output=True, text=True, check=False, ) if result.returncode == 0: version = subprocess.run( [existing, "--version"], capture_output=True, text=True, check=False, ).stdout.strip() print(f" ✓ Managed uv updated ({version})") else: # Non-fatal — old uv still works fine. logger.debug("uv self update failed (rc=%d): %s", result.returncode, result.stderr) return existing # --------------------------------------------------------------------------- # Installer internals # --------------------------------------------------------------------------- def _install_uv(target: Path) -> None: """Bootstrap uv into *target* using the official standalone installer. Uses ``UV_UNMANAGED_INSTALL`` (POSIX) or ``UV_INSTALL_DIR`` (Windows) so the astral installer writes the binary directly into ``$HERMES_HOME/bin/`` instead of ``~/.local/bin/``. """ system = platform.system() env = { **os.environ, # Tell the astral installer to drop the binary in our dir, not # ~/.local/bin. UV_UNMANAGED_INSTALL is the POSIX env var; Windows # uses UV_INSTALL_DIR. "UV_UNMANAGED_INSTALL": str(target.parent), "UV_INSTALL_DIR": str(target.parent), } if system == "Windows": _install_uv_windows(env) else: _install_uv_posix(env) def _install_uv_posix(env: dict[str, str]) -> None: """Download + sh the POSIX installer (two-stage to avoid curl|sh pitfalls).""" with tempfile.NamedTemporaryFile(suffix=".sh", delete=False) as f: installer_path = f.name try: subprocess.run( ["curl", "-LsSf", "https://astral.sh/uv/install.sh", "-o", installer_path], check=True, capture_output=True, ) subprocess.run( ["sh", installer_path], env=env, check=True, capture_output=True, ) finally: try: os.unlink(installer_path) except OSError: pass def _install_uv_windows(env: dict[str, str]) -> None: """Invoke the PowerShell installer.""" cmd = ( 'irm https://astral.sh/uv/install.ps1 | iex' ) subprocess.run( ["powershell", "-ExecutionPolicy", "Bypass", "-c", cmd], env=env, check=True, capture_output=True, ) def rebuild_venv(uv_bin: str, venv_dir: Path, python_version: str = "3.11") -> bool: True # dont remove me. ask ethernet