#requires -Version 5.1 <# .SYNOPSIS Build the Coworker Windows desktop app + NSIS (.exe) and MSI installers. .DESCRIPTION The Windows counterpart to build_dmg.sh: 1. PyInstaller-bundle the server into a standalone onedir folder (no venv at runtime). 2. Stage it at binaries\sidecar\ for Tauri's `resources` slot. 3. `tauri build --bundles nsis,msi` -> Coworker NSIS setup .exe + .msi (resources copied in). Prerequisites (see the toolchain notes in the PR/plan): - Rust (rustup) with the x86_64-pc-windows-msvc target + the MSVC C++ build tools (link.exe). - Node + npm (frontend build). - A Python venv at platform\.venv with this package installed editable, plus pyinstaller. `typer` is needed only at build time: PyInstaller walks the `mcp` package and `mcp.cli` calls sys.exit() at import if typer is absent, which aborts the freeze. py -m venv .venv ; .\.venv\Scripts\pip install -e ".[bedrock]" pyinstaller tzdata typer The result is UNSIGNED — first launch shows a SmartScreen warning ("More info" -> "Run anyway"). Authenticode signing is a later step. Experimental (use-at-your-own-risk) connectors are EXCLUDED from this build by default — the spec strips coworker.connectors.experimental. Self-builders can opt in with: $env:COWORKER_EXPERIMENTAL = "1"; .\build_windows.ps1 #> [CmdletBinding()] param( # Which installer bundles to produce. Both by default. [string]$Bundles = "nsis,msi" ) $ErrorActionPreference = "Stop" $Here = Split-Path -Parent $MyInvocation.MyCommand.Path $Platform = Split-Path -Parent $Here $Gui = Join-Path $Platform "surfaces\gui" $Venv = Join-Path $Platform ".venv" $PyInst = Join-Path $Venv "Scripts\pyinstaller.exe" function Require-Cmd($name) { if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { throw "Required tool '$name' not found on PATH. See the prerequisites in this script's header." } } Require-Cmd rustc Require-Cmd npm if (-not (Test-Path $PyInst)) { throw "PyInstaller not found at $PyInst. Create the venv and install deps (see header)." } # Host target triple, e.g. x86_64-pc-windows-msvc — Tauri's externalBin suffix. $Triple = (& rustc -vV | Select-String '^host:').ToString().Split()[-1] $Arch = $Triple.Split('-')[0] # A running openworker-server.exe (e.g. a prior sidecar/smoke test) locks the output exe and # makes PyInstaller's overwrite fail with Access-is-denied. Stop any before bundling. $running = Get-Process -Name "openworker-server" -ErrorAction SilentlyContinue if ($running) { Write-Host "==> stopping $($running.Count) running openworker-server process(es) holding the output exe" $running | Stop-Process -Force Start-Sleep -Seconds 1 } Write-Host "==> [1/3] PyInstaller: bundling openworker-server ($Triple)" -ForegroundColor Cyan & $PyInst --noconfirm --clean ` --distpath (Join-Path $Here "dist") --workpath (Join-Path $Here "build") ` (Join-Path $Here "openworker-server.spec") if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed (exit $LASTEXITCODE)" } Write-Host "==> [2/3] staging sidecar resources" -ForegroundColor Cyan # Onedir bundle (exe + _internal\) ships via Tauri `resources`, landing at \sidecar\ # next to the app exe — onefile's per-launch self-extraction cost seconds of boot splash. $BinDir = Join-Path $Gui "src-tauri\binaries" New-Item -ItemType Directory -Force -Path $BinDir | Out-Null $Src = Join-Path $Here "dist\openworker-server" $Dst = Join-Path $BinDir "sidecar" if (Test-Path $Dst) { Remove-Item -Recurse -Force $Dst } # Clear any stale onefile binary from pre-onedir builds. Remove-Item -Force (Join-Path $BinDir "openworker-server-$Triple.exe") -ErrorAction SilentlyContinue Copy-Item -Recurse -Force $Src $Dst Write-Host " -> $Dst" Write-Host "==> [3/3] tauri build (--bundles $Bundles)" -ForegroundColor Cyan # Auto-update artifacts (NSIS setup .exe + minisign .sig): produced only when the updater # signing key env is present (CI secret TAURI_SIGNING_PRIVATE_KEY). Keyless builds skip # the overlay so dev builds keep working; keyless RELEASES strand installs without # auto-update. $UpdaterArgs = @() if ($env:TAURI_SIGNING_PRIVATE_KEY) { # Pass the overlay as a FILE: inline JSON loses its quotes through the # PowerShell -> npm.cmd -> cmd hop ("key must be a string", v0.1.3 run). $Overlay = Join-Path ([IO.Path]::GetTempPath()) "ocw-updater-overlay.json" Set-Content -Path $Overlay -Value '{"bundle":{"createUpdaterArtifacts":true}}' -Encoding ascii $UpdaterArgs = @("--config", $Overlay) } else { Write-Host " WARNING: no updater signing key - building WITHOUT auto-update artifacts (not releasable)." -ForegroundColor Yellow } Push-Location $Gui try { & npm run tauri build -- --bundles $Bundles @UpdaterArgs if ($LASTEXITCODE -ne 0) { throw "tauri build failed (exit $LASTEXITCODE)" } } finally { Pop-Location } $BundleDir = Join-Path $Gui "src-tauri\target\release\bundle" Write-Host "" Write-Host "Done. Installers under: $BundleDir" -ForegroundColor Green Get-ChildItem -Path $BundleDir -Recurse -Include *.exe, *.msi -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.FullName)" }