Repository navigation
Expand file tree
/
Copy pathbuild.ps1
More file actions
487 lines (449 loc) · 21.2 KB
/
Copy pathbuild.ps1
File metadata and controls
487 lines (449 loc) · 21.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# One-click build: desktop app + appcontainer launcher -> portable zip -> installer exe
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Force UTF-8 so child-process output isn't garbled (e.g. 'ΓÇó' instead of '•').
$OutputEncoding = [System.Text.UTF8Encoding]::new()
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()
$env:PYTHONIOENCODING = 'utf-8'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
function Get-FileSetId {
param(
[Parameter(Mandatory = $true)][System.IO.FileInfo[]]$Files,
[Parameter(Mandatory = $true)][string]$BasePath
)
$entries = $Files | Sort-Object FullName | ForEach-Object {
$relativePath = $_.FullName.Substring($BasePath.Length).TrimStart('\').Replace('\', '/')
$fileHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$relativePath|$($_.Length)|$fileHash"
}
$bytes = [Text.Encoding]::UTF8.GetBytes(($entries -join "`n"))
$hasher = [Security.Cryptography.SHA256]::Create()
try {
$hash = $hasher.ComputeHash($bytes)
return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
} finally {
$hasher.Dispose()
}
}
. "$root\scripts\windows\node-runtime.ps1"
# Prefer a supported standard MSI install, then the legacy zip and PATH.
$nodeCandidates = @(
"$env:ProgramFiles\nodejs",
"$env:LOCALAPPDATA\Programs\nodejs",
"$env:USERPROFILE\.openclaw-node"
)
$nodeFound = $false
foreach ($candidate in $nodeCandidates) {
if (Test-Path "$candidate\node.exe") {
$version = & "$candidate\node.exe" --version
if ($LASTEXITCODE -ne 0 -or -not (Test-SupportedNodeVersion $version)) {
Write-Host " Skipping unsupported Node: $candidate ($version)" -ForegroundColor Yellow
continue
}
$env:PATH = "$candidate;$env:PATH"
Write-Host " Using Node: $candidate ($version)"
$nodeFound = $true
break
}
}
if (-not $nodeFound) {
# Fall back to whatever node.exe is already on PATH (e.g. nvm, chocolatey,
# winget shims, or a non-standard install). Many dev machines keep node
# outside the three "standard" locations above, and after an uninstall the
# MSI directory is gone — but a system-wide node may still be available.
$nodeCmd = Get-Command node.exe -ErrorAction SilentlyContinue
if ($nodeCmd) {
$nodeDir = Split-Path -Parent $nodeCmd.Source
$version = & $nodeCmd.Source --version
if ($LASTEXITCODE -eq 0 -and (Test-SupportedNodeVersion $version)) {
Write-Host " Using Node from PATH: $nodeDir ($version)"
$nodeFound = $true
}
}
}
if (-not $nodeFound) {
Write-Host " ERROR: supported node.exe not found in any of:" -ForegroundColor Red
foreach ($candidate in $nodeCandidates) { Write-Host " - $candidate" -ForegroundColor Red }
Write-Host " - PATH (Get-Command node.exe)" -ForegroundColor Red
Write-Host " OpenClaw 2026.9.3 requires Node.js >=24.16.0 <25 || >=26.1.0." -ForegroundColor Red
Write-Host " Install Node.js 26 (https://nodejs.org/) and re-run build.ps1." -ForegroundColor Red
exit 1
}
$windowsNodeSharedProject = "$root\third_party\openclaw-windows-node\source\src\OpenClaw.Shared\OpenClaw.Shared.csproj"
if (-not (Test-Path $windowsNodeSharedProject)) {
Write-Host " Initializing pinned OpenClaw Windows Node source..." -ForegroundColor Yellow
git -C $root submodule update --init --recursive -- third_party/openclaw-windows-node/source
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $windowsNodeSharedProject)) {
Write-Host " ERROR: unable to initialize the pinned OpenClaw Windows Node submodule" -ForegroundColor Red
exit 1
}
}
# Bootstrap desktop dependencies before building the Windows node, because the
# resource preparation step consumes the pinned @microsoft/mxc-sdk package.
Push-Location "$root\desktop"
try {
$needsNpmInstall = -not (Test-Path "$root\desktop\node_modules")
if (-not $needsNpmInstall) {
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
npm ls --depth=0 *> $null
$npmLsExit = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($npmLsExit -ne 0) {
$needsNpmInstall = $true
Write-Host " desktop dependencies are incomplete — running 'npm install'..." -ForegroundColor Yellow
}
} else {
Write-Host " desktop\node_modules not found — running 'npm install'..." -ForegroundColor Yellow
}
if ($needsNpmInstall) {
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
npm install 2>&1 | ForEach-Object { Write-Host " $_" }
$ErrorActionPreference = $prev
if ($LASTEXITCODE -ne 0) {
Write-Host " ERROR: npm install failed" -ForegroundColor Red
exit 1
}
}
} finally {
Pop-Location
}
# -- Step 1: Build native security helpers --
Write-Host "`n=== Step 1/7: Build security helpers ===" -ForegroundColor Cyan
$acProject = "$root\appcontainer"
if (-not (Test-Path "$acProject\AppContainerLauncher.csproj")) {
Write-Host " ERROR: appcontainer project not found at $acProject" -ForegroundColor Red
exit 1
}
# Pipe to ForEach-Object loses the native exit code in $LASTEXITCODE detection
# under StrictMode, so temporarily relax ErrorActionPreference (matches the
# npm/pyinstaller invocation style below) and then check $LASTEXITCODE.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
dotnet publish $acProject -c Release -o "$acProject\bin\Release\net9.0-windows\win-x64" 2>&1 |
ForEach-Object { Write-Host " $_" }
$publishExit = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($publishExit -ne 0) {
Write-Host " ERROR: dotnet publish failed with exit code $publishExit" -ForegroundColor Red
exit 1
}
$acExe = "$acProject\bin\Release\net9.0-windows\win-x64\AppContainerLauncher.exe"
if (-not (Test-Path $acExe)) {
Write-Host " ERROR: AppContainerLauncher.exe not found after build at $acExe" -ForegroundColor Red
exit 1
}
Write-Host " AppContainerLauncher.exe built" -ForegroundColor Green
Push-Location "$root\desktop"
try {
npm run prepare-windows-node-resources
if ($LASTEXITCODE -ne 0) {
Write-Host " ERROR: bundled Windows Node/MXC resource preparation failed" -ForegroundColor Red
exit 1
}
} finally {
Pop-Location
}
# Copy sandbox-preload.js and its modules alongside launcher (used by electron-builder extraResources)
$preloadSrc = "$acProject\sandbox-preload.js"
if (Test-Path $preloadSrc) {
$releaseDir = "$acProject\bin\Release\net9.0-windows\win-x64"
Copy-Item $preloadSrc "$releaseDir\sandbox-preload.js" -Force
foreach ($mod in @('sandbox-state.js','sandbox-permission.js','sandbox-fs-hooks.js','sandbox-cp-hooks.js','sandbox-sensitive.js','path-extraction.js')) {
$modSrc = "$acProject\$mod"
if (Test-Path $modSrc) { Copy-Item $modSrc "$releaseDir\$mod" -Force }
}
Write-Host " sandbox-preload.js + modules copied" -ForegroundColor Green
}
# -- Step 2: Clean dist/ to prevent stale TypeScript output --
Write-Host "`n=== Step 2/7: Clean stale build artifacts ===" -ForegroundColor Cyan
$distDir = "$root\desktop\dist"
if (Test-Path $distDir) {
Remove-Item "$distDir\*.js" -Force -ErrorAction SilentlyContinue
Remove-Item "$distDir\*.js.map" -Force -ErrorAction SilentlyContinue
Write-Host " Cleaned desktop\dist\"
}
# Ensure top-level dist/ exists so Compress-Archive can write into it later.
$outDist = "$root\dist"
if (-not (Test-Path $outDist)) {
New-Item -ItemType Directory -Path $outDist -Force | Out-Null
Write-Host " Created $outDist"
}
# -- Step 3: Build & pack desktop --
Write-Host "`n=== Step 3/7: Build & pack desktop ===" -ForegroundColor Cyan
Push-Location "$root\desktop"
try {
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
npm run pack:prepared 2>&1 | ForEach-Object { Write-Host " $_" }
$ErrorActionPreference = $prev
if ($LASTEXITCODE -ne 0) {
Write-Host " ERROR: desktop build failed" -ForegroundColor Red
exit 1
}
} finally {
Pop-Location
}
# -- Verify: ensure packed asar contains freshly compiled code --
$desktopAsar = "$root\desktop\release\win-unpacked\resources\app.asar"
if (Test-Path $desktopAsar) {
$asarAge = (Get-Item $desktopAsar).LastWriteTime
$srcAge = (Get-ChildItem "$root\desktop\src\*.ts" | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime
if ($asarAge -lt $srcAge) {
Write-Host " WARNING: app.asar is older than source - possible stale build!" -ForegroundColor Yellow
} else {
Write-Host " Verified: app.asar is newer than source files" -ForegroundColor Green
}
}
# Step 4: Create portable zip
Write-Host "`n=== Step 4/7: Create portable zip ===" -ForegroundColor Cyan
$zipPath = "$root\dist\microclaw-portable.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive -Path "$root\desktop\release\win-unpacked\*" -DestinationPath $zipPath
$zipSizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
Write-Host " -> $zipPath ${zipSizeMB} MB"
# Build identity consumed by repeat-install fast paths. It is generated before
# PyInstaller so the onedir bundle carries the identity of every managed input.
$manifestPath = "$root\dist\install-manifest.json"
$skillsFiles = @(Get-ChildItem "$root\skills" -File -Recurse)
$installerIdentityFiles = @(
Get-Item "$root\deploy.py"
Get-Item "$root\MicroClawDeployer.spec"
Get-Item "$root\requirements.txt"
Get-ChildItem "$root\deployer" -File -Recurse |
Where-Object { $_.FullName -notmatch '\\(__pycache__|logs)\\' }
Get-ChildItem "$root\scripts" -File -Recurse
)
$versionSource = Get-Content "$root\deployer\openclaw_version.py" -Raw
if ($versionSource -notmatch 'OPENCLAW_TARGET_VERSION\s*=\s*"([^"]+)"') {
Write-Host " ERROR: could not resolve OPENCLAW_TARGET_VERSION" -ForegroundColor Red
exit 1
}
$installManifest = [ordered]@{
schema = 1
desktopArchiveSha256 = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
installerBundleId = Get-FileSetId -Files $installerIdentityFiles -BasePath $root
managedSkillsId = Get-FileSetId -Files $skillsFiles -BasePath "$root\skills"
openClawVersion = $Matches[1]
appContainerSchema = 1
}
$installManifest | ConvertTo-Json | Set-Content $manifestPath -Encoding utf8
Write-Host " Install manifest: $manifestPath" -ForegroundColor DarkGray
# Step 5: Build installer (onedir mode to avoid WDAC blocking DLLs from temp)
Write-Host "`n=== Step 5/7: Build installer ===" -ForegroundColor Cyan
Push-Location $root
$installerBuilt = $false
# --- Ensure Python dependencies are installed (like npm install for Node) ---
$uvCmd = Get-Command uv -ErrorAction SilentlyContinue
$hasUvProject = $false
if ($uvCmd -and (Test-Path "$root\pyproject.toml")) {
$hasUvProject = [bool](Select-String -Path "$root\pyproject.toml" -Pattern '^\s*\[project\]\s*$')
}
if ($hasUvProject) {
# uv manages .venv automatically for installable projects.
if (-not (Test-Path "$root\.venv\Scripts\pyinstaller.exe")) {
Write-Host " Python deps not found — running 'uv sync'..." -ForegroundColor Yellow
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& $uvCmd.Source sync 2>&1 | ForEach-Object { Write-Host " $_" }
$uvExitCode = $LASTEXITCODE
$ErrorActionPreference = $previousPreference
if ($uvExitCode -ne 0) {
Write-Host " WARNING: uv sync failed" -ForegroundColor Yellow
}
}
} elseif ($uvCmd) {
# This repository declares Python dependencies in requirements.txt.
if (-not (Test-Path "$root\.venv\Scripts\pyinstaller.exe")) {
$venvPython = "$root\.venv\Scripts\python.exe"
$venvReady = Test-Path $venvPython
if (-not $venvReady) {
Write-Host " Python environment not found — running 'uv venv --python 3.12'..." -ForegroundColor Yellow
& $uvCmd.Source venv --python 3.12 "$root\.venv"
$venvReady = $LASTEXITCODE -eq 0 -and (Test-Path $venvPython)
}
if ($venvReady) {
Write-Host " Python deps not found — running 'uv pip install -r requirements.txt'..." -ForegroundColor Yellow
& $uvCmd.Source pip install --python $venvPython -r "$root\requirements.txt"
}
if (-not $venvReady -or $LASTEXITCODE -ne 0 -or
-not (Test-Path "$root\.venv\Scripts\pyinstaller.exe")) {
Write-Host " WARNING: uv dependency installation failed" -ForegroundColor Yellow
}
}
} else {
# This repository keeps runtime/build dependencies in requirements.txt;
# pyproject.toml only configures Ruff and is not an installable uv project.
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
python -c "import PyInstaller" *> $null
$pythonHasPyInstaller = $LASTEXITCODE -eq 0
$ErrorActionPreference = $previousPreference
if (-not (Test-Path "$root\.venv\Scripts\pyinstaller.exe") -and
-not (Get-Command pyinstaller -ErrorAction SilentlyContinue) -and
-not $pythonHasPyInstaller) {
Write-Host " Python deps not found — running 'pip install -r requirements.txt'..." -ForegroundColor Yellow
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
python -m pip install -r "$root\requirements.txt" 2>&1 | ForEach-Object { Write-Host " $_" }
$pipExitCode = $LASTEXITCODE
$ErrorActionPreference = $previousPreference
if ($pipExitCode -ne 0) {
Write-Host " WARNING: pip install failed" -ForegroundColor Yellow
}
}
}
# --- Run PyInstaller ---
# Strategy 1: `uv run` — uses project .venv with all deps
if (-not $installerBuilt -and $hasUvProject) {
Write-Host " Trying: uv run pyinstaller" -ForegroundColor DarkGray
& $uvCmd.Source run pyinstaller MicroClawDeployer.spec --noconfirm
if ($LASTEXITCODE -eq 0) { $installerBuilt = $true }
}
# Strategy 2: project-local .venv
if (-not $installerBuilt -and (Test-Path "$root\.venv\Scripts\pyinstaller.exe")) {
Write-Host " Trying: .venv\Scripts\pyinstaller.exe" -ForegroundColor DarkGray
& "$root\.venv\Scripts\pyinstaller.exe" MicroClawDeployer.spec --noconfirm
if ($LASTEXITCODE -eq 0) { $installerBuilt = $true }
}
# Strategy 3: pyinstaller on PATH
if (-not $installerBuilt) {
$pyinstaller = Get-Command pyinstaller -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Source
if ($pyinstaller) {
Write-Host " Trying: $pyinstaller" -ForegroundColor DarkGray
& $pyinstaller MicroClawDeployer.spec --noconfirm
if ($LASTEXITCODE -eq 0) { $installerBuilt = $true }
}
}
# Strategy 4: python -m PyInstaller
if (-not $installerBuilt) {
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
$pythonOk = $false
if ($pythonCmd) {
$ver = & python --version 2>&1
if ($LASTEXITCODE -eq 0 -and $ver -match '^Python ') { $pythonOk = $true }
}
if ($pythonOk) {
Write-Host " Trying: python -m PyInstaller" -ForegroundColor DarkGray
python -m PyInstaller MicroClawDeployer.spec --noconfirm
if ($LASTEXITCODE -eq 0) { $installerBuilt = $true }
}
}
if (-not $installerBuilt) {
Write-Host " ERROR: Could not build installer. All strategies failed." -ForegroundColor Red
Write-Host " The .spec file requires both pyinstaller and pywebview." -ForegroundColor Red
Write-Host " Recommended: install uv (https://docs.astral.sh/uv/) then run build.ps1 again." -ForegroundColor Yellow
Write-Host " Or manually: pip install -r requirements.txt" -ForegroundColor Yellow
}
Pop-Location
if (-not $installerBuilt) {
Write-Host "`n=== Build FAILED: installer was not produced ===" -ForegroundColor Red
Write-Host " Portable zip was built at: $zipPath" -ForegroundColor Yellow
Write-Host " But MicroClawInstaller.exe is missing — end users cannot install." -ForegroundColor Yellow
exit 1
}
# Step 6: Pack onedir output into a single distributable zip
Write-Host "`n=== Step 6/7: Pack installer directory ===" -ForegroundColor Cyan
$installerDir = "$root\dist\MicroClawInstaller"
$installerZip = "$root\dist\MicroClawInstaller.zip"
if (-not (Test-Path $installerDir)) {
Write-Host " ERROR: installer directory not found at $installerDir" -ForegroundColor Red
Write-Host " PyInstaller reported success but produced no output." -ForegroundColor Red
exit 1
}
if (Test-Path $installerZip) { Remove-Item $installerZip -Force }
Compress-Archive -Path "$installerDir\*" -DestinationPath $installerZip
$instZipSizeMB = [math]::Round((Get-Item $installerZip).Length / 1MB, 1)
Write-Host " -> $installerZip ${instZipSizeMB} MB" -ForegroundColor Green
# Step 7: Build the single-exe setup (NSIS self-extractor) and code-sign it.
# This is the ONE file end users download. It extracts the onedir installer to a
# real directory under %LOCALAPPDATA% (not %TEMP%, preserving WDAC safety) and
# auto-launches MicroClawInstaller.exe. Only this stub needs signing to clear
# SmartScreen (it is the only file that carries Mark-of-the-Web on download).
Write-Host "`n=== Step 7/7: Build single-exe setup + sign ===" -ForegroundColor Cyan
$setupExe = "$root\dist\MicroClawSetup.exe"
$nsiScript = "$root\installer\microclaw-setup.nsi"
$setupIcon = "$root\deployer\assets\microclaw.ico"
# Resolve makensis: PATH first, then common install locations, then the copy
# that ships inside electron-builder's cache (already present after Step 3).
$makensis = $null
$mkCmd = Get-Command makensis -ErrorAction SilentlyContinue
if ($mkCmd) { $makensis = $mkCmd.Source }
if (-not $makensis) {
$mkCandidates = @(
"${env:ProgramFiles(x86)}\NSIS\makensis.exe",
"$env:ProgramFiles\NSIS\makensis.exe"
)
$mkCandidates += Get-ChildItem "$env:LOCALAPPDATA\electron-builder\Cache\nsis" -Recurse -Filter makensis.exe -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName
$makensis = $mkCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
}
if (-not $makensis) {
Write-Host " ERROR: makensis (NSIS) not found. Install NSIS 3.x (e.g. 'choco install nsis')." -ForegroundColor Red
Write-Host " The onedir installer + zip were still produced." -ForegroundColor Yellow
exit 1
}
Write-Host " Using makensis: $makensis"
# Derive a 4-part version (NSIS VIProductVersion requires X.X.X.X).
$pkgVersion = '0.0.0'
try {
$pkgVersion = (Get-Content "$root\desktop\package.json" -Raw | ConvertFrom-Json).version
} catch { }
$verParts = @($pkgVersion -split '\.') + @('0','0','0','0')
$version4 = ($verParts[0..3]) -join '.'
# Fingerprint the complete onedir payload. The bootstrapper persists this ID
# only after extraction finishes, allowing subsequent launches of the same
# setup build to reuse the verified-complete staging directory.
$payloadEntries = Get-ChildItem $installerDir -File -Recurse | Sort-Object FullName | ForEach-Object {
$relativePath = $_.FullName.Substring($installerDir.Length).TrimStart('\').Replace('\', '/')
$fileHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$relativePath|$($_.Length)|$fileHash"
}
$payloadManifest = [Text.Encoding]::UTF8.GetBytes(($payloadEntries -join "`n"))
$payloadHasher = [Security.Cryptography.SHA256]::Create()
try {
$payloadHash = $payloadHasher.ComputeHash($payloadManifest)
$payloadId = ([BitConverter]::ToString($payloadHash)).Replace('-', '').ToLowerInvariant()
} finally {
$payloadHasher.Dispose()
}
Write-Host " Payload ID: $payloadId" -ForegroundColor DarkGray
if (Test-Path $setupExe) { Remove-Item $setupExe -Force }
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& $makensis `
"/INPUTCHARSET" `
"UTF8" `
"/DPAYLOAD_DIR=$installerDir" `
"/DOUT_FILE=$setupExe" `
"/DICON=$setupIcon" `
"/DVERSION=$version4" `
"/DPAYLOAD_ID=$payloadId" `
$nsiScript 2>&1 | ForEach-Object { Write-Host " $_" }
$nsisExit = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($nsisExit -ne 0 -or -not (Test-Path $setupExe)) {
Write-Host " ERROR: makensis failed (exit $nsisExit) — MicroClawSetup.exe not produced." -ForegroundColor Red
exit 1
}
$setupSizeMB = [math]::Round((Get-Item $setupExe).Length / 1MB, 1)
Write-Host " -> $setupExe ${setupSizeMB} MB" -ForegroundColor Green
# Code-sign the setup exe. This is a NO-OP unless Trusted Signing is configured
# (TRUSTED_SIGNING_ENDPOINT/ACCOUNT/PROFILE), so local + PR builds are unchanged.
# Run in a child process using the SAME PowerShell host (the signer calls
# `exit`, which would otherwise terminate this build script).
$psHost = (Get-Process -Id $PID).Path
if (-not $psHost) { $psHost = 'powershell' }
& $psHost -NoProfile -File "$root\scripts\windows\sign-artifact.ps1" -Path $setupExe
if ($LASTEXITCODE -ne 0) {
Write-Host " ERROR: signing step failed (exit $LASTEXITCODE)." -ForegroundColor Red
exit 1
}
Write-Host "`n=== Done ===" -ForegroundColor Green
Write-Host " Setup (one-exe): $setupExe"
Write-Host " Installer dir: $root\dist\MicroClawInstaller\"
Write-Host " Installer zip: $installerZip"
Write-Host " Portable: $zipPath"