Files
theta-agent/scripts/setup-build-env.ps1
T
wmantly 5013148ffe fix(installer): Theta Directory branding, visible URL/join-key fields, GUI tray, service autostart
User-reported install fixes:

- Branding: every user-facing 'SSO Manager' string now says 'Theta Directory'
  (agent logs, CLI usage, agent.yml.example, installer wizard).
- Wizard page: the URL/join-key text boxes were never shown. The layout used
  Surface.Width (0 at wizard init) instead of SurfaceWidth and combined
  WordWrap with AutoSize (mutually exclusive in VCL). Rewritten with the
  canonical Inno pattern (SurfaceWidth + ScaleY + explicit label height).
- No console window after install: the tray and helper now build as
  GUI-subsystem binaries (-H=windowsgui) in build_all.sh and
  scripts/setup-build-env.ps1. The agent stays a console app for foreground
  debugging (as a service it never shows a console).
- The daemon never came up after install: install-service now starts the
  service immediately, so the tray IPC socket exists right away and the tray
  connects instead of logging 'actively refused' until a reboot.

Verified: go build/vet/test green; tray+helper PE subsystem = GUI (2), agent =
console (3); installer compiles; tray runs silently.
2026-08-09 20:33:06 -07:00

295 lines
11 KiB
PowerShell

