From 67316fdc94bae09a2aee6318bc75cd3162decc25 Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:06:29 +0200 Subject: [PATCH] fix(install): relax native stderr handling in install.ps1 (#48352) --- scripts/install.ps1 | 27 ++++++-- tests/test_install_ps1_native_stderr_eap.py | 69 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tests/test_install_ps1_native_stderr_eap.py diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 58f136207b2..45bd884c730 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -185,6 +185,18 @@ function Write-Err { Write-Host "[X] $Message" -ForegroundColor Red } +function Invoke-NativeWithRelaxedErrorAction { + param([scriptblock]$Script) + + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & $Script + } finally { + $ErrorActionPreference = $prevEAP + } +} + # Inspect npm output for a TLS-trust failure and, if found, print actionable # remediation. npm/Node surface corporate MITM proxies and missing root CAs as # "unable to get local issuer certificate" / "self-signed certificate in @@ -1306,7 +1318,7 @@ function Install-Repository { Write-Info "Trying SSH clone..." $env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5" try { - git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlSsh $InstallDir + Invoke-NativeWithRelaxedErrorAction { git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlSsh $InstallDir } if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true } } catch { } $env:GIT_SSH_COMMAND = $null @@ -1315,7 +1327,7 @@ function Install-Repository { if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } Write-Info "SSH failed, trying HTTPS..." try { - git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlHttps $InstallDir + Invoke-NativeWithRelaxedErrorAction { git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlHttps $InstallDir } if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true } } catch { } } @@ -1443,8 +1455,11 @@ function Install-Venv { Remove-Item -Recurse -Force "venv" } - # uv creates the venv and pins the Python version in one step - & $UvCmd venv venv --python $PythonVersion + # uv creates the venv and pins the Python version in one step. uv emits + # normal progress such as "Using CPython ..." on stderr; under Windows + # PowerShell 5.1 with EAP=Stop that stderr is a NativeCommandError unless + # we temporarily relax EAP and trust $LASTEXITCODE for real failures. + Invoke-NativeWithRelaxedErrorAction { & $UvCmd venv venv --python $PythonVersion } # Neutralize any inherited UV_PYTHON (e.g. $env:UV_PYTHON = "3.14" left in # the user's shell). uv honours UV_PYTHON over an existing venv for the @@ -1514,7 +1529,7 @@ function Install-Dependencies { # in the wrong directory and imports fail with ModuleNotFoundError. # (Mirrors the same flag in scripts/install.sh::install_deps.) $env:UV_PROJECT_ENVIRONMENT = "$InstallDir\venv" - & $UvCmd sync --extra all --locked + Invoke-NativeWithRelaxedErrorAction { & $UvCmd sync --extra all --locked } if ($LASTEXITCODE -eq 0) { Write-Success "Main package installed (hash-verified via uv.lock)" $script:InstalledTier = "hash-verified (uv.lock)" @@ -1589,7 +1604,7 @@ except Exception: if (-not $skipPipFallback) { foreach ($tier in $installTiers) { Write-Info "Trying tier: $($tier.Name) ..." - & $UvCmd pip install -e $tier.Spec + Invoke-NativeWithRelaxedErrorAction { & $UvCmd pip install -e $tier.Spec } if ($LASTEXITCODE -eq 0) { Write-Success "Main package installed ($($tier.Name))" $script:InstalledTier = $tier.Name diff --git a/tests/test_install_ps1_native_stderr_eap.py b/tests/test_install_ps1_native_stderr_eap.py new file mode 100644 index 00000000000..608a282841d --- /dev/null +++ b/tests/test_install_ps1_native_stderr_eap.py @@ -0,0 +1,69 @@ +"""Regression tests for #48352: Windows PowerShell 5.1 native stderr. + +PowerShell 5.1 turns stderr from native commands into ``NativeCommandError`` +records when ``$ErrorActionPreference = "Stop"``. ``scripts/install.ps1`` has a +few git/uv calls where stderr can be normal progress output, so those calls must +run with EAP temporarily relaxed and then inspect ``$LASTEXITCODE``. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1" + + +def _install_ps1() -> str: + return INSTALL_PS1.read_text(encoding="utf-8") + + +def _assert_relaxed_call(text: str, command_pattern: str) -> None: + helper_block_pattern = ( + r"Invoke-NativeWithRelaxedErrorAction\s*\{[^}]*" + + command_pattern + + r"[^}]*\}" + ) + inline_pattern = ( + r"\$ErrorActionPreference\s*=\s*\"Continue\"[\s\S]{0,900}?" + + command_pattern + ) + assert re.search(helper_block_pattern, text) or re.search(inline_pattern, text), ( + f"install.ps1 must relax ErrorActionPreference around {command_pattern}" + ) + + +def test_repository_stage_relieves_eap_for_ssh_and_https_git_clone() -> None: + text = _install_ps1() + assert "function Invoke-NativeWithRelaxedErrorAction" in text + _assert_relaxed_call( + text, + r"git -c windows\.appendAtomically=false clone --depth 1 --branch \$Branch \$RepoUrlSsh \$InstallDir", + ) + _assert_relaxed_call( + text, + r"git -c windows\.appendAtomically=false clone --depth 1 --branch \$Branch \$RepoUrlHttps \$InstallDir", + ) + + +def test_uv_venv_and_dependency_installs_relax_eap() -> None: + text = _install_ps1() + _assert_relaxed_call(text, r"& \$UvCmd venv venv --python \$PythonVersion") + _assert_relaxed_call(text, r"& \$UvCmd sync --extra all --locked") + _assert_relaxed_call(text, r"& \$UvCmd pip install -e \$tier\.Spec") + + +def test_native_eap_helper_always_restores_previous_preference() -> None: + text = _install_ps1() + m = re.search( + r"function Invoke-NativeWithRelaxedErrorAction \{(?P[\s\S]*?)^\}", + text, + re.MULTILINE, + ) + assert m is not None, "expected a shared helper for NativeCommandError-safe calls" + body = m.group("body") + assert "$prevEAP = $ErrorActionPreference" in body + assert '$ErrorActionPreference = "Continue"' in body + assert "finally" in body + assert "$ErrorActionPreference = $prevEAP" in body