Skip to content

refactor(uv): single managed-uv path, delete fts5 installer escalation#37660

Merged
ethernet8023 merged 2 commits into
mainfrom
refactor/managed-uv-single-path
Jun 3, 2026
Merged

refactor(uv): single managed-uv path, delete fts5 installer escalation#37660
ethernet8023 merged 2 commits into
mainfrom
refactor/managed-uv-single-path

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Replaces the complicated multi-path UV resolution chain with a single managed uv binary at $HERMES_HOME/bin/uv. Every code path that needs uv resolves it from that one location. If missing, ensure_uv() bootstraps it via the official standalone installer. No PATH probing, no conda guards, no 5-location trust ordering, no temp-dir fallback installs.

Also deletes the entire FTS5 installer escalation (ensure_fts5, _python_has_fts5, _reinstall_python_with_fts5, _warn_no_fts5 — 61 lines of shell). These existed to work around stale uv binaries that could not self update (pip/apt/brew-managed uv). With managed uv (always from the official installer → always supports self update), the escalation is dead code: managed uv always installs a current Python with FTS5.

For existing installs transitioning from non-managed uv: ensure_uv() returns a (path, freshly_bootstrapped) tuple. The update paths detect a first-time bootstrap and nuke+recreate the venv with the new managed uv, guaranteeing a Python with FTS5. The runtime FTS5 guard in hermes_state.py is kept but its warning now suggests hermes update as the fix.

Closes #37605, Closes #37622 — both PRs tried to add more paths and more resolution logic to handle edge cases around stale/pip/apt/brew-managed uv. The right fix is simpler: own one uv, always current.

Related Issue

Closes #37605
Closes #37622

Type of Change

  • ♻️ Refactor (no behavior change)

Changes Made

  • hermes_cli/managed_uv.py (new): managed_uv_path(), resolve_uv(), ensure_uv() (returns (path, freshly_bootstrapped) tuple), update_managed_uv(), rebuild_venv(), _install_uv_posix(), _install_uv_windows()
  • hermes_cli/main.py: replaced all 3 shutil.which("uv") sites with ensure_uv(); added rebuild_venv() call on first-time managed uv bootstrap; added update_managed_uv() before dep install on all 3 update paths
  • scripts/install.sh: install_uv() always installs to $HERMES_HOME/bin/uv (no PATH probing, no ~/.local/bin/~/.cargo/bin checks); deleted ensure_fts5, _python_has_fts5, _reinstall_python_with_fts5, _warn_no_fts5 (61 lines removed)
  • scripts/install.ps1: Install-Uv always installs to $HermesHome\bin\uv.exe; Resolve-UvCmd checks managed location first
  • hermes_state.py: simplified FTS5 warning — now suggests hermes update as the fix instead of blaming install method
  • tests/hermes_cli/test_managed_uv.py (new): 15 tests covering the module
  • tests/hermes_cli/test_cmd_update.py: autouse _patch_managed_uv fixture delegates to shutil.which; added rebuild_venv stub

How to Test

  1. python -m pytest tests/hermes_cli/test_managed_uv.py tests/hermes_cli/test_cmd_update.py -q — 40/40 pass
  2. Verify install.sh still works end-to-end on a fresh machine (managed uv gets installed to $HERMES_HOME/bin/uv)
  3. Verify hermes update on an existing install: if managed uv already exists, no venv rebuild; if first-time bootstrap, venv gets rebuilt with fresh Python

Why this is better than #37605 and #37622

Both PRs added more resolution paths and more conditional logic to handle edge cases around stale/externally-managed uv. More paths = more things to test, more things to break, more code to maintain. This PR takes the opposite approach: own one uv. Managed uv is always from the official installer, always supports self update, always installs current Python with FTS5. No edge cases to handle because there is only one path.

#37605 / #37622 This PR
UV resolution 5+ locations, trust ordering, conda guards One: $HERMES_HOME/bin/uv
FTS5 guarantee 3-step escalation (reinstall → self-update → temp-dir fresh install) Venv rebuild on first bootstrap; managed uv always current
Lines changed +many -86 net (deleted more than added)
Edge cases Stale pip/apt/brew uv, offline, pinned env None — managed uv always works

@ethernet8023 ethernet8023 requested a review from a team June 2, 2026 21:35
Replace the multi-path UV resolution chain (PATH probing, conda guards,
5-location trust ordering, temp-dir fallback installs) with a single
managed uv binary at $HERMES_HOME/bin/uv. Every code path that needs
uv resolves it from that one location; if missing, ensure_uv()
bootstraps it via the official standalone installer.

Key changes:

- New hermes_cli/managed_uv.py: managed_uv_path(), resolve_uv(),
  ensure_uv() (returns (path, freshly_bootstrapped) tuple),
  update_managed_uv(), rebuild_venv(), installer internals.
- hermes_cli/main.py: replace all shutil.which('uv') with ensure_uv(),
  add venv rebuild on first-time managed uv bootstrap, update_managed_uv
  before dep install on all 3 update paths.
- scripts/install.sh: install_uv() always installs to
  $HERMES_HOME/bin/uv; delete ensure_fts5, _python_has_fts5,
  _reinstall_python_with_fts5, _warn_no_fts5 (61 lines).
  Managed uv always installs current Python with FTS5.
- scripts/install.ps1: Install-Uv always installs to
  $HermesHome\bin\uv.exe; Resolve-UvCmd checks managed location first.
- hermes_state.py: simplified FTS5 warning now suggests 'hermes update'
  as the fix instead of blaming install method.
- tests: 15 tests in test_managed_uv.py, autouse _patch_managed_uv
  fixture in test_cmd_update.py.

