Windows installer Python detection and venv fixes
NousResearch/hermes-agent · cluster 24424 · hermes cli
Data as of
Nothing open on GitHub. Fix merged in #26586. Check it covers your case.
The short answer
- Known?
- Yes. 8 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #26586. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #51602. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and fixes address Windows-specific installer failures related to Python detection, including skipping Microsoft Store stub executables, re-resolving the Python fallback at venv creation time, and handling missing Python installations.
Open issues0
Open PRs0
Fixes on record
-
#26586 fix(install.ps1): restore EAP=Continue around uv python install, skip Store stub
files:
scripts/install.ps1Show the diff
-
scripts/install.ps1@@ -145,19 +145,39 @@ function Test-Python { # Python not found — use uv to install it (no admin needed!) Write-Info "Python $PythonVersion not found, installing via uv..." try { + # Temporarily relax ErrorActionPreference: uv writes download progress + # ("Downloading cpython-3.11.15-windows-x86_64-none (24.5MiB)") to + # stderr. With $ErrorActionPreference = "Stop" (set at the top of this + # script) PowerShell wraps stderr lines from native commands as + # ErrorRecord objects when captured via 2>&1, then throws a terminating + # exception on the first one — even though uv exits 0 and Python was + # installed successfully. Verify success via `uv python find` + # afterwards, which is the reliable signal regardless of exit-code + # semantics or stderr noise. This fix was previously landed as + # commit ec1714e71 and then lost in a release squash; reapplied here. + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" $uvOutput = & $UvCmd python install $PythonVersion 2>&1 - if ($LASTEXITCODE -eq 0) { - $pythonPath = & $UvCmd python find $PythonVersion 2>$null - if ($pythonPath) { - $ver = & $pythonPath --version 2>$null - Write-Success "Python installed: $ver" - return $true - } - } else { + $uvExitCode = $LASTEXITCODE + $ErrorActionPreference = $prevEAP + + # Check if Python is now available (more reliable than exit code + # since uv may return non-zero due to "already installed" etc.) + $pythonPath = & $UvCmd python find $PythonVersion 2>$null + if ($pythonPath) { + $ver = & $pythonPath --version 2>$null + Write-Success "Python installed: $ver" + return $true + } + + # uv ran but Python still not findable — show what happened + if ($uvExitCode -ne 0) { Write-Warn "uv python install output:" Write-Host $uvOutput -ForegroundColor DarkGray } } catch { + # Restore EAP in case the try block threw before the assignment + if ($prevEAP) { $ErrorActionPreference = $prevEAP } Write-Warn "uv python install error: $_" } @@ -175,15 +195,42 @@ function Test-Python { } catch { } } - # Fallback: try system python - if (Get-Command python -ErrorAction SilentlyContinue) { - $sysVer = python --version 2>$null - if ($sysVer -match "3\.(1[0-9]|[1-9][0-9])") { - Write-Success "Using system Python: $sysVer" - return $true + # Fallback: try system python — but skip the Microsoft Store stub. + # On Windows, %LOCALAPPDATA%\Microsoft\WindowsApps\python.exe is a 0-byte + # reparse-point stub that prints "Python was not found; run without + # arguments to install from the Microsoft Store..." to stdout and exits + # non-zero. Get-Command finds it; invoking it produces a confusing error + # that the user sees as our installer crashing. + $pythonCmd = Get-Command python -ErrorAction SilentlyContinue + if ($pythonCmd) { + $isStoreStub = $false + try { + $pythonSource = $pythonCmd.Source + if ($pythonSource -and $pythonSource -like "*\WindowsApps\*") { + $isStoreStub = $true + } else { + # Even outside WindowsApps, a 0-byte file is the stub + $item = Get-Item $pythonSource -ErrorAction SilentlyContinue + if ($item -and $item.Length -eq 0) { $isStoreStub = $true } + } + } catch { } + + if (-not $isStoreStub) { + try { + $prevEAP2 = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $sysVer = & python --version 2>&1 + $ErrorActionPreference = $prevEAP2 + if ($sysVer -match "Python 3\.(1[0-9]|[1-9][0-9])") { + Write-Success "Using system Python: $sysVer" + return $true + } + } catch { + if ($prevEAP2) { $ErrorActionPreference = $prevEAP2 } + } } } - + Write-Err "Failed to install Python $PythonVersion" Write-Info "Install Python 3.11 manually, then re-run this script:" Write-Info " https://www.python.org/downloads/"
-
-
#51602 fix(installer): re-resolve Python fallback at venv stage on Windows (#50769)
files:
scripts/install.ps1,tests/test_install_ps1_python_fallback_venv.pyShow the diff
-
scripts/install.ps1@@ -139,6 +139,11 @@ foreach ($tmpVar in @('TEMP', 'TMP')) { $RepoUrlSsh = "[redacted:email]:NousResearch/hermes-agent.git" $RepoUrlHttps = "https://github.com/NousResearch/hermes-agent.git" $PythonVersion = "3.11" +# Minor versions the installer accepts when the requested $PythonVersion isn't +# available, in preference order. uv discovers both uv-managed and system +# interpreters, so this list also matches a pre-existing system Python. Single +# source of truth shared by Test-Python's fallback and Resolve-AvailablePythonVersion. +$PythonFallbackVersions = @("3.12", "3.13", "3.10") $NodeVersion = "22" # Stage-protocol version. Bumped only for genuinely breaking changes to the @@ -510,6 +515,31 @@ function Resolve-UvCmd { throw "uv is not installed. Run install.ps1 -Stage uv first." } +function Resolve-AvailablePythonVersion { + # Return the first Python minor version uv can actually find, preferring the + # requested $PythonVersion and then $PythonFallbackVersions. Returns $null + # when none are available. + # + # This is the cross-process-safe counterpart to Test-Python's in-memory + # ``$script:PythonVersion = $fallbackVer`` mutation. Under Hermes-Setup.exe + # each ``-Stage NAME`` runs in a *fresh* powershell.exe, so the fallback the + # ``python`` stage settled on (e.g. 3.12 when 3.11 is absent) does NOT + # survive into the ``venv`` stage's process -- there $PythonVersion is back + # at its "3.11" default. Consumers re-resolve here instead of trusting that + # default, which is exactly the propagation gap behind issue #50769. + $candidates = @($PythonVersion) + $PythonFallbackVersions + $seen = @{} + foreach ($ver in $candidates) { + if (-not $ver -or $seen.ContainsKey($ver)) { continue } + $seen[$ver] = $true + try { + $found = & $UvCmd python find $ver 2>$null + if ($found) { return $ver } + } catch { } + } + return $null +} + function Test-Python { Write-Info "Checking Python $PythonVersion..." @@ -566,7 +596,7 @@ function Test-Python { # Fallback: check if ANY Python 3.10+ is already available on the system Write-Info "Trying to find any existing Python 3.10+..." - foreach ($fallbackVer in @("3.12", "3.13", "3.10")) { + foreach ($fallbackVer in $PythonFallbackVersions) { try { $pythonPath = & $UvCmd python find $fallbackVer 2>$null if ($pythonPath) { @@ -1513,7 +1543,19 @@ function Install-Venv { Write-Info "Skipping virtual environment (-NoVenv)" return } - + + # Re-resolve the interpreter before creating the venv. Under Hermes-Setup.exe + # each stage runs in its own powershell.exe, so the fallback the `python` + # stage picked (e.g. 3.12 when 3.11 is absent) did NOT propagate into this + # fresh process -- $PythonVersion is back at its "3.11" default. Trusting it + # here made `uv venv venv --python 3.11` fail with exit 2 on machines without + # 3.11 even though the `python` stage reported success (issue #50769). + $resolved = Resolve-AvailablePythonVersion + if ($resolved -and $resolved -ne $PythonVersion) { + Write-Info "Python $PythonVersion not available; using detected Python $resolved" + $script:PythonVersion = $resolved + } + Write-Info "Creating virtual environment with Python $PythonVersion..." Push-Location $InstallDir -
tests/test_install_ps1_python_fallback_venv.py@@ -0,0 +1,113 @@ +"""Regression: the Windows installer must honor its Python fallback at venv time. + +A user on Windows 11 reported (#50769) that the installer correctly detected a +Python 3.12 fallback when 3.11 was absent:: + + [OK] Found fallback: Python 3.12.8 + ... + -> Creating virtual environment with Python 3.11... + +and then failed with ``Failed to create virtual environment (uv venv exited +with 2)``. + +Root cause: ``Test-Python`` records the fallback via an in-memory +``$script:PythonVersion = $fallbackVer`` mutation, but under Hermes-Setup.exe +each ``-Stage NAME`` runs in its *own* fresh ``powershell.exe`` process. The +``venv`` stage therefore starts with ``$PythonVersion`` back at its ``"3.11"`` +default, so ``uv venv venv --python 3.11`` runs on a machine that has no 3.11. + +The fix re-resolves the interpreter inside ``Install-Venv`` (via the +cross-process-safe ``Resolve-AvailablePythonVersion`` helper) before creating +the venv, so the venv stage uses whatever interpreter is actually present. +These tests lock that contract at the source level (the script only runs on +Windows, so there's no runner to execute it on Linux CI). +""" + +import re +from pathlib import Path + +import pytest + +_INSTALL_PS1 = Path(__file__).resolve().parents[1] / "scripts" / "install.ps1" + + [redacted:email](scope="module") +def source() -> str: + return _INSTALL_PS1.read_text(encoding="utf-8") + + +def _function_body(source: str, name: str) -> str: + """Return the text of a PowerShell ``function <name> { ... }`` block.""" + start = source.index(f"function {name}") + brace = source.index("{", start) + depth = 0 + for i in range(brace, len(source)): + if source[i] == "{": + depth += 1 + elif source[i] == "}": + depth -= 1 + if depth == 0: + return source[brace : i + 1] + raise AssertionError(f"unterminated function body for {name}") + + +def test_resolver_helper_is_defined(source: str): + """A cross-process-safe Python-version resolver must exist.""" + assert "function Resolve-AvailablePythonVersion" in source, ( + "expected a Resolve-AvailablePythonVersion helper that re-resolves the " + "interpreter independently of the in-memory $script:PythonVersion mutation" + ) + + +def test_install_venv_reresolves_before_creating_venv(source: str): + """Install-Venv must re-resolve the interpreter BEFORE `uv venv`. + + Otherwise the venv stage's fresh process trusts the stale "3.11" default + and fails on machines where only the fallback (e.g. 3.12) is installed. + """ + body = _function_body(source, "Install-Venv") + resolve_at = body.find("Resolve-AvailablePythonVersion") + assert resolve_at != -1, ( + "Install-Venv must call Resolve-AvailablePythonVersion so the venv " + "stage doesn't trust the stale $PythonVersion default across processes" + ) + create_at = body.find("Creating virtual environment with Python") + assert create_at != -1, "expected the venv-creation log line in Install-Venv" + assert resolve_at < create_at, ( + "the interpreter must be re-resolved BEFORE the 'Creating virtual " + "environment' step (and before `uv venv` runs)" + ) + + +def test_fallback_list_is_single_source_of_truth(source: str): + """The fallback versions live in one shared constant, used by both paths. + + A drifting second copy of the list is how detection and venv creation + disagree in the first place. + """ + assert re.search(r"\$PythonFallbackVersions\s*=", source), ( + "expected a shared $PythonFallbackVersions constant" + ) + # Test-Python's fallback loop must iterate the shared constant, not an + # inline literal list. + test_python = _function_body(source, "Test-Python") + assert "foreach ($fallbackVer in $PythonFallbackVersions)" in test_python, ( + "Test-Python must iterate the shared $PythonFallbackVersions constant" + ) + # The resolver must seed its candidate list from the same constant. + resolver = _function_body(source, "Resolve-AvailablePythonVersion") + assert "$PythonFallbackVersions" in resolver, ( + "Resolve-AvailablePythonVersion must reuse the shared fallback constant" + ) + + +def test_resolver_prefers_requested_version_then_fallbacks(source: str): + """The resolver tries the requested version first, then the fallbacks.""" + resolver = _function_body(source, "Resolve-AvailablePythonVersion") + assert "@($PythonVersion) + $PythonFallbackVersions" in resolver, ( + "resolver candidate order must be: requested $PythonVersion first, " + "then the shared fallbacks" + ) + assert "uv" in resolver and "python find" in resolver, ( + "resolver must probe availability via `uv python find`" + )
-
4 PRs closed without merging
From the close comments: done elsewhere 1 · duplicate 1 · merged via another change 1 · stale or abandoned 1
-
#24543 fix(install): skip Microsoft Store python.exe stub in Test-Python fallback (#24424) · done elsewhere →
622c27e,#26586 -
#50790 fix(installer): re-resolve Python fallback at venv stage on Windows (#50769) · merged via another change →
23683c3,34bd6a0,#51602 - #50840 fix(installer): use detected Python fallback for venv creation · stale or abandoned
-
#50850 fix(installer): use detected Python fallback for venv creation · duplicate →
#50790
Closed issues · 2
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.23: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab