The bundle carries ~40 wheels, a Python installer and two MSIs. All of them run as SYSTEM on the target server, and nothing verified any of them. A missing wheelhouse printed MISSING and the script still exited 0, so an empty bundle compiled into a shippable installer and the failure surfaced on an air-gapped server with no way to fix it. bundle-lock.json now records that payload exactly - sha256 and byte size per file - and verification is set equality: a missing file, an unexpected extra file, or changed content all fail. Both builders check it and refuse to produce an unverified bundle; the lock ships inside the bundle and shopdb-install.ps1 re-checks it on the server before running any of it. This is deliberately a layer above requirements.txt hashes. pip lists every artifact of a pinned version (cffi 2.1.0 alone has 100 hashes), so it proves a wheel is genuine, not that it is the wheel this bundle was built and tested with; it ignores extra files in the wheelhouse; and it covers none of the executables. refresh-bundle-lock.ps1 regenerates the lock but refuses to overwrite one until the operator has seen the diff, because the commit is the review - it is the only place a change to what runs as SYSTEM becomes visible to a human. build-installer.ps1 is the whole build natively on Windows, so a work PC needs no Bash. It shares the plugin closure resolver with build-site.sh. Both builders now copy the installer scripts from the repository. They were copied from a downloads folder, so the logic that shipped was not the logic that was committed and the build worked on exactly one machine. Two verifiers exist because PowerShell is the only thing guaranteed present on the target server, while the Linux builder should not need pwsh. tests/test_bundle_lock.py runs both against the same fixtures and fails if they disagree.
111 lines
4.2 KiB
PowerShell
111 lines
4.2 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Regenerate bundle-lock.json from the staged bundle, after showing what would
|
|
change.
|
|
|
|
.DESCRIPTION
|
|
Run this on the machine that assembled the wheelhouse, once the bundle holds
|
|
the payload you intend to ship:
|
|
|
|
bundle\wheels\ the wheels, built with the matching Python
|
|
bundle\python\ the Python installer
|
|
bundle\httpplatformhandler\ the IIS module MSI
|
|
bundle\urlrewrite\ URL Rewrite MSI (optional)
|
|
bundle\mysql\ MySQL MSI (optional)
|
|
|
|
Then COMMIT the resulting bundle-lock.json. That commit is the review: it is
|
|
the only place a change to what runs as SYSTEM on a customer's server becomes
|
|
visible to a human. A lock regenerated and committed without reading the diff
|
|
provides nothing, so this refuses to overwrite an existing lock until you
|
|
have seen the change and passed -Yes.
|
|
|
|
.EXAMPLE
|
|
.\refresh-bundle-lock.ps1 # show the diff, write nothing
|
|
.\refresh-bundle-lock.ps1 -Yes # write it
|
|
|
|
.NOTES
|
|
Building the wheelhouse itself no longer requires Windows. From any machine:
|
|
pip download -r requirements.txt -d wheels --only-binary=:all: `
|
|
--platform win_amd64 --python-version 314 --implementation cp --abi cp314
|
|
Do it wherever you like; this script records what came out.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string] $BundleRoot = (Join-Path $PSScriptRoot 'bundle'),
|
|
[string] $LockPath = (Join-Path $PSScriptRoot 'bundle-lock.json'),
|
|
[string] $PythonTag = 'cp314',
|
|
[string] $Platform = 'win_amd64',
|
|
[switch] $Yes
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'bundle-lock.ps1')
|
|
|
|
function Say { param($m, $c = 'Gray') Write-Host $m -ForegroundColor $c }
|
|
|
|
if (-not (Test-Path $BundleRoot)) {
|
|
Say "bundle not found: $BundleRoot" 'Red'
|
|
Say 'Stage it first with build-installer.ps1 (or build-installer.sh), then add' 'Yellow'
|
|
Say 'the wheels and installers by hand.' 'Yellow'
|
|
exit 1
|
|
}
|
|
|
|
Say ''
|
|
Say " Hashing $BundleRoot" 'Cyan'
|
|
$new = New-BundleLock -BundleRoot $BundleRoot -PythonTag $PythonTag -Platform $Platform
|
|
|
|
foreach ($name in $new.payloads.Keys) {
|
|
Say (" {0,-22} {1,4} files" -f $name, $new.payloads[$name].files.Count)
|
|
}
|
|
|
|
$old = Read-BundleLock $LockPath
|
|
if (-not $old) {
|
|
Say ''
|
|
Say ' No existing lock - this will be the first one.' 'Yellow'
|
|
} else {
|
|
# Diff by file, per payload, so the operator sees exactly which artifacts
|
|
# changed rather than "the lock is different".
|
|
Say ''
|
|
Say ' Changes against the committed lock:' 'Cyan'
|
|
$changes = 0
|
|
foreach ($name in $new.payloads.Keys) {
|
|
$oldFiles = @{}
|
|
if ($old.payloads.PSObject.Properties.Name -contains $name) {
|
|
foreach ($p in $old.payloads.$name.files.PSObject.Properties) { $oldFiles[$p.Name] = $p.Value.sha256 }
|
|
}
|
|
$newFiles = $new.payloads[$name].files
|
|
foreach ($rel in ($newFiles.Keys | Sort-Object)) {
|
|
if (-not $oldFiles.ContainsKey($rel)) { Say " + $name/$rel" 'Green'; $changes++ }
|
|
elseif ($oldFiles[$rel] -ne $newFiles[$rel].sha256) { Say " ~ $name/$rel (content changed)" 'Yellow'; $changes++ }
|
|
}
|
|
foreach ($rel in ($oldFiles.Keys | Sort-Object)) {
|
|
if (-not $newFiles.ContainsKey($rel)) { Say " - $name/$rel" 'Red'; $changes++ }
|
|
}
|
|
}
|
|
foreach ($p in $old.payloads.PSObject.Properties.Name) {
|
|
if (-not $new.payloads.Contains($p)) { Say " - $p/ (whole payload gone)" 'Red'; $changes++ }
|
|
}
|
|
if ($changes -eq 0) {
|
|
Say ' none - the bundle already matches the lock' 'Green'
|
|
exit 0
|
|
}
|
|
Say ''
|
|
Say (" {0} change(s)." -f $changes) 'White'
|
|
}
|
|
|
|
if (-not $Yes) {
|
|
Say ''
|
|
Say ' Nothing written. Read the list above, then re-run with -Yes.' 'Yellow'
|
|
Say ' Commit the resulting bundle-lock.json - that commit IS the review.' 'Yellow'
|
|
exit 2
|
|
}
|
|
|
|
# ConvertTo-Json defaults to a depth of 2, which silently flattens the per-file
|
|
# entries into "System.Collections.Hashtable" strings and produces a lock that
|
|
# verifies against nothing.
|
|
$new | ConvertTo-Json -Depth 8 | Set-Content -Path $LockPath -Encoding UTF8
|
|
Say ''
|
|
Say " Written: $LockPath" 'Green'
|
|
Say ' Commit it.' 'Green'
|