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-<pkg>-<n>.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.
This commit is contained in:
emozilla
2026-05-18 20:26:45 -07:00
parent da3bd34c08
commit 5dcfb0b82e
+39 -4
View File
@@ -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: <exception>" | 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 }
}