Audited before its first run, because deploy.sh taught us what a script nobody has executed contains. PowerShell's $ErrorActionPreference = "Stop" governs PowerShell errors. A native .exe returning non-zero is not one, so `npm ci` and `npm run build` were unchecked and the script sailed past them. That matters here more than anywhere else: a built frontend/dist is COMMITTED to this repository so `go build` type-checks without npm, which means a silently failed npm build leaves the old one in place and it embeds perfectly. The output is an installer that builds, installs, opens and shows a stale UI, with nothing anywhere saying so - the silent-wrong outcome, reached through the single most likely failure on a fresh Windows box. A Run() helper now throws on any non-zero native exit, across nine call sites: venv, both pip installs, pytest, pyinstaller, npm ci, npm build, both go builds, and the frozen engine's own smoke test. The pip installs were also piped to Out-Null, so a failure there produced no output AND no stop. run-local.sh has already been caught making exactly that mistake, where it "exited at step 5 with no output at all - the single hardest failure to diagnose, and it took three runs to find". Not worth repeating in a script that runs on a machine nobody is sitting at. Two smaller ones from the same read: - frontend\dist\index.html is deleted before npm runs, and its absence afterwards is an error. Checking the exit code is not enough when the artefact it was meant to produce is already sitting there from git. - `go build -o dist\...` does not create its target directory, and dist\ is gitignored. It exists on a fresh clone only because PyInstaller ran first and made it - an ordering dependency nothing stated. Stated now, and created explicitly. None of this has been run on Windows. It cannot be from here - PyInstaller freezes the interpreter and native wheels of the machine it runs on. What this buys is that the first Windows run fails for a real reason rather than for a bug in the script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
168 lines
7.1 KiB
PowerShell
168 lines
7.1 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Builds the Behavision Windows package: engine, app, agent, installer.
|
|
|
|
.DESCRIPTION
|
|
This script must run ON WINDOWS. Everything else in this repository
|
|
cross-compiles from a Mac - the Go binaries with GOOS=windows, the web bundle
|
|
with npm - but the ENGINE cannot. PyInstaller freezes the interpreter and the
|
|
native wheels (onnxruntime, OpenCV) of the machine it runs on; there is no
|
|
cross-target flag, and there never has been. So the engine .exe is built here
|
|
or it is not built at all.
|
|
|
|
Output: dist\Behavision-Setup-<version>.exe, plus dist\Behavision\ which is
|
|
the unpacked tree the installer copies (useful for testing without
|
|
installing).
|
|
|
|
.PARAMETER Version
|
|
Stamped into the installer and shown in Add/Remove Programs.
|
|
|
|
.PARAMETER SkipInstaller
|
|
Build the payload but not the setup .exe. Use when Inno Setup is absent.
|
|
#>
|
|
param(
|
|
[string]$Version = "0.1.0",
|
|
[switch]$SkipInstaller
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$dist = Join-Path $root "dist"
|
|
$stage = Join-Path $dist "Behavision"
|
|
|
|
function Step($msg) { Write-Host "`n=== $msg ===" -ForegroundColor Cyan }
|
|
function Need($exe, $hint) {
|
|
if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) {
|
|
throw "$exe not found on PATH. $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")) { 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.
|
|
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") }
|
|
Run .\.venv\Scripts\pyinstaller behavision.spec --noconfirm --distpath (Join-Path $dist "engine-build")
|
|
|
|
Step "Desktop app (Wails)"
|
|
Push-Location (Join-Path $root "desktop\frontend")
|
|
# 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
|
|
# toolchain beyond Go itself. A plain go build is used on purpose: the icon
|
|
# and the manifest are compiled in from rsrc_windows_amd64.syso (go-winres,
|
|
# 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"
|
|
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` 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"
|
|
# Bundled rather than downloaded at install time: a shop PC being set up often
|
|
# has no working internet yet, and the app is a blank white window without it.
|
|
$vendor = Join-Path $root "installer\vendor"
|
|
New-Item -ItemType Directory -Force -Path $vendor | Out-Null
|
|
$wv2 = Join-Path $vendor "MicrosoftEdgeWebview2Setup.exe"
|
|
if (-not (Test-Path $wv2)) {
|
|
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile $wv2
|
|
}
|
|
|
|
Step "Staging"
|
|
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
|
|
New-Item -ItemType Directory -Force -Path $stage | Out-Null
|
|
# The engine keeps its own folder: it is a one-folder PyInstaller build with
|
|
# ~150 native DLLs beside it, and `behavision.exe` would otherwise collide with
|
|
# `Behavision.exe` on a case-insensitive filesystem.
|
|
Copy-Item -Recurse (Join-Path $dist "engine-build\behavision") (Join-Path $stage "engine")
|
|
Copy-Item (Join-Path $root "desktop\build\bin\Behavision.exe") $stage
|
|
Copy-Item (Join-Path $dist "behavision-agent.exe") $stage
|
|
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" }
|
|
# 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
|
|
|
|
if ($SkipInstaller) {
|
|
Step "Done (payload only)"
|
|
Write-Host "Unpacked tree: $stage"
|
|
exit 0
|
|
}
|
|
|
|
Step "Installer (Inno Setup)"
|
|
$iscc = @(
|
|
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
|
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
|
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
if (-not $iscc) {
|
|
throw "Inno Setup 6 not found. Install it from https://jrsoftware.org/isdl.php, or re-run with -SkipInstaller."
|
|
}
|
|
& $iscc "/DMyAppVersion=$Version" (Join-Path $root "installer\behavision.iss")
|
|
if ($LASTEXITCODE -ne 0) { throw "ISCC failed" }
|
|
|
|
Step "Done"
|
|
Get-ChildItem (Join-Path $dist "Behavision-Setup-*.exe") | ForEach-Object {
|
|
Write-Host ("{0} ({1:N1} MB)" -f $_.FullName, ($_.Length / 1MB))
|
|
}
|