From 14fedcee4c3e72bcb55b4feb7e0f529bbbd3a60e Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 4 Aug 2026 13:04:05 -0400 Subject: [PATCH] fix(installer): a clean payload crashed stage 2 Reported from Server 2019: "The property 'Count' cannot be found on this object" immediately into stage 2. Test-BundleLock returns an array of problems, and an EMPTY array means the payload is exactly right. PowerShell unrolls a zero-element return into $null, and under Set-StrictMode 2.0 $null.Count throws - so the branch that runs when everything is correct was the one that could not run. Every failing bundle got past it fine, which is why nothing caught it until the 8.3 path fix made verification succeed for the first time on a real server. Fixed at both ends: the call site wraps in @(), and Test-BundleLock returns ,$problems so no caller can be handed $null or a bare string depending on how many problems there happen to be. The other .Count uses in this file were already @()-wrapped and are unaffected. --- deploy/windows/installer/build-installer.ps1 | 2 +- deploy/windows/installer/bundle-lock.ps1 | 6 +++++- deploy/windows/installer/shopdb-install.ps1 | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/deploy/windows/installer/build-installer.ps1 b/deploy/windows/installer/build-installer.ps1 index bba59a2..3930c30 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 6fc2843..1acfde8 100644 --- a/deploy/windows/installer/bundle-lock.ps1 +++ b/deploy/windows/installer/bundle-lock.ps1 @@ -206,7 +206,11 @@ function Test-BundleLock { } } $problems += Test-WheelhouseCoversRequirements -BundleRoot $BundleRoot - return $problems + # The comma keeps this an ARRAY through the return. Without it PowerShell + # unrolls an empty result to $null and a single result to a bare string, and + # every caller that asks for .Count then behaves differently depending on how + # many problems there happen to be. + return ,$problems } function Test-WheelhouseCoversRequirements { diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index 678fecf..2347d4d 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -309,7 +309,12 @@ function Assert-BundleIntegrity { } . $checker $lock = Read-BundleLock $lockFile - $problems = Test-BundleLock -BundleRoot $BundleRoot -Lock $lock + # @() 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) 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) @'