From 5dcfb0b82e1fe3102002c9b7d60a1914fb129fda Mon Sep 17 00:00:00 2001 From: emozilla Date: Mon, 18 May 2026 20:26:45 -0700 Subject: [PATCH] install.ps1: harden Install-SystemPackages against winget msstore failures The previous winget invocation discarded stdout/stderr and trusted no signal at all -- not the exit code (winget exits 0 even when it bails "please specify --source"), not output (sent to Out-Null), not the catch handler (winget returning 0 means no exception fires). The only trust signal was a post-install Get-Command rg / Get-Command ffmpeg check, which would also miss the package because %LOCALAPPDATA%\ Microsoft\WinGet\Links (where winget puts command aliases) is added to PATH by AppExecutionAlias machinery only in fresh shells. End result on machines where the msstore source has a cert problem (0x8a15005e -- common on Windows-on-ARM and some corporate networks): silent failure, no log, no breadcrumb, and the user is told the install succeeded. Specifically: - Pin --source winget on every winget install call. Defeats the broken- msstore-source path. We ship nothing from msstore so this is safe and forward-compatible. - Add --exact --id for a tighter package match. - Capture each winget invocation's combined stdout/stderr + exit code to %TEMP%\hermes-winget--.log instead of Out-Null. On the happy path the log is deleted after the post-install check confirms the binary is on PATH; on failure the log is kept and its path is named in a Write-Warn so the user has something to grep. - Refresh PATH to include %LOCALAPPDATA%\Microsoft\WinGet\Links in addition to the User/Machine env-var hives, so Get-Command sees newly- installed winget aliases in the same process. - No behavior change on the happy path. Same Write-Info/Success/Warn cadence, same fallback order (winget -> choco -> scoop -> manual), same $script:HasRipgrep / $script:HasFfmpeg outputs. Verified end-to-end on a real Snapdragon ARM64 Windows host: ripgrep uninstalled, stage re-run, [OK] ripgrep installed in 1.4s, ok:true. --- scripts/install.ps1 | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index af30d4c263..4c8305013a 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -913,22 +913,57 @@ function Install-SystemPackages { # Try winget first (most common on modern Windows) if ($hasWinget) { Write-Info "Installing $description via winget..." + # Per-package log paths -- key the lookup by package id so we can + # decide AFTER the post-install Get-Command check whether to keep + # the log (still missing -> keep as breadcrumb) or delete it (now + # present -> happy path, no clutter). + $pkgLogs = @{} foreach ($pkg in $wingetPkgs) { + $log = "$env:TEMP\hermes-winget-$($pkg -replace '[^A-Za-z0-9]','_')-$(Get-Random).log" + $pkgLogs[$pkg] = $log + # --source winget pins us to the github-backed source. Without this, + # a broken msstore source (cert validation failures like 0x8a15005e + # are common on Windows-on-ARM and some corporate networks) makes + # winget bail with "please specify --source" *before* attempting any + # install -- and it exits 0, so the surrounding try/catch never fires. + # We don't ship anything from msstore, so pinning is safe. try { - winget install $pkg --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null - } catch { } + $output = winget install --exact --id $pkg --source winget --silent ` + --accept-package-agreements --accept-source-agreements 2>&1 + $output | Out-File -FilePath $log -Encoding utf8 + "winget exit: $LASTEXITCODE" | Out-File -FilePath $log -Encoding utf8 -Append + } catch { + $_ | Out-File -FilePath $log -Encoding utf8 -Append + "winget exit: " | Out-File -FilePath $log -Encoding utf8 -Append + } } - # Refresh PATH and recheck - $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") + # Refresh PATH from both env-var hives AND winget's alias shim directory. + # winget exposes packages via "command line aliases" in %LOCALAPPDATA%\ + # Microsoft\WinGet\Links, which is added to PATH by the AppExecutionAlias + # machinery only in *newly-spawned* shells -- not the current process. + # Without this addition, Get-Command rg below would falsely return null + # immediately after a successful install. + $wingetLinks = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links" + $envPath = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") + if (Test-Path $wingetLinks) { + $envPath = "$envPath;$wingetLinks" + } + $env:Path = $envPath if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { Write-Success "ripgrep installed" $script:HasRipgrep = $true $needRipgrep = $false + Remove-Item -Path $pkgLogs["BurntSushi.ripgrep.MSVC"] -ErrorAction SilentlyContinue + } elseif ($pkgLogs.ContainsKey("BurntSushi.ripgrep.MSVC")) { + Write-Warn "winget could not install ripgrep; details: $($pkgLogs['BurntSushi.ripgrep.MSVC'])" } if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { Write-Success "ffmpeg installed" $script:HasFfmpeg = $true $needFfmpeg = $false + Remove-Item -Path $pkgLogs["Gyan.FFmpeg"] -ErrorAction SilentlyContinue + } elseif ($pkgLogs.ContainsKey("Gyan.FFmpeg")) { + Write-Warn "winget could not install ffmpeg; details: $($pkgLogs['Gyan.FFmpeg'])" } if (-not $needRipgrep -and -not $needFfmpeg) { return } }