ba2a619db5
The Windows CI/CD story is now one idempotent script that both local dev and
GitHub Actions run, so they cannot drift.
scripts/setup-build-env.ps1:
- idempotent: skips anything already present/valid; safe to re-run (verified
no-op on second run)
- Go toolchain: pinned 1.22.2, user-space zip extract, no admin; accepts >= 1.22
- Inno Setup: pinned 7.0.2 from jrsoftware GitHub release, per-user install
(/CURRENTUSER, no admin), sha256-verified installer
- vendor assets: fetches WireGuard MSI, VC++ redist, OpenCredential CP into
installer/windows/vendor/ and verifies each against the pinned manifest;
writes .sha256 sidecars
- -Build: builds agent/tray/helper for windows amd64+arm64, runs go test, and
compiles the installer; -CI: fail loudly for workflows
installer/windows/vendor-manifest.json: pinned urls + sha256 for all three
third-party assets and the toolchain; nothing large is committed to git
(installer/windows/vendor/ is gitignored).
installer/windows/installer.iss: bundle + silently install the real
OpenCredential installer (Inno-built -> /VERYSILENT) and read /SERVER_URL and
/JOIN_KEY via {param:...}; validated by compiling with Inno Setup 7.0.2.
.github/workflows/build-windows.yml now delegates the entire build to the script
(setup-go + setup-build-env.ps1 -SkipGo -Build -CI), then hashes, optionally
signs with Azure Trusted Signing, attaches to the release, and publishes to the
SSO resource tree.
Built locally: dist/theta-agent-2.1.0-windows-amd64-setup.exe (63MB fully
offline bundle) plus windows amd64/arm64 agent, tray, and helper, all verified.
292 lines
11 KiB
PowerShell
292 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'
|
|
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=$flags" -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=$flags" -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
|
|
}
|