#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-.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)) }