@typecad/framework-zephyr 1.0.0-alpha.13 → 1.0.0-alpha.15

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.
Files changed (191) hide show
  1. package/README.md +22 -35
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -1,522 +1,666 @@
1
- // ---------------------------------------------------------------------------
2
- // debug-config.ts — VS Code / GDB debug artifact generation for `--debug`
3
- //
4
- // After a successful gdb-mode build, writes the artifacts that let the user
5
- // press F5 in VS Code and attach GDB to the running Zephyr target.
6
- //
7
- // Uses the cortex-debug extension (NOT the ESP-IDF gdbtarget adapter) so the
8
- // debug session is self-contained. cortex-debug starts OpenOCD as a child
9
- // process via `servertype: "openocd"`; the preLaunch task handles only the
10
- // build + flash step. After attach we issue `monitor reset init`, set a
11
- // temporary hardware breakpoint at setup(), and continue — this ensures the
12
- // breakpoint is deferred until the bootloader maps the app flash region.
13
- //
14
- // All generators are deterministic + idempotent so toggling --debug does not
15
- // churn the tree.
16
- // ---------------------------------------------------------------------------
17
-
18
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
19
- import { join, resolve, dirname, relative } from 'node:path';
20
- import { ZephyrStrategy } from '../strategy.js';
21
-
22
- export interface DebugConfigOptions {
23
- /** Absolute path to the Zephyr project root (contains CMakeLists.txt + src/). */
24
- projectRoot: string;
25
- /** Absolute path to the workspace root (the cuttlefish config dir — the folder VS Code has open). */
26
- workspaceRoot: string;
27
- /** The Zephyr app dir relative to the workspace root (e.g. 'src/out'). */
28
- sketchRel: string;
29
- /** The Zephyr board id (e.g. 'esp32s3_devkitc'). */
30
- target: string;
31
- /** Absolute path to the Zephyr build dir (<projectRoot>/build) read for the SDK/GDB path. */
32
- buildDir: string;
33
- /** Absolute path to the emitted source map (*.thcppmap.json), if any. */
34
- sourceMapPath?: string;
35
- }
36
-
37
- /**
38
- * Resolve the GDB binary path for the target from the build cache, falling
39
- * back to a filesystem scan of known Zephyr SDK locations when no build
40
- * exists yet (the create-time starter artifacts path). Zephyr records
41
- * ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
42
- * xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
43
- * the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
44
- *
45
- * Returns the absolute gdb path on success, or undefined (the launch.json then
46
- * omits gdbPath and relies on Cortex-Debug's default resolution).
47
- */
48
- export function resolveGdbPath(buildDir: string, target: string): string | undefined {
49
- void target; // toolchain dir is esp32s3-specific today; see gdbPathFromSdkRoot
50
- const cachePath = join(buildDir, 'CMakeCache.txt');
51
- if (existsSync(cachePath)) {
52
- try {
53
- const cache = readFileSync(cachePath, 'utf-8');
54
- const m = cache.match(/^ZEPHYR_SDK_INSTALL_DIR:PATH=(.+)$/m);
55
- if (m) {
56
- const fromCache = gdbPathFromSdkRoot(m[1].trim());
57
- if (fromCache) return fromCache;
58
- }
59
- } catch {
60
- // unreadable cache — fall through to the SDK scan
61
- }
62
- }
63
- // No build dir yet (project just created): probe known SDK locations.
64
- for (const sdkRoot of discoverZephyrSdkRoots()) {
65
- const p = gdbPathFromSdkRoot(sdkRoot);
66
- if (p) return p;
67
- }
68
- return undefined;
69
- }
70
-
71
- /** The esp32s3 xtensa GDB location inside a Zephyr SDK root (verified against
72
- * zephyr-sdk-0.17.4). Returns a forward-slash absolute path or undefined. */
73
- export function gdbPathFromSdkRoot(sdkRoot: string): string | undefined {
74
- const gdbName = 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
75
- const gdbPath = join(sdkRoot, 'xtensa-espressif_esp32s3_zephyr-elf', 'bin', gdbName);
76
- return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
77
- }
78
-
79
- /** Compare two dotted version strings numerically (0.17.10 > 0.17.4). */
80
- function compareSdkVersions(a: string, b: string): number {
81
- const segsOf = (v: string): number[] => v.split('.').map((s) => parseInt(s, 10) || 0);
82
- const aa = segsOf(a);
83
- const bb = segsOf(b);
84
- for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
85
- const d = (aa[i] ?? 0) - (bb[i] ?? 0);
86
- if (d !== 0) return d;
87
- }
88
- return 0;
89
- }
90
-
91
- /**
92
- * Probe the well-known Zephyr SDK install locations, newest version first:
93
- * 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
94
- * 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> the
95
- * @typecad/zephyr-installer layout
96
- * 3. ~/zephyr-sdk-<ver> — the standalone download layout
97
- *
98
- * Only roots that actually contain the esp32s3 GDB are useful to callers;
99
- * this returns candidate roots (gdbPathFromSdkRoot does the existence check)
100
- * so tests can inject home/env overrides.
101
- */
102
- export function discoverZephyrSdkRoots(opts?: {
103
- home?: string;
104
- env?: Record<string, string | undefined>;
105
- }): string[] {
106
- const env = opts?.env ?? process.env;
107
- const home = opts?.home ?? (env.USERPROFILE || env.HOME || '');
108
- const scanned: string[] = [];
109
-
110
- const versionedDirs = (base: string): string[] => {
111
- try {
112
- return readdirSync(base)
113
- .filter((d) => existsSync(join(base, d)) && d.startsWith('zephyr-sdk-'))
114
- .map((d) => join(base, d));
115
- } catch {
116
- return []; // dir absent
117
- }
118
- };
119
- const mambaRoot = env.MAMBA_ROOT_PREFIX || (home ? join(home, 'micromamba') : '');
120
- if (mambaRoot) scanned.push(...versionedDirs(join(mambaRoot, 'zephyr-sdk')));
121
- if (home) scanned.push(...versionedDirs(home));
122
-
123
- // Scanned roots newest version first; the env var stays pinned first
124
- // (explicit user intent outranks any discovered location).
125
- scanned.sort((a, b) => {
126
- const va = a.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
127
- const vb = b.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
128
- return compareSdkVersions(vb, va);
129
- });
130
- const roots = env.ZEPHYR_SDK_INSTALL_DIR
131
- ? [env.ZEPHYR_SDK_INSTALL_DIR, ...scanned]
132
- : scanned;
133
- // De-duplicate (an env var may repeat a scan hit) preserving order.
134
- return roots.filter((r, i) => roots.indexOf(r) === i);
135
- }
136
-
137
- /**
138
- * Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
139
- * OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
140
- * generic/GDB-stub build — because only it carries the Xtensa + esp_usb_jtag
141
- * support. It is NOT on PATH by default, so Cortex-Debug must be pointed at it
142
- * explicitly or it fails with `spawn openocd.exe ENOENT`.
143
- *
144
- * Discovery order:
145
- * 1. ESPRESSIF_TOOLCHAIN_PATH env (the var board.cmake reads) — if set, its
146
- * openocd-esp32/bin/openocd.exe.
147
- * 2. The standard ESP-IDF install layout: ~/.espressif/tools/openocd-esp32/
148
- * <version>/openocd-esp32/bin/openocd.exe. Pick the newest version dir.
149
- * Returns undefined if not found (the launch.json then omits openOCDPath and
150
- * Cortex-Debug falls back to PATH / its openocdPath setting).
151
- */
152
- export function resolveOpenOcdPath(): string | undefined {
153
- const candidates: string[] = [];
154
- // 1. ESPRESSIF_TOOLCHAIN_PATH env
155
- const envPath = process.env.ESPRESSIF_TOOLCHAIN_PATH;
156
- if (envPath) {
157
- candidates.push(join(envPath, 'openocd-esp32', 'bin', 'openocd.exe'));
158
- }
159
- // 2. ~/.espressif/tools/openocd-esp32/<version>/openocd-esp32/bin/openocd.exe
160
- const home = process.env.USERPROFILE || process.env.HOME;
161
- if (home) {
162
- const base = join(home, '.espressif', 'tools', 'openocd-esp32');
163
- let versions: string[] = [];
164
- try {
165
- versions = readdirSync(base).filter((v) =>
166
- existsSync(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe')),
167
- );
168
- } catch {
169
- // dir absent
170
- }
171
- // newest version last — sort then reverse so the highest wins on match.
172
- versions.sort().reverse();
173
- for (const v of versions) {
174
- candidates.push(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe'));
175
- }
176
- }
177
- for (const c of candidates) {
178
- if (existsSync(c)) return resolve(c).replace(/\\/g, '/');
179
- }
180
- return undefined;
181
- }
182
-
183
- /**
184
- * Resolve where to write the VS Code debug artifacts and how to express paths
185
- * in them.
186
- *
187
- * VS Code reads `.vscode/` from the folder the user has OPENED — and for a
188
- * cuttlefish project that is almost always the **cuttlefish project root**
189
- * (the directory containing `cuttlefish.config.ts`), NOT the git repo root.
190
- * The toolchain's `projectRoot` is the Zephyr *app* dir (e.g.
191
- * `<projectRoot>/src/out`), which sits below the cuttlefish config dir. So we
192
- * walk up from `projectRoot` to the nearest `cuttlefish.config.ts` and treat
193
- * THAT as the workspace root. This makes F5 work when a user opens the project
194
- * folder directly, and keeps launch.json paths relative to it.
195
- *
196
- * Returns { workspaceRoot, sketchRel } where workspaceRoot is the cuttlefish
197
- * project root (the `.vscode/` target) and sketchRel is the Zephyr app dir
198
- * (`projectRoot`) relative to it (e.g. 'src/out').
199
- */
200
- export function resolveDebugLocations(projectRoot: string): {
201
- workspaceRoot: string;
202
- sketchRel: string;
203
- } {
204
- // Walk up from the Zephyr app dir to find the cuttlefish project root (the
205
- // nearest ancestor containing cuttlefish.config.ts). Fall back to projectRoot
206
- // itself if none is found (single-dir project where the app sits at root).
207
- let workspaceRoot = resolve(projectRoot);
208
- let dir = resolve(projectRoot);
209
- for (let i = 0; i < 20; i++) {
210
- if (existsSync(join(dir, 'cuttlefish.config.ts'))) {
211
- workspaceRoot = dir;
212
- break;
213
- }
214
- const parent = dirname(dir);
215
- if (parent === dir) break;
216
- dir = parent;
217
- }
218
-
219
- const sketchRel = relative(workspaceRoot, resolve(projectRoot)).replace(/\\/g, '/');
220
- return { workspaceRoot, sketchRel };
221
- }
222
-
223
- /** Read-merge-write a JSON file, adding/replacing a single config by a key. */
224
- function mergeJsonArrayEntry<T extends Record<string, unknown>>(
225
- filePath: string,
226
- arrayKey: string,
227
- matchKey: string,
228
- entry: T,
229
- ): void {
230
- let doc: Record<string, unknown> = {};
231
- if (existsSync(filePath)) {
232
- try {
233
- doc = JSON.parse(readFileSync(filePath, 'utf-8'));
234
- } catch {
235
- // malformed — start fresh
236
- }
237
- }
238
- const arr = Array.isArray(doc[arrayKey]) ? (doc[arrayKey] as T[]) : [];
239
- const idx = arr.findIndex((e) => e[matchKey] === entry[matchKey]);
240
- if (idx >= 0) arr[idx] = entry;
241
- else arr.push(entry);
242
- doc[arrayKey] = arr;
243
- mkdirSync(dirname(filePath), { recursive: true });
244
- writeFileSync(filePath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
245
- }
246
-
247
- /**
248
- * Generate the GDB Python frame-filter that rewrites cuttlefish's hoisted
249
- * lambda frame names (`${prefix}_isr_N`) into readable `<lambda> @ file:line`
250
- * in the call stack. Only emitted when the source map references `_isr_N`
251
- * symbols; otherwise returns null (launch.json omits the `source` initCommand).
252
- *
253
- * Ported verbatim from the deleted framework-esp32/toolchain/gdb-script.ts —
254
- * the filter is cuttlefish-internal (lambda hoisting is framework-agnostic).
255
- */
256
- export function generateGdbScript(sourceMapPath?: string): string | null {
257
- if (!sourceMapPath || !existsSync(sourceMapPath)) return null;
258
- let mapText = '';
259
- try {
260
- mapText = readFileSync(sourceMapPath, 'utf-8');
261
- } catch {
262
- return null;
263
- }
264
- // Only emit when the emitted code contains hoisted-lambda symbols.
265
- if (!/_isr_\d+/.test(mapText)) return null;
266
-
267
- // A GDB Python frame-filter. Registered via the launch.json initCommand
268
- // `source <path>` so GDB auto-loads it on attach.
269
- return [
270
- '# Auto-generated by @typecad/framework-zephyr. GDB frame-filter that',
271
- '# rewrites cuttlefish hoisted-lambda frame names (*_isr_N) into readable',
272
- '# <lambda> form so the VS Code call stack is legible.',
273
- 'import gdb',
274
- 'import re',
275
- '',
276
- 'class CuttlefishLambdaFilter:',
277
- ' def __init__(self):',
278
- ' self.name = "cuttlefish_lambda"',
279
- ' self.priority = 100',
280
- ' self.enabled = True',
281
- '',
282
- ' def filter(self, frame_iter):',
283
- ' isr_re = re.compile(r"(.*)_isr_(\\d+)")',
284
- ' return (CuttlefishFrame(f) for f in frame_iter)',
285
- '',
286
- 'class CuttlefishFrame:',
287
- ' def __init__(self, frame):',
288
- ' self.frame = frame',
289
- ' def __getattr__(self, name):',
290
- ' val = getattr(self.frame, name)',
291
- ' if name == "function":',
292
- ' m = isr_re.match(val)',
293
- ' if m: return "<lambda>"',
294
- ' return val',
295
- '',
296
- 'gdb.frame_filters[CuttlefishLambdaFilter().name] = CuttlefishLambdaFilter()',
297
- '',
298
- ].join('\n');
299
- }
300
-
301
- /**
302
- * Build the cortex-debug launch.json config for an ESP32-S3 (built-in USB-JTAG).
303
- * `openOcdCfgRel` is the workspace-relative path to the generated
304
- * .cuttlefish/openocd.cfg, passed to cortex-debug's configFiles.
305
- */
306
- function buildLaunchConfig(
307
- o: DebugConfigOptions,
308
- gdbScriptRel: string | undefined,
309
- openOcdCfgRel: string,
310
- ): Record<string, unknown> {
311
- // The ELF is at <projectRoot>/build/zephyr/zephyr.elf (Zephyr's standard
312
- // build output). cortex-debug uses `executable` (not `program`).
313
- const executable = `\${workspaceFolder}/${o.sketchRel}/build/zephyr/zephyr.elf`;
314
-
315
- // OpenOCD cfg relative to the workspace root so cortex-debug can pass it
316
- // via the -f flag. Must be a list; cortex-debug prepends -f per entry.
317
- const configFiles = [`\${workspaceFolder}/${openOcdCfgRel}`];
318
-
319
- // GDB path resolved from the Zephyr SDK build cache (CMakeCache.txt).
320
- const gdbPath = resolveGdbPath(o.buildDir, o.target);
321
-
322
- // OpenOCD binary path — the Espressif fork (openocd-esp32) is required for
323
- // the esp_usb_jtag adapter. cortex-debug's `serverpath` tells it where to
324
- // find the binary (not on PATH by default).
325
- const openocdPath = resolveOpenOcdPath();
326
-
327
- // Post-attach commands executed after GDB connects to the OpenOCD gdbserver.
328
- // set mem inaccessible-by-default off suppresses "Cannot access memory"
329
- // errors caused by overlapping Xtensa memory regions (flash-mapped
330
- // 0x4200xxxx isn't accessible until the bootloader runs).
331
- // mem 0x42000000 0x44000000 ro cache — tells GDB the app flash region IS
332
- // read-only so -break-insert uses hw breakpoints, not sw breakpoints
333
- // (which would fail with "Cannot access memory at 0x4200xxxx").
334
- // monitor reset init — reset target + halt (bootloader maps flash)
335
- // thb setup — temporary HW breakpoint at setup()
336
- // c — continue; bootloader maps flash, breaks at setup()
337
- //
338
- // Paths use forward slashes — ${workspaceFolder} on Windows produces
339
- // backslashes that GDB interprets as escape sequences (\t → tab, etc.).
340
- const ws = o.workspaceRoot.replace(/\\/g, '/');
341
- const postAttachCommands = [
342
- `set directories ${ws}`,
343
- 'set remote hardware-watchpoint-limit 2',
344
- 'set remote hardware-breakpoint-limit 2',
345
- 'set mem inaccessible-by-default off',
346
- 'mem 0x42000000 0x44000000 ro cache',
347
- 'monitor reset init',
348
- 'thb setup',
349
- 'c',
350
- ];
351
- if (gdbScriptRel) {
352
- postAttachCommands.splice(1, 0, `source ${ws}/${gdbScriptRel}`);
353
- }
354
-
355
- const cfg: Record<string, unknown> = {
356
- name: 'TypeCAD Debug (Zephyr, ESP32-S3)',
357
- type: 'cortex-debug',
358
- // Attach mode: no download (the ELF is already flashed). The server
359
- // controller's attachCommands() just halts the target, then our
360
- // postAttachCommands reset it, set a HW breakpoint at setup(), and
361
- // continue. HW breakpoints use debug registers and work before the
362
- // bootloader maps the app flash region.
363
- request: 'attach',
364
- cwd: '${workspaceFolder}',
365
- executable,
366
- servertype: 'openocd',
367
- configFiles,
368
- interface: 'jtag',
369
- ...(gdbPath ? { gdbPath } : {}),
370
- ...(openocdPath ? { serverpath: openocdPath } : {}),
371
- postAttachCommands,
372
- preLaunchTask: 'cuttlefish: build + flash (debug)',
373
- };
374
-
375
- // openOcdCfgRel is consumed by configFiles above. Referenced here only to
376
- // keep the signature honest.
377
- void openOcdCfgRel;
378
-
379
- return cfg;
380
- }
381
-
382
- /**
383
- * Build the tasks.json entry: rebuild + flash only. cortex-debug starts
384
- * OpenOCD as a child process (servertype: "openocd"), so the preLaunch task
385
- * just needs to build and upload — no isBackground / OpenOCD wrapping.
386
- */
387
- function buildTask(o: DebugConfigOptions): Record<string, unknown> {
388
- return {
389
- label: 'cuttlefish: build + flash (debug)',
390
- type: 'shell',
391
- command: 'npx cuttlefish build --compile --upload --debug',
392
- options: { cwd: `\${workspaceFolder}/${o.sketchRel}` },
393
- group: { kind: 'build', isDefault: false },
394
- problemMatcher: [],
395
- };
396
- }
397
-
398
- /**
399
- * The adapter speed the S3's built-in USB-Serial-JTAG runs stably at.
400
- *
401
- * The interface cfg (esp_usb_jtag.cfg) defaults to 40000 (40 MHz), the chip
402
- * max. The USB-Serial-JTAG peripheral is a software bitq adapter that bit-bangs
403
- * JTAG over USB bulk transfers; at 40 MHz it can't keep up, drops transfers
404
- * (LIBUSB_ERROR_IO / "missing data from bitq interface"), and the reset/halt
405
- * sequence silently fails — leaving the target running with no breakpoint set
406
- * (the "debugger starts but never stops / buttons don't work" symptom).
407
- *
408
- * 4000 (4 MHz) is the empirically-stable speed for this peripheral.
409
- */
410
- const OPENOCD_ADAPTER_SPEED = 4000;
411
-
412
- /**
413
- * Build the OpenOCD cfg content. Sources the board's own openocd.cfg (which
414
- * sets ESP_RTOS Zephyr + ESP_ONLYCPU + the esp_usb_jtag driver + the esp32s3
415
- * target) then overrides the adapter speed AFTER the driver loads — the order
416
- * that a bare `-c "adapter speed N"` cannot guarantee (OpenOCD applies -c args
417
- * in command-line order relative to -f, and Cortex-Debug injects its own
418
- * helper/RTOS tcl, so the override can land before the driver exists or be
419
- * re-defaulted). Putting it in the cfg, after the source, is deterministic.
420
- */
421
- function buildOpenOcdCfg(): string {
422
- return [
423
- '# Auto-generated by @typecad/framework-zephyr. Do not edit — regenerate',
424
- '# with `cuttlefish build --debug`. Sources the board cfg (which loads the',
425
- '# esp_usb_jtag adapter driver + esp32s3 target + ESP_RTOS Zephyr) then',
426
- '# overrides the adapter speed to a USB-JTAG-stable value AFTER the driver',
427
- '# is loaded. See debug-config.ts for the rationale.',
428
- 'source [find board/esp32s3-builtin.cfg]',
429
- `adapter speed ${OPENOCD_ADAPTER_SPEED}`,
430
- '',
431
- ].join('\n');
432
- }
433
-
434
- /**
435
- * Write all gdb-mode debug artifacts for the given target. Called from the
436
- * toolchain compile() after a successful build when debugMode === 'gdb'.
437
- *
438
- * Writes (all idempotent):
439
- * <workspaceRoot>/.vscode/launch.json (cortex-debug config)
440
- * <workspaceRoot>/.vscode/tasks.json (build + flash preLaunch task)
441
- * <projectRoot>/.cuttlefish/openocd.cfg (OpenOCD cfg, adapter speed override)
442
- * <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
443
- */
444
- export function writeDebugConfig(o: DebugConfigOptions): void {
445
- const vscodeDir = join(o.workspaceRoot, '.vscode');
446
- const cuttlefishDir = join(o.projectRoot, '.cuttlefish');
447
- mkdirSync(cuttlefishDir, { recursive: true });
448
-
449
- // openocd.cfg written first so its relative path can be wired into the
450
- // cortex-debug configFiles. cortex-debug starts OpenOCD as a child process
451
- // and passes this file via -f.
452
- const openOcdCfgPath = join(cuttlefishDir, 'openocd.cfg');
453
- writeFileSync(openOcdCfgPath, buildOpenOcdCfg(), 'utf-8');
454
- const openOcdCfgRel = `${o.sketchRel}/.cuttlefish/openocd.cfg`;
455
-
456
- // launch.json — merge the cortex-debug config by name.
457
- const gdbScript = generateGdbScript(o.sourceMapPath);
458
- let gdbScriptRel: string | undefined;
459
- if (gdbScript) {
460
- const scriptPath = join(cuttlefishDir, '.cuttlefish-gdb.py');
461
- writeFileSync(scriptPath, gdbScript, 'utf-8');
462
- gdbScriptRel = `${o.sketchRel}/.cuttlefish/.cuttlefish-gdb.py`;
463
- }
464
-
465
- const launchConfig = buildLaunchConfig(o, gdbScriptRel, openOcdCfgRel);
466
- mergeJsonArrayEntry(join(vscodeDir, 'launch.json'), 'configurations', 'name', launchConfig);
467
-
468
- // tasks.json — merge the build+flash task by label. OpenOCD is managed by
469
- // cortex-debug so the task is a simple synchronous build step.
470
- const task = buildTask(o);
471
- mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
472
- }
473
-
474
- /**
475
- * The Zephyr app dir a `cuttlefish create` scaffold produces, relative to the
476
- * project root: the scaffold fixes entry `./src/main.ts` + outDir `./out`, and
477
- * the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
478
- * the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
479
- * debug artifacts — always lands at `src/out`. Keep in sync with
480
- * generateProjectConfig in @typecad/cuttlefish create/init-templates.ts.
481
- */
482
- const STARTER_SKETCH_REL = 'src/out';
483
-
484
- /**
485
- * Create-time starter debug artifacts. Called by the cuttlefish `create` flow
486
- * (via the package's `writeProjectDebugArtifacts` export) so a fresh project
487
- * has a working F5 before any build exists:
488
- *
489
- * The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
490
- * --debug`, which builds + flashes AND rewrites this same launch entry (merged
491
- * by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
492
- * themselves on the first debug build.
493
- *
494
- * No-ops (returns []) for targets without native GDB support (debugMode() !==
495
- * 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
496
- *
497
- * Returns the workspace-relative paths written, for CLI reporting.
498
- */
499
- export function writeProjectDebugArtifacts(o: {
500
- /** Absolute path to the cuttlefish project root (contains cuttlefish.config.ts). */
501
- workspaceRoot: string;
502
- /** The Zephyr board id from the project config (frameworkData.buildTarget). */
503
- buildTarget?: string;
504
- }): string[] {
505
- if (new ZephyrStrategy().debugMode(o.buildTarget) !== 'gdb') return [];
506
- const workspaceRoot = resolve(o.workspaceRoot);
507
- const projectRoot = join(workspaceRoot, STARTER_SKETCH_REL);
508
- writeDebugConfig({
509
- projectRoot,
510
- workspaceRoot,
511
- sketchRel: STARTER_SKETCH_REL,
512
- target: o.buildTarget ?? '',
513
- // No build dir exists yet — resolveGdbPath falls back to probing known
514
- // Zephyr SDK locations so gdbPath is still filled in when possible.
515
- buildDir: join(projectRoot, 'build'),
516
- });
517
- return [
518
- '.vscode/launch.json',
519
- '.vscode/tasks.json',
520
- `${STARTER_SKETCH_REL}/.cuttlefish/openocd.cfg`,
521
- ];
522
- }
1
+ // ---------------------------------------------------------------------------
2
+ // debug-config.ts — VS Code / GDB debug artifact generation for `--debug`
3
+ //
4
+ // After a successful gdb-mode build, writes the artifacts that let the user
5
+ // press F5 in VS Code and attach GDB to the running Zephyr target.
6
+ //
7
+ // Uses the cortex-debug extension (NOT the ESP-IDF gdbtarget adapter) so the
8
+ // debug session is self-contained. cortex-debug starts OpenOCD as a child
9
+ // process via `servertype: "openocd"`; the preLaunch task handles only the
10
+ // build + flash step. After attach we issue `monitor reset init`, set a
11
+ // temporary hardware breakpoint at main(), and continue — this ensures the
12
+ // breakpoint is deferred until the bootloader maps the app flash region.
13
+ //
14
+ // All generators are deterministic + idempotent so toggling --debug does not
15
+ // churn the tree.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
19
+ import { join, resolve, dirname, relative } from 'node:path';
20
+ import { ZephyrStrategy } from '../strategy.js';
21
+ import { resolveChipFromBoard } from '../chips/resolve.js';
22
+ import { resolveProbeMethod } from './index.js';
23
+ import { loadCuttlefishConfig } from '@typecad/cuttlefish/config-loader';
24
+ import type { ZephyrProbeMethod } from '../chips/types.js';
25
+
26
+ export interface DebugConfigOptions {
27
+ /** Absolute path to the Zephyr project root (contains CMakeLists.txt + src/). */
28
+ projectRoot: string;
29
+ /** Absolute path to the workspace root (the cuttlefish config dir — the folder VS Code has open). */
30
+ workspaceRoot: string;
31
+ /** The Zephyr app dir relative to the workspace root (e.g. 'src/out'). */
32
+ appRel: string;
33
+ /** The Zephyr board id (e.g. 'esp32s3_devkitc'). */
34
+ target: string;
35
+ /** Absolute path to the Zephyr build dir (<projectRoot>/build) — read for the SDK/GDB path. */
36
+ buildDir: string;
37
+ /** Absolute path to the emitted source map (*.thcppmap.json), if any. */
38
+ sourceMapPath?: string;
39
+ }
40
+
41
+ /**
42
+ * Resolve the GDB binary path for the target from the build cache, falling
43
+ * back to a filesystem scan of known Zephyr SDK locations when no build
44
+ * exists yet (the create-time starter artifacts path). Zephyr records
45
+ * ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
46
+ * xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
47
+ * the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
48
+ *
49
+ * Returns the absolute gdb path on success, or undefined (the launch.json then
50
+ * omits gdbPath and relies on Cortex-Debug's default resolution).
51
+ */
52
+ export function resolveGdbPath(buildDir: string, target: string): string | undefined {
53
+ const cachePath = join(buildDir, 'CMakeCache.txt');
54
+ if (existsSync(cachePath)) {
55
+ try {
56
+ const cache = readFileSync(cachePath, 'utf-8');
57
+ const m = cache.match(/^ZEPHYR_SDK_INSTALL_DIR:PATH=(.+)$/m);
58
+ if (m) {
59
+ const fromCache = gdbPathFromSdkRoot(m[1].trim(), target);
60
+ if (fromCache) return fromCache;
61
+ }
62
+ } catch {
63
+ // unreadable cache fall through to the SDK scan
64
+ }
65
+ }
66
+ // No build dir yet (project just created): probe known SDK locations.
67
+ for (const sdkRoot of discoverZephyrSdkRoots()) {
68
+ const p = gdbPathFromSdkRoot(sdkRoot, target);
69
+ if (p) return p;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /** The GDB location inside a Zephyr SDK root, per target architecture:
75
+ * ARM boards use the arm-zephyr-eabi toolchain (verified against
76
+ * zephyr-sdk-0.17.4), Espressif the xtensa-espressif_esp32s3_zephyr-elf one.
77
+ * Returns a forward-slash absolute path or undefined. */
78
+ export function gdbPathFromSdkRoot(sdkRoot: string, target?: string): string | undefined {
79
+ // No target given (legacy callers/tests): the historical Espresif default.
80
+ const boardId = (target ?? '').split('/')[0];
81
+ const isArm = boardId.length > 0 && !boardId.startsWith('esp32');
82
+ const toolchainDir = isArm ? 'arm-zephyr-eabi' : 'xtensa-espressif_esp32s3_zephyr-elf';
83
+ const gdbName = isArm ? 'arm-zephyr-eabi-gdb.exe' : 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
84
+ const gdbPath = join(sdkRoot, toolchainDir, 'bin', gdbName);
85
+ return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
86
+ }
87
+
88
+ /** Compare two dotted version strings numerically (0.17.10 > 0.17.4). */
89
+ function compareSdkVersions(a: string, b: string): number {
90
+ const segsOf = (v: string): number[] => v.split('.').map((s) => parseInt(s, 10) || 0);
91
+ const aa = segsOf(a);
92
+ const bb = segsOf(b);
93
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
94
+ const d = (aa[i] ?? 0) - (bb[i] ?? 0);
95
+ if (d !== 0) return d;
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ /**
101
+ * Probe the well-known Zephyr SDK install locations, newest version first:
102
+ * 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
103
+ * 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> — the
104
+ * the bundled Zephyr installer layout
105
+ * 3. ~/zephyr-sdk-<ver> — the standalone download layout
106
+ *
107
+ * Only roots that actually contain the esp32s3 GDB are useful to callers;
108
+ * this returns candidate roots (gdbPathFromSdkRoot does the existence check)
109
+ * so tests can inject home/env overrides.
110
+ */
111
+ export function discoverZephyrSdkRoots(opts?: {
112
+ home?: string;
113
+ env?: Record<string, string | undefined>;
114
+ }): string[] {
115
+ const env = opts?.env ?? process.env;
116
+ const home = opts?.home ?? (env.USERPROFILE || env.HOME || '');
117
+ const scanned: string[] = [];
118
+
119
+ const versionedDirs = (base: string): string[] => {
120
+ try {
121
+ return readdirSync(base)
122
+ .filter((d) => existsSync(join(base, d)) && d.startsWith('zephyr-sdk-'))
123
+ .map((d) => join(base, d));
124
+ } catch {
125
+ return []; // dir absent
126
+ }
127
+ };
128
+ const mambaRoot = env.MAMBA_ROOT_PREFIX || (home ? join(home, 'micromamba') : '');
129
+ if (mambaRoot) scanned.push(...versionedDirs(join(mambaRoot, 'zephyr-sdk')));
130
+ if (home) scanned.push(...versionedDirs(home));
131
+
132
+ // Scanned roots newest version first; the env var stays pinned first
133
+ // (explicit user intent outranks any discovered location).
134
+ scanned.sort((a, b) => {
135
+ const va = a.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
136
+ const vb = b.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
137
+ return compareSdkVersions(vb, va);
138
+ });
139
+ const roots = env.ZEPHYR_SDK_INSTALL_DIR
140
+ ? [env.ZEPHYR_SDK_INSTALL_DIR, ...scanned]
141
+ : scanned;
142
+ // De-duplicate (an env var may repeat a scan hit) preserving order.
143
+ return roots.filter((r, i) => roots.indexOf(r) === i);
144
+ }
145
+
146
+ /**
147
+ * Resolve the OpenOCD binary path. The esp32s3 needs the Espressif OpenOCD
148
+ * fork (openocd-esp32) not the Zephyr SDK's openocd and not a
149
+ * generic/GDB-stub build because only it carries the Xtensa + esp_usb_jtag
150
+ * support. ARM targets prefer the Zephyr SDK's own openocd (it ships the
151
+ * interface/target cfgs boards reference); the Espressif fork is only
152
+ * consulted for esp32 targets.
153
+ *
154
+ * Discovery order:
155
+ * 1. ESPRESSIF_TOOLCHAIN_PATH env (the var board.cmake reads) — esp32
156
+ * targets; its openocd-esp32/bin/openocd.exe.
157
+ * 2. The standard ESP-IDF install layout: ~/.espressif/tools/openocd-esp32/
158
+ * <version>/openocd-esp32/bin/openocd.exe (esp32 targets; newest first).
159
+ * 3. A Zephyr SDK root (discoverZephyrSdkRoots) — its
160
+ * tools/opt/openocd/bin/openocd(.exe), any target.
161
+ * Returns undefined if not found (the launch.json then omits openOCDPath and
162
+ * Cortex-Debug falls back to PATH / its openocdPath setting).
163
+ */
164
+ export function resolveOpenOcdPath(target?: string): string | undefined {
165
+ const isEsp32Target = (target ?? '').split('/')[0].startsWith('esp32');
166
+ const candidates: string[] = [];
167
+ if (isEsp32Target) {
168
+ // 1. ESPRESSIF_TOOLCHAIN_PATH env
169
+ const envPath = process.env.ESPRESSIF_TOOLCHAIN_PATH;
170
+ if (envPath) {
171
+ candidates.push(join(envPath, 'openocd-esp32', 'bin', 'openocd.exe'));
172
+ }
173
+ // 2. ~/.espressif/tools/openocd-esp32/<version>/openocd-esp32/bin/openocd.exe
174
+ const home = process.env.USERPROFILE || process.env.HOME;
175
+ if (home) {
176
+ const base = join(home, '.espressif', 'tools', 'openocd-esp32');
177
+ let versions: string[] = [];
178
+ try {
179
+ versions = readdirSync(base).filter((v) =>
180
+ existsSync(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe')),
181
+ );
182
+ } catch {
183
+ // dir absent
184
+ }
185
+ // newest version last — sort then reverse so the highest wins on match.
186
+ versions.sort().reverse();
187
+ for (const v of versions) {
188
+ candidates.push(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe'));
189
+ }
190
+ }
191
+ }
192
+ // 3. The Zephyr SDK's openocd (ARM targets' interface/target cfgs ship
193
+ // with it interface/stlink-dap.cfg, target/stm32f4x.cfg, ...). Layout
194
+ // differs by SDK generation: 1.0.x hosttools/openocd, 0.17.x
195
+ // tools/opt/openocd (when present).
196
+ const exe = process.platform === 'win32' ? 'openocd.exe' : 'openocd';
197
+ for (const sdkRoot of discoverZephyrSdkRoots()) {
198
+ candidates.push(join(sdkRoot, 'hosttools', 'openocd', 'bin', exe));
199
+ candidates.push(join(sdkRoot, 'tools', 'opt', 'openocd', 'bin', exe));
200
+ }
201
+ for (const c of candidates) {
202
+ if (existsSync(c)) return resolve(c).replace(/\\/g, '/');
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ /**
208
+ * Resolve where to write the VS Code debug artifacts and how to express paths
209
+ * in them.
210
+ *
211
+ * VS Code reads `.vscode/` from the folder the user has OPENED — and for a
212
+ * cuttlefish project that is almost always the **cuttlefish project root**
213
+ * (the directory containing `cuttlefish.config.ts`), NOT the git repo root.
214
+ * The toolchain's `projectRoot` is the Zephyr *app* dir (e.g.
215
+ * `<projectRoot>/src/out`), which sits below the cuttlefish config dir. So we
216
+ * walk up from `projectRoot` to the nearest `cuttlefish.config.ts` and treat
217
+ * THAT as the workspace root. This makes F5 work when a user opens the project
218
+ * folder directly, and keeps launch.json paths relative to it.
219
+ *
220
+ * Returns { workspaceRoot, appRel } where workspaceRoot is the cuttlefish
221
+ * project root (the `.vscode/` target) and appRel is the Zephyr app dir
222
+ * (`projectRoot`) relative to it (e.g. 'src/out').
223
+ */
224
+ export function resolveDebugLocations(projectRoot: string): {
225
+ workspaceRoot: string;
226
+ appRel: string;
227
+ } {
228
+ // Walk up from the Zephyr app dir to find the cuttlefish project root (the
229
+ // nearest ancestor containing cuttlefish.config.ts). Fall back to projectRoot
230
+ // itself if none is found (single-dir project where the app sits at root).
231
+ let workspaceRoot = resolve(projectRoot);
232
+ let dir = resolve(projectRoot);
233
+ for (let i = 0; i < 20; i++) {
234
+ if (existsSync(join(dir, 'cuttlefish.config.ts'))) {
235
+ workspaceRoot = dir;
236
+ break;
237
+ }
238
+ const parent = dirname(dir);
239
+ if (parent === dir) break;
240
+ dir = parent;
241
+ }
242
+
243
+ const appRel = relative(workspaceRoot, resolve(projectRoot)).replace(/\\/g, '/');
244
+ return { workspaceRoot, appRel };
245
+ }
246
+
247
+ /** Read-merge-write a JSON file, adding/replacing a single config by a key. */
248
+ function mergeJsonArrayEntry<T extends Record<string, unknown>>(
249
+ filePath: string,
250
+ arrayKey: string,
251
+ matchKey: string,
252
+ entry: T,
253
+ ): void {
254
+ let doc: Record<string, unknown> = {};
255
+ if (existsSync(filePath)) {
256
+ try {
257
+ doc = JSON.parse(readFileSync(filePath, 'utf-8'));
258
+ } catch {
259
+ // malformed — start fresh
260
+ }
261
+ }
262
+ const arr = Array.isArray(doc[arrayKey]) ? (doc[arrayKey] as T[]) : [];
263
+ const idx = arr.findIndex((e) => e[matchKey] === entry[matchKey]);
264
+ if (idx >= 0) arr[idx] = entry;
265
+ else arr.push(entry);
266
+ doc[arrayKey] = arr;
267
+ mkdirSync(dirname(filePath), { recursive: true });
268
+ writeFileSync(filePath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
269
+ }
270
+
271
+ /**
272
+ * Generate the GDB Python frame-filter that rewrites cuttlefish's hoisted
273
+ * lambda frame names (`${prefix}_isr_N`) into readable `<lambda> @ file:line`
274
+ * in the call stack. Only emitted when the source map references `_isr_N`
275
+ * symbols; otherwise returns null (launch.json omits the `source` initCommand).
276
+ *
277
+ * Ported verbatim from the deleted framework-esp32/toolchain/gdb-script.ts —
278
+ * the filter is cuttlefish-internal (lambda hoisting is framework-agnostic).
279
+ */
280
+ export function generateGdbScript(sourceMapPath?: string): string | null {
281
+ if (!sourceMapPath || !existsSync(sourceMapPath)) return null;
282
+ let mapText = '';
283
+ try {
284
+ mapText = readFileSync(sourceMapPath, 'utf-8');
285
+ } catch {
286
+ return null;
287
+ }
288
+ // Only emit when the emitted code contains hoisted-lambda symbols.
289
+ if (!/_isr_\d+/.test(mapText)) return null;
290
+
291
+ // A GDB Python frame-filter. Registered via the launch.json initCommand
292
+ // `source <path>` so GDB auto-loads it on attach.
293
+ return [
294
+ '# Auto-generated by @typecad/framework-zephyr. GDB frame-filter that',
295
+ '# rewrites cuttlefish hoisted-lambda frame names (*_isr_N) into readable',
296
+ '# <lambda> form so the VS Code call stack is legible.',
297
+ 'import gdb',
298
+ 'import re',
299
+ '',
300
+ 'class CuttlefishLambdaFilter:',
301
+ ' def __init__(self):',
302
+ ' self.name = "cuttlefish_lambda"',
303
+ ' self.priority = 100',
304
+ ' self.enabled = True',
305
+ '',
306
+ ' def filter(self, frame_iter):',
307
+ ' isr_re = re.compile(r"(.*)_isr_(\\d+)")',
308
+ ' return (CuttlefishFrame(f) for f in frame_iter)',
309
+ '',
310
+ 'class CuttlefishFrame:',
311
+ ' def __init__(self, frame):',
312
+ ' self.frame = frame',
313
+ ' def __getattr__(self, name):',
314
+ ' val = getattr(self.frame, name)',
315
+ ' if name == "function":',
316
+ ' m = isr_re.match(val)',
317
+ ' if m: return "<lambda>"',
318
+ ' return val',
319
+ '',
320
+ 'gdb.frame_filters[CuttlefishLambdaFilter().name] = CuttlefishLambdaFilter()',
321
+ '',
322
+ ].join('\n');
323
+ }
324
+
325
+ /**
326
+ * Build the cortex-debug launch.json config for an ESP32-S3 (built-in USB-JTAG).
327
+ * `openOcdCfgRel` is the workspace-relative path to the generated
328
+ * .cuttlefish/openocd.cfg, passed to cortex-debug's configFiles.
329
+ */
330
+ function buildLaunchConfig(
331
+ o: DebugConfigOptions,
332
+ gdbScriptRel: string | undefined,
333
+ openOcdCfgRel: string,
334
+ method?: ZephyrProbeMethod,
335
+ ): Record<string, unknown> {
336
+ // The ELF is at <projectRoot>/build/zephyr/zephyr.elf (Zephyr's standard
337
+ // build output). cortex-debug uses `executable` (not `program`).
338
+ const executable = `\${workspaceFolder}/${o.appRel}/build/zephyr/zephyr.elf`;
339
+
340
+ // OpenOCD cfg relative to the workspace root so cortex-debug can pass it
341
+ // via the -f flag. Must be a list; cortex-debug prepends -f per entry.
342
+ const configFiles = [`\${workspaceFolder}/${openOcdCfgRel}`];
343
+
344
+ // GDB path resolved from the Zephyr SDK build cache (CMakeCache.txt).
345
+ const gdbPath = resolveGdbPath(o.buildDir, o.target);
346
+
347
+ // OpenOCD binary path — the Espressif fork (openocd-esp32) is required for
348
+ // the esp_usb_jtag adapter. cortex-debug's `serverpath` tells it where to
349
+ // find the binary (not on PATH by default).
350
+ const openocdPath = resolveOpenOcdPath(o.target);
351
+
352
+ // Post-attach commands executed after GDB connects to the OpenOCD gdbserver.
353
+ // set mem inaccessible-by-default off — suppresses "Cannot access memory"
354
+ // errors caused by overlapping Xtensa memory regions (flash-mapped
355
+ // 0x4200xxxx isn't accessible until the bootloader runs).
356
+ // mem 0x42000000 0x44000000 ro cache — tells GDB the app flash region IS
357
+ // read-only so -break-insert uses hw breakpoints, not sw breakpoints
358
+ // (which would fail with "Cannot access memory at 0x4200xxxx").
359
+ // monitor reset init — reset target + halt (bootloader maps flash)
360
+ // thb main — temporary HW breakpoint at main()
361
+ // c — continue; bootloader maps flash, breaks at main()
362
+ //
363
+ // The trailing `c` is ESP32-only. On instant-reset ARM targets the
364
+ // thb-main stop lands within milliseconds — WHILE cortex-debug is still
365
+ // chewing through this command list — and a stop event mid-initialization
366
+ // leaves the session half-started (toolbar never enables, user breakpoints
367
+ // never bind). The ESP32's bootloader takes hundreds of milliseconds, so
368
+ // its stop safely arrives after init completes. ARM keeps the pending
369
+ // thb (the first user Continue stops at main()) but hands the run/stop
370
+ // transition to cortex-debug.
371
+ //
372
+ // Paths use forward slashes — ${workspaceFolder} on Windows produces
373
+ // backslashes that GDB interprets as escape sequences (\t → tab, etc.).
374
+ const ws = o.workspaceRoot.replace(/\\/g, '/');
375
+ // The Xtensa flash-mapping commands are ESP32-specific (app flash isn't
376
+ // readable until the bootloader maps it); ARM targets drop them.
377
+ const isEsp32Target = o.target.split('/')[0].startsWith('esp32');
378
+ const postAttachCommands = [
379
+ `set directories ${ws}`,
380
+ 'set remote hardware-watchpoint-limit 2',
381
+ 'set remote hardware-breakpoint-limit 2',
382
+ ...(isEsp32Target ? [
383
+ 'set mem inaccessible-by-default off',
384
+ 'mem 0x42000000 0x44000000 ro cache',
385
+ ] : []),
386
+ 'monitor reset init',
387
+ 'thb main',
388
+ ...(isEsp32Target ? ['c'] : []),
389
+ ];
390
+ if (gdbScriptRel) {
391
+ postAttachCommands.splice(1, 0, `source ${ws}/${gdbScriptRel}`);
392
+ }
393
+
394
+ // Probe-method-driven server shape: jlink-runner methods use cortex-debug's
395
+ // jlink server (needs the device name from the board table); openocd-runner
396
+ // methods keep the openocd server with the generated cfg file. Without a
397
+ // method (ESP32 builtin-JTAG targets), the historical default applies.
398
+ const servertype = method?.runner === 'jlink' ? 'jlink' : 'openocd';
399
+ const isEsp32 = o.target.split('/')[0].startsWith('esp32');
400
+
401
+ const cfg: Record<string, unknown> = {
402
+ name: `TypeCAD Debug (Zephyr, ${o.target.split('/')[0]})`,
403
+ type: 'cortex-debug',
404
+ // Attach mode: no download (the ELF is already flashed). The server
405
+ // controller's attachCommands() just halts the target, then our
406
+ // postAttachCommands reset it, set a HW breakpoint at main(), and
407
+ // continue. HW breakpoints use debug registers and work before the
408
+ // bootloader maps the app flash region.
409
+ request: 'attach',
410
+ cwd: '${workspaceFolder}',
411
+ executable,
412
+ servertype,
413
+ ...(servertype === 'openocd' ? { configFiles } : {}),
414
+ ...(method?.debugDevice ? { device: method.debugDevice } : {}),
415
+ interface: method?.debugInterface ?? (isEsp32 ? 'jtag' : 'swd'),
416
+ ...(gdbPath ? { gdbPath } : {}),
417
+ ...(servertype === 'openocd' && openocdPath ? { serverpath: openocdPath } : {}),
418
+ postAttachCommands,
419
+ preLaunchTask: 'cuttlefish: build + flash (debug)',
420
+ };
421
+
422
+ // openOcdCfgRel is consumed by configFiles above. Referenced here only to
423
+ // keep the signature honest.
424
+ void openOcdCfgRel;
425
+
426
+ return cfg;
427
+ }
428
+
429
+ /**
430
+ * Build the tasks.json entry: rebuild + flash only. cortex-debug starts
431
+ * OpenOCD as a child process (servertype: "openocd"), so the preLaunch task
432
+ * just needs to build and upload — no isBackground / OpenOCD wrapping.
433
+ */
434
+ function buildTask(o: DebugConfigOptions): Record<string, unknown> {
435
+ return {
436
+ label: 'cuttlefish: build + flash (debug)',
437
+ type: 'shell',
438
+ command: 'npx cuttlefish build --compile --upload --debug',
439
+ options: { cwd: `\${workspaceFolder}/${o.appRel}` },
440
+ group: { kind: 'build', isDefault: false },
441
+ problemMatcher: [],
442
+ };
443
+ }
444
+
445
+ /**
446
+ * The adapter speed the S3's built-in USB-Serial-JTAG runs stably at.
447
+ *
448
+ * The interface cfg (esp_usb_jtag.cfg) defaults to 40000 (40 MHz), the chip
449
+ * max. The USB-Serial-JTAG peripheral is a software bitq adapter that bit-bangs
450
+ * JTAG over USB bulk transfers; at 40 MHz it can't keep up, drops transfers
451
+ * (LIBUSB_ERROR_IO / "missing data from bitq interface"), and the reset/halt
452
+ * sequence silently fails — leaving the target running with no breakpoint set
453
+ * (the "debugger starts but never stops / buttons don't work" symptom).
454
+ *
455
+ * 4000 (4 MHz) is the empirically-stable speed for this peripheral.
456
+ */
457
+ const OPENOCD_ADAPTER_SPEED = 4000;
458
+
459
+ /**
460
+ * Build the OpenOCD cfg content. Sources the board's own openocd.cfg (which
461
+ * sets ESP_RTOS Zephyr + ESP_ONLYCPU + the esp_usb_jtag driver + the esp32s3
462
+ * target) then overrides the adapter speed AFTER the driver loads — the order
463
+ * that a bare `-c "adapter speed N"` cannot guarantee (OpenOCD applies -c args
464
+ * in command-line order relative to -f, and Cortex-Debug injects its own
465
+ * helper/RTOS tcl, so the override can land before the driver exists or be
466
+ * re-defaulted). Putting it in the cfg, after the source, is deterministic.
467
+ */
468
+ /**
469
+ * Drop the gdb-attach/gdb-detach target-event blocks from a cfg line list.
470
+ * Board cfgs use them for standalone openocd sessions (reset on attach,
471
+ * resume on detach), but under VS Code they fire DURING cortex-debug's
472
+ * initialization — the unexpected stop event aborts the session setup
473
+ * ("Program stopped, probably due to a reset and/or halt issued by
474
+ * debugger") and the toolbar never enables. The IDE owns the lifecycle.
475
+ */
476
+ function stripGdbEventBlocks(lines: readonly string[]): string[] {
477
+ const out: string[] = [];
478
+ let skipping = false;
479
+ let depth = 0;
480
+ for (const line of lines) {
481
+ if (!skipping && /configure\s+-event\s+gdb-(attach|detach)/.test(line)) {
482
+ skipping = true;
483
+ depth = 0;
484
+ }
485
+ if (skipping) {
486
+ depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
487
+ if (depth <= 0 && (line.includes('}') || !line.includes('{'))) skipping = false;
488
+ continue;
489
+ }
490
+ out.push(line);
491
+ }
492
+ return out;
493
+ }
494
+
495
+ function buildOpenOcdCfg(method?: ZephyrProbeMethod, target?: string): string {
496
+ const header = [
497
+ '# Auto-generated by @typecad/framework-zephyr. Do not edit regenerate',
498
+ '# with `cuttlefish build --debug`.',
499
+ ];
500
+ // Zephyr RTOS awareness (thread names in the call stack) — appended after
501
+ // the target source on ARM targets; the ESP32 board cfgs already carry
502
+ // ESP_RTOS Zephyr.
503
+ const isEsp32 = (target ?? '').split('/')[0].startsWith('esp32');
504
+ const rtosLine = isEsp32 ? [] : ['$_TARGETNAME configure -rtos Zephyr'];
505
+ if (method?.debugCfgSource && method.debugCfgSource.length > 0) {
506
+ // Probe-method-driven config: the board package's verified interface +
507
+ // target sources, then the method's quirk lines (e.g. reset_config for an
508
+ // unwired SRST) — the cfg-file form of the west runner args.
509
+ return [
510
+ ...header,
511
+ `# Probe method '${method.id}' — from the board package's probeMethods table.`,
512
+ ...method.debugCfgSource.map((src) => `source [find ${src}]`),
513
+ ...stripGdbEventBlocks(method.debugCfg ?? []),
514
+ ...rtosLine,
515
+ '',
516
+ ].join('\n');
517
+ }
518
+ // Pack-derived methods (the board's own support/openocd.cfg, extracted by
519
+ // the board data pack) carry complete cfg lines — including their
520
+ // `source [find ...]` directives — verbatim in file order, minus the
521
+ // gdb-attach/detach events (IDE-managed lifecycle; see stripGdbEventBlocks).
522
+ if (method?.debugCfg && method.debugCfg.length > 0) {
523
+ return [
524
+ ...header,
525
+ `# Probe method '${method.id}' — the board's own support/openocd.cfg,`,
526
+ `# extracted by the board data pack (gdb-attach/detach events removed —`,
527
+ `# the IDE owns the session lifecycle).`,
528
+ ...stripGdbEventBlocks(method.debugCfg),
529
+ ...rtosLine,
530
+ '',
531
+ ].join('\n');
532
+ }
533
+ return [
534
+ ...header,
535
+ '# Sources the board cfg (which loads the esp_usb_jtag adapter driver +',
536
+ '# esp32s3 target + ESP_RTOS Zephyr) then overrides the adapter speed to a',
537
+ '# USB-JTAG-stable value AFTER the driver is loaded. See debug-config.ts',
538
+ '# for the rationale.',
539
+ 'source [find board/esp32s3-builtin.cfg]',
540
+ `adapter speed ${OPENOCD_ADAPTER_SPEED}`,
541
+ '',
542
+ ].join('\n');
543
+ }
544
+
545
+ /**
546
+ * Write all gdb-mode debug artifacts for the given target. Called from the
547
+ * toolchain compile() after a successful build when debugMode === 'gdb'.
548
+ *
549
+ * Writes (all idempotent):
550
+ * <workspaceRoot>/.vscode/launch.json (cortex-debug config)
551
+ * <workspaceRoot>/.vscode/tasks.json (build + flash preLaunch task)
552
+ * <projectRoot>/.cuttlefish/openocd.cfg (OpenOCD cfg, adapter speed override)
553
+ * <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
554
+ */
555
+ /**
556
+ * Resolve the debug-side probe method for a project: the board constants the
557
+ * transpile persists + the workspace config's zephyr.probe selection.
558
+ * Returns the method object (for cfg/servertype data) or undefined — ESP32
559
+ * targets and boards without tables fall back to the framework's default
560
+ * (esp_usb_jtag openocd) behavior.
561
+ */
562
+ function resolveDebugProbeMethod(
563
+ projectRoot: string,
564
+ workspaceRoot: string,
565
+ ): ZephyrProbeMethod | undefined {
566
+ try {
567
+ const bcPath = [
568
+ join(projectRoot, 'src', 'board-constants.json'),
569
+ join(projectRoot, 'board-constants.json'),
570
+ ].find((p) => existsSync(p));
571
+ if (!bcPath) return undefined;
572
+ const raw = JSON.parse(readFileSync(bcPath, 'utf8')) as Record<string, string | number | boolean>;
573
+ const chip = resolveChipFromBoard(new Map(Object.entries(raw)));
574
+ if (!chip) return undefined;
575
+ const zc = loadCuttlefishConfig(workspaceRoot)?.zephyrConfig as Record<string, unknown> | undefined;
576
+ const probe = resolveProbeMethod(zc, chip, 'debug');
577
+ if (!probe.ok) return undefined;
578
+ return chip.probeMethods?.find((m) => m.id === (zc?.probe as string | undefined));
579
+ } catch {
580
+ return undefined;
581
+ }
582
+ }
583
+
584
+ export function writeDebugConfig(o: DebugConfigOptions): void {
585
+ const vscodeDir = join(o.workspaceRoot, '.vscode');
586
+ const cuttlefishDir = join(o.projectRoot, '.cuttlefish');
587
+ mkdirSync(cuttlefishDir, { recursive: true });
588
+
589
+ // The selected probe method (if any) drives the debug server shape:
590
+ // openocd cfg source/quirks, cortex-debug servertype, wire interface.
591
+ const method = resolveDebugProbeMethod(o.projectRoot, o.workspaceRoot);
592
+
593
+ // openocd.cfg — written first so its relative path can be wired into the
594
+ // cortex-debug configFiles. cortex-debug starts OpenOCD as a child process
595
+ // and passes this file via -f.
596
+ const openOcdCfgPath = join(cuttlefishDir, 'openocd.cfg');
597
+ writeFileSync(openOcdCfgPath, buildOpenOcdCfg(method, o.target), 'utf-8');
598
+ const openOcdCfgRel = `${o.appRel}/.cuttlefish/openocd.cfg`;
599
+
600
+ // launch.json — merge the cortex-debug config by name.
601
+ const gdbScript = generateGdbScript(o.sourceMapPath);
602
+ let gdbScriptRel: string | undefined;
603
+ if (gdbScript) {
604
+ const scriptPath = join(cuttlefishDir, '.cuttlefish-gdb.py');
605
+ writeFileSync(scriptPath, gdbScript, 'utf-8');
606
+ gdbScriptRel = `${o.appRel}/.cuttlefish/.cuttlefish-gdb.py`;
607
+ }
608
+
609
+ const launchConfig = buildLaunchConfig(o, gdbScriptRel, openOcdCfgRel, method);
610
+ mergeJsonArrayEntry(join(vscodeDir, 'launch.json'), 'configurations', 'name', launchConfig);
611
+
612
+ // tasks.json — merge the build+flash task by label. OpenOCD is managed by
613
+ // cortex-debug so the task is a simple synchronous build step.
614
+ const task = buildTask(o);
615
+ mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
616
+ }
617
+
618
+ /**
619
+ * The Zephyr app dir a `cuttlefish create` scaffold produces, relative to the
620
+ * project root: the scaffold fixes entry `./src/main.ts` + outDir `./out`, and
621
+ * the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
622
+ * the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
623
+ * debug artifacts — always lands at `src/out`. Keep in sync with
624
+ * generateProjectConfig in @typecad/cuttlefish create/templates.ts.
625
+ */
626
+ const STARTER_APP_REL = 'src/out';
627
+
628
+ /**
629
+ * Create-time starter debug artifacts. Called by the cuttlefish `create` flow
630
+ * (via the package's `writeProjectDebugArtifacts` export) so a fresh project
631
+ * has a working F5 before any build exists:
632
+ *
633
+ * The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
634
+ * --debug`, which builds + flashes AND rewrites this same launch entry (merged
635
+ * by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
636
+ * themselves on the first debug build.
637
+ *
638
+ * No-ops (returns []) for targets without native GDB support (debugMode() !==
639
+ * 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
640
+ *
641
+ * Returns the workspace-relative paths written, for CLI reporting.
642
+ */
643
+ export function writeProjectDebugArtifacts(o: {
644
+ /** Absolute path to the cuttlefish project root (contains cuttlefish.config.ts). */
645
+ workspaceRoot: string;
646
+ /** The Zephyr board id from the project config (frameworkData.buildTarget). */
647
+ buildTarget?: string;
648
+ }): string[] {
649
+ if (new ZephyrStrategy().debugMode(o.buildTarget) !== 'gdb') return [];
650
+ const workspaceRoot = resolve(o.workspaceRoot);
651
+ const projectRoot = join(workspaceRoot, STARTER_APP_REL);
652
+ writeDebugConfig({
653
+ projectRoot,
654
+ workspaceRoot,
655
+ appRel: STARTER_APP_REL,
656
+ target: o.buildTarget ?? '',
657
+ // No build dir exists yet — resolveGdbPath falls back to probing known
658
+ // Zephyr SDK locations so gdbPath is still filled in when possible.
659
+ buildDir: join(projectRoot, 'build'),
660
+ });
661
+ return [
662
+ '.vscode/launch.json',
663
+ '.vscode/tasks.json',
664
+ `${STARTER_APP_REL}/.cuttlefish/openocd.cfg`,
665
+ ];
666
+ }