From 5f350179b12c5dde0d328daa490dc427eb896409 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 4 Aug 2026 13:35:30 -0400 Subject: [PATCH] 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. --- deploy/windows/installer/build-installer.ps1 | 2 +- deploy/windows/installer/bundle-lock.ps1 | 4 ++- deploy/windows/installer/shopdb-install.ps1 | 26 ++++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/deploy/windows/installer/build-installer.ps1 b/deploy/windows/installer/build-installer.ps1 index 3930c30..bba59a2 100644 --- a/deploy/windows/installer/build-installer.ps1 +++ b/deploy/windows/installer/build-installer.ps1 @@ -260,7 +260,7 @@ it must not be shipped. '@ } } else { - $problems = @(Test-BundleLock -BundleRoot $BundleRoot -Lock $lock) + $problems = Test-BundleLock -BundleRoot $BundleRoot -Lock $lock if ($problems.Count -eq 0) { Say (" payload matches the lock ({0}, {1})" -f ` (Get-JsonProperty $lock 'pythontag' 'unknown'), (Get-JsonProperty $lock 'platform' 'unknown')) 'Green' diff --git a/deploy/windows/installer/bundle-lock.ps1 b/deploy/windows/installer/bundle-lock.ps1 index 1acfde8..9ba92b3 100644 --- a/deploy/windows/installer/bundle-lock.ps1 +++ b/deploy/windows/installer/bundle-lock.ps1 @@ -159,7 +159,9 @@ function Test-BundleLock { ) $problems = @() $payloads = Get-JsonProperty $Lock 'payloads' - if ($null -eq $payloads) { return @('bundle-lock.json has no "payloads" section') } + # ,@(...) for the same reason as the final return: a bare one-element array + # unrolls to a string, and the caller's .Count then measures the wrong thing. + if ($null -eq $payloads) { return ,@('bundle-lock.json has no "payloads" section') } foreach ($p in $script:BundlePayloads) { $name = $p.Name diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index 2347d4d..de3f038 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -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) @'