@typecad/framework-zephyr 1.0.0-alpha.11 → 1.0.0-alpha.13
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/dist/display/index.d.ts +1 -1
- package/dist/display/index.js +1 -1
- package/dist/display/profiles.d.ts +33 -0
- package/dist/display/profiles.js +36 -0
- package/dist/display/touch-adapter.d.ts +3 -4
- package/dist/display/touch-adapter.js +119 -16
- package/dist/display/ui-adapter.d.ts +4 -0
- package/dist/display/ui-adapter.js +348 -139
- package/dist/dt-config/kconfig.d.ts +5 -1
- package/dist/dt-config/kconfig.js +22 -5
- package/dist/dt-config/overlay.d.ts +26 -2
- package/dist/dt-config/overlay.js +138 -27
- package/dist/framework.manifest.d.ts +29 -28
- package/dist/framework.manifest.js +9 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lowering/ble.js +3 -1
- package/dist/lowering/gpio.js +7 -3
- package/dist/strategy.d.ts +24 -0
- package/dist/strategy.js +326 -134
- package/dist/toolchain/debug-config.d.ts +43 -2
- package/dist/toolchain/debug-config.js +129 -17
- package/dist/toolchain/index.d.ts +13 -0
- package/dist/toolchain/index.js +104 -23
- package/dist/toolchain/scaffold.js +40 -18
- package/dist/toolchain/west-discover.js +4 -1
- package/package.json +4 -4
- package/src/display/index.ts +1 -1
- package/src/display/profiles.ts +63 -0
- package/src/display/touch-adapter.ts +119 -15
- package/src/display/ui-adapter.ts +357 -139
- package/src/dt-config/kconfig.ts +26 -6
- package/src/dt-config/overlay.ts +450 -298
- package/src/framework.manifest.ts +9 -3
- package/src/index.ts +6 -0
- package/src/lowering/ble.ts +3 -1
- package/src/lowering/gpio.ts +7 -3
- package/src/strategy.ts +355 -136
- package/src/toolchain/debug-config.ts +137 -14
- package/src/toolchain/index.ts +107 -24
- package/src/toolchain/scaffold.ts +39 -16
- package/src/toolchain/west-discover.ts +4 -1
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
19
19
|
import { join, resolve, dirname, relative } from 'node:path';
|
|
20
|
+
import { ZephyrStrategy } from '../strategy.js';
|
|
20
21
|
|
|
21
22
|
export interface DebugConfigOptions {
|
|
22
23
|
/** Absolute path to the Zephyr project root (contains CMakeLists.txt + src/). */
|
|
@@ -34,8 +35,10 @@ export interface DebugConfigOptions {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Resolve the GDB binary path for the target from the build cache
|
|
38
|
-
*
|
|
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
|
|
39
42
|
* xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
|
|
40
43
|
* the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
|
|
41
44
|
*
|
|
@@ -43,24 +46,94 @@ export interface DebugConfigOptions {
|
|
|
43
46
|
* omits gdbPath and relies on Cortex-Debug's default resolution).
|
|
44
47
|
*/
|
|
45
48
|
export function resolveGdbPath(buildDir: string, target: string): string | undefined {
|
|
49
|
+
void target; // toolchain dir is esp32s3-specific today; see gdbPathFromSdkRoot
|
|
46
50
|
const cachePath = join(buildDir, 'CMakeCache.txt');
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
55
62
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 {
|
|
59
74
|
const gdbName = 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
|
|
60
|
-
const gdbPath = join(
|
|
75
|
+
const gdbPath = join(sdkRoot, 'xtensa-espressif_esp32s3_zephyr-elf', 'bin', gdbName);
|
|
61
76
|
return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
|
|
62
77
|
}
|
|
63
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
|
+
|
|
64
137
|
/**
|
|
65
138
|
* Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
|
|
66
139
|
* OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
|
|
@@ -397,3 +470,53 @@ export function writeDebugConfig(o: DebugConfigOptions): void {
|
|
|
397
470
|
const task = buildTask(o);
|
|
398
471
|
mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
|
|
399
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
|
+
}
|
package/src/toolchain/index.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
22
|
import { basename, dirname, join } from 'node:path';
|
|
23
|
-
import { readdirSync, readFileSync, mkdirSync, rmSync
|
|
23
|
+
import { readdirSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
24
24
|
import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
|
|
25
25
|
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
26
26
|
import { scaffoldZephyrProject, writeIfChanged } from './scaffold.js';
|
|
@@ -28,7 +28,7 @@ import { westSpawn, buildEnv } from './west-spawn.js';
|
|
|
28
28
|
import { discoverWest } from './west-discover.js';
|
|
29
29
|
import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
|
|
30
30
|
import { ZephyrStrategy } from '../strategy.js';
|
|
31
|
-
import { generateOverlay, type DisplayWiring, type TouchWiring } from '../dt-config/overlay.js';
|
|
31
|
+
import { generateOverlay, type DisplayWiring, type TouchWiring, type OverlayDiagnostic } from '../dt-config/overlay.js';
|
|
32
32
|
import { chipForTarget } from '../chips/index.js';
|
|
33
33
|
import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
|
|
34
34
|
import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
|
|
@@ -167,6 +167,33 @@ export function cleanseUploadOutput(
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Whether a failed `west build` output carries ninja's `dependency cycle`
|
|
172
|
+
* signature. Zephyr 4.3.99-dev snapshots have a regression
|
|
173
|
+
* (zephyrproject-rtos/zephyr#104757, fixed upstream by the #104784 revert,
|
|
174
|
+
* in v4.4+): after CMake re-runs from a .config change, the build dir's
|
|
175
|
+
* .ninja_deps records an `offsets.h -> offsets.c.obj -> offsets.h` cycle and
|
|
176
|
+
* ninja aborts with `ninja: error: dependency cycle: ...` before compiling
|
|
177
|
+
* anything. The cycle lives in the build dir, not the sources, so compile()
|
|
178
|
+
* recovers by deleting the dir and retrying once.
|
|
179
|
+
*
|
|
180
|
+
* Exported (pure) so the detection is unit-testable without spawning west.
|
|
181
|
+
*/
|
|
182
|
+
export function isDependencyCycleFailure(output: string): boolean {
|
|
183
|
+
return output.includes('dependency cycle');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** stdout+stderr of a spawnSync result coerced to one string. Defensive about
|
|
187
|
+
* the buffer form (spawnSync only returns strings when `encoding` is set,
|
|
188
|
+
* which every call site here does — but the coercion costs nothing). */
|
|
189
|
+
function combinedSpawnOutput(
|
|
190
|
+
result: { stdout?: string | Buffer | null; stderr?: string | Buffer | null },
|
|
191
|
+
): string {
|
|
192
|
+
const so = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
193
|
+
const se = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
194
|
+
return so + se;
|
|
195
|
+
}
|
|
196
|
+
|
|
170
197
|
/**
|
|
171
198
|
* FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
|
|
172
199
|
* The target board is carried via frameworkData.buildTarget; scaffolding
|
|
@@ -206,12 +233,17 @@ export const Toolchain = {
|
|
|
206
233
|
// for either driver. Thread a non-default profile here only if a future
|
|
207
234
|
// board carries a display node under a different nodelabel.
|
|
208
235
|
const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
|
|
236
|
+
// Touch controller kind comes from which DT nodelabel the emitted adapter
|
|
237
|
+
// references (FT6336U on I2C, XPT2046 on the display's SPI bus).
|
|
238
|
+
const usesTouch = uses('ft6336u') || uses('touch_');
|
|
239
|
+
const usesXpt = uses('xpt2046');
|
|
209
240
|
const overlay = generateOverlay(chip, {
|
|
210
241
|
usesI2c: uses('i2c_'),
|
|
211
242
|
usesSpi: uses('spi_'),
|
|
212
243
|
usesUart: uses('uart_'),
|
|
213
244
|
usesDisplay,
|
|
214
|
-
usesTouch:
|
|
245
|
+
usesTouch: usesTouch || usesXpt,
|
|
246
|
+
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
215
247
|
}, displayProfile);
|
|
216
248
|
const overlayDir = join(projectRoot, 'boards');
|
|
217
249
|
mkdirSync(overlayDir, { recursive: true });
|
|
@@ -301,27 +333,58 @@ export const Toolchain = {
|
|
|
301
333
|
mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
|
|
302
334
|
miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
|
|
303
335
|
backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
|
|
336
|
+
tearingEffectPin: typeof dispCfg.tearingEffectPin === 'number' ? dispCfg.tearingEffectPin : undefined,
|
|
304
337
|
}
|
|
305
338
|
: undefined;
|
|
306
|
-
// Extract touch pin wiring
|
|
307
|
-
//
|
|
339
|
+
// Extract touch pin wiring from the config display.touch section so the
|
|
340
|
+
// DT overlay wires the bus + touch node. I2C (FT6336U) carries
|
|
341
|
+
// irq/resetPin/sda/scl; SPI (XPT2046) carries irq/cs + the calibration
|
|
342
|
+
// range the xptek,xpt2046 binding requires.
|
|
308
343
|
const touchCfg = dispCfg?.touch as Record<string, unknown> | undefined;
|
|
309
|
-
const
|
|
344
|
+
const isXpt = touchCfg?.library === 'XPT2046_Touchscreen';
|
|
345
|
+
const touchCal = touchCfg?.calibration as
|
|
346
|
+
{ xMin?: unknown; xMax?: unknown; yMin?: unknown; yMax?: unknown } | undefined;
|
|
347
|
+
const num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined);
|
|
348
|
+
let touchWiring: TouchWiring | undefined = touchCfg
|
|
310
349
|
? {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
controller: isXpt ? 'xpt2046' : 'ft6336u',
|
|
351
|
+
irq: num(touchCfg.irq),
|
|
352
|
+
resetPin: num(touchCfg.resetPin),
|
|
353
|
+
sda: num(touchCfg.sda),
|
|
354
|
+
scl: num(touchCfg.scl),
|
|
355
|
+
cs: num(touchCfg.cs),
|
|
356
|
+
calibration: touchCal
|
|
357
|
+
? {
|
|
358
|
+
xMin: num(touchCal.xMin) ?? 0,
|
|
359
|
+
xMax: num(touchCal.xMax) ?? 4095,
|
|
360
|
+
yMin: num(touchCal.yMin) ?? 0,
|
|
361
|
+
yMax: num(touchCal.yMax) ?? 4095,
|
|
362
|
+
}
|
|
363
|
+
: undefined,
|
|
364
|
+
minPressure: num(touchCfg.minPressure),
|
|
315
365
|
}
|
|
316
366
|
: undefined;
|
|
367
|
+
// Touch controller kind for Kconfig (bus driver selection) and the DT
|
|
368
|
+
// node shape: from the config when available, else from the DT nodelabel
|
|
369
|
+
// the emitted adapter references. Forced onto touchWiring so a source
|
|
370
|
+
// scan match without a config section still emits the right node.
|
|
371
|
+
const usesXpt = isXpt || uses('xpt2046');
|
|
372
|
+
if (usesXpt) {
|
|
373
|
+
touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
|
|
374
|
+
}
|
|
375
|
+
const overlayDiagnostics: OverlayDiagnostic[] = [];
|
|
317
376
|
const overlay = generateOverlay(chip, {
|
|
318
377
|
usesI2c: uses('i2c_'),
|
|
319
378
|
usesSpi: uses('spi_'),
|
|
320
379
|
usesUart: uses('uart_'),
|
|
321
380
|
usesDisplay,
|
|
322
|
-
usesTouch: uses('ft6336u') || uses('touch_'),
|
|
381
|
+
usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
|
|
382
|
+
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
323
383
|
psram: o.psram,
|
|
324
|
-
}, displayProfile, wiring, touchWiring);
|
|
384
|
+
}, displayProfile, wiring, touchWiring, overlayDiagnostics);
|
|
385
|
+
for (const d of overlayDiagnostics) {
|
|
386
|
+
console.warn(`overlay: ${d.message}`);
|
|
387
|
+
}
|
|
325
388
|
const overlayDir = join(projectRoot, 'boards');
|
|
326
389
|
mkdirSync(overlayDir, { recursive: true });
|
|
327
390
|
// Write the board-specific overlay (the one west loads). Zephyr looks for
|
|
@@ -336,16 +399,23 @@ export const Toolchain = {
|
|
|
336
399
|
// west defaults to <projectRoot>/build.
|
|
337
400
|
const buildDir = join(projectRoot, 'build');
|
|
338
401
|
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
402
|
+
// Reuse the build dir across builds so ninja recompiles only the changed
|
|
403
|
+
// app translation units and re-links — a pristine configure + the
|
|
404
|
+
// ~280-target Zephyr library rebuild costs minutes on Windows
|
|
405
|
+
// (demo-shadcn measures 69s of ninja wall time, 448s of summed compile
|
|
406
|
+
// work, and every build redid all of it). Nuke it only when the generated
|
|
407
|
+
// config changed (prj.conf / CMakeLists content), the one path that must
|
|
408
|
+
// not reuse a cached graph: Zephyr 4.3.99-dev snapshots carry a
|
|
409
|
+
// regression (zephyrproject-rtos/zephyr#104757, fixed by the #104784
|
|
410
|
+
// revert on 2026-03-03, in v4.4+) where re-running CMake after a .config
|
|
411
|
+
// change records an `offsets.h -> offsets.c.obj -> offsets.h` cycle in
|
|
412
|
+
// .ninja_deps, after which every ninja run fails with `dependency cycle`.
|
|
413
|
+
// Plain source edits never reconfigure CMake, so they cannot trigger it —
|
|
414
|
+
// and the retry after the spawn below self-heals any path that still does.
|
|
415
|
+
// Board switches need no nuke here: `west build` is --pristine=auto by
|
|
416
|
+
// default and recreates the dir itself when -b <board> mismatches the
|
|
417
|
+
// cached board.
|
|
418
|
+
if (configChanged) {
|
|
349
419
|
try { rmSync(buildDir, { recursive: true, force: true }); } catch { /* may not exist */ }
|
|
350
420
|
}
|
|
351
421
|
|
|
@@ -373,11 +443,24 @@ export const Toolchain = {
|
|
|
373
443
|
buildArgs,
|
|
374
444
|
{ cwd: projectRoot, encoding: 'utf-8', timeout: BUILD_TIMEOUT_MS },
|
|
375
445
|
);
|
|
376
|
-
|
|
446
|
+
let result = spawnSync(inv.command, inv.args, inv.options);
|
|
447
|
+
// Self-heal the Zephyr 4.3.99 dep-cycle regression (see the nuke comment
|
|
448
|
+
// above): when the cached .ninja_deps carries the cycle, ninja aborts with
|
|
449
|
+
// `dependency cycle` before compiling anything. The cycle lives in the
|
|
450
|
+
// build dir, not the sources — one pristine retry clears it and the build
|
|
451
|
+
// proceeds. On fixed Zephyr (>=4.4) this never fires.
|
|
452
|
+
let pristineRetry = false;
|
|
453
|
+
if (result.status !== 0 && isDependencyCycleFailure(combinedSpawnOutput(result))) {
|
|
454
|
+
try { rmSync(buildDir, { recursive: true, force: true }); } catch { /* may not exist */ }
|
|
455
|
+
result = spawnSync(inv.command, inv.args, inv.options);
|
|
456
|
+
pristineRetry = true;
|
|
457
|
+
}
|
|
377
458
|
|
|
378
459
|
const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
379
460
|
const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
380
|
-
const output = stdout + stderr
|
|
461
|
+
const output = stdout + stderr + (pristineRetry
|
|
462
|
+
? '\n[cuttlefish] dependency cycle detected in the cached build dir — retried with a pristine build'
|
|
463
|
+
: '');
|
|
381
464
|
// Prefix the build log with how west was resolved, for transparency.
|
|
382
465
|
const header = `Using west via ${inv.install.source}` +
|
|
383
466
|
(inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
|
|
@@ -25,24 +25,34 @@ export function writeIfChanged(filePath: string, content: string): boolean {
|
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Names of the cuttlefish-emitted C/C++ sources under src/ (top level only,
|
|
30
|
+
* matching the old `src/*.cpp src/*.c` glob; sorted so the generated
|
|
31
|
+
* CMakeLists.txt is stable across readdir orderings). Empty when no sources
|
|
32
|
+
* exist yet (first prepare call).
|
|
33
|
+
*/
|
|
34
|
+
function listEmittedSources(srcDir: string): string[] {
|
|
35
|
+
if (!existsSync(srcDir)) return [];
|
|
36
|
+
const names = readdirSync(srcDir).filter((name) => name.endsWith('.cpp') || name.endsWith('.c'));
|
|
37
|
+
names.sort();
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
42
|
* Concatenate all emitted source under src/ so the scaffold can detect which
|
|
30
43
|
* peripherals the program actually uses. The cuttlefish lowering emits
|
|
31
44
|
* well-known driver API tokens (adc_read, spi_transceive, bt_*, …), so scanning
|
|
32
|
-
* the post-transpile source is an authoritative usage signal — and it keeps
|
|
33
|
-
* scaffold self-contained (no need to thread analysis through the toolchain
|
|
45
|
+
* the post-transpile source is an authoritative usage signal — and it keeps
|
|
46
|
+
* the scaffold self-contained (no need to thread analysis through the toolchain
|
|
34
47
|
* contract). Returns '' when no sources exist yet (first prepare call).
|
|
35
48
|
*/
|
|
36
49
|
function readEmittedSources(srcDir: string): string {
|
|
37
|
-
if (!existsSync(srcDir)) return '';
|
|
38
50
|
let out = '';
|
|
39
|
-
for (const name of
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// ignore unreadable files
|
|
45
|
-
}
|
|
51
|
+
for (const name of listEmittedSources(srcDir)) {
|
|
52
|
+
try {
|
|
53
|
+
out += readFileSync(join(srcDir, name), 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore unreadable files
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
return out;
|
|
@@ -120,8 +130,13 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
|
|
|
120
130
|
let changed = false;
|
|
121
131
|
|
|
122
132
|
// ── Root CMakeLists.txt ─────────────────────────────────────────────────
|
|
123
|
-
// The canonical Zephyr CMake application.
|
|
124
|
-
//
|
|
133
|
+
// The canonical Zephyr CMake application. The emitted source list is
|
|
134
|
+
// explicit (no file(GLOB CONFIGURE_DEPENDS ...)): CONFIGURE_DEPENDS puts a
|
|
135
|
+
// cmake.verify_globs step in the ninja graph that spawns CMake to re-check
|
|
136
|
+
// the glob on every build, and the scaffold already rewrites this file via
|
|
137
|
+
// writeIfChanged whenever the emitted file set changes — which flips
|
|
138
|
+
// configChanged and reconfigures with the new list baked in.
|
|
139
|
+
const sourceFiles = listEmittedSources(srcDir);
|
|
125
140
|
const cmakeLists = [
|
|
126
141
|
'# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
|
|
127
142
|
'cmake_minimum_required(VERSION 3.20.0)',
|
|
@@ -130,10 +145,18 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
|
|
|
130
145
|
'',
|
|
131
146
|
'project(zephyr_app)',
|
|
132
147
|
'',
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
...(sourceFiles.length > 0
|
|
149
|
+
? [
|
|
150
|
+
'# Cuttlefish-emitted sources. This list is regenerated whenever the',
|
|
151
|
+
'# emitted file set changes (the scaffold rewrites CMakeLists.txt).',
|
|
152
|
+
'target_sources(app PRIVATE',
|
|
153
|
+
...sourceFiles.map((name) => ` src/${name}`),
|
|
154
|
+
')',
|
|
155
|
+
]
|
|
156
|
+
: [
|
|
157
|
+
'# No emitted sources yet — the scaffold regenerates this list on the',
|
|
158
|
+
'# next compile once src/ contains .cpp/.c files.',
|
|
159
|
+
]),
|
|
137
160
|
// When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
|
|
138
161
|
// PSRAM canvas allocator (ui_create_canvas_best) is compiled in.
|
|
139
162
|
...(psram ? ['', '# PSRAM enabled: activate the runtime PSRAM canvas paths.', 'target_compile_definitions(app PRIVATE BOARD_HAS_PSRAM)', ''] : ['']),
|
|
@@ -97,9 +97,12 @@ export function isZephyrBase(dir: string): boolean {
|
|
|
97
97
|
// ── Strategy 1: `west` on PATH ──────────────────────────────────────────────
|
|
98
98
|
|
|
99
99
|
export function discoverFromPath(): WestInstall | null {
|
|
100
|
+
// shell only on Windows (where.exe resolution through cmd) — an args array
|
|
101
|
+
// with shell: true triggers Node's DEP0190 deprecation warning on Linux/
|
|
102
|
+
// macOS, where `which` is a plain executable that needs no shell.
|
|
100
103
|
const which = spawnSync(IS_WIN ? 'where' : 'which', ['west'], {
|
|
101
104
|
encoding: 'utf8',
|
|
102
|
-
shell:
|
|
105
|
+
shell: IS_WIN,
|
|
103
106
|
windowsHide: true,
|
|
104
107
|
});
|
|
105
108
|
if (which.status !== 0) return null;
|