mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-18 14:52:04 +00:00
fix(desktop): detect Python via registry/filesystem; pin to 3.11–3.13
Two related fixes for Python detection on Windows:
1. py.exe (Python launcher) is missing from per-user installs that
didn't check the launcher option, so 'py -3.X --version' alone
misses real Python installs. User-reported case: clean Win11 +
official Python.org 3.14 install -> 'where py' returned nothing,
our installer offered to install Python again. Both NSIS prereq
page and main.cjs now probe in this order:
1. py.exe launcher (when present)
2. PEP 514 registry: HKLM/HKCU\SOFTWARE\Python\PythonCore\<v>\InstallPath
3. Filesystem: %ProgramFiles%\Python<v>, %LocalAppData%\Programs\Python\Python<v>
Crucially, we never fall back to running 'python.exe' from PATH
on Windows — the WindowsApps stub at %LOCALAPPDATA%\Microsoft\
WindowsApps\python.exe is a redirector that opens the Microsoft
Store window if no Store Python is installed. Triggering that
during boot would be terrible UX. Registry/filesystem probes
never execute the binary.
2. Drop 3.14 from the supported version set. Several Hermes deps
(notably pywinpty, which carries Rust crates like
windows_x86_64_msvc) don't yet publish 3.14 wheels. With wheels
missing, 'pip install -e .' falls back to building from sdist,
which needs a Rust toolchain — users see 'could not compile
windows_x86_64_msvc build script' on first run. install.ps1
sidesteps this by pinning to 3.11 via uv; the desktop installer
doesn't yet have the same uv-managed-Python pathway, so for now
we accept 3.11/3.12/3.13 and tell winget to install 3.11 if
none of those are present. Revisit when the wheel ecosystem
catches up to 3.14 (~early 2026).
This commit is contained in:
parent
708d2a0c33
commit
49de1adc49
2 changed files with 256 additions and 15 deletions
|
|
@ -19,7 +19,7 @@ const https = require('node:https')
|
|||
const net = require('node:net')
|
||||
const path = require('node:path')
|
||||
const { fileURLToPath, pathToFileURL } = require('node:url')
|
||||
const { spawn } = require('node:child_process')
|
||||
const { execFileSync, spawn } = require('node:child_process')
|
||||
const { bundledRuntimeImportCheck, isWindowsBinaryPathInWsl, isWslEnvironment } = require('./bootstrap-platform.cjs')
|
||||
const {
|
||||
DATA_URL_READ_MAX_BYTES,
|
||||
|
|
@ -438,13 +438,120 @@ function findPythonForRoot(root) {
|
|||
}
|
||||
|
||||
function findSystemPython() {
|
||||
const commands = IS_WINDOWS ? ['python.exe', 'py.exe', 'python'] : ['python3', 'python']
|
||||
|
||||
for (const command of commands) {
|
||||
const candidate = findOnPath(command)
|
||||
if (candidate) return candidate
|
||||
if (!IS_WINDOWS) {
|
||||
// POSIX systems: PATH lookup is safe.
|
||||
for (const command of ['python3', 'python']) {
|
||||
const candidate = findOnPath(command)
|
||||
if (candidate) return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Windows: PATH-based detection has TWO landmines we have to dodge.
|
||||
//
|
||||
// (1) The Microsoft Store "Python stub" lives at
|
||||
// %LOCALAPPDATA%\Microsoft\WindowsApps\python.exe and is on PATH
|
||||
// by default on modern Windows. It's a redirector that opens the
|
||||
// Store window if no Store Python is installed. Running it for
|
||||
// `-m venv` would either succeed (real Store install — fine) or
|
||||
// pop the Store dialog (bad UX during boot).
|
||||
// (2) `py.exe` (Python launcher) is missing from per-user installs
|
||||
// that didn't check the launcher option, so PATH-only checks
|
||||
// miss real Python 3.13 installs (user-reported case).
|
||||
//
|
||||
// We also restrict ourselves to Python 3.11–3.13. 3.14 is the latest
|
||||
// CPython but several Hermes deps (notably pywinpty's Rust-built
|
||||
// windows_x86_64_msvc crate) don't yet publish 3.14 wheels, and
|
||||
// `pip install -e .` falls back to source-build, which fails without
|
||||
// a Rust toolchain. install.ps1 sidesteps this by pinning to 3.11
|
||||
// via uv; until we add the same uv-managed Python pathway here, the
|
||||
// simplest fix is to refuse 3.14 detection and let the NSIS prereq
|
||||
// page offer to install 3.11 alongside.
|
||||
//
|
||||
// Strategy: probe in three passes, in order from most-precise to
|
||||
// least-precise, and ONLY use PATH lookup as a last resort after
|
||||
// confirming the candidate isn't the WindowsApps redirector.
|
||||
//
|
||||
// Pass 1: PEP 514 registry — every standards-compliant Python
|
||||
// installer registers itself at SOFTWARE\Python\PythonCore.
|
||||
// The MS Store stub does NOT register here, so a hit means
|
||||
// a real Python install. Versions are explicit so we
|
||||
// inherently filter 3.14 out.
|
||||
// Pass 2: Filesystem probe of standard install locations
|
||||
// (Program Files, LocalAppData\Programs\Python). Same
|
||||
// version filtering by directory name.
|
||||
// Pass 3: PATH lookup of `py.exe` (the launcher itself never
|
||||
// triggers the Store) — but call it with a version flag so
|
||||
// we resolve to a SPECIFIC supported version, not whatever
|
||||
// py.exe's default is (which on a 3.14-only box would be
|
||||
// 3.14).
|
||||
|
||||
const SUPPORTED_VERSIONS = ['3.11', '3.12', '3.13']
|
||||
const SUPPORTED_VERSIONS_NO_DOT = ['311', '312', '313']
|
||||
|
||||
// Pass 1: registry. Use `reg query` since main process doesn't have
|
||||
// a reliable in-process registry API across all electron versions.
|
||||
for (const hive of ['HKLM', 'HKCU']) {
|
||||
for (const version of SUPPORTED_VERSIONS) {
|
||||
try {
|
||||
const out = execFileSync(
|
||||
'reg',
|
||||
['query', `${hive}\\SOFTWARE\\Python\\PythonCore\\${version}\\InstallPath`, '/ve', '/reg:64'],
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
)
|
||||
// Output format: " (Default) REG_SZ C:\Path\To\Python\"
|
||||
const match = out.match(/REG_SZ\s+(.+?)\s*$/m)
|
||||
if (match) {
|
||||
const installPath = match[1].trim()
|
||||
const pythonExe = path.join(installPath, 'python.exe')
|
||||
if (fileExists(pythonExe)) return pythonExe
|
||||
}
|
||||
} catch {
|
||||
// Key not present — try next.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: filesystem probe of standard locations.
|
||||
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'
|
||||
const localAppData = process.env.LOCALAPPDATA || ''
|
||||
for (const versionDir of SUPPORTED_VERSIONS_NO_DOT) {
|
||||
const systemWide = path.join(programFiles, `Python${versionDir}`, 'python.exe')
|
||||
if (fileExists(systemWide)) return systemWide
|
||||
if (localAppData) {
|
||||
const perUser = path.join(localAppData, 'Programs', 'Python', `Python${versionDir}`, 'python.exe')
|
||||
if (fileExists(perUser)) return perUser
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: py.exe with explicit version flag. The launcher itself is
|
||||
// safe to invoke (no Store popup) and `py -3.13 -c "import sys;
|
||||
// print(sys.executable)"` resolves to the actual python.exe path of
|
||||
// the requested version. We try in version-priority order so the
|
||||
// first hit wins.
|
||||
const pyExe = findOnPath('py.exe')
|
||||
if (pyExe) {
|
||||
for (const version of SUPPORTED_VERSIONS) {
|
||||
try {
|
||||
const out = execFileSync(
|
||||
pyExe,
|
||||
[`-${version}`, '-c', 'import sys; print(sys.executable)'],
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
)
|
||||
const candidate = out.trim()
|
||||
if (candidate && fileExists(candidate)) return candidate
|
||||
} catch {
|
||||
// py couldn't find that version — try next.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We deliberately do NOT fall back to plain `python.exe` on PATH.
|
||||
// Without a way to verify the version safely (running `python -V`
|
||||
// risks the Microsoft Store popup), accepting whatever's there
|
||||
// could land us on 3.14 and trigger the Rust-build-from-source
|
||||
// failure. Better to return null and let the NSIS prereq page
|
||||
// offer to install a known-good 3.11 via winget.
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,103 @@ Var HermesInstallPython
|
|||
Var HermesInstallGit
|
||||
Var HermesInstallRipgrep
|
||||
|
||||
; ----------------------------------------------------------------------------
|
||||
; HermesDetectPythonViaRegistry — sets $HermesHasPython="1" if a PEP 514
|
||||
; entry exists for any of the supported Python versions. Reads HKLM
|
||||
; (system-wide installs) then HKCU (per-user installs). Vendor "PythonCore"
|
||||
; covers official python.org distributions; "ContinuumAnalytics" covers
|
||||
; Anaconda/Miniconda. We don't enumerate other vendors because they're
|
||||
; rare in our user base and we'd rather miss them and let winget add a
|
||||
; second Python than misclassify something else as a working Python.
|
||||
; ----------------------------------------------------------------------------
|
||||
Function HermesDetectPythonViaRegistry
|
||||
Push $1
|
||||
Push $2
|
||||
|
||||
; Set view to 64-bit on x64 systems so we read the right hive — the
|
||||
; default 32-bit view would miss a 64-bit Python install on 64-bit
|
||||
; Windows. SetRegView 32 restored at function exit.
|
||||
SetRegView 64
|
||||
|
||||
; Iterate the supported versions. Each is its own ReadRegStr — NSIS
|
||||
; doesn't have loops over arrays inside functions easily, and four
|
||||
; copies is clearer than gymnastics with $R0-$R9.
|
||||
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.11\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.12\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.13\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.11\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.12\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.13\InstallPath" ""
|
||||
${If} $1 != ""
|
||||
StrCpy $HermesHasPython "1"
|
||||
Goto hermes_py_reg_done
|
||||
${EndIf}
|
||||
|
||||
hermes_py_reg_done:
|
||||
SetRegView 32
|
||||
Pop $2
|
||||
Pop $1
|
||||
FunctionEnd
|
||||
|
||||
; ----------------------------------------------------------------------------
|
||||
; HermesDetectPythonViaFilesystem — sets $HermesHasPython="1" if a Python
|
||||
; install exists at one of the standard locations. FileExists never runs
|
||||
; the binary so this is safe even if the user has the MS Store stub on
|
||||
; their PATH. We probe both system-wide (Program Files) and per-user
|
||||
; (LocalAppData\Programs) install locations for versions 3.11–3.14.
|
||||
; ----------------------------------------------------------------------------
|
||||
Function HermesDetectPythonViaFilesystem
|
||||
; System-wide installs (default location for python.org with admin)
|
||||
${If} ${FileExists} "$PROGRAMFILES64\Python311\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$PROGRAMFILES64\Python312\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$PROGRAMFILES64\Python313\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
; Per-user installs (default location for python.org without admin
|
||||
; or with "Install for me only"). Covers the user-reported case.
|
||||
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python311\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python312\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python313\python.exe"
|
||||
StrCpy $HermesHasPython "1"
|
||||
Return
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; ----------------------------------------------------------------------------
|
||||
; HermesDetectPrereqs — populates $HermesHasWinget / $HermesHasPython /
|
||||
; $HermesHasGit / $HermesHasRipgrep with "0" or "1". Called from the
|
||||
|
|
@ -86,10 +183,32 @@ Function HermesDetectPrereqs
|
|||
StrCpy $HermesHasWinget "0"
|
||||
${EndIf}
|
||||
|
||||
; --- Python 3.11+ ---
|
||||
; The py launcher returns exit 0 only when that specific version is
|
||||
; installed. We probe each version Hermes' pyproject.toml accepts.
|
||||
; --- Python 3.11 / 3.12 / 3.13 ---
|
||||
; We deliberately accept 3.11–3.13 only and NOT 3.14, because some of
|
||||
; Hermes' transitive deps (notably pywinpty, which carries Rust crates
|
||||
; like windows_x86_64_msvc) don't yet publish 3.14 wheels. Without
|
||||
; wheels, `pip install -e .` falls back to building from sdist, which
|
||||
; needs a Rust toolchain. Users without one see a confusing "could
|
||||
; not compile windows_x86_64_msvc build script" error. install.ps1
|
||||
; sidesteps this by pinning to 3.11 via uv; the desktop installer
|
||||
; can't easily install uv in the same flow yet, so we just refuse to
|
||||
; accept 3.14 as "good" and offer 3.11 via winget instead. Revisit
|
||||
; when 3.14 wheels are widely available across our dep tree.
|
||||
;
|
||||
; Detection strategy, in order from most-precise to least-precise.
|
||||
; Each step uses ONLY operations that don't execute `python.exe`
|
||||
; directly off PATH — running `python` on Windows can open the
|
||||
; Microsoft Store if only the "Python stub" is installed, which is
|
||||
; terrible UX during an installer. We avoid that by:
|
||||
; (a) launcher checks (py.exe runs no python until -V),
|
||||
; (b) registry reads (PEP 514, no execution at all),
|
||||
; (c) filesystem probes via FileExists.
|
||||
StrCpy $HermesHasPython "0"
|
||||
|
||||
; (1) The py launcher. Ships with python.org installer when
|
||||
; "Install launcher for all users" is checked (default for some
|
||||
; paths, not for per-user installs without elevation). When
|
||||
; present, py -3.X --version returns 0 iff that version exists.
|
||||
nsExec::Exec 'cmd.exe /c py -3.11 --version >nul 2>&1'
|
||||
Pop $0
|
||||
${If} $0 == 0
|
||||
|
|
@ -104,16 +223,31 @@ Function HermesDetectPrereqs
|
|||
Pop $0
|
||||
${If} $0 == 0
|
||||
StrCpy $HermesHasPython "1"
|
||||
${Else}
|
||||
nsExec::Exec 'cmd.exe /c py -3.14 --version >nul 2>&1'
|
||||
Pop $0
|
||||
${If} $0 == 0
|
||||
StrCpy $HermesHasPython "1"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
; (2) PEP 514 registry probe. Every standards-compliant Python
|
||||
; installer registers itself under HKLM or HKCU at
|
||||
; SOFTWARE\Python\PythonCore\<version>\InstallPath. The MS Store
|
||||
; stub does NOT register here — so we get a clean signal for
|
||||
; "real Python is installed" without ever risking the Store
|
||||
; popup. Covers the case the user reported: per-user Python.org
|
||||
; install without launcher checkbox, plus Anaconda which writes
|
||||
; similar keys under a different vendor name.
|
||||
${If} $HermesHasPython == "0"
|
||||
Call HermesDetectPythonViaRegistry
|
||||
${EndIf}
|
||||
|
||||
; (3) Filesystem probe of common install locations. Catches edge
|
||||
; cases where the installer didn't update the registry (rare
|
||||
; but possible with hand-extracted Python or some third-party
|
||||
; installers). We only check standard paths — running anything
|
||||
; would risk spawning the Store stub.
|
||||
${If} $HermesHasPython == "0"
|
||||
Call HermesDetectPythonViaFilesystem
|
||||
${EndIf}
|
||||
|
||||
; --- Git ---
|
||||
nsExec::Exec 'cmd.exe /c where git >nul 2>&1'
|
||||
Pop $0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue