build(win): idempotent setup script + verified vendor manifest; real installer build
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.
This commit is contained in:
@@ -4,6 +4,10 @@ name: build-windows
|
||||
# installer; signs them with Azure Trusted Signing; generates SHA256SUMS; and
|
||||
# publishes the artifacts to the SSO resource tree (DESIGN-WINDOWS.md §9).
|
||||
#
|
||||
# The build itself is driven by scripts/setup-build-env.ps1 (idempotent dev/build
|
||||
# environment setup: pinned Go version, per-user Inno Setup, checksum-verified
|
||||
# vendor assets). The workflow only adds signing and publishing on top.
|
||||
#
|
||||
# Signing requires GitHub OIDC federation to Azure. If the Azure secrets are
|
||||
# not configured the workflow still builds and attaches unsigned artifacts.
|
||||
|
||||
@@ -29,56 +33,20 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Run unit tests
|
||||
shell: bash
|
||||
run: go test ./...
|
||||
|
||||
- name: Build agent, tray, helper
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
LDFLAGS="-s -w"
|
||||
for arch in amd64 arm64; do
|
||||
GOOS=windows GOARCH=$arch go build -ldflags="$LDFLAGS" -o dist/theta-agent-windows-$arch.exe .
|
||||
GOOS=windows GOARCH=$arch go build -ldflags="$LDFLAGS" -o dist/theta-agent-tray-windows-$arch.exe ./cmd/theta-agent-tray/
|
||||
GOOS=windows GOARCH=$arch go build -ldflags="$LDFLAGS" -o dist/theta-agent-helper-windows-$arch.exe ./cmd/theta-agent-helper/
|
||||
done
|
||||
|
||||
- name: Fetch vendored, vendor-signed dependencies
|
||||
# One idempotent step sets up Inno Setup + fetches/verifies the vendor
|
||||
# assets and builds binaries + installer + runs the test suite.
|
||||
- name: Setup build environment and build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$vendor = "installer/windows/vendor"
|
||||
New-Item -ItemType Directory -Force -Path $vendor | Out-Null
|
||||
# Pinned by sha256; WireGuard MSI and VC++ redist are Microsoft/vendor signed.
|
||||
$assets = @(
|
||||
@{ url = "https://download.wireguard.com/windows-client/wireguard-amd64-0.5.3.msi"; out = "wireguard-amd64-0.5.3.msi"; sha = "PINNED_WIREGUARD_SHA256" },
|
||||
@{ url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; out = "vc_redist.x64.exe"; sha = "PINNED_VC_REDIST_SHA256" }
|
||||
)
|
||||
foreach ($a in $assets) {
|
||||
$dest = Join-Path $vendor $a.out
|
||||
Invoke-WebRequest -Uri $a.url -OutFile $dest
|
||||
$h = (Get-FileHash -Algorithm SHA256 $dest).Hash.ToLower()
|
||||
if ($h -ne $a.sha) { throw "sha256 mismatch for $($a.out): $h" }
|
||||
Set-Content -Path "$dest.sha256" -Value $h
|
||||
}
|
||||
# OpenCredential (pGina fork) release — swap PINNED below for a real release URL.
|
||||
$oc = "https://github.com/pedropablobm/OpenCredential/releases/latest/download/OpenCredential.zip"
|
||||
Invoke-WebRequest -Uri $oc -OutFile (Join-Path $vendor "opencredential.zip")
|
||||
Expand-Archive -Path (Join-Path $vendor "opencredential.zip") -DestinationPath (Join-Path $vendor "OpenCredential")
|
||||
|
||||
- name: Compile Inno installer
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install innosetup -y --no-progress
|
||||
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||
if (-not (Test-Path $iscc)) { $iscc = "C:\Program Files\Inno Setup 6\ISCC.exe" }
|
||||
& $iscc "/DMyAppVersion=${{ env.VERSION }}" installer/windows/installer.iss
|
||||
if ($LASTEXITCODE -ne 0) { throw "ISCC failed" }
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/setup-build-env.ps1 -SkipGo -Build -CI
|
||||
if ($LASTEXITCODE -ne 0) { throw "setup-build-env failed" }
|
||||
|
||||
- name: Generate SHA256SUMS
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-ChildItem dist -File | Where-Object { $_.Extension -in ".exe",".msi" -or $_.Name -like "*setup*" } | ForEach-Object {
|
||||
Get-ChildItem dist -File | Where-Object {
|
||||
$_.Name -match 'windows-.*\.exe$|setup\.exe$' -or $_.Name -eq 'SHA256SUMS'
|
||||
} | ForEach-Object {
|
||||
"{0} {1}" -f (Get-FileHash -Algorithm SHA256 $_.FullName).Hash.ToLower(), $_.Name
|
||||
} | Set-Content -Path dist/SHA256SUMS
|
||||
|
||||
|
||||
@@ -6,3 +6,5 @@ dist/
|
||||
agent.yml
|
||||
!cmd/theta-agent-helper/
|
||||
!cmd/theta-agent-helper/main.go
|
||||
# Fetched by scripts/setup-build-env.ps1, pinned in installer/windows/vendor-manifest.json
|
||||
installer/windows/vendor/
|
||||
|
||||
+7
-1
@@ -183,13 +183,19 @@ One `.exe`, built on a connected machine, runnable on an air-gapped one. Bundles
|
||||
|
||||
- `theta-agent-windows-amd64.exe` (+ arm64) and `theta-agent-tray-windows-amd64.exe`
|
||||
- `theta-agent-helper.exe` (desktop-control helper)
|
||||
- OpenCredential CP binaries + **VC++ v14 redistributable** (its native runtime; .NET
|
||||
- OpenCredential CP installer + **VC++ v14 redistributable** (its native runtime; .NET
|
||||
Framework 4.8 is built into Windows 10/11 so needs no bundle)
|
||||
- The official, vendor-signed WireGuard for Windows client (signed drivers install
|
||||
offline without signature phone-home)
|
||||
- OpenCredential's pre-seeded LDAP plugin config
|
||||
- Agent `agent.yml` template + self-signed CP cert installed into the machine trusted root
|
||||
|
||||
The build environment is bootstrapped idempotently by
|
||||
`scripts/setup-build-env.ps1` (pinned Go version, per-user Inno Setup install, and
|
||||
vendor assets fetched into `installer/windows/vendor/` — verified against the pinned
|
||||
sha256 in `installer/windows/vendor-manifest.json`). The CI workflow calls the same
|
||||
script, so a local build and CI/CD cannot drift.
|
||||
|
||||
Install-time behavior:
|
||||
|
||||
- `/SILENT` supported; `SERVER_URL=` and `JOIN_KEY=` as install parameters (the SSO's
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -58,15 +58,14 @@ Source: "{#AgentDir}\theta-agent-windows-amd64.exe"; DestDir: "{app}"; Flags: ig
|
||||
Source: "{#AgentDir}\theta-agent-tray-windows-amd64.exe"; DestDir: "{app}\tray"; Flags: ignoreversion
|
||||
Source: "{#AgentDir}\theta-agent-helper-windows-amd64.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
; WireGuard for Windows — official, vendor-signed MSI. Install happens offline
|
||||
; (the driver is signed; no signature phone-home).
|
||||
; WireGuard for Windows — official, vendor-signed MSI. Installs offline (the
|
||||
; driver is signed; no signature phone-home).
|
||||
Source: "{#VendorDir}\wireguard-amd64-0.5.3.msi"; DestDir: "{app}\vendor"; Flags: ignoreversion
|
||||
Source: "{#VendorDir}\wireguard-amd64-0.5.3.msi.sha256"; DestDir: "{app}\vendor"; Flags: ignoreversion
|
||||
|
||||
; OpenCredential credential provider (BSD-3 pGina fork) + VC++ runtime it needs.
|
||||
Source: "{#VendorDir}\OpenCredential\*"; DestDir: "{app}\OpenCredential"; Flags: ignoreversion recursesubdirs
|
||||
; OpenCredential credential provider installer (BSD-3 pGina fork) + the VC++
|
||||
; runtime it needs. Both install silently at [Run].
|
||||
Source: "{#VendorDir}\OpenCredentialInstaller-1.0.0.0.exe"; DestDir: "{app}\vendor"; Flags: ignoreversion
|
||||
Source: "{#VendorDir}\vc_redist.x64.exe"; DestDir: "{app}\vendor"; Flags: ignoreversion
|
||||
Source: "{#VendorDir}\vc_redist.x64.exe.sha256"; DestDir: "{app}\vendor"; Flags: ignoreversion
|
||||
|
||||
[Registry]
|
||||
; Start the tray for every interactive logon.
|
||||
@@ -75,23 +74,33 @@ Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType:
|
||||
[Run]
|
||||
; VC++ v14 runtime (OpenCredential native deps).
|
||||
Filename: "{app}\vendor\vc_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installing VC++ runtime..."; Flags: runhidden waituntilterminated
|
||||
; OpenCredential credential provider — must be registered before logon.
|
||||
Filename: "{app}\vendor\OpenCredentialInstaller-1.0.0.0.exe"; Parameters: "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; StatusMsg: "Installing OpenCredential credential provider..."; Flags: runhidden waituntilterminated
|
||||
; WireGuard for Windows client.
|
||||
Filename: "msiexec.exe"; Parameters: "/i ""{app}\vendor\wireguard-amd64-0.5.3.msi"" /qn /norestart"; StatusMsg: "Installing WireGuard client..."; Flags: runhidden waituntilterminated
|
||||
; Register the agent as a SYSTEM auto-start service.
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "install-service"; StatusMsg: "Registering theta-agent service..."; Flags: runhidden waituntilterminated
|
||||
; Credential provider registration + agent.yml are handled in [Code] so we can
|
||||
; feed SERVER_URL/JOIN_KEY in and sequence the CP install before logon.
|
||||
Filename: "{app}\OpenCredential\OpenCredentialInstaller.exe"; Parameters: "/S"; StatusMsg: "Installing OpenCredential credential provider..."; Flags: runhidden waituntilterminated skipifsilent
|
||||
|
||||
[Code]
|
||||
var
|
||||
ServerURL: String;
|
||||
JoinKey: String;
|
||||
|
||||
// Reads a custom setup command-line parameter (e.g. /SERVER_URL=https://...).
|
||||
// {param:...} raises when the parameter is absent, so the exception becomes "".
|
||||
function GetCmdParam(const Name: String): String;
|
||||
begin
|
||||
try
|
||||
Result := ExpandConstant('{param:' + Name + '}');
|
||||
except
|
||||
Result := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
ServerURL := GetCmdLineParam('/SERVER_URL', '');
|
||||
JoinKey := GetCmdLineParam('/JOIN_KEY', '');
|
||||
ServerURL := GetCmdParam('SERVER_URL');
|
||||
JoinKey := GetCmdParam('JOIN_KEY');
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"comment": "Pinned third-party assets bundled into the Windows installer (DESIGN-WINDOWS.md §8). The setup script (scripts/setup-build-env.ps1) fetches these into installer/windows/vendor/ and verifies sha256; nothing is committed to git.",
|
||||
"assets": [
|
||||
{
|
||||
"name": "wireguard-amd64-0.5.3.msi",
|
||||
"url": "https://download.wireguard.com/windows-client/wireguard-amd64-0.5.3.msi",
|
||||
"sha256": "76FCEC042C5989C5B816CD32EAED1E5B1C3B998A4B1C9ECA55F299E3314EF7E4",
|
||||
"purpose": "Official WireGuard for Windows client (vendor-signed drivers install offline)"
|
||||
},
|
||||
{
|
||||
"name": "vc_redist.x64.exe",
|
||||
"url": "https://aka.ms/vs/17/release/vc_redist.x64.exe",
|
||||
"sha256": "CC0FF0EB1DC3F5188AE6300FAEF32BF5BEEBA4BDD6E8E445A9184072096B713B",
|
||||
"purpose": "VC++ v14 runtime required by the OpenCredential credential provider"
|
||||
},
|
||||
{
|
||||
"name": "OpenCredentialInstaller-1.0.0.0.exe",
|
||||
"url": "https://github.com/pedropablobm/OpenCredential/releases/download/v1.0.0.0/OpenCredentialInstaller-1.0.0.0.exe",
|
||||
"sha256": "7687A99F0B3D6E910BBFB883C31231EB8C8F6E4439134CC3522A86CC63DA1A52",
|
||||
"purpose": "OpenCredential (BSD-3 pGina fork) credential provider installer — LDAP-backed Windows logon"
|
||||
}
|
||||
],
|
||||
"toolchain": {
|
||||
"go": {
|
||||
"version": "1.22.2",
|
||||
"url": "https://go.dev/dl/go1.22.2.windows-amd64.zip"
|
||||
},
|
||||
"inno": {
|
||||
"version": "7.0.2",
|
||||
"url": "https://github.com/jrsoftware/issrc/releases/download/is-7_0_2/innosetup-7.0.2-x64.exe",
|
||||
"sha256": "5AD54CA3DEF786F8F4212552E54CC6D8D61329E2D24A1CFEE0571D42C2684FF1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
Reference in New Issue
Block a user