fix(installer): undo a fix applied twice, and load the checker at script scope

A review of the installer for Windows-only defect classes found seven live
issues. These two would have stopped the next attempt on any server.

DOUBLE-APPLIED GUARD. Yesterday's $null.Count fix was applied at BOTH ends:
Test-BundleLock returns ,$problems, and the call site also wrapped it in @().
The comma already hands the array back intact, so the extra @() nests it and
.Count becomes 1 regardless of how many problems there are. Every install would
have failed with "the bundle does not match bundle-lock.json (1 problem(s))" on
a byte-perfect payload. Applying the same guard at both ends was worse than
applying it at neither. Verified in a Windows VM against a real bundle: clean 0,
tampered 1, restored 0.

DOT-SOURCE SCOPE. bundle-lock.ps1 was dot-sourced INSIDE
Assert-BundleIntegrity, which loads it into that function's scope - every helper
it defines disappears when the function returns. Assert-BundleIntegrity itself
worked; the next caller, Get-WheelhousePythonTag, died with "The term
'Get-JsonProperty' is not recognized". It only fires where a venv already
exists, so greenfield was fine and every retry after a part-completed install
was not. Now loaded once at script scope, guarded so the stages that run without
a bundle still work.

Both were confirmed by running them rather than by reading: the nesting with a
three-case pwsh test, the scoping with a minimal repro.
This commit is contained in:
cproudlock
2026-08-04 13:35:30 -04:00
parent 14fedcee4c
commit 5f350179b1
3 changed files with 23 additions and 9 deletions

View File

@@ -272,6 +272,19 @@ $EnvFile = Join-Path $AppRoot '.env'
$WheelDir = Join-Path $BundleRoot 'wheels'
$AppSource = Join-Path $BundleRoot 'app'
# Dot-sourced HERE, at script scope, not inside Assert-BundleIntegrity. Dot-
# sourcing inside a function loads into that FUNCTION's scope, so every helper
# it defines - Get-JsonProperty, Read-BundleLock, Test-BundleLock - disappears
# the moment the function returns. Assert-BundleIntegrity itself worked; the
# next caller, Get-WheelhousePythonTag, died with "The term 'Get-JsonProperty'
# is not recognized" on any run where a venv already existed, which is every
# retry after a part-completed install.
#
# Absent on a stage that runs without the bundle (uninstall), so this is a
# guarded load and Assert-BundleIntegrity still fails loudly if it is missing.
$BundleLockLib = Join-Path $BundleRoot 'bundle-lock.ps1'
if (Test-Path $BundleLockLib) { . $BundleLockLib }
# =============================================================================
# Version awareness and database safety (upgrades)
@@ -307,14 +320,13 @@ function Assert-BundleIntegrity {
Fail 'bundle-lock.json is missing from the bundle' `
'This bundle was not assembled by build-installer.ps1/.sh. Rebuild it.'
}
. $checker
$lock = Read-BundleLock $lockFile
# @() around the call, NOT just the assignment. PowerShell unrolls a
# zero-element return into $null, and under Set-StrictMode 2.0 $null.Count
# THROWS - so a bundle that verified perfectly cleanly crashed stage 2 with
# "The property 'Count' cannot be found on this object". The success path was
# the broken one, which is why it survived every failing test.
$problems = @(Test-BundleLock -BundleRoot $BundleRoot -Lock $lock)
# NO @() here. Test-BundleLock returns ,$problems, which already hands the
# array back intact - wrapping it again nests it, and .Count becomes 1 no
# matter how many problems there are. That turns a byte-perfect bundle into
# "1 problem(s)" on every install. Applying the same guard at both ends was
# worse than applying it at neither.
$problems = Test-BundleLock -BundleRoot $BundleRoot -Lock $lock
if ($problems.Count -gt 0) {
foreach ($p in $problems) { Write-Log " $p" 'FAIL' }
Fail ("the bundle does not match bundle-lock.json ({0} problem(s))" -f $problems.Count) @'