| 1 |
<# |
| 2 |
.SYNOPSIS |
| 3 |
Build AudioFiles MSI natively on windows-x86 (per the native-builds-per-arch |
| 4 |
policy). Also produces a matching standalone .exe. |
| 5 |
|
| 6 |
.DESCRIPTION |
| 7 |
Runs on the windows-x86 build machine. Uses the WiX Toolset (candle.exe, |
| 8 |
light.exe) — install from https://wixtoolset.org/ or `winget install |
| 9 |
WiXToolset.WiXToolset`. |
| 10 |
|
| 11 |
Code signing is opt-in via environment variables. If unset, the script |
| 12 |
produces an unsigned MSI/EXE (current state — Azure certificate blocker |
| 13 |
documented in docs/deploy.md). |
| 14 |
|
| 15 |
.EXAMPLE |
| 16 |
# From repo root: |
| 17 |
PS> pwsh -File dist\build-msi-native.ps1 |
| 18 |
|
| 19 |
.ENVIRONMENT |
| 20 |
AF_SIGN_CERT Path to .pfx or thumbprint of an installed cert. |
| 21 |
When set, MSI and EXE are signed via signtool.exe. |
| 22 |
AF_SIGN_PASS Password for the .pfx (skip if cert is in store). |
| 23 |
AF_SIGN_TIMESTAMP_URL RFC 3161 timestamp URL. |
| 24 |
Default: http://timestamp.digicert.com |
| 25 |
#> |
| 26 |
|
| 27 |
$ErrorActionPreference = 'Stop' |
| 28 |
|
| 29 |
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path |
| 30 |
$ProjectDir = Resolve-Path (Join-Path $ScriptDir '..') |
| 31 |
$DistDir = $ScriptDir |
| 32 |
$Target = 'x86_64-pc-windows-msvc' |
| 33 |
|
| 34 |
# Read version from workspace Cargo.toml (workspace.package.version) or app crate. |
| 35 |
$AppCargo = Join-Path $ProjectDir 'crates\audiofiles-app\Cargo.toml' |
| 36 |
$VersionLine = Select-String -Path $AppCargo -Pattern '^version\s*=\s*"([^"]+)"' | Select-Object -First 1 |
| 37 |
if (-not $VersionLine) { throw "Could not parse version from $AppCargo" } |
| 38 |
$Version = $VersionLine.Matches[0].Groups[1].Value |
| 39 |
$WixVersion = "$Version.0" # WiX requires four-part version |
| 40 |
Write-Host "Building AudioFiles v$Version MSI ($Target)" -ForegroundColor Cyan |
| 41 |
|
| 42 |
# Prerequisites |
| 43 |
foreach ($tool in @('candle.exe', 'light.exe')) { |
| 44 |
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { |
| 45 |
throw "$tool not on PATH. Install WiX Toolset from https://wixtoolset.org/" |
| 46 |
} |
| 47 |
} |
| 48 |
$installedTargets = & rustup target list --installed |
| 49 |
if ($installedTargets -notcontains $Target) { |
| 50 |
throw "Rust target $Target not installed. Run: rustup target add $Target" |
| 51 |
} |
| 52 |
|
| 53 |
# Step 1: Native release build |
| 54 |
Write-Host "==> Building release binary..." -ForegroundColor Cyan |
| 55 |
Push-Location $ProjectDir |
| 56 |
try { |
| 57 |
& cargo build --release -p audiofiles-app --target $Target |
| 58 |
if ($LASTEXITCODE -ne 0) { throw "cargo build failed (exit $LASTEXITCODE)" } |
| 59 |
} finally { |
| 60 |
Pop-Location |
| 61 |
} |
| 62 |
|
| 63 |
$ExeSrc = Join-Path $ProjectDir "target\$Target\release\audiofiles-app.exe" |
| 64 |
if (-not (Test-Path $ExeSrc)) { throw "Build artifact missing: $ExeSrc" } |
| 65 |
|
| 66 |
# Step 2: Stage files |
| 67 |
Write-Host "==> Staging files..." -ForegroundColor Cyan |
| 68 |
$Staging = Join-Path $DistDir '.msi-staging' |
| 69 |
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging } |
| 70 |
New-Item -ItemType Directory -Path $Staging | Out-Null |
| 71 |
Copy-Item $ExeSrc (Join-Path $Staging 'AudioFiles.exe') |
| 72 |
Copy-Item (Join-Path $DistDir 'audiofiles.png') (Join-Path $Staging 'audiofiles.png') |
| 73 |
|
| 74 |
# Optional: sign the EXE before embedding it in the MSI so it's signed in-place. |
| 75 |
function Invoke-Signtool { |
| 76 |
param([string]$Path) |
| 77 |
if (-not $env:AF_SIGN_CERT) { return $false } |
| 78 |
$signtool = Get-Command 'signtool.exe' -ErrorAction SilentlyContinue |
| 79 |
if (-not $signtool) { throw "AF_SIGN_CERT set but signtool.exe not on PATH" } |
| 80 |
$timestampUrl = if ($env:AF_SIGN_TIMESTAMP_URL) { $env:AF_SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' } |
| 81 |
$args = @('sign', '/fd', 'SHA256', '/tr', $timestampUrl, '/td', 'SHA256') |
| 82 |
if ($env:AF_SIGN_CERT -match '\.pfx$') { |
| 83 |
$args += @('/f', $env:AF_SIGN_CERT) |
| 84 |
if ($env:AF_SIGN_PASS) { $args += @('/p', $env:AF_SIGN_PASS) } |
| 85 |
} else { |
| 86 |
# Treat as SHA1 thumbprint of a cert in the local store. |
| 87 |
$args += @('/sha1', $env:AF_SIGN_CERT) |
| 88 |
} |
| 89 |
$args += $Path |
| 90 |
Write-Host "==> Signing $Path" -ForegroundColor Cyan |
| 91 |
& $signtool.Source @args |
| 92 |
if ($LASTEXITCODE -ne 0) { throw "signtool failed on $Path (exit $LASTEXITCODE)" } |
| 93 |
return $true |
| 94 |
} |
| 95 |
|
| 96 |
$StagedExe = Join-Path $Staging 'AudioFiles.exe' |
| 97 |
$signed = Invoke-Signtool -Path $StagedExe |
| 98 |
if (-not $signed) { |
| 99 |
Write-Host "==> AF_SIGN_CERT unset — producing UNSIGNED build (see docs/deploy.md)" -ForegroundColor Yellow |
| 100 |
} |
| 101 |
|
| 102 |
# Step 3: Generate WiX source from template |
| 103 |
$WxsTemplate = Join-Path $DistDir 'audiofiles.wxs.in' |
| 104 |
$Wxs = Join-Path $Staging 'audiofiles.wxs' |
| 105 |
(Get-Content -Raw $WxsTemplate).Replace('@VERSION@', $WixVersion) | Set-Content -Path $Wxs -Encoding utf8 |
| 106 |
|
| 107 |
# Step 4: candle + light → MSI |
| 108 |
Write-Host "==> Building MSI..." -ForegroundColor Cyan |
| 109 |
$MsiName = "AudioFiles_${Version}_x86_64.msi" |
| 110 |
$MsiPath = Join-Path $DistDir $MsiName |
| 111 |
if (Test-Path $MsiPath) { Remove-Item -Force $MsiPath } |
| 112 |
|
| 113 |
$Wixobj = Join-Path $Staging 'audiofiles.wixobj' |
| 114 |
& candle.exe -nologo -out $Wixobj $Wxs |
| 115 |
if ($LASTEXITCODE -ne 0) { throw "candle.exe failed (exit $LASTEXITCODE)" } |
| 116 |
& light.exe -nologo -b $Staging -out $MsiPath $Wixobj |
| 117 |
if ($LASTEXITCODE -ne 0) { throw "light.exe failed (exit $LASTEXITCODE)" } |
| 118 |
|
| 119 |
# Step 5: Sign the MSI (if signing is configured) |
| 120 |
Invoke-Signtool -Path $MsiPath | Out-Null |
| 121 |
|
| 122 |
# Step 6: Cleanup + ship the standalone .exe alongside |
| 123 |
Remove-Item -Recurse -Force $Staging |
| 124 |
$ExeDest = Join-Path $DistDir "AudioFiles_${Version}_x86_64.exe" |
| 125 |
Copy-Item $ExeSrc $ExeDest |
| 126 |
# Re-sign the standalone exe (the one we signed earlier was the staged copy). |
| 127 |
Invoke-Signtool -Path $ExeDest | Out-Null |
| 128 |
|
| 129 |
Write-Host "" |
| 130 |
Write-Host "Done:" -ForegroundColor Green |
| 131 |
Write-Host " MSI: $MsiPath" |
| 132 |
Write-Host " EXE: $ExeDest" |
| 133 |
|