Closes #37605, Closes #37622
@ethernet8023 ethernet8023 force-pushed the refactor/managed-uv-single-path branch from 212eca2 to 1a9da1a Compare June 2, 2026 21:42
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: refactor/managed-uv-single-path vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9683 on HEAD, 9682 on base (🆕 +1)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/hermes_cli/test_managed_uv.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 5016 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR centralizes uv handling by introducing a single “managed uv” binary location under $HERMES_HOME/bin/uv (or uv.exe on Windows), and removes the installer-side FTS5 escalation logic that previously worked around stale externally-managed uv binaries. It updates both runtime update flows and bootstrap installers to align on the managed-uv approach.

Changes:

  • Added hermes_cli/managed_uv.py to resolve/install/update a single managed uv binary and optionally rebuild the venv on first bootstrap.
  • Updated hermes_cli/main.py update flows to use managed uv (ensure_uv, update_managed_uv) and trigger a venv rebuild on first-time bootstrap.
  • Simplified installers (install.sh / install.ps1) to always install/use managed uv at $HERMES_HOME/bin, and removed the shell-based FTS5 escalation logic.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
hermes_cli/managed_uv.py New managed-uv resolver/installer/updater utilities and venv rebuild helper.
hermes_cli/main.py Switched update flows from PATH uv to managed uv; added self-update + venv rebuild on first bootstrap.
scripts/install.sh Installer now installs/uses managed uv only; removed FTS5 escalation logic.
scripts/install.ps1 PowerShell installer now installs/uses managed uv first; resolver checks managed location before PATH.
hermes_state.py Updated FTS5 warning messaging to recommend hermes update.
tests/hermes_cli/test_managed_uv.py New tests covering managed_uv behavior.
tests/hermes_cli/test_cmd_update.py Added autouse fixture to keep existing tests compatible with managed_uv changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/managed_uv.py
Comment on lines +138 to +140
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).
Comment thread hermes_cli/main.py
Comment on lines +7491 to +7492
if fresh_bootstrap and uv_bin:
rebuild_venv(uv_bin, PROJECT_ROOT / "venv")
Comment thread hermes_cli/main.py
Comment on lines +9690 to +9691
if fresh_bootstrap and uv_bin:
rebuild_venv(uv_bin, PROJECT_ROOT / "venv")
Comment thread scripts/install.ps1
Comment on lines 310 to 314
$prevEAP = $ErrorActionPreference
try {
# Relax ErrorActionPreference around the nested astral installer.
# The astral installer (a separate `powershell -c "irm ... | iex"`)
# writes download progress to stderr. With $ErrorActionPreference
# = "Stop" set at the top of this script, PowerShell wraps stderr
# lines from native commands (which `powershell -c` is, from our
# perspective) as ErrorRecord objects when captured via 2>&1, then
# throws a terminating exception on the first one -- even though
# uv installs successfully and the child exits 0. Same fix
# pattern Test-Python uses for `uv python install`; verify success
# via Test-Path on the expected binary afterwards, which is more
# reliable than exit-code/stderr signal anyway.
$ErrorActionPreference = "Continue"
$env:UV_INSTALL_DIR = Join-Path $HermesHome "bin"
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 2>&1 | Out-Null
Comment thread scripts/install.ps1
Comment on lines 327 to 330
} catch {
# Restore EAP in case the try block threw before the assignment
if ($prevEAP) { $ErrorActionPreference = $prevEAP }
Write-Err "Failed to install uv: $_"
Write-Info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
class TestInstallUvInternals:
def test_posix_sets_uv_unmanaged_install(self, tmp_path):
target = tmp_path / "bin" / "uv"
with patch("hermes_cli.managed_uv._install_uv_posix") as mock_posix:
Comment thread hermes_cli/main.py
Comment on lines 9282 to 9286
if is_uv_tool_install():
if not uv:
print("✗ Detected a uv-tool install but `uv` is not on PATH; install uv and retry.")
print("✗ Detected a uv-tool install but managed uv install failed.")
print(" Install uv manually: https://docs.astral.sh/uv/getting-started/installation/")
sys.exit(1)
Comment thread hermes_state.py
@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard python:uv Pull requests that update python:uv code area/nix Nix flake, NixOS module, container packaging labels Jun 2, 2026
…t files

Production code now uses ensure_uv()/update_managed_uv() from
managed_uv.py instead of shutil.which("uv") directly. Tests that
patched shutil.which to control uv availability no longer controlled
the actual code path, causing CI failures.

Add an autouse _patch_managed_uv fixture to test_update_autostash.py
and test_uv_tool_update.py (matching the existing fixture in
test_cmd_update.py). The fixture makes managed_uv functions delegate
to shutil.which so existing test patches flow through naturally.
ethernet8023 added a commit that referenced this pull request Jun 2, 2026
- Fix update_managed_uv() docstring: returns path even on self-update
  failure, None only when no managed uv exists
- Fail fast on rebuild_venv() failure in _update_via_zip and
  _cmd_update_impl — avoids continuing with a nuked venv
- Save/restore $env:UV_INSTALL_DIR in install.ps1 Install-Uv
  to prevent env var leak into subsequent stages
- Patch platform.system() in test_posix_sets_uv_unmanaged_install
  for determinism on Windows
- Soften FTS5 warning: drop 'managed uv guarantees FTS5' claim,
  keep actionable 'hermes update' advice
@helix4u

helix4u commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

until this goes in, i'm broken by every update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/nix Nix flake, NixOS module, container packaging comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists python:uv Pull requests that update python:uv code type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants