@typecad/framework-zephyr 1.0.0-alpha.14 → 1.0.0-alpha.16
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.
- package/README.md +22 -47
- package/dist/as-built.d.ts +33 -0
- package/dist/as-built.js +188 -0
- package/dist/async/timer-polyfill.js +1 -1
- package/dist/boardgen.d.ts +128 -0
- package/dist/boardgen.js +1366 -0
- package/dist/chips/board-overrides.d.ts +7 -0
- package/dist/chips/board-overrides.js +11 -0
- package/dist/chips/esp32s3.js +49 -0
- package/dist/chips/index.d.ts +6 -12
- package/dist/chips/index.js +18 -34
- package/dist/chips/resolve.d.ts +16 -2
- package/dist/chips/resolve.js +267 -10
- package/dist/chips/soc/.d.ts +2 -0
- package/dist/chips/soc/.js +129 -0
- package/dist/chips/soc/esp32.d.ts +2 -0
- package/dist/chips/soc/esp32.js +120 -0
- package/dist/chips/soc/esp32c3.d.ts +2 -0
- package/dist/chips/soc/esp32c3.js +90 -0
- package/dist/chips/soc/esp32c6.d.ts +2 -0
- package/dist/chips/soc/esp32c6.js +109 -0
- package/dist/chips/soc/esp32s3.d.ts +2 -0
- package/dist/chips/soc/esp32s3.js +189 -0
- package/dist/chips/soc/index.d.ts +2 -0
- package/dist/chips/soc/index.js +23 -0
- package/dist/chips/soc/nrf52840.d.ts +2 -0
- package/dist/chips/soc/nrf52840.js +130 -0
- package/dist/chips/soc/rp2040.d.ts +2 -0
- package/dist/chips/soc/rp2040.js +141 -0
- package/dist/chips/soc/rp2350a.d.ts +2 -0
- package/dist/chips/soc/rp2350a.js +145 -0
- package/dist/chips/soc/samd21g18a.d.ts +2 -0
- package/dist/chips/soc/samd21g18a.js +143 -0
- package/dist/chips/soc/stm32f411xe.d.ts +2 -0
- package/dist/chips/soc/stm32f411xe.js +251 -0
- package/dist/chips/types.d.ts +319 -0
- package/dist/chips/xiao-ble.js +21 -0
- package/dist/display/profiles.js +1 -1
- package/dist/doctor.js +38 -4
- package/dist/dt-config/custom-board.d.ts +23 -0
- package/dist/dt-config/custom-board.js +227 -0
- package/dist/dt-config/kconfig.d.ts +72 -1
- package/dist/dt-config/kconfig.js +115 -26
- package/dist/dt-config/overlay.js +548 -28
- package/dist/framework.manifest.d.ts +3 -3
- package/dist/framework.manifest.js +122 -185
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/lowering/adc.d.ts +20 -2
- package/dist/lowering/adc.js +121 -29
- package/dist/lowering/ble.js +31 -28
- package/dist/lowering/dac.js +9 -9
- package/dist/lowering/fs.js +18 -10
- package/dist/lowering/gpio.d.ts +2 -0
- package/dist/lowering/gpio.js +82 -5
- package/dist/lowering/http.js +39 -14
- package/dist/lowering/hwtimer.d.ts +4 -0
- package/dist/lowering/hwtimer.js +35 -10
- package/dist/lowering/i2c.js +46 -54
- package/dist/lowering/index.d.ts +5 -5
- package/dist/lowering/index.js +15 -16
- package/dist/lowering/interrupts.d.ts +22 -5
- package/dist/lowering/interrupts.js +104 -22
- package/dist/lowering/mqtt.js +3 -5
- package/dist/lowering/power.js +0 -4
- package/dist/lowering/preferences.js +49 -77
- package/dist/lowering/pulse.js +0 -31
- package/dist/lowering/pwm.d.ts +12 -1
- package/dist/lowering/pwm.js +78 -14
- package/dist/lowering/random.js +3 -3
- package/dist/lowering/sensor.d.ts +37 -0
- package/dist/lowering/sensor.js +119 -0
- package/dist/lowering/spi.d.ts +12 -0
- package/dist/lowering/spi.js +65 -40
- package/dist/lowering/thread.d.ts +14 -0
- package/dist/lowering/thread.js +55 -0
- package/dist/lowering/timing.js +20 -46
- package/dist/lowering/tone.js +16 -4
- package/dist/lowering/uart.d.ts +13 -0
- package/dist/lowering/uart.js +72 -51
- package/dist/lowering/usb.d.ts +34 -0
- package/dist/lowering/usb.js +206 -0
- package/dist/lowering/wdt.d.ts +1 -1
- package/dist/lowering/wdt.js +17 -7
- package/dist/lowering/wifi.d.ts +3 -2
- package/dist/lowering/wifi.js +97 -111
- package/dist/sdk/board-catalog-sync.d.ts +16 -0
- package/dist/sdk/board-catalog-sync.js +41 -0
- package/dist/sdk/board-data.generated.d.ts +2 -0
- package/dist/sdk/board-data.generated.js +4 -0
- package/dist/sdk/catalog-walker.d.ts +90 -0
- package/dist/sdk/catalog-walker.js +682 -0
- package/dist/sdk/dts-reader.d.ts +83 -0
- package/dist/sdk/dts-reader.js +596 -0
- package/dist/strategy.d.ts +136 -17
- package/dist/strategy.js +839 -222
- package/dist/toolchain/bossac-touch.d.ts +18 -0
- package/dist/toolchain/bossac-touch.js +88 -0
- package/dist/toolchain/compat.d.ts +2 -2
- package/dist/toolchain/compat.js +8 -3
- package/dist/toolchain/debug-config.d.ts +21 -26
- package/dist/toolchain/debug-config.js +226 -76
- package/dist/toolchain/env-check.d.ts +4 -4
- package/dist/toolchain/env-check.js +5 -5
- package/dist/toolchain/index.d.ts +115 -1
- package/dist/toolchain/index.js +680 -71
- package/dist/toolchain/scaffold.js +39 -7
- package/dist/toolchain/west-discover.d.ts +4 -0
- package/dist/toolchain/west-discover.js +17 -6
- package/installer/CHANGELOG.md +473 -0
- package/installer/README.md +328 -0
- package/installer/environment.yml +46 -0
- package/installer/etc/conda/activate.d/zephyr.bat +7 -0
- package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
- package/installer/etc/conda/activate.d/zephyr.sh +20 -0
- package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
- package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
- package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
- package/installer/install.mjs +580 -0
- package/installer/install.ps1 +619 -0
- package/installer/install.sh +310 -0
- package/installer/lib/detect-platform.sh +62 -0
- package/installer/lib/fetch-sdk.sh +304 -0
- package/installer/lib/init-workspace.sh +153 -0
- package/installer/lib/write-activation.sh +76 -0
- package/installer/patches/tfm-c-include-separators.patch +19 -0
- package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
- package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
- package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
- package/installer/templates/project/.vscode/settings.json +30 -0
- package/installer/templates/project/README.md +58 -0
- package/installer/versions.env +110 -0
- package/package.json +15 -15
- package/src/as-built.ts +206 -0
- package/src/boardgen.ts +1428 -0
- package/src/chips/index.ts +35 -54
- package/src/chips/resolve.ts +285 -10
- package/src/chips/types.ts +325 -1
- package/src/display/profiles.ts +1 -1
- package/src/doctor.ts +110 -77
- package/src/dt-config/custom-board.ts +325 -0
- package/src/dt-config/kconfig.ts +165 -18
- package/src/dt-config/overlay.ts +537 -25
- package/src/framework.manifest.ts +123 -185
- package/src/index.ts +57 -41
- package/src/lowering/adc.ts +222 -118
- package/src/lowering/ble.ts +32 -28
- package/src/lowering/dac.ts +82 -82
- package/src/lowering/fs.ts +18 -10
- package/src/lowering/gpio.ts +242 -155
- package/src/lowering/http.ts +512 -488
- package/src/lowering/hwtimer.ts +139 -101
- package/src/lowering/i2c.ts +116 -126
- package/src/lowering/index.ts +91 -93
- package/src/lowering/interrupts.ts +113 -21
- package/src/lowering/mqtt.ts +3 -5
- package/src/lowering/preferences.ts +327 -354
- package/src/lowering/pwm.ts +167 -99
- package/src/lowering/random.ts +3 -3
- package/src/lowering/sensor.ts +155 -0
- package/src/lowering/spi.ts +164 -124
- package/src/lowering/thread.ts +63 -0
- package/src/lowering/timing.ts +46 -72
- package/src/lowering/uart.ts +73 -51
- package/src/lowering/usb.ts +221 -0
- package/src/lowering/wdt.ts +95 -84
- package/src/lowering/wifi.ts +97 -112
- package/src/sdk/board-catalog-sync.ts +62 -0
- package/src/strategy.ts +848 -237
- package/src/toolchain/bossac-touch.ts +112 -0
- package/src/toolchain/compat.ts +8 -3
- package/src/toolchain/debug-config.ts +676 -522
- package/src/toolchain/env-check.ts +9 -9
- package/src/toolchain/index.ts +1359 -645
- package/src/toolchain/scaffold.ts +38 -7
- package/src/toolchain/west-discover.ts +19 -6
- package/src/async/timer-polyfill.ts +0 -107
- package/src/chips/esp32.ts +0 -74
- package/src/chips/esp32s3.ts +0 -57
- package/src/chips/xiao-ble.ts +0 -82
- package/src/lowering/power.ts +0 -70
- package/src/lowering/pulse.ts +0 -93
- package/src/lowering/tone.ts +0 -60
- package/src/lowering/worker-backing.ts +0 -94
- package/src/lowering/worker.ts +0 -17
package/src/toolchain/index.ts
CHANGED
|
@@ -1,645 +1,1359 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// FrameworkToolchain impl for Zephyr (west / CMake)
|
|
3
|
-
//
|
|
4
|
-
// compile() scaffolds the project (idempotent) then runs `west build -b <board>`.
|
|
5
|
-
// upload() runs `west flash`. monitor() runs a best-effort serial monitor.
|
|
6
|
-
//
|
|
7
|
-
// west resolution goes through westSpawn(), which finds a usable west without
|
|
8
|
-
// requiring the user to have activated the Zephyr Python venv — it prefers
|
|
9
|
-
// `<python> -m west` (robust cross-platform form) and injects ZEPHYR_BASE when
|
|
10
|
-
// a SDK root is discovered. See west-discover.ts / west-spawn.ts.
|
|
11
|
-
//
|
|
12
|
-
// The board target is carried via frameworkData.buildTarget (populated as
|
|
13
|
-
// ToolchainOptions.buildTarget by the cuttlefish CLI), defaulting to the
|
|
14
|
-
// framework's canonical MVP target (xiao_ble).
|
|
15
|
-
//
|
|
16
|
-
// Mirrors framework-esp32/src/toolchain/index.ts structure: projectRoot derived
|
|
17
|
-
// from outputDir, prepare is a no-op (scaffold happens in compile when the
|
|
18
|
-
// target is known), GCC errors parsed via the shared parseCompileErrors helper.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { basename, dirname, join } from 'node:path';
|
|
23
|
-
import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
-
import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
|
|
25
|
-
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
26
|
-
import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Resolve the chip for a build the same way the strategy does at emit time —
|
|
44
|
-
* from the board constants the transpile persisted next to the emitted
|
|
45
|
-
* source (`board-constants.json`)
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
const
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// FrameworkToolchain impl for Zephyr (west / CMake)
|
|
3
|
+
//
|
|
4
|
+
// compile() scaffolds the project (idempotent) then runs `west build -b <board>`.
|
|
5
|
+
// upload() runs `west flash`. monitor() runs a best-effort serial monitor.
|
|
6
|
+
//
|
|
7
|
+
// west resolution goes through westSpawn(), which finds a usable west without
|
|
8
|
+
// requiring the user to have activated the Zephyr Python venv — it prefers
|
|
9
|
+
// `<python> -m west` (robust cross-platform form) and injects ZEPHYR_BASE when
|
|
10
|
+
// a SDK root is discovered. See west-discover.ts / west-spawn.ts.
|
|
11
|
+
//
|
|
12
|
+
// The board target is carried via frameworkData.buildTarget (populated as
|
|
13
|
+
// ToolchainOptions.buildTarget by the cuttlefish CLI), defaulting to the
|
|
14
|
+
// framework's canonical MVP target (xiao_ble).
|
|
15
|
+
//
|
|
16
|
+
// Mirrors framework-esp32/src/toolchain/index.ts structure: projectRoot derived
|
|
17
|
+
// from outputDir, prepare is a no-op (scaffold happens in compile when the
|
|
18
|
+
// target is known), GCC errors parsed via the shared parseCompileErrors helper.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { basename, delimiter, dirname, join } from 'node:path';
|
|
23
|
+
import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
|
|
25
|
+
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
26
|
+
import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
|
|
27
|
+
import { parseZephyrDts, asBuiltJson } from '../as-built.js';
|
|
28
|
+
import { westSpawn, buildEnv } from './west-spawn.js';
|
|
29
|
+
import { discoverWest } from './west-discover.js';
|
|
30
|
+
import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
|
|
31
|
+
import { bossacTouchReset } from './bossac-touch.js';
|
|
32
|
+
import { ZephyrStrategy } from '../strategy.js';
|
|
33
|
+
import { generateOverlay, type DisplayWiring, type TouchWiring, type OverlayDiagnostic } from '../dt-config/overlay.js';
|
|
34
|
+
import { generateCustomBoard } from '../dt-config/custom-board.js';
|
|
35
|
+
import { NO_BOARD_CHIP } from '../chips/index.js';
|
|
36
|
+
import { resolveChipFromBoard } from '../chips/resolve.js';
|
|
37
|
+
import type { ZephyrChipDescriptor } from '../chips/types.js';
|
|
38
|
+
import { pwmDtAliasToken } from '../lowering/pwm.js';
|
|
39
|
+
import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
|
|
40
|
+
import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the chip for a build the same way the strategy does at emit time —
|
|
44
|
+
* from the board constants the transpile persisted next to the emitted
|
|
45
|
+
* source (`board-constants.json`). There is no registry fallback: a build
|
|
46
|
+
* whose constants did not persist stays NO_BOARD_CHIP, exactly like the
|
|
47
|
+
* emit-time path.
|
|
48
|
+
*/
|
|
49
|
+
function chipForBuild(projectRoot: string, board: string): ZephyrChipDescriptor {
|
|
50
|
+
try {
|
|
51
|
+
// The transpile writes the constants into the emit outDir, which is
|
|
52
|
+
// <projectRoot>/src for the standard layout (basename 'src' collapsed by
|
|
53
|
+
// projectRootFromOptions); check both locations.
|
|
54
|
+
const bcPath = [join(projectRoot, 'src', 'board-constants.json'), join(projectRoot, 'board-constants.json')]
|
|
55
|
+
.find(p => existsSync(p));
|
|
56
|
+
if (bcPath) {
|
|
57
|
+
const raw = JSON.parse(readFileSync(bcPath, 'utf8')) as Record<string, string | number | boolean>;
|
|
58
|
+
const bc = new Map(Object.entries(raw));
|
|
59
|
+
const fromBoard = resolveChipFromBoard(bc);
|
|
60
|
+
if (fromBoard) return fromBoard;
|
|
61
|
+
}
|
|
62
|
+
} catch { /* constants unreadable — no board resolved */ }
|
|
63
|
+
return NO_BOARD_CHIP;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* HAL pins the emitted sources read via adc.* — scanned from the emitted
|
|
68
|
+
* `__tc_adc<N>_setup()` call sites (N = channel index, mapped back to the HAL
|
|
69
|
+
* pin via the chip descriptor). Feeds the overlay's ADC pinctrl rewrite: on
|
|
70
|
+
* SoCs that mux ADC pads via pinctrl (STM32), only the read channels are
|
|
71
|
+
* switched to analog mode.
|
|
72
|
+
*/
|
|
73
|
+
function scanAdcReadPins(src: string, chip: ZephyrChipDescriptor): number[] {
|
|
74
|
+
const pins: number[] = [];
|
|
75
|
+
// Two used-signal forms:
|
|
76
|
+
// - `__tc_adc<N>_setup()` / `__tc_adc_<ctrl>_<N>_setup()` CALL SITES
|
|
77
|
+
// (empty parens — the adcInitLines definitions have `(void)` and would
|
|
78
|
+
// otherwise mark every descriptor channel as used).
|
|
79
|
+
// - `__tc_adct<pin>_done` lazy-guard vars — the thin-ADC read lowering's
|
|
80
|
+
// inline setup (families without pinctrl groups, e.g. ESP32 SARADC,
|
|
81
|
+
// never emit the setup-function form at all).
|
|
82
|
+
// Channel indices are unique per CONTROLLER, so the labeled form resolves
|
|
83
|
+
// (controller, channel) before mapping back to the HAL pin.
|
|
84
|
+
for (const m of src.matchAll(/__tc_adc(?:(\w+?)_)?(\d+)_setup\(\)/g)) {
|
|
85
|
+
const ch = Number(m[2]);
|
|
86
|
+
const label = m[1];
|
|
87
|
+
const c = chip.adc?.channels.find(
|
|
88
|
+
(x) => x.channel === ch && (x.controller ?? chip.adc?.nodeLabel) === (label ?? chip.adc?.nodeLabel),
|
|
89
|
+
);
|
|
90
|
+
if (c && !pins.includes(c.pin)) pins.push(c.pin);
|
|
91
|
+
}
|
|
92
|
+
for (const m of src.matchAll(/__tc_adct(\d+)_done/g)) {
|
|
93
|
+
const pin = Number(m[1]);
|
|
94
|
+
if (chip.adc?.channels.some((x) => x.pin === pin) && !pins.includes(pin)) pins.push(pin);
|
|
95
|
+
}
|
|
96
|
+
return pins;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* HAL pins the emitted sources drive with dac.* — the DAC lowering's lazy
|
|
101
|
+
* per-pin setup guard is `__tc_dact<pin>_done` (lowering/dac.ts), so
|
|
102
|
+
* var-presence is the authoritative used-signal. Feeds the overlay's DAC
|
|
103
|
+
* pinctrl gating (same pattern as scanAdcReadPins).
|
|
104
|
+
*/
|
|
105
|
+
function scanDacWritePins(src: string): number[] {
|
|
106
|
+
const pins: number[] = [];
|
|
107
|
+
for (const m of src.matchAll(/__tc_dact(\d+)_done/g)) {
|
|
108
|
+
const pin = Number(m[1]);
|
|
109
|
+
if (!pins.includes(pin)) pins.push(pin);
|
|
110
|
+
}
|
|
111
|
+
return pins;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* HAL pins the emitted sources drive with pwm.* — the emitted source
|
|
116
|
+
* references each used spec as `__tc_pwm_<alias token>` (pwmVarName in
|
|
117
|
+
* lowering/pwm.ts), and the lowering only emits specs for driven pins, so
|
|
118
|
+
* var-presence is the authoritative signal. Feeds the overlay's per-pin
|
|
119
|
+
* pwm-leds gating (no dead DT channels).
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* Inline-override markers (the escape hatch): the adc/pwm lowerings emit
|
|
123
|
+
* `/* cuttlefish-user-facts: <kind> pin=N [device=X] [pinctrl=P] channel=C */`
|
|
124
|
+
* comments when a construction carries routing overrides. Merged into the
|
|
125
|
+
* chip so the overlay synthesis + used-pin scans treat them as facts —
|
|
126
|
+
* the transpiler cannot create DT nodes, but this regen can.
|
|
127
|
+
*/
|
|
128
|
+
function applyUserFactMarkers(chip: ZephyrChipDescriptor, src: string): ZephyrChipDescriptor {
|
|
129
|
+
const adcAdds: { pin: number; channel: number; controller?: string; pinctrl?: string }[] = [];
|
|
130
|
+
const pwmAdds: { pin: number; controller: string; channel: number }[] = [];
|
|
131
|
+
for (const m of src.matchAll(/\/\* cuttlefish-user-facts: (adc|pwm) ([^*]*?) \*\//g)) {
|
|
132
|
+
const kind = m[1];
|
|
133
|
+
const fields = new Map<string, string>();
|
|
134
|
+
for (const kv of m[2]!.split(/\s+/).filter(Boolean)) {
|
|
135
|
+
const eq = kv.indexOf('=');
|
|
136
|
+
if (eq > 0) fields.set(kv.slice(0, eq), kv.slice(eq + 1));
|
|
137
|
+
}
|
|
138
|
+
const pin = Number(fields.get('pin'));
|
|
139
|
+
const channel = Number(fields.get('channel'));
|
|
140
|
+
if (!Number.isFinite(pin) || !Number.isFinite(channel)) continue;
|
|
141
|
+
if (kind === 'adc') {
|
|
142
|
+
adcAdds.push({
|
|
143
|
+
pin,
|
|
144
|
+
channel,
|
|
145
|
+
...(fields.get('device') ? { controller: fields.get('device') } : {}),
|
|
146
|
+
...(fields.get('pinctrl') ? { pinctrl: fields.get('pinctrl') } : {}),
|
|
147
|
+
});
|
|
148
|
+
} else if (fields.get('controller')) {
|
|
149
|
+
pwmAdds.push({ pin, channel, controller: fields.get('controller')! });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (adcAdds.length === 0 && pwmAdds.length === 0) return chip;
|
|
153
|
+
const adc = chip.adc
|
|
154
|
+
? chip.adc
|
|
155
|
+
: { nodeLabel: adcAdds.find((a) => !a.controller)?.controller ?? 'adc', resolution: 12, vrefMv: 3000, channels: [] };
|
|
156
|
+
const adcChannels = [...adc.channels];
|
|
157
|
+
for (const a of adcAdds) {
|
|
158
|
+
const existing = adcChannels.findIndex((c) => c.pin === a.pin);
|
|
159
|
+
if (existing >= 0) adcChannels.splice(existing, 1);
|
|
160
|
+
adcChannels.push({ pin: a.pin, channel: a.channel, ...(a.controller ? { controller: a.controller } : {}), ...(a.pinctrl ? { pinctrl: a.pinctrl } : {}) });
|
|
161
|
+
}
|
|
162
|
+
const pwmSpecs = [...(chip.pwm?.specs ?? [])];
|
|
163
|
+
for (const p of pwmAdds) {
|
|
164
|
+
const existing = pwmSpecs.findIndex((s) => s.pin === p.pin);
|
|
165
|
+
if (existing >= 0) pwmSpecs.splice(existing, 1);
|
|
166
|
+
pwmSpecs.push({ pin: p.pin, controller: p.controller, channel: p.channel });
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...chip,
|
|
170
|
+
adc: { ...adc, channels: adcChannels },
|
|
171
|
+
pwm: { ...(chip.pwm ?? { specs: [] }), specs: pwmSpecs },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function scanPwmUsedPins(src: string, chip: ZephyrChipDescriptor): number[] { const pins = (chip.pwm?.specs ?? [])
|
|
176
|
+
.filter((s) => src.includes(`__tc_pwm_${pwmDtAliasToken(s)}`))
|
|
177
|
+
.map((s) => s.pin);
|
|
178
|
+
// Matrix pins (ESP32 LEDC) have no static specs — recover the driven pins
|
|
179
|
+
// from the same alias-var presence signal (`__tc_pwm_tc_pwm<N>`), keeping
|
|
180
|
+
// only pins the descriptor's matrix allows (the regex grabs the full
|
|
181
|
+
// number, so pin 4 never matches a reference to pin 45).
|
|
182
|
+
const matrix = chip.pwm?.matrix;
|
|
183
|
+
if (matrix) {
|
|
184
|
+
const present = new Set<number>();
|
|
185
|
+
for (const match of src.matchAll(/__tc_pwm_tc_pwm(\d+)/g)) {
|
|
186
|
+
present.add(Number(match[1]));
|
|
187
|
+
}
|
|
188
|
+
for (const pin of matrix.pins) {
|
|
189
|
+
if (present.has(pin)) pins.push(pin);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return pins;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Bus controller indexes the emitted sources actually reference — the shim
|
|
197
|
+
* declares one `__tc_<bus><N>_dev` state block per used instance (gated by
|
|
198
|
+
* collectUsedBusIndices at transpile time), so var-presence is the
|
|
199
|
+
* authoritative signal. The overlay enables only these controllers: an
|
|
200
|
+
* enabled-but-unused one claims its default pins (i2c0's GP4/GP5 on the
|
|
201
|
+
* Pico) which a program driving the OTHER controller may want as GPIO.
|
|
202
|
+
* Empty list (no state blocks — e.g. display/touch composites that use the
|
|
203
|
+
* driver API directly) means "no signal"; the caller then passes undefined
|
|
204
|
+
* so the overlay enables every declared controller, preserving old behavior.
|
|
205
|
+
*/
|
|
206
|
+
function scanUsedBusInstances(
|
|
207
|
+
src: string,
|
|
208
|
+
controllers: readonly { nodeLabel: string }[] | undefined,
|
|
209
|
+
bus: 'i2c' | 'spi' | 'uart',
|
|
210
|
+
): number[] | undefined {
|
|
211
|
+
if (!controllers) return undefined;
|
|
212
|
+
const used: number[] = [];
|
|
213
|
+
for (let i = 0; i < controllers.length; i++) {
|
|
214
|
+
if (src.includes(`__tc_${bus}${i}_dev`)) used.push(i);
|
|
215
|
+
}
|
|
216
|
+
return used.length > 0 ? used : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Distinct DT-bound sensors the emitted sources reference — the lowering's
|
|
221
|
+
* state blocks name each one `__tc_sensor_<part>_i2c<N>_0x<addr>_dev`
|
|
222
|
+
* (lowering/sensor.ts sensorNames), so var-presence is the authoritative
|
|
223
|
+
* signal. Feeds the overlay's DT child nodes; the part group is greedy so a
|
|
224
|
+
* compatible containing '_i2c<N>_' still resolves to the longest part match.
|
|
225
|
+
*/
|
|
226
|
+
export interface ScannedSensorPart {
|
|
227
|
+
part: string; busIndex: number; port: number; busKind: 'i2c' | 'spi';
|
|
228
|
+
spiHz: number; spiMode: number; alertPin: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ScannedSpiTarget {
|
|
232
|
+
busIndex: number; cs: number; hz: number; mode: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Scan the emitted source for thin SPI targets (hal/spi-target.ts): the
|
|
236
|
+
* spi_dt_spec state block's tc-spit-cfg comment carries the construction
|
|
237
|
+
* facts, the same channel tc-sensor-cfg uses. */
|
|
238
|
+
export function scanSpiTargets(src: string): ScannedSpiTarget[] {
|
|
239
|
+
const out = new Map<string, ScannedSpiTarget>();
|
|
240
|
+
for (const m of src.matchAll(/tc-spit-cfg: tc_spit_spi(\d+)_cs(\d+) hz=(\d+) mode=(\d+)/g)) {
|
|
241
|
+
const ref: ScannedSpiTarget = { busIndex: parseInt(m[1], 10), cs: parseInt(m[2], 10), hz: parseInt(m[3], 10), mode: parseInt(m[4], 10) };
|
|
242
|
+
out.set(`${ref.busIndex}|${ref.cs}`, ref);
|
|
243
|
+
}
|
|
244
|
+
return [...out.values()];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function scanSensorParts(src: string): ScannedSensorPart[] {
|
|
248
|
+
const out = new Map<string, ScannedSensorPart>();
|
|
249
|
+
for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+)_dev\b/g)) {
|
|
250
|
+
const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
|
|
251
|
+
const ref: ScannedSensorPart = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2] as 'i2c' | 'spi', spiHz: 0, spiMode: 0, alertPin: -1 };
|
|
252
|
+
out.set(`${ref.part}|${ref.busKind}${ref.busIndex}|${ref.port}`, ref);
|
|
253
|
+
}
|
|
254
|
+
// Construction facts ride the state block's config comment — merge by
|
|
255
|
+
// nodelabel so the scanner stays the single source for the overlay.
|
|
256
|
+
for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+) hz=(\d+) mode=(\d+) alert=(-?\d+)/g)) {
|
|
257
|
+
const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
|
|
258
|
+
const key = `${m[1]}|${m[2]}${m[3]}|${port}`;
|
|
259
|
+
const existing = out.get(key);
|
|
260
|
+
if (existing) {
|
|
261
|
+
existing.spiHz = parseInt(m[5], 10);
|
|
262
|
+
existing.spiMode = parseInt(m[6], 10);
|
|
263
|
+
existing.alertPin = parseInt(m[7], 10);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return [...out.values()];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function targetFromOptions(o: ToolchainOptions): string {
|
|
270
|
+
// The cuttlefish CLI populates ToolchainOptions.buildTarget from the
|
|
271
|
+
// config's board: (frameworkData.buildTarget for board-less projects).
|
|
272
|
+
// Accept frameworkData.target as an alias. No silent default: building for
|
|
273
|
+
// a wrong hard-coded board is the split-brain trap.
|
|
274
|
+
const fcTarget = (o.frameworkConfig?.target as string | undefined);
|
|
275
|
+
const board = (o.buildTarget as string | undefined) ?? fcTarget;
|
|
276
|
+
if (!board) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
'No build target: set board: in cuttlefish.config.ts (or frameworkData.buildTarget for custom-board projects).',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return board;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Derive the Zephyr project root from the cuttlefish-emitted source path.
|
|
286
|
+
*
|
|
287
|
+
* Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
|
|
288
|
+
* `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
|
|
289
|
+
* For Zephyr, the project root is the parent of `src/` — one level above
|
|
290
|
+
* `outputDir`. Detect that shape and adjust; otherwise fall back to `outputDir`.
|
|
291
|
+
*/
|
|
292
|
+
export function projectRootFromOptions(o: ToolchainOptions): string {
|
|
293
|
+
const outDir = o.outputDir;
|
|
294
|
+
if (basename(outDir) === 'src') {
|
|
295
|
+
return dirname(outDir);
|
|
296
|
+
}
|
|
297
|
+
return outDir;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* west build timeout. Zephyr's first build fetches the toolchain modules and
|
|
302
|
+
* configures CMake/Ninja, which can take several minutes; allow generous headroom.
|
|
303
|
+
*/
|
|
304
|
+
const BUILD_TIMEOUT_MS = 600_000;
|
|
305
|
+
const FLASH_TIMEOUT_MS = 120_000;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Build the `west flash` argument list for a board.
|
|
309
|
+
*
|
|
310
|
+
* Runner selection: each board's board.cmake declares a sensible default flash
|
|
311
|
+
* runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
|
|
312
|
+
* flash` resolves it automatically. The framework only intervenes where the
|
|
313
|
+
* board default needs an argument it can't infer:
|
|
314
|
+
* - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
|
|
315
|
+
* - ESP32 boards forward the port via `--esp-device` (esptool reads the
|
|
316
|
+
* device from it); board.cmake still picks the runner.
|
|
317
|
+
* - Every other board trusts the board.cmake default. Previously this forced
|
|
318
|
+
* `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
|
|
319
|
+
* default is not nrfjprog (xiao_ble defaults to nrfutil) and required
|
|
320
|
+
* Nordic J-Link tools that a USB-bootloader board does not have.
|
|
321
|
+
*
|
|
322
|
+
* Exported (pure) so the runner-selection contract is unit-testable without
|
|
323
|
+
* spawning west.
|
|
324
|
+
*/
|
|
325
|
+
/**
|
|
326
|
+
* Resolve HOW this build attaches to the board for flashing OR debugging:
|
|
327
|
+
* the friendly `zephyr.probe` id from the board's probeMethods table (quirks
|
|
328
|
+
* included), or the raw `zephyr.runner` escape hatch. Exported (pure) so the
|
|
329
|
+
* selection contract is unit-testable without spawning west.
|
|
330
|
+
*
|
|
331
|
+
* Rules:
|
|
332
|
+
* - `probe` + `runner` together is an error (two ways of saying it — pick one).
|
|
333
|
+
* - An unknown `probe` id is an error listing what the board supports; a board
|
|
334
|
+
* with no probeMethods table gets a hint to use `runner` directly.
|
|
335
|
+
* - purpose 'debug': the chosen method must be debug-capable (`debug` is not
|
|
336
|
+
* false — a bootloader is not a debugger). Non-capable or unknown ids list
|
|
337
|
+
* the debug-capable methods.
|
|
338
|
+
* - User `runnerArgs` are appended AFTER the method's args, so they can
|
|
339
|
+
* override the method's baked-in flags (argparse takes the last value).
|
|
340
|
+
*/
|
|
341
|
+
export type ProbeResolution =
|
|
342
|
+
| { ok: true; runner?: string; args: string[] }
|
|
343
|
+
| { ok: false; error: string };
|
|
344
|
+
|
|
345
|
+
export function resolveProbeMethod(
|
|
346
|
+
zc: Record<string, unknown> | undefined,
|
|
347
|
+
chip: ZephyrChipDescriptor,
|
|
348
|
+
purpose: 'flash' | 'debug' = 'flash',
|
|
349
|
+
): ProbeResolution {
|
|
350
|
+
const probe = zc?.probe as string | undefined;
|
|
351
|
+
const runner = zc?.runner as string | undefined;
|
|
352
|
+
const userArgs = (zc?.runnerArgs as string[] | undefined) ?? [];
|
|
353
|
+
|
|
354
|
+
if (probe && runner) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
error:
|
|
358
|
+
`cuttlefish.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
|
|
359
|
+
`They are two ways to choose the probe method — remove one.`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (probe) {
|
|
364
|
+
const methods = chip.probeMethods ?? [];
|
|
365
|
+
const method = methods.find((m) => m.id === probe);
|
|
366
|
+
if (!method) {
|
|
367
|
+
const listAll = methods
|
|
368
|
+
.map((m) => `${m.id} (${m.runner}${m.description ? ` — ${m.description}` : ''})`)
|
|
369
|
+
.join('; ');
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
error: methods.length > 0
|
|
373
|
+
? `Unknown probe method '${probe}' for ${chip.id}. Supported: ${listAll}.`
|
|
374
|
+
: `This board (${chip.id}) ships no probe-method table, so 'zephyr.probe' cannot resolve '${probe}'. ` +
|
|
375
|
+
`Use the raw 'zephyr.runner' field instead (run 'west flash --context' in the build dir for options).`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (purpose === 'debug' && method.debug === false) {
|
|
379
|
+
const debuggable = methods.filter((m) => m.debug !== false).map((m) => m.id).join(', ');
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
error:
|
|
383
|
+
`The '${probe}' method cannot debug ${chip.id} — a bootloader is not a debugger. ` +
|
|
384
|
+
`Debug-capable methods: ${debuggable || '(none — this board needs an external probe)'}.`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
runner: method.runner,
|
|
390
|
+
args: [...(method.args ?? []), ...userArgs],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { ok: true, runner, args: userArgs };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run an openocd session against the board's probe config — the shared
|
|
399
|
+
* engine for the pre-flash quiesce and the post-flash SYSRESETREQ (see the
|
|
400
|
+
* call sites in upload()). The config is the probe method's verbatim
|
|
401
|
+
* debugCfg from the board catalog (the same lines `west debug` uses);
|
|
402
|
+
* `commands` are appended after `-f <cfg> -c init`. Returns a flash note on
|
|
403
|
+
* success, undefined when skipped or failed (best-effort by design).
|
|
404
|
+
*/
|
|
405
|
+
/**
|
|
406
|
+
* Resolve the openocd binary (plus script search dirs) for the probe session.
|
|
407
|
+
*
|
|
408
|
+
* Resolution order mirrors how west's own openocd runner finds the binary, so
|
|
409
|
+
* the session and `west flash` drive the SAME openocd:
|
|
410
|
+
* 1. $OPENOCD — the variable Zephyr's CMake reads into the build cache.
|
|
411
|
+
* 2. A Zephyr-SDK-hosted install ($ZEPHYR_SDK_INSTALL_DIR, else the
|
|
412
|
+
* discovered west install's SDK). Layout differs by SDK generation —
|
|
413
|
+
* hosttools/openocd/share/openocd/scripts vs hosttools/openocd/scripts —
|
|
414
|
+
* so both script dirs are collected.
|
|
415
|
+
* 3. PATH — Linux distro / conda / micromamba installs (a micromamba-env
|
|
416
|
+
* openocd is the common Linux setup; the SDK layout check alone made the
|
|
417
|
+
* deterministic probe session unreachable there, silently dropping every
|
|
418
|
+
* Linux flash to the racy `west flash` fallback).
|
|
419
|
+
*
|
|
420
|
+
* A non-SDK binary resolves with no explicit -s dirs: it finds its own
|
|
421
|
+
* interface/target scripts via its compiled-in search path. Returns undefined
|
|
422
|
+
* when no openocd can be found (the caller then uses the west fallback).
|
|
423
|
+
*
|
|
424
|
+
* Exported (pure) so the resolution contract is unit-testable without
|
|
425
|
+
* spawning openocd.
|
|
426
|
+
*/
|
|
427
|
+
export interface SessionOpenOcd {
|
|
428
|
+
readonly exe: string;
|
|
429
|
+
readonly searchDirs: readonly string[];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function resolveSessionOpenOcd(
|
|
433
|
+
env: {
|
|
434
|
+
OPENOCD?: string | undefined;
|
|
435
|
+
ZEPHYR_SDK_INSTALL_DIR?: string | undefined;
|
|
436
|
+
PATH?: string | undefined;
|
|
437
|
+
} = process.env,
|
|
438
|
+
sdkInstallDir?: string,
|
|
439
|
+
): SessionOpenOcd | undefined {
|
|
440
|
+
const exeName = process.platform === 'win32' ? 'openocd.exe' : 'openocd';
|
|
441
|
+
if (env.OPENOCD && existsSync(env.OPENOCD)) {
|
|
442
|
+
return { exe: env.OPENOCD, searchDirs: [] };
|
|
443
|
+
}
|
|
444
|
+
const sdkRoot = env.ZEPHYR_SDK_INSTALL_DIR || sdkInstallDir;
|
|
445
|
+
if (sdkRoot) {
|
|
446
|
+
const exe = join(sdkRoot, 'hosttools', 'openocd', 'bin', exeName);
|
|
447
|
+
if (existsSync(exe)) {
|
|
448
|
+
const searchDirs = [
|
|
449
|
+
join(sdkRoot, 'hosttools', 'openocd', 'share', 'openocd', 'scripts'),
|
|
450
|
+
join(sdkRoot, 'hosttools', 'openocd', 'scripts'),
|
|
451
|
+
].filter((d) => existsSync(d));
|
|
452
|
+
return { exe, searchDirs };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
456
|
+
if (!dir) continue;
|
|
457
|
+
const candidate = join(dir, exeName);
|
|
458
|
+
if (existsSync(candidate)) return { exe: candidate, searchDirs: [] };
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function openocdProbeSession(
|
|
464
|
+
buildDir: string,
|
|
465
|
+
zc: Record<string, unknown> | undefined,
|
|
466
|
+
chip: ZephyrChipDescriptor,
|
|
467
|
+
commands: readonly string[],
|
|
468
|
+
): string | undefined {
|
|
469
|
+
// Config resolution — two sources, in order:
|
|
470
|
+
// 1. The named probe method's verbatim debugCfg from the board catalog
|
|
471
|
+
// (written to a temp cfg), when zephyr.probe names a method that has
|
|
472
|
+
// one.
|
|
473
|
+
// 2. The board's own support/openocd.cfg in the Zephyr tree — the exact
|
|
474
|
+
// config `west flash` resolves for the openocd runner. This covers
|
|
475
|
+
// raw `zephyr.runner: 'openocd'` (no named probe) and probe methods
|
|
476
|
+
// that ship no debugCfg of their own.
|
|
477
|
+
const probeId = zc?.probe as string | undefined;
|
|
478
|
+
const method = chip.probeMethods?.find((m) => m.id === probeId);
|
|
479
|
+
const cfgLines = method?.debugCfg;
|
|
480
|
+
// Session reset policy: `reset_config none`. The session's resets are
|
|
481
|
+
// core-domain by design — vector-catch halt before the flash write,
|
|
482
|
+
// SYSRESETREQ to boot — so they must not depend on the SRST pin. Boards
|
|
483
|
+
// like the WeAct Black Pill don't break NRST out at all: under the board
|
|
484
|
+
// cfg's `srst_only`, every `reset` asserts a pin that reaches nothing
|
|
485
|
+
// (the target never resets, `reset halt` catches the core mid-app in
|
|
486
|
+
// dirty state and the flash algorithm times out) while the probe's
|
|
487
|
+
// floating SRST sense reports phantom "external reset detected" events
|
|
488
|
+
// that leave the session's halt state inconsistent. Method-declared west
|
|
489
|
+
// quirks (`--cmd-pre-init=…`) are appended after and override the
|
|
490
|
+
// default for boards whose facts carry one.
|
|
491
|
+
const preInit = [
|
|
492
|
+
'reset_config none',
|
|
493
|
+
...(method?.args ?? [])
|
|
494
|
+
.filter((a) => a.startsWith('--cmd-pre-init='))
|
|
495
|
+
.map((a) => a.slice('--cmd-pre-init='.length)),
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
const install = discoverWest();
|
|
499
|
+
const sessionOpenOcd = resolveSessionOpenOcd(process.env, install?.sdkInstallDir);
|
|
500
|
+
if (!sessionOpenOcd) return undefined;
|
|
501
|
+
const openocdExe = sessionOpenOcd.exe;
|
|
502
|
+
const searchArgs = sessionOpenOcd.searchDirs.flatMap((d) => ['-s', d] as [string, string]);
|
|
503
|
+
|
|
504
|
+
let cfgArgs: string[] | undefined;
|
|
505
|
+
let sessionCfg: string | undefined;
|
|
506
|
+
if (cfgLines && cfgLines.length > 0) {
|
|
507
|
+
sessionCfg = join(buildDir, 'cuttlefish-probe.cfg');
|
|
508
|
+
} else {
|
|
509
|
+
// The board target's qualifier ('blackpill_f411ce/stm32f411xe' →
|
|
510
|
+
// 'blackpill_f411ce') identifies the board dir; the vendor segment is
|
|
511
|
+
// not part of the target, so probe the boards/ tree for it.
|
|
512
|
+
const zephyrBase = process.env.ZEPHYR_BASE || install?.zephyrBase;
|
|
513
|
+
const boardDir = (chip.id ?? '').split('/')[0];
|
|
514
|
+
if (!zephyrBase || !boardDir) return undefined;
|
|
515
|
+
const boardsRoot = join(zephyrBase, 'boards');
|
|
516
|
+
let supportCfg: string | undefined;
|
|
517
|
+
try {
|
|
518
|
+
for (const vendor of readdirSync(boardsRoot)) {
|
|
519
|
+
const candidate = join(boardsRoot, vendor, boardDir, 'support', 'openocd.cfg');
|
|
520
|
+
if (existsSync(candidate)) { supportCfg = candidate; break; }
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
if (!supportCfg) return undefined;
|
|
526
|
+
cfgArgs = ['-s', dirname(supportCfg), '-f', supportCfg];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
mkdirSync(buildDir, { recursive: true });
|
|
531
|
+
if (sessionCfg) {
|
|
532
|
+
writeFileSync(sessionCfg, cfgLines!.join('\n') + '\n', 'utf-8');
|
|
533
|
+
cfgArgs = ['-f', sessionCfg];
|
|
534
|
+
}
|
|
535
|
+
const res = spawnSync(openocdExe, [
|
|
536
|
+
...searchArgs, ...cfgArgs!,
|
|
537
|
+
// Pre-init TCL AFTER the cfg (overrides its reset_config) and BEFORE
|
|
538
|
+
// init — the same position west gives --cmd-pre-init.
|
|
539
|
+
...preInit.map((c) => ['-c', c] as [string, string]).flat(),
|
|
540
|
+
'-c', 'init',
|
|
541
|
+
...commands.map((c) => ['-c', c]).flat(),
|
|
542
|
+
'-c', 'shutdown',
|
|
543
|
+
], {
|
|
544
|
+
cwd: buildDir,
|
|
545
|
+
encoding: 'utf-8' as const,
|
|
546
|
+
timeout: 20_000,
|
|
547
|
+
});
|
|
548
|
+
return res.status === 0 ? `-- probe session ok: ${commands.join('; ')}` : undefined;
|
|
549
|
+
} catch {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Whether a flash runner carries the upload over a serial port. Runner-gated,
|
|
556
|
+
* never board-name-gated: esptool and bossac are the only runners
|
|
557
|
+
* `buildFlashArgs` forwards `--port` to, so they are the only ones that
|
|
558
|
+
* cannot flash without one. Probe runners (openocd, jlink) and USB flows
|
|
559
|
+
* (dfu-util, uf2 mass storage) need no port — a missing `--port` must not
|
|
560
|
+
* block them.
|
|
561
|
+
*
|
|
562
|
+
* Exported (pure) so the port-requirement contract is unit-testable without
|
|
563
|
+
* spawning west.
|
|
564
|
+
*/
|
|
565
|
+
export function uploadRequiresPort(runner: string | undefined): boolean {
|
|
566
|
+
return runner === 'esptool' || runner === 'bossac';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function buildFlashArgs(
|
|
570
|
+
buildDir: string,
|
|
571
|
+
userRunner: string | undefined,
|
|
572
|
+
port: string | undefined,
|
|
573
|
+
flashRunner?: string,
|
|
574
|
+
runnerArgs?: readonly string[],
|
|
575
|
+
): string[] {
|
|
576
|
+
// flashRunner is the runner the flash will actually use — the explicit
|
|
577
|
+
// zephyr.runner when set, else the board's declared default from its probe
|
|
578
|
+
// table. It gates the esptool port forwarding below; only an EXPLICIT
|
|
579
|
+
// userRunner forces west's --runner (board defaults stay board.cmake's
|
|
580
|
+
// choice). Runner-gated, never board-name-gated: any board whose flash
|
|
581
|
+
// runs esptool gets the same forwarding.
|
|
582
|
+
const args = ['flash', '-d', buildDir];
|
|
583
|
+
if (userRunner) {
|
|
584
|
+
args.push('--runner', userRunner);
|
|
585
|
+
}
|
|
586
|
+
if (port && flashRunner === 'esptool') {
|
|
587
|
+
args.push('--esp-device', port);
|
|
588
|
+
}
|
|
589
|
+
// The bossac runner defaults its port to /dev/ttyACM0 — on Windows that
|
|
590
|
+
// never matches, so the port MUST be forwarded or bossac fails with
|
|
591
|
+
// "No device found on /dev/ttyACM0" (same class of port-forwarding
|
|
592
|
+
// problem as the esptool --esp-device above).
|
|
593
|
+
if (port && flashRunner === 'bossac') {
|
|
594
|
+
args.push('--bossac-port', port);
|
|
595
|
+
}
|
|
596
|
+
// Extra runner-specific flags, appended verbatim (west's runner parsers
|
|
597
|
+
// accept them after the runner is selected).
|
|
598
|
+
if (runnerArgs && runnerArgs.length > 0) {
|
|
599
|
+
args.push(...runnerArgs);
|
|
600
|
+
}
|
|
601
|
+
return args;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Classify a `west flash` result as success/failure.
|
|
606
|
+
*
|
|
607
|
+
* west's exit status is authoritative except for one known race in the uf2
|
|
608
|
+
* runner on Windows: the UF2 bootloader reboots to run new firmware the instant
|
|
609
|
+
* the file copy completes, unmounting the USB-MSC drive before `shutil.copy`'s
|
|
610
|
+
* trailing `copymode`/chmod runs. That raises `OSError: [WinError 433] A
|
|
611
|
+
* device which does not exist was specified` and makes west exit non-zero —
|
|
612
|
+
* even though the firmware copied and flashed correctly (the LED blinks).
|
|
613
|
+
*
|
|
614
|
+
* The copy starting is logged ("Copying UF2 file to '<drive>'"); WinError 433
|
|
615
|
+
* during `copymode` after that point proves the data write finished and the
|
|
616
|
+
* drive only vanished on the metadata step. Treat that exact signature as
|
|
617
|
+
* success so the upload isn't reported as a failure. Genuine uf2 failures
|
|
618
|
+
* (no partition found, write errors before the copy) still surface as failures.
|
|
619
|
+
*
|
|
620
|
+
* Exported (pure) so the classification is unit-testable without spawning west.
|
|
621
|
+
*/
|
|
622
|
+
export function classifyUploadResult(
|
|
623
|
+
runner: string | undefined,
|
|
624
|
+
status: number | null,
|
|
625
|
+
output: string,
|
|
626
|
+
): boolean {
|
|
627
|
+
if (status === 0) return true;
|
|
628
|
+
if (isUf2DriveVanishRace(output)) return runner === 'uf2';
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Whether `output` carries the benign UF2 copymode/WinError-433 race signature
|
|
634
|
+
* (see classifyUploadResult). Centralized so classify + cleanse share one match.
|
|
635
|
+
*/
|
|
636
|
+
function isUf2DriveVanishRace(output: string): boolean {
|
|
637
|
+
return /Copying UF2 file to/.test(output)
|
|
638
|
+
&& /WinError 433/.test(output)
|
|
639
|
+
&& /copymode/.test(output);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Whether a `west flash` (openocd) output carries one of the known
|
|
644
|
+
* target-ignored-SWD signatures — the DAP connect failing ("init mode
|
|
645
|
+
* failed (unable to connect to the target)", i.e. the DPIDR read never
|
|
646
|
+
* succeeded) or a reset/halt never landing ("timed out while waiting for
|
|
647
|
+
* target halted" / "TARGET: <name> - Not halted"). Both mean the board (or
|
|
648
|
+
* probe) needs a power-cycle or the SWD-free DFU path, not a retry of the
|
|
649
|
+
* same command. Centralized so the upload hint stays testable.
|
|
650
|
+
*/
|
|
651
|
+
export function isTargetSwdFailure(output: string): boolean {
|
|
652
|
+
return /unable to connect to the target|timed out while waiting for target halted|TARGET: \S+ - Not halted/.test(output);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Cleanse the `west flash` output shown to the user.
|
|
657
|
+
*
|
|
658
|
+
* When classifyUploadResult has decided a non-zero west exit was the benign UF2
|
|
659
|
+
* race (firmware copied, drive unmounted on the trailing chmod), the raw output
|
|
660
|
+
* is a wall of Python traceback that reads like a hard failure. Drop everything
|
|
661
|
+
* after the "Copying UF2 file to" line — i.e. the entire traceback — so a
|
|
662
|
+
* successful flash reads as a success (the framework's ✓ Done follows). Non-race
|
|
663
|
+
* output is returned untouched; genuine errors stay fully visible for diagnosis.
|
|
664
|
+
*
|
|
665
|
+
* Exported (pure) so the cleansing is unit-testable without spawning west.
|
|
666
|
+
*/
|
|
667
|
+
export function cleanseUploadOutput(
|
|
668
|
+
runner: string | undefined,
|
|
669
|
+
status: number | null,
|
|
670
|
+
output: string,
|
|
671
|
+
): string {
|
|
672
|
+
if (status === 0) return output;
|
|
673
|
+
if (runner === 'uf2' && isUf2DriveVanishRace(output)) {
|
|
674
|
+
// Keep everything west printed up to and including "Copying UF2 file to",
|
|
675
|
+
// then stop — everything after that is the drive-vanish traceback.
|
|
676
|
+
const upto = output.match(/[\s\S]*Copying UF2 file to[^\n]*/);
|
|
677
|
+
const head = upto ? upto[0] : '-- west flash: using runner uf2';
|
|
678
|
+
return head;
|
|
679
|
+
}
|
|
680
|
+
return output;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Whether a failed `west build` output carries ninja's `dependency cycle`
|
|
686
|
+
* signature. Zephyr 4.3.99-dev snapshots have a regression
|
|
687
|
+
* (zephyrproject-rtos/zephyr#104757, fixed upstream by the #104784 revert,
|
|
688
|
+
* in v4.4+): after CMake re-runs from a .config change, the build dir's
|
|
689
|
+
* .ninja_deps records an `offsets.h -> offsets.c.obj -> offsets.h` cycle and
|
|
690
|
+
* ninja aborts with `ninja: error: dependency cycle: ...` before compiling
|
|
691
|
+
* anything. The cycle lives in the build dir, not the sources, so compile()
|
|
692
|
+
* recovers by deleting the dir and retrying once.
|
|
693
|
+
*
|
|
694
|
+
* Exported (pure) so the detection is unit-testable without spawning west.
|
|
695
|
+
*/
|
|
696
|
+
export function isDependencyCycleFailure(output: string): boolean {
|
|
697
|
+
return output.includes('dependency cycle');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** stdout+stderr of a spawnSync result coerced to one string. Defensive about
|
|
701
|
+
* the buffer form (spawnSync only returns strings when `encoding` is set,
|
|
702
|
+
* which every call site here does — but the coercion costs nothing). */
|
|
703
|
+
function combinedSpawnOutput(
|
|
704
|
+
result: { stdout?: string | Buffer | null; stderr?: string | Buffer | null },
|
|
705
|
+
): string {
|
|
706
|
+
const so = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
707
|
+
const se = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
708
|
+
return so + se;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
|
|
713
|
+
* The target board is carried via frameworkData.buildTarget; scaffolding
|
|
714
|
+
* happens at compile time when the target is known.
|
|
715
|
+
*/
|
|
716
|
+
export const Toolchain = {
|
|
717
|
+
prepare(outputDir: string, entryPoint: string): void {
|
|
718
|
+
// Write the DT overlay for the default board (the real target is known at
|
|
719
|
+
// compile time; prepare runs before compile, so use the default board id).
|
|
720
|
+
// The overlay is additive and idempotent; compile re-runs prepare-equivalent
|
|
721
|
+
// logic in scaffold via the usage scan. Mirrors how Arduino's library
|
|
722
|
+
// resolution is a pre-build artifact step.
|
|
723
|
+
const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
|
|
724
|
+
// Scan the emitted source for usage tokens (same authoritative signal the
|
|
725
|
+
// scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
|
|
726
|
+
const srcDir = dirname(entryPoint);
|
|
727
|
+
let src = '';
|
|
728
|
+
try {
|
|
729
|
+
for (const name of readdirSync(srcDir)) {
|
|
730
|
+
if (name.endsWith('.cpp') || name.endsWith('.c')) {
|
|
731
|
+
src += readFileSync(join(srcDir, name), 'utf8');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} catch { /* src may not exist yet on first prepare */ }
|
|
735
|
+
const uses = (t: string): boolean => src.includes(t);
|
|
736
|
+
// Inline-override markers (the escape hatch): the lowerings emit
|
|
737
|
+
// `cuttlefish-user-facts` comments carrying routing the transpiler
|
|
738
|
+
// cannot synthesize (adc device/pinctrl, pwm controller/channel).
|
|
739
|
+
// Merged into the chip BEFORE the scans + overlay generation, so the DT
|
|
740
|
+
// nodes, pinctrl groups, and used-pin recovery treat them as facts.
|
|
741
|
+
const chip = applyUserFactMarkers(chipForBuild(projectRoot, ''), src);
|
|
742
|
+
// Display usage tokens: the minimal GFX runtime (display_write/_fill_rect)
|
|
743
|
+
// and the UI display adapter (display_init / __tc_display_dev /
|
|
744
|
+
// DEVICE_DT_GET on the display nodelabel). Both paths need the DT overlay
|
|
745
|
+
// to enable the display node.
|
|
746
|
+
const usesDisplay = uses('display_write') || uses('display_init')
|
|
747
|
+
|| uses('display_fill_rect') || uses('__tc_display_dev')
|
|
748
|
+
|| uses('CuttlefishDisplayTarget');
|
|
749
|
+
// Both registered Zephyr display profiles use dtLabel 'display0', so the
|
|
750
|
+
// default profile's overlay block (&display0 { status="okay" }) is correct
|
|
751
|
+
// for either driver. Thread a non-default profile here only if a future
|
|
752
|
+
// board carries a display node under a different nodelabel.
|
|
753
|
+
const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
|
|
754
|
+
// Touch controller kind comes from which DT nodelabel the emitted adapter
|
|
755
|
+
// references (FT6336U on I2C, XPT2046 on the display's SPI bus).
|
|
756
|
+
const usesTouch = uses('ft6336u') || uses('touch_');
|
|
757
|
+
const usesXpt = uses('xpt2046');
|
|
758
|
+
const sensorParts = scanSensorParts(src);
|
|
759
|
+
const spiTargetParts = scanSpiTargets(src);
|
|
760
|
+
const overlay = generateOverlay(chip, {
|
|
761
|
+
// __tc_<bus> matches the shim state block — a begin()-only program
|
|
762
|
+
// emits no driver API call but still declares the DT device. A
|
|
763
|
+
// constructed sensor is also a bus user (its device handle is the
|
|
764
|
+
// only i2c reference a sensor-only program carries).
|
|
765
|
+
usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
|
|
766
|
+
usesSensor: uses('sensor_') || sensorParts.length > 0,
|
|
767
|
+
usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
|
|
768
|
+
sensorParts,
|
|
769
|
+
spiTargets: spiTargetParts,
|
|
770
|
+
usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
|
|
771
|
+
usesUart: uses('__tc_uart'),
|
|
772
|
+
usesUsb: uses('__tc_usb'),
|
|
773
|
+
usesPwm: uses('pwm_'),
|
|
774
|
+
usesAdc: uses('adc_'),
|
|
775
|
+
adcReadPins: scanAdcReadPins(src, chip),
|
|
776
|
+
dacWritePins: scanDacWritePins(src),
|
|
777
|
+
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
778
|
+
i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
|
|
779
|
+
spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
|
|
780
|
+
uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
|
|
781
|
+
// Preferences/FS — same tokens scaffoldZephyrProject scans (the ZMS
|
|
782
|
+
// settings_* API + __tc_prefs shim, the __tc_fs mount shim); drive the
|
|
783
|
+
// storage-partition synthesis + /chosen settings pointer.
|
|
784
|
+
usesPreferences: uses('settings_') || uses('__tc_prefs'),
|
|
785
|
+
usesFS: uses('__tc_fs'),
|
|
786
|
+
usesWdt: uses('wdt_'),
|
|
787
|
+
usesHwtimer: uses('counter_') || uses('__tc_hw'),
|
|
788
|
+
usesDisplay,
|
|
789
|
+
usesTouch: usesTouch || usesXpt,
|
|
790
|
+
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
791
|
+
}, displayProfile);
|
|
792
|
+
const overlayDir = join(projectRoot, 'boards');
|
|
793
|
+
mkdirSync(overlayDir, { recursive: true });
|
|
794
|
+
// prepare() runs before the real target is known — the placeholder name
|
|
795
|
+
// never matches `west build -b <board>` (compile rewrites the overlay
|
|
796
|
+
// under the actual board's name below).
|
|
797
|
+
writeIfChanged(join(overlayDir, 'board.overlay'), overlay);
|
|
798
|
+
// Thin SPI targets need an app-local binding: a compatible-less DT node
|
|
799
|
+
// generates NO property macros, so SPI_DT_SPEC_GET's spi-max-frequency
|
|
800
|
+
// lookup would not exist. The binding has no driver — it exists so
|
|
801
|
+
// gen_defines emits the spi properties for the target nodes.
|
|
802
|
+
if (spiTargetParts.length > 0) {
|
|
803
|
+
const bindingsDir = join(projectRoot, 'dts', 'bindings');
|
|
804
|
+
mkdirSync(bindingsDir, { recursive: true });
|
|
805
|
+
writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
|
|
806
|
+
'description: |',
|
|
807
|
+
' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
|
|
808
|
+
' peer. No driver binds this compatible; it exists so devicetree',
|
|
809
|
+
' generation emits the spi properties (spi-max-frequency,',
|
|
810
|
+
' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
|
|
811
|
+
' consumes from the generated C++.',
|
|
812
|
+
'compatible: "cuttlefish,spi-target"',
|
|
813
|
+
'include: spi-device.yaml',
|
|
814
|
+
'',
|
|
815
|
+
].join('\n'));
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
compile(o: ToolchainOptions): CompileResult {
|
|
820
|
+
const projectRoot = projectRootFromOptions(o);
|
|
821
|
+
const rawBoard = targetFromOptions(o);
|
|
822
|
+
|
|
823
|
+
// Fail fast on an incompatible Zephyr (clear message vs. a cryptic west/
|
|
824
|
+
// CMake board error), then normalize the board target for the installed
|
|
825
|
+
// version — Zephyr 4.3+ rejects bare multi-core board names, so a stale
|
|
826
|
+
// config (esp32s3_devkitc) is rewritten to the qualified form
|
|
827
|
+
// (esp32s3_devkitc/esp32s3/procpu). See toolchain/compat.ts.
|
|
828
|
+
const zephyrVersion = detectZephyrVersion();
|
|
829
|
+
const compat = checkZephyrCompat(zephyrVersion);
|
|
830
|
+
if (compat.status === 'out-of-range') {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr. ` +
|
|
833
|
+
`Set ZEPHYR_BASE to a compatible Zephyr checkout, or install one via '@typecad/zephyr-installer'.`,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (compat.status === 'undetectable') {
|
|
837
|
+
console.warn(
|
|
838
|
+
`! Could not detect the installed Zephyr version (is ZEPHYR_BASE set?). ` +
|
|
839
|
+
`Skipping compat check; declared range is ${compat.range}.`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const board = resolveBoardTarget(rawBoard, zephyrVersion);
|
|
843
|
+
|
|
844
|
+
const debugMode = new ZephyrStrategy().debugMode(board);
|
|
845
|
+
const isGdbDebug = o.debug === true && debugMode === 'gdb';
|
|
846
|
+
const zc = o.zephyrConfig as Record<string, unknown> | undefined;
|
|
847
|
+
const userKconfig = zc?.kconfig as Record<string, string> | undefined;
|
|
848
|
+
const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram);
|
|
849
|
+
|
|
850
|
+
// Regenerate the DT overlay for the ACTUAL target board. prepare() writes
|
|
851
|
+
// it for the default board (the real target is unknown until compile), so
|
|
852
|
+
// the <default>.overlay it wrote does not match `west build -b <board>`.
|
|
853
|
+
// Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
|
|
854
|
+
try {
|
|
855
|
+
const chip = chipForBuild(projectRoot, board);
|
|
856
|
+
// Custom-board generation: an MCU-only target (no board package) has no
|
|
857
|
+
// upstream Zephyr board — generate one under boards/typecad/<name>/ from
|
|
858
|
+
// the chip's silicon data. Opt-in via `zephyr.customBoard: true` in
|
|
859
|
+
// cuttlefish.config.ts; the board takes its name from the build target.
|
|
860
|
+
// Idempotent — regenerated on every compile, before the overlay pass.
|
|
861
|
+
if (zc?.customBoard === true) {
|
|
862
|
+
const generated = generateCustomBoard(projectRoot, chip, board.split('/')[0]);
|
|
863
|
+
if (!generated) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`zephyr.customBoard is set, but the resolved chip ('${chip.id}') carries no ` +
|
|
866
|
+
`silicon board data. Custom-board generation requires an MCU-only config ` +
|
|
867
|
+
`(mcu set, board absent) whose MCU package ships a zephyr block.`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const srcDir = join(projectRoot, 'src');
|
|
872
|
+
let src = '';
|
|
873
|
+
try {
|
|
874
|
+
for (const name of readdirSync(srcDir)) {
|
|
875
|
+
if (name.endsWith('.cpp') || name.endsWith('.c')) {
|
|
876
|
+
src += readFileSync(join(srcDir, name), 'utf-8');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} catch { /* src may not exist */ }
|
|
880
|
+
const uses = (t: string): boolean => src.includes(t);
|
|
881
|
+
const usesDisplay = uses('display_write') || uses('display_init')
|
|
882
|
+
|| uses('display_fill_rect') || uses('__tc_display_dev')
|
|
883
|
+
|| uses('CuttlefishDisplayTarget');
|
|
884
|
+
// Derive the display dimensions from the emitted adapter code
|
|
885
|
+
// (display_width/height return the profile's w/h). This ensures the DT
|
|
886
|
+
// overlay's width/height match the panel the adapter targets, not the
|
|
887
|
+
// default profile — critical for drivers like ST7796S that initialize
|
|
888
|
+
// the panel geometry from the DT node.
|
|
889
|
+
let displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
|
|
890
|
+
if (usesDisplay) {
|
|
891
|
+
const wMatch = src.match(/display_width\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
|
|
892
|
+
const hMatch = src.match(/display_height\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
|
|
893
|
+
if (wMatch && hMatch) {
|
|
894
|
+
displayProfile = {
|
|
895
|
+
...DEFAULT_ZEPHYR_DISPLAY_PROFILE,
|
|
896
|
+
width: parseInt(wMatch[1], 10),
|
|
897
|
+
height: parseInt(hMatch[1], 10),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// Extract display pin wiring (cs/dc/rst/spiFrequency/spiPins) from the
|
|
902
|
+
// config display section so the DT overlay wires the MIPI DBI bridge to
|
|
903
|
+
// the correct GPIOs + SPI bus pins.
|
|
904
|
+
const dispCfg = o.display as Record<string, unknown> | undefined;
|
|
905
|
+
const spiPins = (dispCfg?.spiPins ?? undefined) as
|
|
906
|
+
{ sck?: unknown; mosi?: unknown; miso?: unknown } | undefined;
|
|
907
|
+
const wiring: DisplayWiring | undefined = dispCfg
|
|
908
|
+
? {
|
|
909
|
+
cs: typeof dispCfg.cs === 'number' ? dispCfg.cs : undefined,
|
|
910
|
+
dc: typeof dispCfg.dc === 'number' ? dispCfg.dc : undefined,
|
|
911
|
+
rst: typeof dispCfg.rst === 'number' ? dispCfg.rst : undefined,
|
|
912
|
+
spiFrequency: typeof dispCfg.spiFrequency === 'number' ? dispCfg.spiFrequency : undefined,
|
|
913
|
+
sck: typeof spiPins?.sck === 'number' ? spiPins.sck : undefined,
|
|
914
|
+
mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
|
|
915
|
+
miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
|
|
916
|
+
backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
|
|
917
|
+
tearingEffectPin: typeof dispCfg.tearingEffectPin === 'number' ? dispCfg.tearingEffectPin : undefined,
|
|
918
|
+
}
|
|
919
|
+
: undefined;
|
|
920
|
+
// Extract touch pin wiring from the config display.touch section so the
|
|
921
|
+
// DT overlay wires the bus + touch node. I2C (FT6336U) carries
|
|
922
|
+
// irq/resetPin/sda/scl; SPI (XPT2046) carries irq/cs + the calibration
|
|
923
|
+
// range the xptek,xpt2046 binding requires.
|
|
924
|
+
const touchCfg = dispCfg?.touch as Record<string, unknown> | undefined;
|
|
925
|
+
const isXpt = touchCfg?.library === 'XPT2046_Touchscreen';
|
|
926
|
+
const touchCal = touchCfg?.calibration as
|
|
927
|
+
{ xMin?: unknown; xMax?: unknown; yMin?: unknown; yMax?: unknown } | undefined;
|
|
928
|
+
const num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined);
|
|
929
|
+
let touchWiring: TouchWiring | undefined = touchCfg
|
|
930
|
+
? {
|
|
931
|
+
controller: isXpt ? 'xpt2046' : 'ft6336u',
|
|
932
|
+
irq: num(touchCfg.irq),
|
|
933
|
+
resetPin: num(touchCfg.resetPin),
|
|
934
|
+
sda: num(touchCfg.sda),
|
|
935
|
+
scl: num(touchCfg.scl),
|
|
936
|
+
cs: num(touchCfg.cs),
|
|
937
|
+
calibration: touchCal
|
|
938
|
+
? {
|
|
939
|
+
xMin: num(touchCal.xMin) ?? 0,
|
|
940
|
+
xMax: num(touchCal.xMax) ?? 4095,
|
|
941
|
+
yMin: num(touchCal.yMin) ?? 0,
|
|
942
|
+
yMax: num(touchCal.yMax) ?? 4095,
|
|
943
|
+
}
|
|
944
|
+
: undefined,
|
|
945
|
+
minPressure: num(touchCfg.minPressure),
|
|
946
|
+
}
|
|
947
|
+
: undefined;
|
|
948
|
+
// Touch controller kind for Kconfig (bus driver selection) and the DT
|
|
949
|
+
// node shape: from the config when available, else from the DT nodelabel
|
|
950
|
+
// the emitted adapter references. Forced onto touchWiring so a source
|
|
951
|
+
// scan match without a config section still emits the right node.
|
|
952
|
+
const usesXpt = isXpt || uses('xpt2046');
|
|
953
|
+
if (usesXpt) {
|
|
954
|
+
touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
|
|
955
|
+
}
|
|
956
|
+
const overlayDiagnostics: OverlayDiagnostic[] = [];
|
|
957
|
+
const sensorParts = scanSensorParts(src);
|
|
958
|
+
const spiTargetParts = scanSpiTargets(src);
|
|
959
|
+
const overlay = generateOverlay(chip, {
|
|
960
|
+
// __tc_<bus> matches the shim state block — a begin()-only program
|
|
961
|
+
// emits no driver API call but still declares the DT device. A
|
|
962
|
+
// constructed sensor is also a bus user (see scanSensorParts).
|
|
963
|
+
usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
|
|
964
|
+
usesSensor: uses('sensor_') || sensorParts.length > 0,
|
|
965
|
+
usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
|
|
966
|
+
sensorParts,
|
|
967
|
+
spiTargets: spiTargetParts,
|
|
968
|
+
usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
|
|
969
|
+
usesUart: uses('__tc_uart'),
|
|
970
|
+
usesUsb: uses('__tc_usb'),
|
|
971
|
+
usesPwm: uses('pwm_'),
|
|
972
|
+
usesAdc: uses('adc_'),
|
|
973
|
+
adcReadPins: scanAdcReadPins(src, chip),
|
|
974
|
+
usesDac: uses('dac_') || uses('__tc_dac'),
|
|
975
|
+
dacWritePins: scanDacWritePins(src),
|
|
976
|
+
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
977
|
+
i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
|
|
978
|
+
spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
|
|
979
|
+
uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
|
|
980
|
+
// Preferences/FS — same tokens scaffoldZephyrProject scans; drive the
|
|
981
|
+
// storage-partition synthesis + /chosen settings pointer.
|
|
982
|
+
usesPreferences: uses('settings_') || uses('__tc_prefs'),
|
|
983
|
+
usesWdt: uses('wdt_'),
|
|
984
|
+
usesHwtimer: uses('counter_') || uses('__tc_hw'),
|
|
985
|
+
usesFS: uses('__tc_fs'),
|
|
986
|
+
usesDisplay,
|
|
987
|
+
usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
|
|
988
|
+
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
989
|
+
psram: o.psram,
|
|
990
|
+
}, displayProfile, wiring, touchWiring, overlayDiagnostics);
|
|
991
|
+
for (const d of overlayDiagnostics) {
|
|
992
|
+
console.warn(`overlay: ${d.message}`);
|
|
993
|
+
}
|
|
994
|
+
const overlayDir = join(projectRoot, 'boards');
|
|
995
|
+
mkdirSync(overlayDir, { recursive: true });
|
|
996
|
+
// Write the board-specific overlay (the one west loads). Zephyr looks for
|
|
997
|
+
// boards/<board_id>.overlay under APPLICATION_CONFIG_DIR — use the bare
|
|
998
|
+
// board id (before any hardware-qualifier suffix, e.g. 'esp32_devkitc'
|
|
999
|
+
// not the full 'esp32_devkitc/esp32/procpu' target string). Library
|
|
1000
|
+
// packages' overlay fragments are appended by the scaffold helper.
|
|
1001
|
+
const boardId = board.split('/')[0];
|
|
1002
|
+
writeIfChanged(
|
|
1003
|
+
join(overlayDir, `${boardId}.overlay`),
|
|
1004
|
+
appendLibraryOverlayFragments(overlay, projectRoot),
|
|
1005
|
+
);
|
|
1006
|
+
// Thin SPI targets need the app-local binding (see the transpile-side
|
|
1007
|
+
// write for the rationale): no compatible → no generated spi props →
|
|
1008
|
+
// SPI_DT_SPEC_GET's spi-max-frequency lookup does not exist.
|
|
1009
|
+
if (spiTargetParts.length > 0) {
|
|
1010
|
+
const bindingsDir = join(projectRoot, 'dts', 'bindings');
|
|
1011
|
+
mkdirSync(bindingsDir, { recursive: true });
|
|
1012
|
+
writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
|
|
1013
|
+
'description: |',
|
|
1014
|
+
' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
|
|
1015
|
+
' peer. No driver binds this compatible; it exists so devicetree',
|
|
1016
|
+
' generation emits the spi properties (spi-max-frequency,',
|
|
1017
|
+
' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
|
|
1018
|
+
' consumes from the generated C++.',
|
|
1019
|
+
'compatible: "cuttlefish,spi-target"',
|
|
1020
|
+
'include: spi-device.yaml',
|
|
1021
|
+
'',
|
|
1022
|
+
].join('\n'));
|
|
1023
|
+
}
|
|
1024
|
+
} catch { /* best-effort overlay regen; the build surfaces DT errors */ }
|
|
1025
|
+
|
|
1026
|
+
// Use a stable build dir so incremental builds reuse the Ninja graph.
|
|
1027
|
+
// west defaults to <projectRoot>/build.
|
|
1028
|
+
const buildDir = join(projectRoot, 'build');
|
|
1029
|
+
|
|
1030
|
+
// Reuse the build dir across builds so ninja recompiles only the changed
|
|
1031
|
+
// app translation units and re-links — a pristine configure + the
|
|
1032
|
+
// ~280-target Zephyr library rebuild costs minutes on Windows
|
|
1033
|
+
// (demo-shadcn measures 69s of ninja wall time, 448s of summed compile
|
|
1034
|
+
// work, and every build redid all of it). Nuke it only when the generated
|
|
1035
|
+
// config changed (prj.conf / CMakeLists content), the one path that must
|
|
1036
|
+
// not reuse a cached graph: Zephyr 4.3.99-dev snapshots carry a
|
|
1037
|
+
// regression (zephyrproject-rtos/zephyr#104757, fixed by the #104784
|
|
1038
|
+
// revert on 2026-03-03, in v4.4+) where re-running CMake after a .config
|
|
1039
|
+
// change records an `offsets.h -> offsets.c.obj -> offsets.h` cycle in
|
|
1040
|
+
// .ninja_deps, after which every ninja run fails with `dependency cycle`.
|
|
1041
|
+
// Plain source edits never reconfigure CMake, so they cannot trigger it —
|
|
1042
|
+
// and the retry after the spawn below self-heals any path that still does.
|
|
1043
|
+
// Board switches need no nuke here: `west build` is --pristine=auto by
|
|
1044
|
+
// default and recreates the dir itself when -b <board> mismatches the
|
|
1045
|
+
// cached board.
|
|
1046
|
+
if (configChanged) {
|
|
1047
|
+
try { rmSync(buildDir, { recursive: true, force: true }); } catch { /* may not exist */ }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const buildArgs = ['build', '-b', board, '-d', buildDir, projectRoot];
|
|
1051
|
+
// Explicitly pass the generated DT overlay. Zephyr's auto-detection of
|
|
1052
|
+
// boards/<board>.overlay fails for hardware-qualified targets (e.g.
|
|
1053
|
+
// esp32_devkitc/esp32/procpu) because the FILE_SUFFIX matching doesn't
|
|
1054
|
+
// resolve — passing -DDTC_OVERLAY_FILE forces it unconditionally.
|
|
1055
|
+
const boardId = board.split('/')[0];
|
|
1056
|
+
const overlayPath = join(projectRoot, 'boards', `${boardId}.overlay`);
|
|
1057
|
+
try {
|
|
1058
|
+
if (readFileSync(overlayPath, 'utf-8').length > 0) {
|
|
1059
|
+
// CMake parses backslashes as escapes — use forward slashes so the
|
|
1060
|
+
// Windows path survives the -D argument intact.
|
|
1061
|
+
buildArgs.push('--', `-DDTC_OVERLAY_FILE=${overlayPath.replace(/\\/g, '/')}`);
|
|
1062
|
+
}
|
|
1063
|
+
} catch { /* no overlay — let Zephyr auto-detect or build without one */ }
|
|
1064
|
+
// Append user cmake args from cuttlefish.config.ts zephyr.cmakeArgs.
|
|
1065
|
+
const userCmakeArgs = zc?.cmakeArgs as string[] | undefined;
|
|
1066
|
+
if (userCmakeArgs && userCmakeArgs.length > 0) {
|
|
1067
|
+
if (!buildArgs.includes('--')) buildArgs.push('--');
|
|
1068
|
+
buildArgs.push(...userCmakeArgs);
|
|
1069
|
+
}
|
|
1070
|
+
const inv = westSpawn(
|
|
1071
|
+
buildArgs,
|
|
1072
|
+
{ cwd: projectRoot, encoding: 'utf-8', timeout: BUILD_TIMEOUT_MS },
|
|
1073
|
+
);
|
|
1074
|
+
let result = spawnSync(inv.command, inv.args, inv.options);
|
|
1075
|
+
// Self-heal the Zephyr 4.3.99 dep-cycle regression (see the nuke comment
|
|
1076
|
+
// above): when the cached .ninja_deps carries the cycle, ninja aborts with
|
|
1077
|
+
// `dependency cycle` before compiling anything. The cycle lives in the
|
|
1078
|
+
// build dir, not the sources — one pristine retry clears it and the build
|
|
1079
|
+
// proceeds. On fixed Zephyr (>=4.4) this never fires.
|
|
1080
|
+
let pristineRetry = false;
|
|
1081
|
+
if (result.status !== 0 && isDependencyCycleFailure(combinedSpawnOutput(result))) {
|
|
1082
|
+
try { rmSync(buildDir, { recursive: true, force: true }); } catch { /* may not exist */ }
|
|
1083
|
+
result = spawnSync(inv.command, inv.args, inv.options);
|
|
1084
|
+
pristineRetry = true;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
1088
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
1089
|
+
const output = stdout + stderr + (pristineRetry
|
|
1090
|
+
? '\n[cuttlefish] dependency cycle detected in the cached build dir — retried with a pristine build'
|
|
1091
|
+
: '');
|
|
1092
|
+
// Prefix the build log with how west was resolved, for transparency.
|
|
1093
|
+
const header = `Using west via ${inv.install.source}` +
|
|
1094
|
+
(inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
|
|
1095
|
+
|
|
1096
|
+
// After a successful build in gdb mode (--debug on a probe-capable target),
|
|
1097
|
+
// write the VS Code launch.json + tasks.json + gdb-script artifacts so F5
|
|
1098
|
+
// attaches GDB to the chip's debug probe. Non-fatal on failure — a missing
|
|
1099
|
+
// artifact doesn't block the build. Mirrors the deleted framework-esp32
|
|
1100
|
+
// toolchain compile() debug-config wiring.
|
|
1101
|
+
if (result.status === 0 && isGdbDebug) {
|
|
1102
|
+
try {
|
|
1103
|
+
const { workspaceRoot, appRel } = resolveDebugLocations(projectRoot);
|
|
1104
|
+
writeDebugConfig({
|
|
1105
|
+
projectRoot,
|
|
1106
|
+
workspaceRoot,
|
|
1107
|
+
appRel,
|
|
1108
|
+
target: board,
|
|
1109
|
+
buildDir,
|
|
1110
|
+
sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
|
|
1111
|
+
});
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
console.warn(`[cuttlefish] gdb debug config generation failed: ${(e as Error).message}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// As-built snapshot: after a successful build, the resolved devicetree
|
|
1118
|
+
// at <buildDir>/zephyr/zephyr.dts carries the board's pinctrl labels —
|
|
1119
|
+
// the STABLE name grammar, immune to vendor macro churn. Harvest its
|
|
1120
|
+
// routes into .cuttlefish/as-built.json; the next build's board-module
|
|
1121
|
+
// generation merges them per-pin over the catalog harvest (build wins,
|
|
1122
|
+
// silently when they agree). One-build freshness lag on first setup,
|
|
1123
|
+
// self-maintaining after. Best-effort — a missing/unparseable artifact
|
|
1124
|
+
// never fails the build.
|
|
1125
|
+
if (result.status === 0) {
|
|
1126
|
+
try {
|
|
1127
|
+
const dtsPath = join(buildDir, 'zephyr', 'zephyr.dts');
|
|
1128
|
+
const dtsText = readFileSync(dtsPath, 'utf8');
|
|
1129
|
+
const facts = parseZephyrDts(dtsText);
|
|
1130
|
+
const total = facts.adc.length + facts.pwm.length + facts.dac.length;
|
|
1131
|
+
if (total > 0) {
|
|
1132
|
+
// Write beside the project's board module — the .cuttlefish dir the
|
|
1133
|
+
// config loader reads from, discovered by walking up to the
|
|
1134
|
+
// generated board.json (the scaffold root and the config root are
|
|
1135
|
+
// different dirs in the standard layout: src/out vs project root).
|
|
1136
|
+
let cfDir = join(projectRoot, '.cuttlefish');
|
|
1137
|
+
for (let dir = projectRoot; ; dir = dirname(dir)) {
|
|
1138
|
+
if (existsSync(join(dir, '.cuttlefish', 'board.json'))) {
|
|
1139
|
+
cfDir = join(dir, '.cuttlefish');
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
const parent = dirname(dir);
|
|
1143
|
+
if (parent === dir) break;
|
|
1144
|
+
}
|
|
1145
|
+
mkdirSync(cfDir, { recursive: true });
|
|
1146
|
+
writeIfChanged(join(cfDir, 'as-built.json'), asBuiltJson(board, facts));
|
|
1147
|
+
}
|
|
1148
|
+
} catch { /* best-effort snapshot — nothing to harvest or unreadable */ }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return {
|
|
1152
|
+
success: result.status === 0,
|
|
1153
|
+
output: header + output,
|
|
1154
|
+
errors: parseCompileErrors(output, o.sourcePath),
|
|
1155
|
+
};
|
|
1156
|
+
},
|
|
1157
|
+
|
|
1158
|
+
upload(o: ToolchainOptions): UploadResult {
|
|
1159
|
+
const projectRoot = projectRootFromOptions(o);
|
|
1160
|
+
const buildDir = join(projectRoot, 'build');
|
|
1161
|
+
const board = targetFromOptions(o);
|
|
1162
|
+
const zc = o.zephyrConfig as Record<string, unknown> | undefined;
|
|
1163
|
+
const chip = chipForBuild(projectRoot, board);
|
|
1164
|
+
const probe = resolveProbeMethod(zc, chip, 'flash');
|
|
1165
|
+
if (!probe.ok) {
|
|
1166
|
+
return { success: false, output: `-- west flash: ${probe.error}` };
|
|
1167
|
+
}
|
|
1168
|
+
// BOSSA bootloader boards with touch-reset data: open the app's console
|
|
1169
|
+
// port at 1200 baud (the firmware's USB shim reboots into the
|
|
1170
|
+
// bootloader), wait for the bootloader identity, and flash THAT port.
|
|
1171
|
+
// Falls back to the configured port (manual double-tap) on any failure.
|
|
1172
|
+
let flashPort = o.port;
|
|
1173
|
+
const flashNotes: string[] = [];
|
|
1174
|
+
// The runner this flash will actually use: the explicit choice, else the
|
|
1175
|
+
// board's declared default (first probe-method entry). Gates port
|
|
1176
|
+
// forwarding and the bossac touch below — board.cmake still resolves
|
|
1177
|
+
// the default runner itself.
|
|
1178
|
+
const flashRunner = probe.runner ?? chip.probeMethods?.[0]?.runner;
|
|
1179
|
+
// Serial-port runners cannot flash without a port; every other runner
|
|
1180
|
+
// (probe or USB) proceeds — whether a port is required is the runner's
|
|
1181
|
+
// call, not the CLI's blanket gate.
|
|
1182
|
+
if (uploadRequiresPort(flashRunner) && !flashPort) {
|
|
1183
|
+
return {
|
|
1184
|
+
success: false,
|
|
1185
|
+
output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the CUTTLEFISH_PORT env var).`,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (flashRunner === 'bossac' && flashPort && chip.usb?.touchReset) {
|
|
1189
|
+
const touch = bossacTouchReset(flashPort, chip.usb.touchReset);
|
|
1190
|
+
flashNotes.push(`-- ${touch.note}`);
|
|
1191
|
+
if (touch.port) flashPort = touch.port;
|
|
1192
|
+
}
|
|
1193
|
+
const args = buildFlashArgs(buildDir, probe.runner, flashPort, flashRunner, probe.args);
|
|
1194
|
+
|
|
1195
|
+
// openocd flashes go through a dedicated session instead of `west flash`.
|
|
1196
|
+
// west's flow has three sequential races that each strand the board: the
|
|
1197
|
+
// connect happens against a running (often USB-active) application, the
|
|
1198
|
+
// erase precedes the write so the vector table is 0xFFFFFFFF while the
|
|
1199
|
+
// RAM algorithm runs (any exception → core LOCKUP at 0xFFFFFFFE, "timeout
|
|
1200
|
+
// waiting for algorithm"), and the trailing `reset run` frequently does
|
|
1201
|
+
// not reach the core. This session is deterministic end to end: halt at
|
|
1202
|
+
// the reset vector (static target for the DAP), mask interrupts for the
|
|
1203
|
+
// algorithm (exceptions cannot vector through erased flash), unmask
|
|
1204
|
+
// after, and boot the flashed app with a direct SYSRESETREQ. Falls back
|
|
1205
|
+
// to `west flash` when the image or session is unavailable.
|
|
1206
|
+
let westFallback = true;
|
|
1207
|
+
if (probe.runner === 'openocd') {
|
|
1208
|
+
const hex = join(buildDir, 'zephyr', 'zephyr.hex');
|
|
1209
|
+
if (existsSync(hex)) {
|
|
1210
|
+
// Forward slashes + TCL quoting so project paths with spaces work.
|
|
1211
|
+
const hexArg = `"${hex.replace(/\\/g, '/')}"`;
|
|
1212
|
+
// Target addressing: `cortex_m` is a PER-TARGET subcommand — a bare
|
|
1213
|
+
// `cortex_m maskisr on` is an unknown command (a silent no-op inside
|
|
1214
|
+
// catch). Resolve the session's target object once and address it.
|
|
1215
|
+
// Cortex-M-only, self-gating: on other cores the cortex_m method
|
|
1216
|
+
// errors and catch contains it (the plain flash path is safe there
|
|
1217
|
+
// without masking — the lockup class is Cortex-M vectoring).
|
|
1218
|
+
const flashed = openocdProbeSession(buildDir, zc, chip, [
|
|
1219
|
+
'set _tgt [lindex [target names] 0]',
|
|
1220
|
+
'reset halt',
|
|
1221
|
+
'catch { $_tgt cortex_m maskisr on }',
|
|
1222
|
+
`flash write_image erase ${hexArg}`,
|
|
1223
|
+
'catch { $_tgt cortex_m maskisr off }',
|
|
1224
|
+
// Boot the flashed app. Cortex-M: a direct SYSRESETREQ via AIRCR —
|
|
1225
|
+
// pin-independent, always reaches the core, and clears PRIMASK
|
|
1226
|
+
// (so the masked algorithm leaves nothing behind). Other cores:
|
|
1227
|
+
// openocd's generic reset run. A halted core STAYS halted across
|
|
1228
|
+
// a core-initiated reset (debug halt state survives — that is how
|
|
1229
|
+
// reset halt works), so resume it; on a running core resume errors
|
|
1230
|
+
// and the catch swallows it.
|
|
1231
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1232
|
+
'sleep 100',
|
|
1233
|
+
'catch { resume }',
|
|
1234
|
+
'sleep 200',
|
|
1235
|
+
]);
|
|
1236
|
+
if (flashed) {
|
|
1237
|
+
westFallback = false;
|
|
1238
|
+
flashNotes.push('-- probe flash ok: halt → masked write → SYSRESETREQ');
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
let result: ReturnType<typeof spawnSync> | undefined;
|
|
1244
|
+
let raw = '';
|
|
1245
|
+
let ok = false;
|
|
1246
|
+
if (westFallback) {
|
|
1247
|
+
const inv = westSpawn(args, {
|
|
1248
|
+
cwd: projectRoot,
|
|
1249
|
+
encoding: 'utf-8',
|
|
1250
|
+
timeout: FLASH_TIMEOUT_MS,
|
|
1251
|
+
});
|
|
1252
|
+
result = spawnSync(inv.command, inv.args, inv.options);
|
|
1253
|
+
const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
1254
|
+
const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
1255
|
+
raw = fstdout + fstderr;
|
|
1256
|
+
ok = classifyUploadResult(probe.runner, result.status, raw);
|
|
1257
|
+
// A SUCCESSFUL west openocd flash can still leave the core HALTED:
|
|
1258
|
+
// west's trailing `reset run` does not reach a core behind an unwired
|
|
1259
|
+
// SRST, and the user's only recourse is the NRST button. Boot it from
|
|
1260
|
+
// a probe session instead — one core reset (a halted core re-halts at
|
|
1261
|
+
// the reset vector; a running core restarts the just-flashed app)
|
|
1262
|
+
// plus a resume.
|
|
1263
|
+
if (ok && probe.runner === 'openocd') {
|
|
1264
|
+
const booted = openocdProbeSession(buildDir, zc, chip, [
|
|
1265
|
+
'set _tgt [lindex [target names] 0]',
|
|
1266
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1267
|
+
'sleep 100',
|
|
1268
|
+
'catch { resume }',
|
|
1269
|
+
'sleep 200',
|
|
1270
|
+
]);
|
|
1271
|
+
if (booted) flashNotes.push('-- probe boot ok: SYSRESETREQ → resume');
|
|
1272
|
+
}
|
|
1273
|
+
// Fallback-path recovery: a failed west flash leaves the core in
|
|
1274
|
+
// lockup; a direct SYSRESETREQ clears it so the caller's retry (or a
|
|
1275
|
+
// later flash) starts from a clean chip.
|
|
1276
|
+
if (!ok && probe.runner === 'openocd') {
|
|
1277
|
+
const revived = openocdProbeSession(buildDir, zc, chip, [
|
|
1278
|
+
'init',
|
|
1279
|
+
'set _tgt [lindex [target names] 0]',
|
|
1280
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1281
|
+
'sleep 300',
|
|
1282
|
+
]);
|
|
1283
|
+
if (revived) flashNotes.push(revived);
|
|
1284
|
+
// Known SWD-failure signatures get a recovery pointer — a board that
|
|
1285
|
+
// ignores SWD until power-cycled (low-power state, lockup, a wedged
|
|
1286
|
+
// probe) otherwise reads as a toolchain bug.
|
|
1287
|
+
if (isTargetSwdFailure(raw)) {
|
|
1288
|
+
flashNotes.push(
|
|
1289
|
+
'-- target ignored SWD — if a retry fails too: power-cycle the board, replug the probe, or skip SWD entirely (hold BOOT0, tap reset, re-run with --probe dfu)',
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
ok = true;
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
success: ok,
|
|
1298
|
+
output: [...flashNotes, cleanseUploadOutput(probe.runner, westFallback ? result!.status : 0, raw)]
|
|
1299
|
+
.filter(Boolean).join('\n'),
|
|
1300
|
+
};
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
monitor(o: ToolchainOptions): void {
|
|
1304
|
+
// Serial monitor over USB-CDC. Zephyr does NOT ship a `west serial`
|
|
1305
|
+
// subcommand (it's not a real west command — invoking it errors with
|
|
1306
|
+
// "unknown command"). The discovered west install's venv carries pyserial,
|
|
1307
|
+
// so run its bundled miniterm directly: `python -m serial.tools.miniterm`.
|
|
1308
|
+
// That is the same cross-platform terminal pyserial provides in ESP-IDF's
|
|
1309
|
+
// idf.py monitor, and it inherits stdio so Ctrl+C exits cleanly.
|
|
1310
|
+
if (!o.port) {
|
|
1311
|
+
throw new Error(
|
|
1312
|
+
'A serial port is required to monitor. Pass --port <COMx/ttyX>.',
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
// ESP32 USB-CDC console runs at 115200 (the Zephyr ESP32 board default).
|
|
1316
|
+
// The CLI's generic default of 9600 is wrong for this target; honor an
|
|
1317
|
+
// explicit --baud when given, else 115200.
|
|
1318
|
+
const baud = o.baud ?? 115200;
|
|
1319
|
+
const install = discoverWest();
|
|
1320
|
+
const py = install?.pythonExecutable ?? process.env.PYTHON ?? 'python';
|
|
1321
|
+
// Reuse west-spawn's env builder (prepends the venv bin dir to PATH so the
|
|
1322
|
+
// python we spawn resolves pyserial from the same venv). Falls back to the
|
|
1323
|
+
// process env when no install is discovered.
|
|
1324
|
+
const env = install ? buildEnv(install) : process.env;
|
|
1325
|
+
spawnSync(py, ['-m', 'serial.tools.miniterm', o.port, String(baud)], {
|
|
1326
|
+
cwd: projectRootFromOptions(o),
|
|
1327
|
+
env,
|
|
1328
|
+
stdio: 'inherit',
|
|
1329
|
+
});
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
debug(o: ToolchainOptions): void {
|
|
1333
|
+
// Launch an interactive GDB session for the last build. The probe method
|
|
1334
|
+
// resolves exactly like flashing (zephyr.probe / zephyr.runner — the same
|
|
1335
|
+
// attach session, so the same quirks apply); debug-incapable methods
|
|
1336
|
+
// (bootloaders) are rejected with the debug-capable list. west debug
|
|
1337
|
+
// resolves the GDB binary from the build dir's CMakeCache — no
|
|
1338
|
+
// hand-authored gdbinit needed. Inherits stdio so GDB runs interactively.
|
|
1339
|
+
const projectRoot = projectRootFromOptions(o);
|
|
1340
|
+
const buildDir = join(projectRoot, 'build');
|
|
1341
|
+
const board = targetFromOptions(o);
|
|
1342
|
+
const zc = o.zephyrConfig as Record<string, unknown> | undefined;
|
|
1343
|
+
const probe = resolveProbeMethod(zc, chipForBuild(projectRoot, board), 'debug');
|
|
1344
|
+
if (!probe.ok) {
|
|
1345
|
+
console.error(`-- west debug: ${probe.error}`);
|
|
1346
|
+
process.exitCode = 1;
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const debugArgs = ['debug', '-d', buildDir];
|
|
1350
|
+
if (probe.runner) debugArgs.push('--runner', probe.runner);
|
|
1351
|
+
debugArgs.push(...probe.args);
|
|
1352
|
+
const inv = westSpawn(debugArgs, {
|
|
1353
|
+
cwd: projectRoot,
|
|
1354
|
+
encoding: 'utf-8',
|
|
1355
|
+
stdio: 'inherit',
|
|
1356
|
+
});
|
|
1357
|
+
spawnSync(inv.command, inv.args, inv.options);
|
|
1358
|
+
},
|
|
1359
|
+
};
|