firmwareloop 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firmwareloop-0.0.8.dist-info/METADATA +268 -0
- firmwareloop-0.0.8.dist-info/RECORD +26 -0
- firmwareloop-0.0.8.dist-info/WHEEL +5 -0
- firmwareloop-0.0.8.dist-info/entry_points.txt +5 -0
- firmwareloop-0.0.8.dist-info/licenses/LICENSE +21 -0
- firmwareloop-0.0.8.dist-info/top_level.txt +1 -0
- tools/__init__.py +5 -0
- tools/acceptance-scenario.ps1 +455 -0
- tools/build.ps1 +208 -0
- tools/can.ps1 +130 -0
- tools/check-qoder-mcp.ps1 +111 -0
- tools/common/build-backends.psm1 +294 -0
- tools/common/fw.psm1 +311 -0
- tools/common/uart_probe.py +100 -0
- tools/doctor.ps1 +148 -0
- tools/flash.ps1 +181 -0
- tools/fw_mcp_server.py +1272 -0
- tools/instrument_cli.py +417 -0
- tools/lib/__init__.py +3 -0
- tools/lib/instruments.py +181 -0
- tools/logic_capture.ps1 +261 -0
- tools/logic_decode.ps1 +307 -0
- tools/reset.ps1 +141 -0
- tools/setup-agent-mcp.ps1 +147 -0
- tools/test-backends.ps1 +100 -0
- tools/test.ps1 +194 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Final acceptance scenario runner (Spec §33 + M6). Drives the complete
|
|
4
|
+
pipeline in one call and writes a final report with all nine mandated
|
|
5
|
+
sections. In simulator mode every leg runs against the real compiled
|
|
6
|
+
artifact / simulated instruments and is fully verifiable offline; with
|
|
7
|
+
real hardware, -Hardware agentic-hil swaps the flash/reset/UART legs to
|
|
8
|
+
Agentic HIL MCP-equivalent CLI verbs.
|
|
9
|
+
|
|
10
|
+
Pipeline (Spec §33):
|
|
11
|
+
build -> flash -> reset -> UART -> logic -> scope -> pytest -> report
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
.\tools\acceptance-scenario.ps1 -Json # simulator
|
|
15
|
+
.\tools\acceptance-scenario.ps1 -Hardware agentic-hil -Json # real DUT
|
|
16
|
+
|
|
17
|
+
Output: artifacts/runs/<run_id>/final-report.json
|
|
18
|
+
#>
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
param(
|
|
21
|
+
[ValidateSet('simulator', 'real')]
|
|
22
|
+
[string]$Mode = 'simulator',
|
|
23
|
+
[ValidateSet('simulator', 'agentic-hil')]
|
|
24
|
+
[string]$Hardware, # deprecated alias for -Mode
|
|
25
|
+
[string]$Scenario = 'SPI Flash JEDEC ID occasionally fails - fix driver, rebuild, verify',
|
|
26
|
+
[switch]$Json
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if ($Hardware) {
|
|
30
|
+
$Mode = if ($Hardware -eq 'agentic-hil') { 'real' } else { 'simulator' }
|
|
31
|
+
Write-Warning '-Hardware is deprecated; use -Mode simulator|real'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Set-StrictMode -Version Latest
|
|
35
|
+
$ErrorActionPreference = 'Stop'
|
|
36
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
37
|
+
|
|
38
|
+
$repoRoot = Get-FwRepoRoot
|
|
39
|
+
$python = Resolve-FwPython -RepoRoot $repoRoot
|
|
40
|
+
if (-not $python) {
|
|
41
|
+
$err = New-FwError -ErrorClass 'CONFIG_ERROR' -Message 'No Python found; run tools/doctor.ps1.'
|
|
42
|
+
if ($Json) { Write-FwJson $err -Compact } else { Write-FwJson $err }
|
|
43
|
+
exit 2
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Invoke-Tool {
|
|
47
|
+
param([string]$Script, [string[]]$ToolArgs)
|
|
48
|
+
$scriptPath = Join-Path $PSScriptRoot $Script
|
|
49
|
+
if ($Script -like '*.py') {
|
|
50
|
+
$filePath = $python
|
|
51
|
+
$runArgs = @($scriptPath) + $ToolArgs
|
|
52
|
+
} elseif ($Script -like '*.ps1') {
|
|
53
|
+
$filePath = 'pwsh'
|
|
54
|
+
$runArgs = @('-NoProfile', '-NonInteractive', '-File', $scriptPath) + $ToolArgs
|
|
55
|
+
} else {
|
|
56
|
+
$filePath = $scriptPath
|
|
57
|
+
$runArgs = $ToolArgs
|
|
58
|
+
}
|
|
59
|
+
$res = Invoke-FwProcess -FilePath $filePath -Arguments $runArgs `
|
|
60
|
+
-WorkingDirectory $repoRoot -TimeoutMs 600000
|
|
61
|
+
if ($res.exit_code -ne 0 -and $res.exit_code -ne 1) {
|
|
62
|
+
Write-Warning "$Script exited $($res.exit_code): $($res.stderr)"
|
|
63
|
+
}
|
|
64
|
+
# last JSON line of stdout
|
|
65
|
+
$jsonLine = $null
|
|
66
|
+
foreach ($l in ($res.stdout -split "`r?`n")) {
|
|
67
|
+
if ($l.TrimStart().StartsWith('{')) { $jsonLine = $l }
|
|
68
|
+
}
|
|
69
|
+
if ($jsonLine) {
|
|
70
|
+
try { return ($jsonLine | ConvertFrom-Json) }
|
|
71
|
+
catch {
|
|
72
|
+
Write-Warning "$Script JSON parse failed: $($_.Exception.Message) | line=[$jsonLine] | rc=$($res.exit_code) | stderr=[$($res.stderr)]"
|
|
73
|
+
return [pscustomobject]@{ ok = $false; error = "json parse: $($_.Exception.Message)"; raw_line = $jsonLine }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [pscustomobject]@{ ok = $false; error = "no JSON from $Script"; raw = $res.stdout }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ---- run identity --------------------------------------------------------------
|
|
80
|
+
$runId = Get-FwRunId
|
|
81
|
+
$runDir = Join-Path $repoRoot "artifacts\runs\$runId"
|
|
82
|
+
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
|
|
83
|
+
$env:FW_RUN_DIR = $runDir
|
|
84
|
+
|
|
85
|
+
$report = [ordered]@{
|
|
86
|
+
schema = 'firmware-acceptance-report/v1'
|
|
87
|
+
run_id = $runId
|
|
88
|
+
scenario = $Scenario
|
|
89
|
+
execution_mode = $Mode
|
|
90
|
+
steps = [ordered]@{}
|
|
91
|
+
generated_at = Get-FwTimestamp
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---- 1. build --------------------------------------------------------------------
|
|
95
|
+
$build = Invoke-Tool 'build.ps1' @('-Configuration', 'Debug', '-Json')
|
|
96
|
+
$report.steps.build = [ordered]@{
|
|
97
|
+
ok = $build.ok
|
|
98
|
+
errors = $build.errors
|
|
99
|
+
warnings = $build.warnings
|
|
100
|
+
artifact = $build.artifact
|
|
101
|
+
sha256 = $build.artifact_sha256
|
|
102
|
+
detail = if ($build.ok) { $null } else { $build.diagnostics }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# --- evidence: build.json + hardware.json (Spec §25; identity probing in real
|
|
106
|
+
# mode via Agentic HIL read-only commands - release-fix #8) -------------------
|
|
107
|
+
$build | Add-Member -NotePropertyName generated_at -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue
|
|
108
|
+
$build | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $runDir 'build.json') -Encoding utf8
|
|
109
|
+
|
|
110
|
+
$probeSerial = $null
|
|
111
|
+
$comPort = $null
|
|
112
|
+
$targetIdentity = $null
|
|
113
|
+
if ($Mode -eq 'real') {
|
|
114
|
+
$ahilTool = $null
|
|
115
|
+
$g2 = Get-Command agentic-hil -ErrorAction SilentlyContinue
|
|
116
|
+
if ($g2) { $ahilTool = $g2.Source }
|
|
117
|
+
if (-not $ahilTool -and (Test-Path -LiteralPath (Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'))) {
|
|
118
|
+
$ahilTool = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
|
|
119
|
+
}
|
|
120
|
+
if ($ahilTool) {
|
|
121
|
+
$probes = Invoke-FwProcess -FilePath $ahilTool -Arguments @('debugger-probes') -WorkingDirectory $repoRoot -TimeoutMs 60000
|
|
122
|
+
if ($probes.exit_code -eq 0) {
|
|
123
|
+
try {
|
|
124
|
+
$pj = $probes.stdout | ConvertFrom-Json
|
|
125
|
+
if ($pj.Probes) { $probeSerial = ($pj.Probes -join ',') }
|
|
126
|
+
elseif ($pj.probes) { $probeSerial = ($pj.probes -join ',') }
|
|
127
|
+
} catch { }
|
|
128
|
+
}
|
|
129
|
+
$ports = Invoke-FwProcess -FilePath $ahilTool -Arguments @('com-ports') -WorkingDirectory $repoRoot -TimeoutMs 60000
|
|
130
|
+
if ($ports.exit_code -eq 0) {
|
|
131
|
+
try {
|
|
132
|
+
$pt = $ports.stdout | ConvertFrom-Json
|
|
133
|
+
if ($pt.ports) { $comPort = (@($pt.ports | ForEach-Object { $_.device }) -join ',') }
|
|
134
|
+
} catch { }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
$hardwareInfo = [ordered]@{
|
|
139
|
+
schema = 'firmware-hardware-info/v1'
|
|
140
|
+
execution_mode = $Mode
|
|
141
|
+
target_identity = $targetIdentity
|
|
142
|
+
probe_serial = $probeSerial
|
|
143
|
+
com_port = $comPort
|
|
144
|
+
source = if ($Mode -eq 'real') { 'agentic-hil' } else { 'simulator' }
|
|
145
|
+
generated_at = Get-FwTimestamp
|
|
146
|
+
}
|
|
147
|
+
$hardwareInfo | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runDir 'hardware.json') -Encoding utf8
|
|
148
|
+
|
|
149
|
+
# ---- 2+3+4. real lane: Agentic HIL Test Reactor (GAP-004) -------------------------
|
|
150
|
+
# The plan (test-plans/real-smoke.yaml) drives flash -> reset -> UART expect
|
|
151
|
+
# with logical devices only. Missing devices make the reactor fail - never a
|
|
152
|
+
# simulator stand-in, never a PASS without hardware.
|
|
153
|
+
$reactor = $null
|
|
154
|
+
if ($Mode -eq 'real') {
|
|
155
|
+
$ahil = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
|
|
156
|
+
if (-not (Test-Path -LiteralPath $ahil)) {
|
|
157
|
+
$g = Get-Command agentic-hil -ErrorAction SilentlyContinue
|
|
158
|
+
if ($g) { $ahil = $g.Source }
|
|
159
|
+
}
|
|
160
|
+
if (-not $ahil) {
|
|
161
|
+
$report.steps.flash = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'agentic-hil not installed; real mode requires the Agentic HIL test reactor' }
|
|
162
|
+
$report.steps.reset = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'not executed (reactor unavailable)' }
|
|
163
|
+
$report.steps.uart = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'not executed (reactor unavailable)' }
|
|
164
|
+
} else {
|
|
165
|
+
$plan = Join-Path $repoRoot 'test-plans\real-smoke.yaml'
|
|
166
|
+
# release-fix #3: resolve the REAL primary artifact from the
|
|
167
|
+
# firmware-artifacts/v1 manifest (build step) and inject it into a
|
|
168
|
+
# run-local copy of the plan - never hardcode firmware.elf in the plan.
|
|
169
|
+
$artifactManifest = Join-Path $repoRoot 'artifacts\build\artifacts.json'
|
|
170
|
+
$planPath = $plan
|
|
171
|
+
if (Test-Path -LiteralPath $artifactManifest) {
|
|
172
|
+
try {
|
|
173
|
+
$manifest = Get-Content -LiteralPath $artifactManifest -Raw | ConvertFrom-Json
|
|
174
|
+
$primaryProp = $manifest.primary
|
|
175
|
+
if ($primaryProp -and $primaryProp.native_path -and (Test-Path -LiteralPath $primaryProp.native_path)) {
|
|
176
|
+
$runPlan = Join-Path $runDir 'real-smoke.runtime.yaml'
|
|
177
|
+
$planText = Get-Content -LiteralPath $plan -Raw
|
|
178
|
+
$planText = [regex]::Replace($planText, '(?m)^(\s*image_path:\s*).*$', "`${1}$($primaryProp.native_path)")
|
|
179
|
+
$planText | Set-Content -LiteralPath $runPlan -Encoding utf8
|
|
180
|
+
$planPath = $runPlan
|
|
181
|
+
$report.steps.flash = [ordered]@{ artifact_source = 'firmware-artifacts/v1 manifest'; artifact = $primaryProp.native_path }
|
|
182
|
+
}
|
|
183
|
+
} catch { }
|
|
184
|
+
}
|
|
185
|
+
$reactor = Invoke-FwProcess -FilePath $ahil -Arguments @('test-reactor', '--test-config', $planPath, '--wait-s', '0') -WorkingDirectory $repoRoot -TimeoutMs 600000
|
|
186
|
+
$reactorOk = $reactor.exit_code -eq 0
|
|
187
|
+
$reactorText = ($reactor.stdout + $reactor.stderr)
|
|
188
|
+
# structured summary of the reactor verdict
|
|
189
|
+
$verdict = if ($reactorText -match '(?s)\{[^{}]*"ok"\s*:\s*(true|false)[^{}]*\}') {
|
|
190
|
+
$Matches[0]
|
|
191
|
+
} else { $reactorText }
|
|
192
|
+
$report.steps.flash = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; plan = $planPath; result = ($verdict | Select-Object -First 1) }
|
|
193
|
+
$report.steps.reset = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; note = 'flash+reset+UART expect driven by the reactor plan' }
|
|
194
|
+
$report.steps.uart = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; note = 'UART expect inside plan (Application started / JEDEC EF 40 18)' }
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
$report.steps.flash = [ordered]@{
|
|
198
|
+
ok = $true
|
|
199
|
+
note = "simulator mode: artifact is the DUT; flash verified via firmware build + HIL run (real flash requires -Mode real)"
|
|
200
|
+
}
|
|
201
|
+
$reset = Invoke-Tool 'reset.ps1' @('-Backend', 'simulator', '-Json')
|
|
202
|
+
$report.steps.reset = [ordered]@{ ok = $reset.ok; backend = $reset.backend }
|
|
203
|
+
|
|
204
|
+
# ---- simulator UART leg -----------------------------------------------------
|
|
205
|
+
$artifact = Join-Path $repoRoot 'artifacts\build\firmware.elf'
|
|
206
|
+
$probe = Invoke-FwProcess -FilePath $python -Arguments @((Join-Path $PSScriptRoot 'common\uart_probe.py'), $artifact, 'jedec') -WorkingDirectory $repoRoot -TimeoutMs 60000
|
|
207
|
+
try { $uartResult = $probe.stdout | ConvertFrom-Json } catch { $uartResult = [ordered]@{ ok = $false; error = $probe.stderr } }
|
|
208
|
+
if ($probe.exit_code -eq 0) {
|
|
209
|
+
Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "RX: $($uartResult.banner -join ' | ')"
|
|
210
|
+
Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "TX: jedec"
|
|
211
|
+
Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "RX: $($uartResult.reply)"
|
|
212
|
+
}
|
|
213
|
+
$report.steps.uart = $uartResult
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ---- 5. logic analyzer ------------------------------------------------------------
|
|
217
|
+
# --- evidence: flash.json (Spec §25) ----------------------------------------------
|
|
218
|
+
$flashBackendProp = $report.steps.flash.PSObject.Properties['backend']
|
|
219
|
+
$flashEvidence = [ordered]@{
|
|
220
|
+
schema = 'firmware-flash-result/v1'
|
|
221
|
+
ok = $report.steps.flash.ok
|
|
222
|
+
backend = if ($flashBackendProp) { $flashBackendProp.Value } else { 'simulator' }
|
|
223
|
+
artifact = $report.steps.build.artifact
|
|
224
|
+
generated_at = Get-FwTimestamp
|
|
225
|
+
}
|
|
226
|
+
$flashEvidence | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runDir 'flash.json') -Encoding utf8
|
|
227
|
+
# real mode: sigrok (or Saleae MCP in Qoder); simulator data is NEVER used to
|
|
228
|
+
# fabricate real evidence (GAP-007/008). Missing backend => REAL_HARDWARE_REQUIRED.
|
|
229
|
+
$logic = [ordered]@{ ok = $false }
|
|
230
|
+
if ($Mode -eq 'real') {
|
|
231
|
+
$cap = Invoke-Tool 'logic_capture.ps1' @('-Protocol', 'spi', '-Backend', 'sigrok', '-Json')
|
|
232
|
+
if (-not $cap.ok -or $cap.error_class) {
|
|
233
|
+
$logic = [ordered]@{
|
|
234
|
+
ok = $false
|
|
235
|
+
error_class = if ($cap.error_class) { $cap.error_class } else { 'REAL_HARDWARE_REQUIRED' }
|
|
236
|
+
error = 'real logic capture needs sigrok-cli or Saleae; simulator captures are not real evidence'
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
$decode = Invoke-Tool 'logic_decode.ps1' @('-Capture', (Split-Path $cap.capture), '-Expect', '9FEF4018', '-ExpectKind', 'hex', '-Json')
|
|
240
|
+
$logic = [ordered]@{
|
|
241
|
+
ok = $decode.ok
|
|
242
|
+
protocol = 'spi'
|
|
243
|
+
backend = 'sigrok'
|
|
244
|
+
real_hardware = $true
|
|
245
|
+
bytes_hex = $decode.bytes_hex
|
|
246
|
+
frames = $decode.frames
|
|
247
|
+
expectation = '9FEF4018'
|
|
248
|
+
capture = $cap.capture
|
|
249
|
+
decoded = if ($decode.out_file) { $decode.out_file } else { $null }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
$cap = Invoke-Tool 'logic_capture.ps1' @('-Protocol', 'spi', '-Json')
|
|
254
|
+
if ($cap.ok) {
|
|
255
|
+
$decode = Invoke-Tool 'logic_decode.ps1' @('-Capture', (Split-Path $cap.capture), '-Expect', '9FEF4018', '-ExpectKind', 'hex', '-Json')
|
|
256
|
+
$logic = [ordered]@{
|
|
257
|
+
ok = $decode.ok
|
|
258
|
+
protocol = 'spi'
|
|
259
|
+
backend = 'simulator'
|
|
260
|
+
real_hardware = $false
|
|
261
|
+
bytes_hex = $decode.bytes_hex
|
|
262
|
+
frames = $decode.frames
|
|
263
|
+
expectation = '9FEF4018'
|
|
264
|
+
capture = $cap.capture
|
|
265
|
+
decoded = if ($decode.out_file) { $decode.out_file } else { $null }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
$report.steps.logic = $logic
|
|
270
|
+
|
|
271
|
+
# ---- 6. scope / psu ---------------------------------------------------------------
|
|
272
|
+
# real mode reads lab/lab.yaml instruments (visa backend); without a trusted
|
|
273
|
+
# bench policy instrument writes fail closed (GAP-009), reads without a
|
|
274
|
+
# configured visa instrument report INSTRUMENT_NOT_FOUND - never simulator data.
|
|
275
|
+
$freq = Invoke-Tool 'instrument_cli.py' @('scope', 'measure-frequency', '--instrument', 'scope1') # run via python
|
|
276
|
+
$freqOk = $false
|
|
277
|
+
$freqVal = $null
|
|
278
|
+
if ($freq.ok) { $freqOk = $freq.ok; $freqVal = $freq.value }
|
|
279
|
+
$vpp = Invoke-Tool 'instrument_cli.py' @('scope', 'measure-vpp', '--instrument', 'scope1')
|
|
280
|
+
# real mode: measurements must come from a visa backend - simulator data is
|
|
281
|
+
# never real evidence (GAP-007/011)
|
|
282
|
+
$realFreq = ($freq.ok -and $freq.execution_mode -eq 'real')
|
|
283
|
+
$realVpp = ($vpp.ok -and $vpp.execution_mode -eq 'real')
|
|
284
|
+
$report.steps.scope = [ordered]@{
|
|
285
|
+
frequency_hz = $freqVal
|
|
286
|
+
vpp_v = if ($vpp.ok) { $vpp.value } else { $null }
|
|
287
|
+
ok = if ($Mode -eq 'real') { $realFreq -and $realVpp } else { $freqOk -and $vpp.ok }
|
|
288
|
+
execution_mode = $Mode
|
|
289
|
+
note = if ($Mode -eq 'real' -and -not ($realFreq -and $realVpp)) { 'measurements were not visa-backed; simulator data is not real evidence' } else { $null }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# persist instrument evidence (Spec §25 instruments/measurements.json)
|
|
293
|
+
$measPath = Join-Path $runDir 'measurements.json'
|
|
294
|
+
$meas = @()
|
|
295
|
+
if (Test-Path -LiteralPath $measPath) {
|
|
296
|
+
try { $meas = @(Get-Content -LiteralPath $measPath -Raw | ConvertFrom-Json) } catch { $meas = @() }
|
|
297
|
+
}
|
|
298
|
+
$freq | Add-Member -NotePropertyName timestamp -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue
|
|
299
|
+
$meas += $freq
|
|
300
|
+
if ($vpp.ok) { $vpp | Add-Member -NotePropertyName timestamp -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue; $meas += $vpp }
|
|
301
|
+
$meas | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $measPath -Encoding utf8
|
|
302
|
+
|
|
303
|
+
# ---- 7. pytest HIL -----------------------------------------------------------------
|
|
304
|
+
# real mode MUST run through test.ps1 -Mode real: required HIL tests that are
|
|
305
|
+
# skipped become failures there (release-fix #2), and the reported
|
|
306
|
+
# execution_mode is verified below - simulator/skip can never count as PASS.
|
|
307
|
+
$hilArgs = @('-Json')
|
|
308
|
+
if ($Mode -eq 'real') { $hilArgs = @('-Mode', 'real', '-Json') }
|
|
309
|
+
$hil = Invoke-Tool 'test.ps1' $hilArgs
|
|
310
|
+
$pytestOk = $hil.ok
|
|
311
|
+
$modeNote = $null
|
|
312
|
+
if ($Mode -eq 'real') {
|
|
313
|
+
$reported = $hil.execution_mode
|
|
314
|
+
if ($reported -ne 'real') {
|
|
315
|
+
$pytestOk = $false
|
|
316
|
+
$modeNote = "test.ps1 reported execution_mode='$reported' instead of 'real'; simulator/skip cannot count as real PASS"
|
|
317
|
+
} elseif (-not $hil.ok) {
|
|
318
|
+
$modeNote = 'real-mode HIL failed (skips count as failures)'
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
$report.steps.pytest = [ordered]@{
|
|
322
|
+
ok = $pytestOk
|
|
323
|
+
execution_mode = if ($Mode -eq 'real') { $hil.execution_mode } else { 'simulator' }
|
|
324
|
+
total = @($hil.tests).Count
|
|
325
|
+
passed = @($hil.tests | Where-Object status -eq 'passed').Count
|
|
326
|
+
failed = @($hil.tests | Where-Object status -eq 'failed').Count
|
|
327
|
+
skipped = @($hil.tests | Where-Object status -eq 'skipped').Count
|
|
328
|
+
summary = (Join-Path $runDir 'summary.json')
|
|
329
|
+
failures = @($hil.tests | Where-Object status -eq 'failed')
|
|
330
|
+
note = $modeNote
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# ---- final report (Spec §33) --------------------------------------------------------
|
|
334
|
+
$git = Get-FwGitInfo -RepoRoot $repoRoot
|
|
335
|
+
$codeChanges = @()
|
|
336
|
+
$statusLines = git -C $repoRoot status --porcelain 2>$null
|
|
337
|
+
foreach ($line in $statusLines) {
|
|
338
|
+
if ($line -match '^..\s+(.+)$') { $codeChanges += $Matches[1].Trim('"') }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
$finalReport = [ordered]@{
|
|
342
|
+
schema = 'firmware-acceptance-report/v1'
|
|
343
|
+
run_id = $runId
|
|
344
|
+
scenario = $Scenario
|
|
345
|
+
execution_mode = $Mode
|
|
346
|
+
root_cause_analysis = if ($Mode -eq 'real') {
|
|
347
|
+
if ($report.steps.pytest.ok) {
|
|
348
|
+
'No defect reproduced on real hardware: JEDEC read returns 3-byte ID, UART/spi evidence captured from the DUT.'
|
|
349
|
+
} else {
|
|
350
|
+
'Reproduced defect on real hardware: inspect steps.pytest.failures (expected/actual captured) and uart/logic evidence.'
|
|
351
|
+
}
|
|
352
|
+
} elseif ($report.steps.pytest.ok) {
|
|
353
|
+
'No defect reproduced in simulated scenario: JEDEC read returns 3-byte ID, SPI decode 9F EF 40 18, PWM 20 kHz within limits.'
|
|
354
|
+
} else {
|
|
355
|
+
'Reproduced defect: inspect steps.pytest.failures (expected/actual captured) and uart/logic evidence.'
|
|
356
|
+
}
|
|
357
|
+
code_changes = $codeChanges
|
|
358
|
+
build_result = @{
|
|
359
|
+
ok = $report.steps.build.ok
|
|
360
|
+
errors = $report.steps.build.errors
|
|
361
|
+
warnings = $report.steps.build.warnings
|
|
362
|
+
}
|
|
363
|
+
firmware_artifact = @{
|
|
364
|
+
path = $report.steps.build.artifact
|
|
365
|
+
sha256 = $report.steps.build.sha256
|
|
366
|
+
}
|
|
367
|
+
uart_evidence = if ($report.steps.uart.ok) { $report.steps.uart } else { $report.steps.uart }
|
|
368
|
+
logic_evidence = $report.steps.logic
|
|
369
|
+
scope_measurements = $report.steps.scope
|
|
370
|
+
test_result = $report.steps.pytest
|
|
371
|
+
remaining_risks = if ($Mode -eq 'real') {
|
|
372
|
+
@(
|
|
373
|
+
'Real mode: verify the reactor report and physical evidence before trusting PASS.',
|
|
374
|
+
'Instrument writes require an authoritative bench policy (%APPDATA%\FirmwareLoop\benches\<id>\limits.yaml or FIRMWARELOOP_BENCH_CONFIG).'
|
|
375
|
+
)
|
|
376
|
+
} else {
|
|
377
|
+
@(
|
|
378
|
+
'Simulator mode: no silicon exercised; run with -Mode real against the real DUT for final acceptance.',
|
|
379
|
+
'Real instruments (scope/psu) use simulator backend until VISA resources are configured in lab/lab.yaml.'
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
steps = $report.steps
|
|
383
|
+
git = $git
|
|
384
|
+
generated_at = Get-FwTimestamp
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
$reportPath = Join-Path $runDir 'final-report.json'
|
|
388
|
+
$finalReport | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $reportPath -Encoding utf8
|
|
389
|
+
$reportBody = [pscustomobject]$finalReport
|
|
390
|
+
$reportBody | Add-Member -NotePropertyName report_file -NotePropertyValue (Resolve-Path -LiteralPath $reportPath).Path
|
|
391
|
+
|
|
392
|
+
# ---- evidence layout (Spec §25): logic/ + instruments/ + final-report.md ------
|
|
393
|
+
$logicDir = Join-Path $runDir 'logic'
|
|
394
|
+
$instDir = Join-Path $runDir 'instruments'
|
|
395
|
+
New-Item -ItemType Directory -Force -Path $logicDir, $instDir | Out-Null
|
|
396
|
+
$logicCapProp = $report.steps.logic.PSObject.Properties['capture']
|
|
397
|
+
$logicDecProp = $report.steps.logic.PSObject.Properties['decoded']
|
|
398
|
+
if ($logicCapProp -and $logicCapProp.Value -and (Test-Path -LiteralPath $logicCapProp.Value)) {
|
|
399
|
+
Copy-Item -LiteralPath $logicCapProp.Value -Destination (Join-Path $logicDir 'capture.csv') -Force -ErrorAction SilentlyContinue
|
|
400
|
+
}
|
|
401
|
+
if ($logicDecProp -and $logicDecProp.Value -and (Test-Path -LiteralPath $logicDecProp.Value)) {
|
|
402
|
+
Copy-Item -LiteralPath $logicDecProp.Value -Destination (Join-Path $logicDir 'decode.json') -Force -ErrorAction SilentlyContinue
|
|
403
|
+
}
|
|
404
|
+
if (Test-Path -LiteralPath (Join-Path $runDir 'measurements.json')) {
|
|
405
|
+
Copy-Item -LiteralPath (Join-Path $runDir 'measurements.json') -Destination (Join-Path $instDir 'measurements.json') -Force -ErrorAction SilentlyContinue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
$md = @()
|
|
409
|
+
$md += "# FirmwareLoop Acceptance Report — $runId"
|
|
410
|
+
$md += ""
|
|
411
|
+
$md += "- **scenario**: $Scenario"
|
|
412
|
+
$md += "- **execution_mode**: $Mode"
|
|
413
|
+
$md += "- **root cause**: $($finalReport.root_cause_analysis)"
|
|
414
|
+
$md += "- **git**: $($git.commit) (dirty=$($git.dirty))"
|
|
415
|
+
$md += ""
|
|
416
|
+
$md += "## Build"
|
|
417
|
+
$md += ""
|
|
418
|
+
$md += "- ok: $($finalReport.build_result.ok), errors: $($finalReport.build_result.errors), warnings: $($finalReport.build_result.warnings)"
|
|
419
|
+
$md += "- artifact: $($finalReport.firmware_artifact.path)"
|
|
420
|
+
$md += "- sha256: $($finalReport.firmware_artifact.sha256)"
|
|
421
|
+
$md += ""
|
|
422
|
+
$md += "## UART"
|
|
423
|
+
$md += ""
|
|
424
|
+
$uartReply = $finalReport.uart_evidence.PSObject.Properties['reply']
|
|
425
|
+
$uartErr = $finalReport.uart_evidence.PSObject.Properties['error']
|
|
426
|
+
if ($finalReport.uart_evidence.ok -and $uartReply) {
|
|
427
|
+
$md += "- reply: $($uartReply.Value)"
|
|
428
|
+
} else {
|
|
429
|
+
$md += "- ok: $($finalReport.uart_evidence.ok) ($(if ($uartErr) { $uartErr.Value } else { 'n/a' }))"
|
|
430
|
+
}
|
|
431
|
+
$md += ""
|
|
432
|
+
$md += "## Logic"
|
|
433
|
+
$md += ""
|
|
434
|
+
$logicHex = $finalReport.logic_evidence.PSObject.Properties['bytes_hex']
|
|
435
|
+
$logicProto = $finalReport.logic_evidence.PSObject.Properties['protocol']
|
|
436
|
+
$md += "- ok: $($finalReport.logic_evidence.ok), protocol: $(if ($logicProto) { $logicProto.Value } else { 'n/a' }), bytes: $(if ($logicHex) { $logicHex.Value } else { 'n/a' })"
|
|
437
|
+
$md += ""
|
|
438
|
+
$md += "## Scope"
|
|
439
|
+
$md += ""
|
|
440
|
+
$md += "- frequency: $($finalReport.scope_measurements.frequency_hz) Hz, vpp: $($finalReport.scope_measurements.vpp_v) V"
|
|
441
|
+
$md += ""
|
|
442
|
+
$md += "## Tests"
|
|
443
|
+
$md += ""
|
|
444
|
+
$md += "- ok: $($finalReport.test_result.ok), total: $($finalReport.test_result.total), failed: $($finalReport.test_result.failed)"
|
|
445
|
+
$md += ""
|
|
446
|
+
$md += "## Remaining risks"
|
|
447
|
+
$md += ""
|
|
448
|
+
foreach ($risk in $finalReport.remaining_risks) { $md += "- $risk" }
|
|
449
|
+
$md | Set-Content -LiteralPath (Join-Path $runDir 'final-report.md') -Encoding utf8
|
|
450
|
+
|
|
451
|
+
if ($Json) { Write-FwJson $reportBody -Compact } else { Write-FwJson $reportBody }
|
|
452
|
+
|
|
453
|
+
$allOk = $report.steps.build.ok -and $report.steps.logic.ok -and $report.steps.scope.ok -and $report.steps.pytest.ok
|
|
454
|
+
if ($Mode -eq 'real') { $allOk = $allOk -and $report.steps.flash.ok -and $report.steps.uart.ok }
|
|
455
|
+
exit $(if ($allOk) { 0 } else { 1 })
|
tools/build.ps1
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Build adapter (Spec §8, v0.0.2 GAP-002). Dispatches to the project's
|
|
4
|
+
existing build system across seven backends and returns structured
|
|
5
|
+
firmware-build-result/v1 JSON plus a firmware-artifacts/v1 manifest.
|
|
6
|
+
|
|
7
|
+
Backends: cmake | make | platformio | keil | iar | zephyr | esp-idf
|
|
8
|
+
|
|
9
|
+
Detection precedence (GAP-002):
|
|
10
|
+
1. -Backend (explicit)
|
|
11
|
+
2. project config (lab/lab.yaml -> project.build_backend)
|
|
12
|
+
3. strong project marker (west.yml -> zephyr, sdkconfig/idf.py ->
|
|
13
|
+
esp-idf, platformio.ini, *.uvproj*, *.ewp, Makefile, CMakeLists.txt)
|
|
14
|
+
4. generic detection
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
.\tools\build.ps1 -Configuration Debug -Json
|
|
18
|
+
.\tools\build.ps1 -Backend make -SourceDir .\demo-make -Json
|
|
19
|
+
.\tools\build.ps1 -Backend keil -DryRun -Json # command construction
|
|
20
|
+
.\tools\build.ps1 -Backend keil -BackendTool C:\fake\UV4.cmd -Json
|
|
21
|
+
|
|
22
|
+
Exit codes:
|
|
23
|
+
0 build ok (or dry-run construction ok)
|
|
24
|
+
1 build failed (compile/link errors)
|
|
25
|
+
2 configuration/toolchain error
|
|
26
|
+
#>
|
|
27
|
+
[CmdletBinding()]
|
|
28
|
+
param(
|
|
29
|
+
[ValidateSet('cmake', 'make', 'platformio', 'keil', 'iar', 'zephyr', 'esp-idf')]
|
|
30
|
+
[string]$Backend,
|
|
31
|
+
[string]$Configuration = 'Debug',
|
|
32
|
+
[switch]$Json,
|
|
33
|
+
[string]$SourceDir,
|
|
34
|
+
[string]$ArtifactDir,
|
|
35
|
+
[string]$BuildDir,
|
|
36
|
+
[string]$BackendTool, # override tool path (fake executable in tests)
|
|
37
|
+
[int]$TimeoutMs = 600000,
|
|
38
|
+
[switch]$SkipConfigure,
|
|
39
|
+
[switch]$Clean,
|
|
40
|
+
[switch]$DryRun # print constructed command, do not execute
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
Set-StrictMode -Version Latest
|
|
44
|
+
$ErrorActionPreference = 'Stop'
|
|
45
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
46
|
+
Import-Module (Join-Path $PSScriptRoot 'common\build-backends.psm1') -Force
|
|
47
|
+
|
|
48
|
+
$repoRoot = Get-FwRepoRoot
|
|
49
|
+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
|
50
|
+
|
|
51
|
+
function Exit-WithError {
|
|
52
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
53
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
54
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
55
|
+
exit 2
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------- 1. backend resolution
|
|
59
|
+
$projBackend = $null
|
|
60
|
+
$labCfgObj = Get-FwLabConfig -RepoRoot $repoRoot
|
|
61
|
+
if ($labCfgObj) {
|
|
62
|
+
$projProp = $labCfgObj.PSObject.Properties['project']
|
|
63
|
+
if ($projProp) {
|
|
64
|
+
$bbProp = $projProp.Value.PSObject.Properties['build_backend']
|
|
65
|
+
if ($bbProp) { $projBackend = [string]$bbProp.Value }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
$detect = Get-FwBackendDetect -ExplicitBackend $Backend -ProjectBackend $projBackend -SourceDir $SourceDir -RepoRoot $repoRoot
|
|
69
|
+
$backend = $detect.backend
|
|
70
|
+
$src = $detect.src
|
|
71
|
+
$projectFile = $detect.project_file
|
|
72
|
+
$detectSource = $detect.source
|
|
73
|
+
|
|
74
|
+
if (-not $backend) {
|
|
75
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message 'No build system detected (west.yml / sdkconfig / platformio.ini / *.uvproj* / *.ewp / Makefile / CMakeLists.txt). Pass -Backend and/or -SourceDir.' `
|
|
76
|
+
-Detail "project config backend: $projBackend"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (-not $BuildDir) { $BuildDir = Join-Path $repoRoot "artifacts\build\$backend-$Configuration" }
|
|
80
|
+
if (-not $ArtifactDir) { $ArtifactDir = Join-Path $repoRoot 'artifacts\build' }
|
|
81
|
+
New-Item -ItemType Directory -Force -Path $BuildDir, $ArtifactDir, (Join-Path $repoRoot 'artifacts\logs') | Out-Null
|
|
82
|
+
if ($Clean) {
|
|
83
|
+
Remove-Item -Recurse -Force -LiteralPath $BuildDir -ErrorAction SilentlyContinue
|
|
84
|
+
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------- 2. command construction
|
|
88
|
+
$cmd = Get-FwBackendCommand -Backend $backend -Src $src -ProjectFile $projectFile `
|
|
89
|
+
-Configuration $Configuration -BuildDir $BuildDir -Tool $BackendTool `
|
|
90
|
+
-AllowMissingTool:$DryRun
|
|
91
|
+
|
|
92
|
+
if ($cmd.ContainsKey('missing')) {
|
|
93
|
+
$need = $cmd.missing
|
|
94
|
+
$hint = switch ($need) {
|
|
95
|
+
'cmake' { 'winget install Kitware.CMake' }
|
|
96
|
+
'make' { 'winget install GnuWin32.Make (or use CI: apt install make)' }
|
|
97
|
+
'platformio' { 'pip install platformio' }
|
|
98
|
+
'keil' { 'install Keil uVision (UV4.exe must be on PATH)' }
|
|
99
|
+
'iar' { 'install IAR EWARM (IarBuild.exe must be on PATH)' }
|
|
100
|
+
'zephyr' { 'pip install west && west init' }
|
|
101
|
+
'esp-idf' { 'install ESP-IDF (idf.py must be on PATH)' }
|
|
102
|
+
'keil-project' { 'no .uvproj* found in source dir' }
|
|
103
|
+
'iar-project' { 'no .ewp found in source dir' }
|
|
104
|
+
'zephyr-board' { $cmd.detail }
|
|
105
|
+
default { "tool '$need' not found" }
|
|
106
|
+
}
|
|
107
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message "backend '$backend' is unavailable: $need" -Detail $hint
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
111
|
+
$logFile = Join-Path $repoRoot "artifacts\logs\build-$stamp.log"
|
|
112
|
+
$logCanonical = Join-Path $repoRoot 'artifacts\logs\build.log'
|
|
113
|
+
|
|
114
|
+
if ($DryRun) {
|
|
115
|
+
$dry = [ordered]@{
|
|
116
|
+
schema = 'firmware-build-result/v1'
|
|
117
|
+
ok = $true
|
|
118
|
+
dry_run = $true
|
|
119
|
+
configuration = $Configuration
|
|
120
|
+
backend = $backend
|
|
121
|
+
command = $cmd.label
|
|
122
|
+
command_line = $cmd.file + ' ' + ($cmd.args -join ' ')
|
|
123
|
+
cwd = if ($cmd.cwd) { $cmd.cwd } else { $repoRoot }
|
|
124
|
+
artifact = $null
|
|
125
|
+
warnings = 0
|
|
126
|
+
errors = 0
|
|
127
|
+
duration_ms = 0
|
|
128
|
+
log = $null
|
|
129
|
+
git = Get-FwGitInfo -RepoRoot $repoRoot
|
|
130
|
+
generated_at = Get-FwTimestamp
|
|
131
|
+
}
|
|
132
|
+
if ($Json) { Write-FwJson ([pscustomobject]$dry) -Compact } else { Write-FwJson ([pscustomobject]$dry) }
|
|
133
|
+
exit 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------- 3. execute
|
|
137
|
+
$res = Invoke-FwProcess -FilePath $cmd.file -Arguments $cmd.args `
|
|
138
|
+
-WorkingDirectory $(if ($cmd.cwd) { $cmd.cwd } else { $repoRoot }) `
|
|
139
|
+
-TimeoutMs $TimeoutMs -StdoutFile $logFile
|
|
140
|
+
# follow-up steps (e.g. cmake --build after configure)
|
|
141
|
+
$hasSteps = ($cmd -is [hashtable]) -and $cmd.ContainsKey('steps')
|
|
142
|
+
if ($res.exit_code -eq 0 -and $hasSteps) {
|
|
143
|
+
foreach ($step in $cmd.steps) {
|
|
144
|
+
$stepRes = Invoke-FwProcess -FilePath $step.file -Arguments $step.args `
|
|
145
|
+
-WorkingDirectory $(if ($step.cwd) { $step.cwd } else { $repoRoot }) `
|
|
146
|
+
-TimeoutMs $TimeoutMs -StdoutFile $logFile
|
|
147
|
+
$res = $stepRes
|
|
148
|
+
if ($res.exit_code -ne 0) { break }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
Copy-Item -LiteralPath $logFile -Destination $logCanonical -Force -ErrorAction SilentlyContinue
|
|
152
|
+
|
|
153
|
+
$sw.Stop()
|
|
154
|
+
$durationMs = $sw.ElapsedMilliseconds
|
|
155
|
+
|
|
156
|
+
$allLines = @(($res.stdout -split "`r?`n") + ($res.stderr -split "`r?`n") | Where-Object { $_ })
|
|
157
|
+
$diags = Get-FwDiagnostics -Lines $allLines -BaseDir (Resolve-Path $src).Path
|
|
158
|
+
$errors = @($diags | Where-Object severity -eq 'error').Count
|
|
159
|
+
$warnings = @($diags | Where-Object severity -eq 'warning').Count
|
|
160
|
+
$timedOut = $res.timed_out
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------- 4. artifacts
|
|
163
|
+
$manifest = Get-FwBackendArtifacts -Backend $backend -ArtifactDir $ArtifactDir -Src $src -BuildDir $BuildDir
|
|
164
|
+
$manifest.configuration = $Configuration
|
|
165
|
+
if ($projectFile) { $manifest.project_file = $projectFile } else { $manifest.project_file = $null }
|
|
166
|
+
$manifestJson = Join-Path $ArtifactDir 'artifacts.json'
|
|
167
|
+
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestJson -Encoding utf8
|
|
168
|
+
|
|
169
|
+
$artifact = if ($manifest.primary) { $manifest.primary.native_path } else { $null }
|
|
170
|
+
$artifactHash = $null
|
|
171
|
+
if ($artifact) {
|
|
172
|
+
$artifactHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
$git = Get-FwGitInfo -RepoRoot $repoRoot
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------- 5. result
|
|
178
|
+
$base = [ordered]@{
|
|
179
|
+
schema = 'firmware-build-result/v1'
|
|
180
|
+
configuration = $Configuration
|
|
181
|
+
backend = $backend
|
|
182
|
+
artifact = $artifact
|
|
183
|
+
artifact_sha256 = $artifactHash
|
|
184
|
+
warnings = $warnings
|
|
185
|
+
errors = $errors
|
|
186
|
+
duration_ms = $durationMs
|
|
187
|
+
log = $logCanonical
|
|
188
|
+
artifacts = $manifest
|
|
189
|
+
git = $git
|
|
190
|
+
generated_at = Get-FwTimestamp
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if ($timedOut) {
|
|
194
|
+
Exit-WithError -Class 'BUILD_ERROR' -Message "Build exceeded timeout (${TimeoutMs} ms) and was killed." -Detail $logFile
|
|
195
|
+
}
|
|
196
|
+
if ($res.exit_code -ne 0) {
|
|
197
|
+
$base.ok = $false
|
|
198
|
+
$base.artifact = $null
|
|
199
|
+
$base.diagnostics = @($diags | Select-Object file, line, col, severity, message)
|
|
200
|
+
if ($Json) { Write-FwJson ([pscustomobject]$base) -Compact } else { Write-FwJson ([pscustomobject]$base) }
|
|
201
|
+
exit 1
|
|
202
|
+
}
|
|
203
|
+
if (-not $artifact) {
|
|
204
|
+
Exit-WithError -Class 'ARTIFACT_NOT_FOUND' -Message "Build reported success but no $backend artifact was produced." -Detail $manifestJson
|
|
205
|
+
}
|
|
206
|
+
$base.ok = $true
|
|
207
|
+
if ($Json) { Write-FwJson ([pscustomobject]$base) -Compact } else { Write-FwJson ([pscustomobject]$base) }
|
|
208
|
+
exit 0
|