diff --git a/installer/build.ps1 b/installer/build.ps1 index c581bf0..5e7b325 100644 --- a/installer/build.ps1 +++ b/installer/build.ps1 @@ -38,31 +38,60 @@ function Need($exe, $hint) { } } +# Run a NATIVE command and stop if it fails. +# +# $ErrorActionPreference = "Stop" does not do this. It governs PowerShell +# errors; a .exe returning non-zero is not one, so the script sails past it. +# That is not theoretical here: `npm ci` failing on a fresh Windows box would +# have let the build continue, and `go build` would then have embedded the +# STALE frontend/dist that is committed to this repository - producing an +# installer that works, opens, and shows last month's UI, with nothing +# anywhere saying so. The silent-wrong outcome, from the most likely failure. +# +# Output is NOT swallowed. `| Out-Null` on a failing install is how +# run-local.sh once exited with no output at all, which took three runs to +# diagnose; the same mistake is not worth repeating in a script that will be +# run on a machine nobody is sitting at. +function Run($exe) { + $rest = $args + & $exe @rest + if ($LASTEXITCODE -ne 0) { + throw "$exe $($rest -join ' ') failed with exit code $LASTEXITCODE" + } +} + Need python "Install Python 3.11+ and tick 'Add to PATH'." Need go "Install Go 1.21+ from https://go.dev/dl/." Need npm "Install Node.js LTS from https://nodejs.org/." Step "Python environment" Push-Location $root -if (-not (Test-Path ".venv")) { python -m venv .venv } -& .\.venv\Scripts\python -m pip install --upgrade pip | Out-Null -& .\.venv\Scripts\python -m pip install -r requirements.txt pyinstaller | Out-Null +if (-not (Test-Path ".venv")) { Run python -m venv .venv } +Run .\.venv\Scripts\python -m pip install --upgrade pip +Run .\.venv\Scripts\python -m pip install -r requirements.txt pyinstaller Step "Engine tests" # The package is not worth building if the engine is broken, and finding that # out after the installer is signed is the expensive order to do it in. -& .\.venv\Scripts\python -m pytest tests -q -if ($LASTEXITCODE -ne 0) { throw "engine tests failed" } +Run .\.venv\Scripts\python -m pytest tests -q Step "Engine (PyInstaller, one-folder)" if (Test-Path (Join-Path $root "build")) { Remove-Item -Recurse -Force (Join-Path $root "build") } -& .\.venv\Scripts\pyinstaller behavision.spec --noconfirm --distpath (Join-Path $dist "engine-build") -if ($LASTEXITCODE -ne 0) { throw "pyinstaller failed" } +Run .\.venv\Scripts\pyinstaller behavision.spec --noconfirm --distpath (Join-Path $dist "engine-build") Step "Desktop app (Wails)" Push-Location (Join-Path $root "desktop\frontend") -npm ci -npm run build +# A built dist is COMMITTED to this repository so `go build` type-checks +# without npm (the //go:embed directive requires the directory to exist). That +# convenience is a trap at package time: a silently failed npm build leaves +# the old one in place and it embeds perfectly. So the marker is removed +# first, and its reappearance is what proves this build produced the UI being +# shipped rather than inheriting one. +$marker = Join-Path $root "desktop\frontend\dist\index.html" +if (Test-Path $marker) { Remove-Item -Force $marker } +Run npm ci +Run npm run build +if (-not (Test-Path $marker)) { throw "npm run build reported success and produced no dist\index.html" } Pop-Location Push-Location (Join-Path $root "desktop") # Wails v2 talks to WebView2 through pure-Go bindings, so no cgo and no @@ -71,14 +100,17 @@ Push-Location (Join-Path $root "desktop") # from brand/loyaly-icon-512.png), and `wails build` would add a second copy # of both and fail the link with duplicate resources. $env:CGO_ENABLED = "0" -go build -tags desktop,production -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") . -if ($LASTEXITCODE -ne 0) { throw "desktop build failed" } +Run go build -tags desktop,production -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") . Pop-Location Step "Headless agent" Push-Location (Join-Path $root "agent") $env:CGO_ENABLED = "0" # the cgo resolver forces external linking -go build -o (Join-Path $root "dist\behavision-agent.exe") . +# `go build -o` does not create the target directory, and on a fresh clone +# dist\ is gitignored and absent. It exists here only because PyInstaller ran +# first and made it - an ordering dependency nothing states, so state it. +New-Item -ItemType Directory -Force -Path $dist | Out-Null +Run go build -o (Join-Path $root "dist\behavision-agent.exe") . Pop-Location Step "WebView2 bootstrapper" @@ -104,8 +136,11 @@ Copy-Item (Join-Path $root "LICENSE") $stage -ErrorAction SilentlyContinue $engineExe = Join-Path $stage "engine\behavision.exe" if (-not (Test-Path $engineExe)) { throw "engine exe missing at $engineExe" } -& $engineExe paths -if ($LASTEXITCODE -ne 0) { throw "the frozen engine cannot start - `paths` failed" } +# The frozen engine has to START, not merely exist. A PyInstaller build that +# is missing a native DLL links fine and dies on first launch - the classic +# "works in the venv, dies in the bundle" - and finding that out on a shop +# counter is the expensive order to do it in. +Run $engineExe paths Pop-Location