fix(installer): harden managed Python resolution
This commit is contained in:
@@ -391,6 +391,7 @@ $PythonVersion = "3.11"
|
||||
# are eligible. Single source of truth shared by Test-Python's fallback and
|
||||
# Resolve-AvailablePythonVersion.
|
||||
$PythonFallbackVersions = @("3.12", "3.13", "3.10")
|
||||
$PythonFindTimeoutMs = 30000
|
||||
$NodeVersion = "22"
|
||||
# The npm range the root package.json pins in `engines.npm`. A constant rather
|
||||
# than a manifest read like the POSIX side does: Test-Node runs BEFORE the repo
|
||||
@@ -1187,9 +1188,9 @@ function Initialize-ManagedPythonEnvironment {
|
||||
}
|
||||
|
||||
function Resolve-AvailablePythonVersion {
|
||||
# Return the absolute path of the first Hermes-managed interpreter uv can
|
||||
# find, preferring the requested version and then fallback minors. System
|
||||
# and application-owned interpreters are deliberately ineligible.
|
||||
# Return the path and minor version of the first Hermes-managed interpreter
|
||||
# uv can find, preferring the requested version and then fallback minors.
|
||||
# System and application-owned interpreters are deliberately ineligible.
|
||||
#
|
||||
# Under Hermes-Setup.exe each stage runs in a fresh powershell.exe. The
|
||||
# venv stage therefore re-resolves both version and provenance rather than
|
||||
@@ -1217,19 +1218,28 @@ function Resolve-AvailablePythonVersion {
|
||||
$startInfo.RedirectStandardError = $true
|
||||
$process.StartInfo = $startInfo
|
||||
if (-not $process.Start()) { continue }
|
||||
$stdout = $process.StandardOutput.ReadToEnd()
|
||||
$process.StandardError.ReadToEnd() | Out-Null
|
||||
$process.WaitForExit()
|
||||
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderrTask = $process.StandardError.ReadToEndAsync()
|
||||
if (-not $process.WaitForExit($PythonFindTimeoutMs)) {
|
||||
try { $process.Kill() } catch { }
|
||||
$process.WaitForExit()
|
||||
throw "uv python find $ver timed out after $PythonFindTimeoutMs ms"
|
||||
}
|
||||
$stdout = $stdoutTask.Result
|
||||
$stderrTask.Result | Out-Null
|
||||
if ($process.ExitCode -ne 0) { continue }
|
||||
[string]$foundPath = ($stdout.Trim() -split "`r?`n") | Select-Object -Last 1
|
||||
if ($foundPath) {
|
||||
$absolute = [System.IO.Path]::GetFullPath($foundPath)
|
||||
if ($absolute.StartsWith($managedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$script:PythonVersion = $ver
|
||||
return $absolute
|
||||
return [PSCustomObject]@{
|
||||
Path = $absolute
|
||||
Version = $ver
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
throw "Failed to resolve Hermes-managed Python $ver`: $_"
|
||||
} finally {
|
||||
if ($process) { $process.Dispose() }
|
||||
}
|
||||
@@ -1243,9 +1253,9 @@ function Test-Python {
|
||||
|
||||
# Only a checkout-private uv-managed interpreter satisfies this stage.
|
||||
try {
|
||||
$pythonPath = Resolve-AvailablePythonVersion
|
||||
if ($pythonPath) {
|
||||
$ver = & $pythonPath --version 2>$null
|
||||
$resolvedPython = Resolve-AvailablePythonVersion
|
||||
if ($resolvedPython) {
|
||||
$ver = & $resolvedPython.Path --version 2>$null
|
||||
Write-Success "Python found: $ver"
|
||||
return $true
|
||||
}
|
||||
@@ -1274,9 +1284,9 @@ function Test-Python {
|
||||
|
||||
# Check if Python is now available (more reliable than exit code
|
||||
# since uv may return non-zero due to "already installed" etc.)
|
||||
$pythonPath = Resolve-AvailablePythonVersion
|
||||
if ($pythonPath) {
|
||||
$ver = & $pythonPath --version 2>$null
|
||||
$resolvedPython = Resolve-AvailablePythonVersion
|
||||
if ($resolvedPython) {
|
||||
$ver = & $resolvedPython.Path --version 2>$null
|
||||
Write-Success "Python installed: $ver"
|
||||
return $true
|
||||
}
|
||||
@@ -1302,9 +1312,9 @@ function Test-Python {
|
||||
$ErrorActionPreference = "Continue"
|
||||
& $UvCmd python install $fallbackVer --no-bin --no-registry --no-config 2>&1 | Out-Null
|
||||
$ErrorActionPreference = $previousFallbackEAP
|
||||
$pythonPath = Resolve-AvailablePythonVersion
|
||||
if ($pythonPath) {
|
||||
$ver = & $pythonPath --version 2>$null
|
||||
$resolvedPython = Resolve-AvailablePythonVersion
|
||||
if ($resolvedPython) {
|
||||
$ver = & $resolvedPython.Path --version 2>$null
|
||||
Write-Success "Python fallback installed: $ver"
|
||||
return $true
|
||||
}
|
||||
@@ -2557,7 +2567,7 @@ function Install-Venv {
|
||||
throw "Hermes-managed Python is unavailable. Run install.ps1 -Stage python first."
|
||||
}
|
||||
|
||||
Write-Info "Creating virtual environment with Python $PythonVersion..."
|
||||
Write-Info "Creating virtual environment with Python $($resolvedPython.Version)..."
|
||||
|
||||
Push-Location $InstallDir
|
||||
|
||||
@@ -2684,7 +2694,7 @@ function Install-Venv {
|
||||
try {
|
||||
$venvStartInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$venvStartInfo.FileName = $UvCmd
|
||||
$venvStartInfo.Arguments = "venv venv --python `"$resolvedPython`" --managed-python --no-python-downloads --no-config"
|
||||
$venvStartInfo.Arguments = "venv venv --python `"$($resolvedPython.Path)`" --managed-python --no-python-downloads --no-config"
|
||||
$venvStartInfo.WorkingDirectory = $InstallDir
|
||||
$venvStartInfo.UseShellExecute = $false
|
||||
$venvStartInfo.CreateNoWindow = $true
|
||||
@@ -2799,7 +2809,7 @@ function Install-Venv {
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Virtual environment ready (Python $PythonVersion)"
|
||||
Write-Success "Virtual environment ready (Python $($resolvedPython.Version))"
|
||||
}
|
||||
|
||||
function Get-PendingVenvBackup {
|
||||
|
||||
@@ -24,6 +24,11 @@ public static class FakeUv {
|
||||
string.Join(" ", args) + Environment.NewLine);
|
||||
|
||||
if (args.Length >= 2 && args[0] == "python" && args[1] == "find") {
|
||||
string stderrBytes = Environment.GetEnvironmentVariable(
|
||||
"FAKE_UV_FIND_STDERR_BYTES");
|
||||
if (!string.IsNullOrEmpty(stderrBytes)) {
|
||||
Console.Error.Write(new string('x', int.Parse(stderrBytes)));
|
||||
}
|
||||
bool managed = args.Contains("--managed-python");
|
||||
string availableVersion = Environment.GetEnvironmentVariable(
|
||||
"FAKE_MANAGED_PYTHON_VERSION");
|
||||
|
||||
@@ -45,6 +45,7 @@ def _run_venv_stage(
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@@ -140,6 +141,34 @@ def test_fallback_minor_is_reported_from_resolved_managed_interpreter(
|
||||
assert version.stdout.strip() == "Python 3.12.13"
|
||||
|
||||
|
||||
def test_python_find_drains_large_stderr_without_deadlock(tmp_path: Path) -> None:
|
||||
powershell = shutil.which("powershell")
|
||||
if not powershell:
|
||||
pytest.skip("Windows PowerShell is required")
|
||||
|
||||
hermes_home = tmp_path / "hermes-home"
|
||||
install_dir = tmp_path / "install"
|
||||
managed_python = (
|
||||
install_dir / ".hermes-runtime" / "python" / "cpython-3.11" / "python.exe"
|
||||
)
|
||||
uv = hermes_home / "bin" / "uv.exe"
|
||||
uv.parent.mkdir(parents=True)
|
||||
managed_python.parent.mkdir(parents=True)
|
||||
compile_fake_uv(powershell, uv)
|
||||
shutil.copy2(uv, managed_python)
|
||||
env = {
|
||||
**os.environ,
|
||||
"FAKE_UV_LOG": str(tmp_path / "uv.log"),
|
||||
"FAKE_MANAGED_PYTHON": str(managed_python),
|
||||
"FAKE_UV_FIND_STDERR_BYTES": str(1024 * 1024),
|
||||
}
|
||||
|
||||
run = _run_venv_stage(powershell, tmp_path, hermes_home, install_dir, env)
|
||||
|
||||
assert run.returncode == 0, run.stdout + run.stderr
|
||||
assert "Creating virtual environment with Python 3.11" in run.stdout
|
||||
|
||||
|
||||
def test_venv_failure_fails_stage_and_restores_existing_environment(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user