From 237738d928fc3bdd1cf00d338d71f8fb3694cbb7 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2026 10:19:54 -0700 Subject: [PATCH] [eric] build: pin uv + npm ci the builders + add an opt-in squirrel target --- scripts/build-app-win.ps1 | 54 ++++++++++++++++++++++++++++++++------- scripts/build-app.sh | 31 ++++++++++++++++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/scripts/build-app-win.ps1 b/scripts/build-app-win.ps1 index ce27c4ea..1ac8c5cd 100644 --- a/scripts/build-app-win.ps1 +++ b/scripts/build-app-win.ps1 @@ -10,11 +10,19 @@ [CmdletBinding()] param( [switch]$Sign, - [switch]$Publish + [switch]$Publish, + # Phase 7 A/B: build a Squirrel.Windows installer instead of the default + # NSIS one, from the SAME staged tree / SAME commit. Opt-in only; NSIS stays + # the default and shipped target until Squirrel is proven faster AND its + # rollback works on real Win 10/11 machines. EXPERIMENTAL / unverified in CI. + [switch]$Squirrel ) $ErrorActionPreference = 'Stop' if ($Publish) { $Sign = $true } +# Override only the win target; everything else (signing hook, extraResources, +# publish config) merges from electron/package.json's build block unchanged. +$TargetOverride = if ($Squirrel) { @('--config.win.target=squirrel') } else { @() } $ScriptDir = Split-Path -Parent $PSCommandPath $ProjectRoot = Split-Path -Parent $ScriptDir @@ -71,8 +79,13 @@ New-Item -ItemType Directory -Force -Path $UvBinDir | Out-Null $NeedUv = -not (Test-Path (Join-Path $UvBinDir 'uv.exe')) -or ` -not (Test-Path (Join-Path $UvBinDir 'uvx.exe')) if ($NeedUv) { - Write-Host "[0] Downloading uv + uvx for Windows..." - $UvUrl = 'https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip' + # Pinned uv version. "latest" used to mean a fresh uv could appear in any + # build with zero warning, breaking reproducibility (pillar 3). Override + # with $env:UV_VERSION when deliberately bumping; keep mac (build-app.sh) + # in lockstep. 0.11.16 is what "latest" resolved to when this was pinned. + $UvVersion = if ($env:UV_VERSION) { $env:UV_VERSION } else { '0.11.16' } + Write-Host "[0] Downloading uv + uvx $UvVersion for Windows..." + $UvUrl = "https://github.com/astral-sh/uv/releases/download/$UvVersion/uv-x86_64-pc-windows-msvc.zip" $TmpZip = Join-Path $env:TEMP "uv-win-$([guid]::NewGuid()).zip" $TmpExtract = Join-Path $env:TEMP "uv-win-extract-$([guid]::NewGuid())" try { @@ -235,8 +248,11 @@ Write-Host "" Write-Host "[1/5] Building frontend..." Push-Location (Join-Path $ProjectRoot 'frontend') try { - & npm install - if ($LASTEXITCODE -ne 0) { throw "npm install (frontend) failed" } + # npm ci (not install): installs exactly what package-lock.json pins, never + # silently mutates the lock, fails loudly on drift. Reproducible builds + # (pillar 3) depend on the lock being boss. + & npm ci + if ($LASTEXITCODE -ne 0) { throw "npm ci (frontend) failed" } & npm run build if ($LASTEXITCODE -ne 0) { throw "frontend build failed" } } finally { Pop-Location } @@ -396,12 +412,32 @@ Write-Host " Safe to modify your codebase now. " -BackgroundColor Green -Fo Write-Host "========================================" -BackgroundColor Green -ForegroundColor White Write-Host "" +# --- Provenance stamp --- +# Record the exact commit this artifact was built from. electron\build-info.json +# ships inside the asar; main.js reads it for the startup [provenance] log line +# and the About panel. Gitignored + regenerated each build. +$BuildSha = (git -C $ProjectRoot rev-parse HEAD 2>$null) +if (-not $BuildSha) { $BuildSha = 'unknown' } +$BuildVersion = (Get-Content -Raw (Join-Path $ProjectRoot 'electron\package.json') | ConvertFrom-Json).version +$BuildChannel = if ($BuildVersion -match '-') { 'experimental' } else { 'stable' } +$BuildShortSha = if ($BuildSha.Length -ge 12) { $BuildSha.Substring(0, 12) } else { $BuildSha } +$BuildInfo = [ordered]@{ + sha = $BuildSha + shortSha = $BuildShortSha + builtAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + channel = $BuildChannel + version = $BuildVersion +} +$BuildInfo | ConvertTo-Json -Compress | Set-Content -Path (Join-Path $ProjectRoot 'electron\build-info.json') -Encoding utf8 +Write-Host "Stamped build-info.json: sha=$BuildShortSha channel=$BuildChannel" + # --- Step 5: Package with electron-builder --- Write-Host "[5/5] Packaging with electron-builder..." Push-Location (Join-Path $ProjectRoot 'electron') try { - & npm install - if ($LASTEXITCODE -ne 0) { throw "npm install (electron) failed" } + # npm ci: lockfile-exact, no drift. See frontend note above. + & npm ci + if ($LASTEXITCODE -ne 0) { throw "npm ci (electron) failed" } if (-not $Sign) { $env:CSC_IDENTITY_AUTO_DISCOVERY = 'false' @@ -431,9 +467,9 @@ try { Write-Host " -> Continuing in 8s. Press Ctrl+C to abort." -ForegroundColor Yellow Start-Sleep -Seconds 8 } - & npx electron-builder --win --x64 --publish always + & npx electron-builder --win --x64 @TargetOverride --publish always } else { - & npx electron-builder --win --x64 --publish never + & npx electron-builder --win --x64 @TargetOverride --publish never } if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" } } finally { Pop-Location } diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 8322ae8f..2f33e15a 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -82,10 +82,16 @@ NEED_UV=false [[ ! -f "$UV_BIN_DIR/uv" ]] && NEED_UV=true [[ ! -f "$UV_BIN_DIR/uvx" ]] && NEED_UV=true if $NEED_UV; then - echo "[0] Downloading uv + uvx binaries (universal arm64+x64)..." + # Pinned uv version. "latest" used to mean a fresh uv could appear in any + # build with zero warning, breaking reproducibility (pillar 3). Override + # with UV_VERSION when deliberately bumping; keep Windows + # (build-app-win.ps1) in lockstep. 0.11.16 is what "latest" resolved to + # when this was pinned. + UV_VERSION="${UV_VERSION:-0.11.16}" + echo "[0] Downloading uv + uvx $UV_VERSION binaries (universal arm64+x64)..." TMPDIR_UV=$(mktemp -d) - curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar xz -C "$TMPDIR_UV" - curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-apple-darwin.tar.gz" | tar xz -C "$TMPDIR_UV" + curl -sL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-aarch64-apple-darwin.tar.gz" | tar xz -C "$TMPDIR_UV" + curl -sL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-apple-darwin.tar.gz" | tar xz -C "$TMPDIR_UV" lipo -create "$TMPDIR_UV/uv-aarch64-apple-darwin/uv" "$TMPDIR_UV/uv-x86_64-apple-darwin/uv" -output "$UV_BIN_DIR/uv" lipo -create "$TMPDIR_UV/uv-aarch64-apple-darwin/uvx" "$TMPDIR_UV/uv-x86_64-apple-darwin/uvx" -output "$UV_BIN_DIR/uvx" chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx" @@ -240,7 +246,10 @@ echo "" # Step 1: Build frontend echo "[1/4] Building frontend..." cd "$PROJECT_ROOT/frontend" -npm install +# npm ci (not install): installs exactly what package-lock.json pins, never +# silently mutates the lock, and fails loudly on any drift. Reproducible builds +# (pillar 3) depend on the lock being boss. +npm ci npm run build if [[ ! -f "$PROJECT_ROOT/frontend/dist/index.html" ]]; then @@ -446,10 +455,22 @@ printf '\033[1;42;97m%s\033[0m\n' " It is now safe to modify your codebase." printf '\033[1;42;97m%s\033[0m\n' "========================================" echo "" +# Provenance stamp: record the exact commit this artifact was built from. +# electron/build-info.json ships inside the asar; main.js reads it for the +# startup [provenance] log line and the About panel. Gitignored + regenerated. +BUILD_SHA=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null || echo unknown) +BUILD_VERSION=$(node -e "console.log(require('$PROJECT_ROOT/electron/package.json').version)") +BUILD_CHANNEL=stable; [[ "$BUILD_VERSION" == *-* ]] && BUILD_CHANNEL=experimental +cat > "$PROJECT_ROOT/electron/build-info.json" <