<#
.SYNOPSIS
Idempotent setup of the Theta Agent Windows dev/build environment.
.DESCRIPTION
Ensures everything needed to build and package the Windows agent, tray,
session helper, and the fully-offline Inno installer is present:
* Go toolchain (pinned version, user-space, no admin required)
* Inno Setup compiler (pinned version, per-user install, no admin required)
* vendor assets (WireGuard MSI, VC++ redist, OpenCredential CP),
fetched into installer/windows/vendor/ and verified against the pinned
sha256 in installer/windows/vendor-manifest.json
Safe to run repeatedly: each component is skipped when already installed
and valid, so `powershell -File scripts/setup-build-env.ps1` is a no-op on a
ready machine. Pass -Build to also compile the binaries and the installer.
.PARAMETER ToolDir
Where to install toolchains. Default: %LOCALAPPDATA%\Theta42\buildtools
.PARAMETER RepoRoot
Repository root. Default: the parent of this script's directory.
.PARAMETER SkipGo
Do not install/verify the Go toolchain.
.PARAMETER SkipInno
Do not install/verify Inno Setup.
.PARAMETER SkipVendor
Do not fetch/verify vendor assets.
.PARAMETER Build
After setup, build the agent/tray/helper for windows (amd64+arm64), run
`go test ./...`, and compile the installer with ISCC.
.PARAMETER CI
Non-interactive/CI mode: exit non-zero on any failure. (Used by the
GitHub Actions workflow so the runner fails loudly on a broken env.)
.EXAMPLE
powershell -ExecutionPolicy Bypass -File scripts\setup-build-env.ps1 -Build
#>
[CmdletBinding()]
param(
[string]$ToolDir = (Join-Path $env:LOCALAPPDATA 'Theta42\buildtools'),
[string]$RepoRoot = '',
[switch]$SkipGo,
[switch]$SkipInno,
[switch]$SkipVendor,
[switch]$Build,
[switch]$CI
)
$ErrorActionPreference = 'Stop'
$script:anyFailed = $false
# $PSScriptRoot is not populated inside the param() defaults on PowerShell 5.1.
if (-not $RepoRoot) {
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
}
function Write-Step($msg) { Write-Host "== $msg" -ForegroundColor Cyan }
function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Skip($msg) { Write-Host " [skip] $msg" -ForegroundColor DarkGray }
function Write-Fail($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; $script:anyFailed = $true }
function Write-Info($msg) { Write-Host " [info] $msg" -ForegroundColor Gray }
# ---------------------------------------------------------------- manifest ----
$manifestPath = Join-Path $RepoRoot 'installer\windows\vendor-manifest.json'
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
$vendorDir = Join-Path $RepoRoot 'installer\windows\vendor'
# -------------------------------------------------------------- Go toolchain --
function Get-GoVersion {
$v = (go version 2>$null)
if ($v -match 'go([0-9]+\.[0-9]+)') { return $matches[1] }
return ''
}
function Install-Go {
$needGo = $manifest.toolchain.go.version
$minGo = ($needGo -split '\.')[0] + '.' + ($needGo -split '\.')[1] # e.g. 1.22
$have = Get-GoVersion
if ($have -ne '' -and ([version]$have -ge [version]$minGo)) {
Write-OK "Go $have already available ($minGo+ required)"
return
}
$url = $manifest.toolchain.go.url
$goRoot = Join-Path $ToolDir "go$needGo"
# The official zip contains a top-level go/ folder -> goRoot\go\bin\go.exe.
$goBin = Join-Path $goRoot 'go\bin'
if (Test-Path (Join-Path $goBin 'go.exe')) {
Write-OK "Go $needGo found at $goRoot"
} else {
Write-Step "Installing Go $needGo (no admin required, zip extract)"
$zip = Join-Path $env:TEMP "go$needGo.windows-amd64.zip"
if (-not (Test-Path $zip)) {
Write-Info "Downloading $url"
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
}
$staging = Join-Path $goRoot 'staging'
New-Item -ItemType Directory -Force -Path $staging | Out-Null
Expand-Archive -Path $zip -DestinationPath $staging -Force
# staging\go -> goRoot\go
Move-Item -Force (Join-Path $staging 'go') $goRoot
Remove-Item -Recurse -Force $staging -ErrorAction SilentlyContinue
if (-not (Test-Path (Join-Path $goBin 'go.exe'))) {
Write-Fail "Go extract produced no bin\go.exe at $goBin"
return
}
Write-OK "Go $needGo installed at $goRoot"
}
Add-ToUserPath $goBin
$env:Path = "$goBin;$env:Path"
}
# --------------------------------------------------------------- Inno Setup ---
function Find-Iscc {
$candidates = @(
(Join-Path $ToolDir 'InnoSetup7\ISCC.exe'),
"$env:ProgramFiles\Inno Setup 7\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 7\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
)
foreach ($c in $candidates) {
if ($c -and (Test-Path $c)) { return $c }
}
return ''
}
function Install-Inno {
$iscc = Find-Iscc
if ($iscc) {
Write-OK "Inno Setup ISCC found at $iscc"
return $iscc
}
$inno = $manifest.toolchain.inno
$exe = Join-Path $env:TEMP "innosetup-$($inno.version)-x64.exe"
if (-not (Test-Path $exe)) {
Write-Step "Downloading Inno Setup $($inno.version)"
Invoke-WebRequest -Uri $inno.url -OutFile $exe -UseBasicParsing
$h = (Get-FileHash $exe -Algorithm SHA256).Hash.ToUpper()
if ($h -ne $inno.sha256) {
Remove-Item $exe -Force
Write-Fail "Inno Setup installer sha256 mismatch: $h"
return ''
}
Write-OK "Downloaded and verified Inno Setup installer"
}
$dest = Join-Path $ToolDir 'InnoSetup7'
Write-Step "Installing Inno Setup per-user to $dest"
# /CURRENTUSER installs without admin; /DIR is honored in that mode.
$p = Start-Process -FilePath $exe -ArgumentList @(
'/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/CURRENTUSER',"/DIR=$dest"
) -Wait -PassThru
if ($p.ExitCode -ne 0 -or -not (Test-Path (Join-Path $dest 'ISCC.exe'))) {
Write-Fail "Inno Setup install failed (exit $($p.ExitCode)); ISCC not found at $dest"
return ''
}
Write-OK "Inno Setup installed; ISCC at $(Join-Path $dest 'ISCC.exe')"
Add-ToUserPath $dest
return (Join-Path $dest 'ISCC.exe')
}
# ------------------------------------------------- PATH (idempotent) ----------
function Add-ToUserPath($dir) {
if (-not $dir -or -not (Test-Path $dir)) { return }
$userPath = [Environment]::GetEnvironmentVariable('Path','User')
if ($userPath -and ($userPath -split ';' -contains $dir)) {
Write-Skip "$dir already on user PATH"
return
}
$newPath = if ($userPath) { "$userPath;$dir" } else { $dir }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
Write-OK "Added $dir to user PATH"
}
# ------------------------------------------------ Vendor assets (idempotent) --
function Fetch-Asset($asset) {
$dest = Join-Path $vendorDir $asset.name
$ok = $false
if (Test-Path $dest) {
$h = (Get-FileHash $dest -Algorithm SHA256).Hash.ToUpper()
if ($h -eq $asset.sha256) {
$ok = $true
Write-Skip "$($asset.name) present and checksum verified"
} else {
Write-Info "$($asset.name): stale or corrupt (got $h), re-downloading"
}
}
if (-not $ok) {
Write-Info "Downloading $($asset.name)"
Invoke-WebRequest -Uri $asset.url -OutFile $dest -UseBasicParsing
$h = (Get-FileHash $dest -Algorithm SHA256).Hash.ToUpper()
if ($h -ne $asset.sha256) {
Remove-Item $dest -Force
Write-Fail "$($asset.name): sha256 mismatch ($h) - expected $($asset.sha256)"
return
}
Write-OK "$($asset.name) downloaded and checksum verified"
}
Set-Content -Path "$dest.sha256" -Value $asset.sha256 -NoNewline
}
function Ensure-Vendor {
Write-Step "Vendor assets (pinned in vendor-manifest.json)"
New-Item -ItemType Directory -Force -Path $vendorDir | Out-Null
foreach ($a in $manifest.assets) {
Fetch-Asset $a
}
}
# ----------------------------------------------------------------- Verify -----
function Assert-BuildReady {
Write-Step "Verification"
$go = Get-GoVersion
if ($go) { Write-OK "go $go" } elseif (-not $SkipGo) { Write-Fail 'go not found' }
$iscc = Find-Iscc
if ($iscc) { Write-OK "ISCC $iscc" } elseif (-not $SkipInno) { Write-Fail 'ISCC not found' }
foreach ($a in $manifest.assets) {
$dest = Join-Path $vendorDir $a.name
if (Test-Path $dest) {
$h = (Get-FileHash $dest -Algorithm SHA256).Hash.ToUpper()
if ($h -eq $a.sha256) { Write-OK "$($a.name) verified" }
else { Write-Fail "$($a.name) checksum mismatch" }
} elseif (-not $SkipVendor) {
Write-Fail "$($a.name) missing"
}
}
if ($script:anyFailed) {
if ($CI) { throw 'Build environment setup failed' }
exit 1
}
Write-Host "Build environment ready." -ForegroundColor Green
}
# ------------------------------------------------------------------- Build ----
function Invoke-Build {
Write-Step "Building agent, tray, helper (windows amd64+arm64)"
$dist = Join-Path $RepoRoot 'dist'
New-Item -ItemType Directory -Force -Path $dist | Out-Null
$flags = '-s -w'
# The tray and helper are GUI-subsystem binaries: no console window pops up
# when the installer starts the tray or the service spawns the helper.
$guiFlags = '-s -w -H=windowsgui'
foreach ($arch in @('amd64','arm64')) {
$env:GOOS='windows'; $env:GOARCH=$arch; $env:CGO_ENABLED='0'
go build "-ldflags=$flags" -o (Join-Path $dist "theta-agent-windows-$arch.exe") $RepoRoot
if ($LASTEXITCODE -ne 0) { Write-Fail "build agent windows/$arch failed"; return }
go build "-ldflags=$guiFlags" -o (Join-Path $dist "theta-agent-tray-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-tray')
if ($LASTEXITCODE -ne 0) { Write-Fail "build tray windows/$arch failed"; return }
go build "-ldflags=$guiFlags" -o (Join-Path $dist "theta-agent-helper-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-helper')
if ($LASTEXITCODE -ne 0) { Write-Fail "build helper windows/$arch failed"; return }
}
Remove-Item Env:GOOS,Env:GOARCH,Env:CGO_ENABLED -ErrorAction SilentlyContinue
Write-Step "Running go test ./..."
Push-Location $RepoRoot
go test ./...
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Fail 'go test failed'; return }
Pop-Location
$iscc = Find-Iscc
if (-not $iscc) { Write-Fail 'ISCC not found; cannot build installer'; return }
Write-Step "Compiling installer with ISCC"
& $iscc (Join-Path $RepoRoot 'installer\windows\installer.iss')
if ($LASTEXITCODE -ne 0) { Write-Fail 'ISCC compile failed' }
}
# ------------------------------------------------------------------- Main -----
New-Item -ItemType Directory -Force -Path $ToolDir | Out-Null
if (-not $SkipGo) { Install-Go }
if (-not $SkipInno) { $null = Install-Inno }
if (-not $SkipVendor) { Ensure-Vendor }
Assert-BuildReady
if ($Build) { Invoke-Build }
if ($script:anyFailed) {
if ($CI) { throw 'Setup failed' }
exit 1
}