@typecad/framework-zephyr 1.0.0-alpha.12 → 1.0.0-alpha.14
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 +13 -1
- package/dist/chips/controllers.d.ts +28 -8
- package/dist/chips/controllers.js +49 -12
- package/dist/chips/resolve.js +32 -6
- package/dist/chips/types.d.ts +63 -12
- package/dist/chips/xiao-ble.js +12 -0
- package/dist/display/index.d.ts +1 -1
- package/dist/display/index.js +1 -1
- package/dist/display/profiles.d.ts +8 -0
- package/dist/display/profiles.js +19 -0
- package/dist/display/ui-adapter.d.ts +4 -0
- package/dist/display/ui-adapter.js +46 -0
- package/dist/dt-config/kconfig.d.ts +11 -0
- package/dist/dt-config/kconfig.js +1 -1
- package/dist/dt-config/overlay.d.ts +8 -1
- package/dist/dt-config/overlay.js +104 -1
- package/dist/framework.manifest.d.ts +20 -30
- package/dist/framework.manifest.js +12 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lowering/adc.d.ts +7 -4
- package/dist/lowering/adc.js +25 -11
- package/dist/lowering/gpio.js +15 -8
- package/dist/lowering/mqtt.js +9 -1
- package/dist/lowering/pulse.js +7 -7
- package/dist/lowering/pwm.d.ts +21 -3
- package/dist/lowering/pwm.js +28 -4
- package/dist/lowering/spi.js +2 -2
- package/dist/lowering/tone.js +3 -2
- package/dist/lowering/wifi.js +28 -5
- package/dist/strategy.d.ts +20 -0
- package/dist/strategy.js +295 -119
- package/dist/toolchain/debug-config.d.ts +43 -2
- package/dist/toolchain/debug-config.js +129 -17
- package/dist/toolchain/index.d.ts +14 -1
- package/dist/toolchain/index.js +146 -20
- package/dist/toolchain/scaffold.d.ts +9 -0
- package/dist/toolchain/scaffold.js +84 -19
- package/dist/toolchain/west-discover.d.ts +4 -1
- package/dist/toolchain/west-discover.js +2 -0
- package/dist/toolchain/west-spawn.js +17 -5
- package/package.json +5 -5
- package/src/chips/controllers.ts +61 -12
- package/src/chips/resolve.ts +32 -5
- package/src/chips/types.ts +63 -12
- package/src/chips/xiao-ble.ts +82 -70
- package/src/display/index.ts +1 -1
- package/src/display/profiles.ts +23 -0
- package/src/display/ui-adapter.ts +51 -0
- package/src/dt-config/kconfig.ts +12 -1
- package/src/dt-config/overlay.ts +123 -0
- package/src/framework.manifest.ts +12 -3
- package/src/index.ts +6 -0
- package/src/lowering/adc.ts +28 -12
- package/src/lowering/gpio.ts +15 -8
- package/src/lowering/mqtt.ts +9 -1
- package/src/lowering/pulse.ts +7 -7
- package/src/lowering/pwm.ts +29 -4
- package/src/lowering/spi.ts +2 -2
- package/src/lowering/tone.ts +3 -3
- package/src/lowering/wifi.ts +29 -5
- package/src/strategy.ts +320 -123
- package/src/toolchain/debug-config.ts +137 -14
- package/src/toolchain/index.ts +645 -513
- package/src/toolchain/scaffold.ts +81 -17
- package/src/toolchain/west-discover.ts +321 -316
- package/src/toolchain/west-spawn.ts +17 -5
|
@@ -13,8 +13,10 @@ export interface DebugConfigOptions {
|
|
|
13
13
|
sourceMapPath?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Resolve the GDB binary path for the target from the build cache
|
|
17
|
-
*
|
|
16
|
+
* Resolve the GDB binary path for the target from the build cache, falling
|
|
17
|
+
* back to a filesystem scan of known Zephyr SDK locations when no build
|
|
18
|
+
* exists yet (the create-time starter artifacts path). Zephyr records
|
|
19
|
+
* ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
|
|
18
20
|
* xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
|
|
19
21
|
* the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
|
|
20
22
|
*
|
|
@@ -22,6 +24,24 @@ export interface DebugConfigOptions {
|
|
|
22
24
|
* omits gdbPath and relies on Cortex-Debug's default resolution).
|
|
23
25
|
*/
|
|
24
26
|
export declare function resolveGdbPath(buildDir: string, target: string): string | undefined;
|
|
27
|
+
/** The esp32s3 xtensa GDB location inside a Zephyr SDK root (verified against
|
|
28
|
+
* zephyr-sdk-0.17.4). Returns a forward-slash absolute path or undefined. */
|
|
29
|
+
export declare function gdbPathFromSdkRoot(sdkRoot: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Probe the well-known Zephyr SDK install locations, newest version first:
|
|
32
|
+
* 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
|
|
33
|
+
* 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> — the
|
|
34
|
+
* @typecad/zephyr-installer layout
|
|
35
|
+
* 3. ~/zephyr-sdk-<ver> — the standalone download layout
|
|
36
|
+
*
|
|
37
|
+
* Only roots that actually contain the esp32s3 GDB are useful to callers;
|
|
38
|
+
* this returns candidate roots (gdbPathFromSdkRoot does the existence check)
|
|
39
|
+
* so tests can inject home/env overrides.
|
|
40
|
+
*/
|
|
41
|
+
export declare function discoverZephyrSdkRoots(opts?: {
|
|
42
|
+
home?: string;
|
|
43
|
+
env?: Record<string, string | undefined>;
|
|
44
|
+
}): string[];
|
|
25
45
|
/**
|
|
26
46
|
* Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
|
|
27
47
|
* OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
|
|
@@ -80,3 +100,24 @@ export declare function generateGdbScript(sourceMapPath?: string): string | null
|
|
|
80
100
|
* <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
|
|
81
101
|
*/
|
|
82
102
|
export declare function writeDebugConfig(o: DebugConfigOptions): void;
|
|
103
|
+
/**
|
|
104
|
+
* Create-time starter debug artifacts. Called by the cuttlefish `create` flow
|
|
105
|
+
* (via the package's `writeProjectDebugArtifacts` export) so a fresh project
|
|
106
|
+
* has a working F5 before any build exists:
|
|
107
|
+
*
|
|
108
|
+
* The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
|
|
109
|
+
* --debug`, which builds + flashes AND rewrites this same launch entry (merged
|
|
110
|
+
* by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
|
|
111
|
+
* themselves on the first debug build.
|
|
112
|
+
*
|
|
113
|
+
* No-ops (returns []) for targets without native GDB support (debugMode() !==
|
|
114
|
+
* 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
|
|
115
|
+
*
|
|
116
|
+
* Returns the workspace-relative paths written, for CLI reporting.
|
|
117
|
+
*/
|
|
118
|
+
export declare function writeProjectDebugArtifacts(o: {
|
|
119
|
+
/** Absolute path to the cuttlefish project root (contains cuttlefish.config.ts). */
|
|
120
|
+
workspaceRoot: string;
|
|
121
|
+
/** The Zephyr board id from the project config (frameworkData.buildTarget). */
|
|
122
|
+
buildTarget?: string;
|
|
123
|
+
}): string[];
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
18
18
|
import { join, resolve, dirname, relative } from 'node:path';
|
|
19
|
+
import { ZephyrStrategy } from '../strategy.js';
|
|
19
20
|
/**
|
|
20
|
-
* Resolve the GDB binary path for the target from the build cache
|
|
21
|
-
*
|
|
21
|
+
* Resolve the GDB binary path for the target from the build cache, falling
|
|
22
|
+
* back to a filesystem scan of known Zephyr SDK locations when no build
|
|
23
|
+
* exists yet (the create-time starter artifacts path). Zephyr records
|
|
24
|
+
* ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
|
|
22
25
|
* xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
|
|
23
26
|
* the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
|
|
24
27
|
*
|
|
@@ -26,27 +29,92 @@ import { join, resolve, dirname, relative } from 'node:path';
|
|
|
26
29
|
* omits gdbPath and relies on Cortex-Debug's default resolution).
|
|
27
30
|
*/
|
|
28
31
|
export function resolveGdbPath(buildDir, target) {
|
|
32
|
+
void target; // toolchain dir is esp32s3-specific today; see gdbPathFromSdkRoot
|
|
29
33
|
const cachePath = join(buildDir, 'CMakeCache.txt');
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (existsSync(cachePath)) {
|
|
35
|
+
try {
|
|
36
|
+
const cache = readFileSync(cachePath, 'utf-8');
|
|
37
|
+
const m = cache.match(/^ZEPHYR_SDK_INSTALL_DIR:PATH=(.+)$/m);
|
|
38
|
+
if (m) {
|
|
39
|
+
const fromCache = gdbPathFromSdkRoot(m[1].trim());
|
|
40
|
+
if (fromCache)
|
|
41
|
+
return fromCache;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// unreadable cache — fall through to the SDK scan
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// No build dir yet (project just created): probe known SDK locations.
|
|
49
|
+
for (const sdkRoot of discoverZephyrSdkRoots()) {
|
|
50
|
+
const p = gdbPathFromSdkRoot(sdkRoot);
|
|
51
|
+
if (p)
|
|
52
|
+
return p;
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** The esp32s3 xtensa GDB location inside a Zephyr SDK root (verified against
|
|
57
|
+
* zephyr-sdk-0.17.4). Returns a forward-slash absolute path or undefined. */
|
|
58
|
+
export function gdbPathFromSdkRoot(sdkRoot) {
|
|
46
59
|
const gdbName = 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
|
|
47
|
-
const gdbPath = join(
|
|
60
|
+
const gdbPath = join(sdkRoot, 'xtensa-espressif_esp32s3_zephyr-elf', 'bin', gdbName);
|
|
48
61
|
return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
|
|
49
62
|
}
|
|
63
|
+
/** Compare two dotted version strings numerically (0.17.10 > 0.17.4). */
|
|
64
|
+
function compareSdkVersions(a, b) {
|
|
65
|
+
const segsOf = (v) => v.split('.').map((s) => parseInt(s, 10) || 0);
|
|
66
|
+
const aa = segsOf(a);
|
|
67
|
+
const bb = segsOf(b);
|
|
68
|
+
for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
|
|
69
|
+
const d = (aa[i] ?? 0) - (bb[i] ?? 0);
|
|
70
|
+
if (d !== 0)
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Probe the well-known Zephyr SDK install locations, newest version first:
|
|
77
|
+
* 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
|
|
78
|
+
* 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> — the
|
|
79
|
+
* @typecad/zephyr-installer layout
|
|
80
|
+
* 3. ~/zephyr-sdk-<ver> — the standalone download layout
|
|
81
|
+
*
|
|
82
|
+
* Only roots that actually contain the esp32s3 GDB are useful to callers;
|
|
83
|
+
* this returns candidate roots (gdbPathFromSdkRoot does the existence check)
|
|
84
|
+
* so tests can inject home/env overrides.
|
|
85
|
+
*/
|
|
86
|
+
export function discoverZephyrSdkRoots(opts) {
|
|
87
|
+
const env = opts?.env ?? process.env;
|
|
88
|
+
const home = opts?.home ?? (env.USERPROFILE || env.HOME || '');
|
|
89
|
+
const scanned = [];
|
|
90
|
+
const versionedDirs = (base) => {
|
|
91
|
+
try {
|
|
92
|
+
return readdirSync(base)
|
|
93
|
+
.filter((d) => existsSync(join(base, d)) && d.startsWith('zephyr-sdk-'))
|
|
94
|
+
.map((d) => join(base, d));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return []; // dir absent
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const mambaRoot = env.MAMBA_ROOT_PREFIX || (home ? join(home, 'micromamba') : '');
|
|
101
|
+
if (mambaRoot)
|
|
102
|
+
scanned.push(...versionedDirs(join(mambaRoot, 'zephyr-sdk')));
|
|
103
|
+
if (home)
|
|
104
|
+
scanned.push(...versionedDirs(home));
|
|
105
|
+
// Scanned roots newest version first; the env var stays pinned first
|
|
106
|
+
// (explicit user intent outranks any discovered location).
|
|
107
|
+
scanned.sort((a, b) => {
|
|
108
|
+
const va = a.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
|
|
109
|
+
const vb = b.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
|
|
110
|
+
return compareSdkVersions(vb, va);
|
|
111
|
+
});
|
|
112
|
+
const roots = env.ZEPHYR_SDK_INSTALL_DIR
|
|
113
|
+
? [env.ZEPHYR_SDK_INSTALL_DIR, ...scanned]
|
|
114
|
+
: scanned;
|
|
115
|
+
// De-duplicate (an env var may repeat a scan hit) preserving order.
|
|
116
|
+
return roots.filter((r, i) => roots.indexOf(r) === i);
|
|
117
|
+
}
|
|
50
118
|
/**
|
|
51
119
|
* Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
|
|
52
120
|
* OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
|
|
@@ -357,3 +425,47 @@ export function writeDebugConfig(o) {
|
|
|
357
425
|
const task = buildTask(o);
|
|
358
426
|
mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
|
|
359
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* The Zephyr app dir a `cuttlefish create` scaffold produces, relative to the
|
|
430
|
+
* project root: the scaffold fixes entry `./src/main.ts` + outDir `./out`, and
|
|
431
|
+
* the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
|
|
432
|
+
* the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
|
|
433
|
+
* debug artifacts — always lands at `src/out`. Keep in sync with
|
|
434
|
+
* generateProjectConfig in @typecad/cuttlefish create/templates.ts.
|
|
435
|
+
*/
|
|
436
|
+
const STARTER_SKETCH_REL = 'src/out';
|
|
437
|
+
/**
|
|
438
|
+
* Create-time starter debug artifacts. Called by the cuttlefish `create` flow
|
|
439
|
+
* (via the package's `writeProjectDebugArtifacts` export) so a fresh project
|
|
440
|
+
* has a working F5 before any build exists:
|
|
441
|
+
*
|
|
442
|
+
* The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
|
|
443
|
+
* --debug`, which builds + flashes AND rewrites this same launch entry (merged
|
|
444
|
+
* by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
|
|
445
|
+
* themselves on the first debug build.
|
|
446
|
+
*
|
|
447
|
+
* No-ops (returns []) for targets without native GDB support (debugMode() !==
|
|
448
|
+
* 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
|
|
449
|
+
*
|
|
450
|
+
* Returns the workspace-relative paths written, for CLI reporting.
|
|
451
|
+
*/
|
|
452
|
+
export function writeProjectDebugArtifacts(o) {
|
|
453
|
+
if (new ZephyrStrategy().debugMode(o.buildTarget) !== 'gdb')
|
|
454
|
+
return [];
|
|
455
|
+
const workspaceRoot = resolve(o.workspaceRoot);
|
|
456
|
+
const projectRoot = join(workspaceRoot, STARTER_SKETCH_REL);
|
|
457
|
+
writeDebugConfig({
|
|
458
|
+
projectRoot,
|
|
459
|
+
workspaceRoot,
|
|
460
|
+
sketchRel: STARTER_SKETCH_REL,
|
|
461
|
+
target: o.buildTarget ?? '',
|
|
462
|
+
// No build dir exists yet — resolveGdbPath falls back to probing known
|
|
463
|
+
// Zephyr SDK locations so gdbPath is still filled in when possible.
|
|
464
|
+
buildDir: join(projectRoot, 'build'),
|
|
465
|
+
});
|
|
466
|
+
return [
|
|
467
|
+
'.vscode/launch.json',
|
|
468
|
+
'.vscode/tasks.json',
|
|
469
|
+
`${STARTER_SKETCH_REL}/.cuttlefish/openocd.cfg`,
|
|
470
|
+
];
|
|
471
|
+
}
|
|
@@ -26,7 +26,7 @@ export declare function projectRootFromOptions(o: ToolchainOptions): string;
|
|
|
26
26
|
* Exported (pure) so the runner-selection contract is unit-testable without
|
|
27
27
|
* spawning west.
|
|
28
28
|
*/
|
|
29
|
-
export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined): string[];
|
|
29
|
+
export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined, runnerArgs?: readonly string[]): string[];
|
|
30
30
|
/**
|
|
31
31
|
* Classify a `west flash` result as success/failure.
|
|
32
32
|
*
|
|
@@ -59,6 +59,19 @@ export declare function classifyUploadResult(runner: string | undefined, status:
|
|
|
59
59
|
* Exported (pure) so the cleansing is unit-testable without spawning west.
|
|
60
60
|
*/
|
|
61
61
|
export declare function cleanseUploadOutput(runner: string | undefined, status: number | null, output: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a failed `west build` output carries ninja's `dependency cycle`
|
|
64
|
+
* signature. Zephyr 4.3.99-dev snapshots have a regression
|
|
65
|
+
* (zephyrproject-rtos/zephyr#104757, fixed upstream by the #104784 revert,
|
|
66
|
+
* in v4.4+): after CMake re-runs from a .config change, the build dir's
|
|
67
|
+
* .ninja_deps records an `offsets.h -> offsets.c.obj -> offsets.h` cycle and
|
|
68
|
+
* ninja aborts with `ninja: error: dependency cycle: ...` before compiling
|
|
69
|
+
* anything. The cycle lives in the build dir, not the sources, so compile()
|
|
70
|
+
* recovers by deleting the dir and retrying once.
|
|
71
|
+
*
|
|
72
|
+
* Exported (pure) so the detection is unit-testable without spawning west.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isDependencyCycleFailure(output: string): boolean;
|
|
62
75
|
/**
|
|
63
76
|
* FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
|
|
64
77
|
* The target board is carried via frameworkData.buildTarget; scaffolding
|
package/dist/toolchain/index.js
CHANGED
|
@@ -21,17 +21,77 @@ import { spawnSync } from 'node:child_process';
|
|
|
21
21
|
import { basename, dirname, join } from 'node:path';
|
|
22
22
|
import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
23
23
|
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
24
|
-
import { scaffoldZephyrProject, writeIfChanged } from './scaffold.js';
|
|
24
|
+
import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
|
|
25
25
|
import { westSpawn, buildEnv } from './west-spawn.js';
|
|
26
26
|
import { discoverWest } from './west-discover.js';
|
|
27
27
|
import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
|
|
28
28
|
import { ZephyrStrategy } from '../strategy.js';
|
|
29
29
|
import { generateOverlay } from '../dt-config/overlay.js';
|
|
30
30
|
import { chipForTarget } from '../chips/index.js';
|
|
31
|
+
import { resolveChipFromBoard } from '../chips/resolve.js';
|
|
32
|
+
import { pwmDtAliasToken } from '../lowering/pwm.js';
|
|
31
33
|
import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
|
|
32
34
|
import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
|
|
33
35
|
/** Default board target — the framework's MVP canonical board. */
|
|
34
36
|
const DEFAULT_BOARD = 'xiao_ble';
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the chip for a build the same way the strategy does at emit time —
|
|
39
|
+
* from the board constants the transpile persisted next to the emitted
|
|
40
|
+
* source (`board-constants.json`), falling back to the hardcoded registry.
|
|
41
|
+
* Board-package chips (rpi_pico, esp32c3/c6, blackpill) exist only in their
|
|
42
|
+
* board packages; the registry fallback would silently resolve them to the
|
|
43
|
+
* XIAO default and the overlay generator would emit wrong controller labels
|
|
44
|
+
* (e.g. `&uart0` on an STM32, whose node is `usart1`).
|
|
45
|
+
*/
|
|
46
|
+
function chipForBuild(projectRoot, board) {
|
|
47
|
+
try {
|
|
48
|
+
// The transpile writes the constants into the emit outDir, which is
|
|
49
|
+
// <projectRoot>/src for the standard layout (basename 'src' collapsed by
|
|
50
|
+
// projectRootFromOptions); check both locations.
|
|
51
|
+
const bcPath = [join(projectRoot, 'src', 'board-constants.json'), join(projectRoot, 'board-constants.json')]
|
|
52
|
+
.find(p => existsSync(p));
|
|
53
|
+
if (bcPath) {
|
|
54
|
+
const raw = JSON.parse(readFileSync(bcPath, 'utf8'));
|
|
55
|
+
const fromBoard = resolveChipFromBoard(new Map(Object.entries(raw)));
|
|
56
|
+
if (fromBoard)
|
|
57
|
+
return fromBoard;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* fall back to the registry below */ }
|
|
61
|
+
return chipForTarget(board);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* HAL pins the emitted sources read via adc.* — scanned from the emitted
|
|
65
|
+
* `__tc_adc<N>_setup()` call sites (N = channel index, mapped back to the HAL
|
|
66
|
+
* pin via the chip descriptor). Feeds the overlay's ADC pinctrl rewrite: on
|
|
67
|
+
* SoCs that mux ADC pads via pinctrl (STM32), only the read channels are
|
|
68
|
+
* switched to analog mode.
|
|
69
|
+
*/
|
|
70
|
+
function scanAdcReadPins(src, chip) {
|
|
71
|
+
const pins = [];
|
|
72
|
+
// Match CALL SITES only (`__tc_adc<N>_setup()` with empty parens) — the
|
|
73
|
+
// setup definitions emitted by adcInitLines have a `(void)` parameter list
|
|
74
|
+
// and would otherwise mark every descriptor channel as used.
|
|
75
|
+
for (const m of src.matchAll(/__tc_adc(\d+)_setup\(\)/g)) {
|
|
76
|
+
const ch = Number(m[1]);
|
|
77
|
+
const c = chip.adc?.channels.find((x) => x.channel === ch);
|
|
78
|
+
if (c && !pins.includes(c.pin))
|
|
79
|
+
pins.push(c.pin);
|
|
80
|
+
}
|
|
81
|
+
return pins;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* HAL pins the emitted sources drive with pwm.* — the emitted source
|
|
85
|
+
* references each used spec as `__tc_pwm_<alias token>` (pwmVarName in
|
|
86
|
+
* lowering/pwm.ts), and the lowering only emits specs for driven pins, so
|
|
87
|
+
* var-presence is the authoritative signal. Feeds the overlay's per-pin
|
|
88
|
+
* pwm-leds gating (no dead DT channels).
|
|
89
|
+
*/
|
|
90
|
+
function scanPwmUsedPins(src, chip) {
|
|
91
|
+
return (chip.pwm?.specs ?? [])
|
|
92
|
+
.filter((s) => src.includes(`__tc_pwm_${pwmDtAliasToken(s)}`))
|
|
93
|
+
.map((s) => s.pin);
|
|
94
|
+
}
|
|
35
95
|
function targetFromOptions(o) {
|
|
36
96
|
// The cuttlefish CLI populates ToolchainOptions.buildTarget from
|
|
37
97
|
// config.frameworkData.buildTarget. Accept frameworkData.target as an alias.
|
|
@@ -77,7 +137,7 @@ const FLASH_TIMEOUT_MS = 120_000;
|
|
|
77
137
|
* Exported (pure) so the runner-selection contract is unit-testable without
|
|
78
138
|
* spawning west.
|
|
79
139
|
*/
|
|
80
|
-
export function buildFlashArgs(buildDir, board, userRunner, port) {
|
|
140
|
+
export function buildFlashArgs(buildDir, board, userRunner, port, runnerArgs) {
|
|
81
141
|
const args = ['flash', '-d', buildDir];
|
|
82
142
|
if (userRunner) {
|
|
83
143
|
args.push('--runner', userRunner);
|
|
@@ -85,6 +145,11 @@ export function buildFlashArgs(buildDir, board, userRunner, port) {
|
|
|
85
145
|
if (port && board.startsWith('esp32')) {
|
|
86
146
|
args.push('--esp-device', port);
|
|
87
147
|
}
|
|
148
|
+
// Extra runner-specific flags, appended verbatim (west's runner parsers
|
|
149
|
+
// accept them after the runner is selected).
|
|
150
|
+
if (runnerArgs && runnerArgs.length > 0) {
|
|
151
|
+
args.push(...runnerArgs);
|
|
152
|
+
}
|
|
88
153
|
return args;
|
|
89
154
|
}
|
|
90
155
|
/**
|
|
@@ -145,6 +210,29 @@ export function cleanseUploadOutput(runner, status, output) {
|
|
|
145
210
|
}
|
|
146
211
|
return output;
|
|
147
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Whether a failed `west build` output carries ninja's `dependency cycle`
|
|
215
|
+
* signature. Zephyr 4.3.99-dev snapshots have a regression
|
|
216
|
+
* (zephyrproject-rtos/zephyr#104757, fixed upstream by the #104784 revert,
|
|
217
|
+
* in v4.4+): after CMake re-runs from a .config change, the build dir's
|
|
218
|
+
* .ninja_deps records an `offsets.h -> offsets.c.obj -> offsets.h` cycle and
|
|
219
|
+
* ninja aborts with `ninja: error: dependency cycle: ...` before compiling
|
|
220
|
+
* anything. The cycle lives in the build dir, not the sources, so compile()
|
|
221
|
+
* recovers by deleting the dir and retrying once.
|
|
222
|
+
*
|
|
223
|
+
* Exported (pure) so the detection is unit-testable without spawning west.
|
|
224
|
+
*/
|
|
225
|
+
export function isDependencyCycleFailure(output) {
|
|
226
|
+
return output.includes('dependency cycle');
|
|
227
|
+
}
|
|
228
|
+
/** stdout+stderr of a spawnSync result coerced to one string. Defensive about
|
|
229
|
+
* the buffer form (spawnSync only returns strings when `encoding` is set,
|
|
230
|
+
* which every call site here does — but the coercion costs nothing). */
|
|
231
|
+
function combinedSpawnOutput(result) {
|
|
232
|
+
const so = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
233
|
+
const se = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
234
|
+
return so + se;
|
|
235
|
+
}
|
|
148
236
|
/**
|
|
149
237
|
* FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
|
|
150
238
|
* The target board is carried via frameworkData.buildTarget; scaffolding
|
|
@@ -159,7 +247,7 @@ export const Toolchain = {
|
|
|
159
247
|
// resolution is a pre-build artifact step.
|
|
160
248
|
const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
|
|
161
249
|
const board = DEFAULT_BOARD;
|
|
162
|
-
const chip =
|
|
250
|
+
const chip = chipForBuild(projectRoot, board);
|
|
163
251
|
// Scan the emitted source for usage tokens (same authoritative signal the
|
|
164
252
|
// scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
|
|
165
253
|
const srcDir = dirname(entryPoint);
|
|
@@ -193,6 +281,10 @@ export const Toolchain = {
|
|
|
193
281
|
usesI2c: uses('i2c_'),
|
|
194
282
|
usesSpi: uses('spi_'),
|
|
195
283
|
usesUart: uses('uart_'),
|
|
284
|
+
usesPwm: uses('pwm_'),
|
|
285
|
+
usesAdc: uses('adc_'),
|
|
286
|
+
adcReadPins: scanAdcReadPins(src, chip),
|
|
287
|
+
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
196
288
|
usesDisplay,
|
|
197
289
|
usesTouch: usesTouch || usesXpt,
|
|
198
290
|
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
@@ -230,7 +322,7 @@ export const Toolchain = {
|
|
|
230
322
|
// the <default>.overlay it wrote does not match `west build -b <board>`.
|
|
231
323
|
// Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
|
|
232
324
|
try {
|
|
233
|
-
const chip =
|
|
325
|
+
const chip = chipForBuild(projectRoot, board);
|
|
234
326
|
const srcDir = join(projectRoot, 'src');
|
|
235
327
|
let src = '';
|
|
236
328
|
try {
|
|
@@ -277,6 +369,7 @@ export const Toolchain = {
|
|
|
277
369
|
mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
|
|
278
370
|
miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
|
|
279
371
|
backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
|
|
372
|
+
tearingEffectPin: typeof dispCfg.tearingEffectPin === 'number' ? dispCfg.tearingEffectPin : undefined,
|
|
280
373
|
}
|
|
281
374
|
: undefined;
|
|
282
375
|
// Extract touch pin wiring from the config display.touch section so the
|
|
@@ -314,38 +407,54 @@ export const Toolchain = {
|
|
|
314
407
|
if (usesXpt) {
|
|
315
408
|
touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
|
|
316
409
|
}
|
|
410
|
+
const overlayDiagnostics = [];
|
|
317
411
|
const overlay = generateOverlay(chip, {
|
|
318
412
|
usesI2c: uses('i2c_'),
|
|
319
413
|
usesSpi: uses('spi_'),
|
|
320
414
|
usesUart: uses('uart_'),
|
|
415
|
+
usesPwm: uses('pwm_'),
|
|
416
|
+
usesAdc: uses('adc_'),
|
|
417
|
+
adcReadPins: scanAdcReadPins(src, chip),
|
|
418
|
+
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
321
419
|
usesDisplay,
|
|
322
420
|
usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
|
|
323
421
|
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
324
422
|
psram: o.psram,
|
|
325
|
-
}, displayProfile, wiring, touchWiring);
|
|
423
|
+
}, displayProfile, wiring, touchWiring, overlayDiagnostics);
|
|
424
|
+
for (const d of overlayDiagnostics) {
|
|
425
|
+
console.warn(`overlay: ${d.message}`);
|
|
426
|
+
}
|
|
326
427
|
const overlayDir = join(projectRoot, 'boards');
|
|
327
428
|
mkdirSync(overlayDir, { recursive: true });
|
|
328
429
|
// Write the board-specific overlay (the one west loads). Zephyr looks for
|
|
329
430
|
// boards/<board_id>.overlay under APPLICATION_CONFIG_DIR — use the bare
|
|
330
431
|
// board id (before any hardware-qualifier suffix, e.g. 'esp32_devkitc'
|
|
331
|
-
// not the full 'esp32_devkitc/esp32/procpu' target string).
|
|
432
|
+
// not the full 'esp32_devkitc/esp32/procpu' target string). Library
|
|
433
|
+
// packages' overlay fragments are appended by the scaffold helper.
|
|
332
434
|
const boardId = board.split('/')[0];
|
|
333
|
-
writeIfChanged(join(overlayDir, `${boardId}.overlay`), overlay);
|
|
435
|
+
writeIfChanged(join(overlayDir, `${boardId}.overlay`), appendLibraryOverlayFragments(overlay, projectRoot));
|
|
334
436
|
}
|
|
335
437
|
catch { /* best-effort overlay regen; the build surfaces DT errors */ }
|
|
336
438
|
// Use a stable build dir so incremental builds reuse the Ninja graph.
|
|
337
439
|
// west defaults to <projectRoot>/build.
|
|
338
440
|
const buildDir = join(projectRoot, 'build');
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
441
|
+
// Reuse the build dir across builds so ninja recompiles only the changed
|
|
442
|
+
// app translation units and re-links — a pristine configure + the
|
|
443
|
+
// ~280-target Zephyr library rebuild costs minutes on Windows
|
|
444
|
+
// (demo-shadcn measures 69s of ninja wall time, 448s of summed compile
|
|
445
|
+
// work, and every build redid all of it). Nuke it only when the generated
|
|
446
|
+
// config changed (prj.conf / CMakeLists content), the one path that must
|
|
447
|
+
// not reuse a cached graph: Zephyr 4.3.99-dev snapshots carry a
|
|
448
|
+
// regression (zephyrproject-rtos/zephyr#104757, fixed by the #104784
|
|
449
|
+
// revert on 2026-03-03, in v4.4+) where re-running CMake after a .config
|
|
450
|
+
// change records an `offsets.h -> offsets.c.obj -> offsets.h` cycle in
|
|
451
|
+
// .ninja_deps, after which every ninja run fails with `dependency cycle`.
|
|
452
|
+
// Plain source edits never reconfigure CMake, so they cannot trigger it —
|
|
453
|
+
// and the retry after the spawn below self-heals any path that still does.
|
|
454
|
+
// Board switches need no nuke here: `west build` is --pristine=auto by
|
|
455
|
+
// default and recreates the dir itself when -b <board> mismatches the
|
|
456
|
+
// cached board.
|
|
457
|
+
if (configChanged) {
|
|
349
458
|
try {
|
|
350
459
|
rmSync(buildDir, { recursive: true, force: true });
|
|
351
460
|
}
|
|
@@ -374,10 +483,26 @@ export const Toolchain = {
|
|
|
374
483
|
buildArgs.push(...userCmakeArgs);
|
|
375
484
|
}
|
|
376
485
|
const inv = westSpawn(buildArgs, { cwd: projectRoot, encoding: 'utf-8', timeout: BUILD_TIMEOUT_MS });
|
|
377
|
-
|
|
486
|
+
let result = spawnSync(inv.command, inv.args, inv.options);
|
|
487
|
+
// Self-heal the Zephyr 4.3.99 dep-cycle regression (see the nuke comment
|
|
488
|
+
// above): when the cached .ninja_deps carries the cycle, ninja aborts with
|
|
489
|
+
// `dependency cycle` before compiling anything. The cycle lives in the
|
|
490
|
+
// build dir, not the sources — one pristine retry clears it and the build
|
|
491
|
+
// proceeds. On fixed Zephyr (>=4.4) this never fires.
|
|
492
|
+
let pristineRetry = false;
|
|
493
|
+
if (result.status !== 0 && isDependencyCycleFailure(combinedSpawnOutput(result))) {
|
|
494
|
+
try {
|
|
495
|
+
rmSync(buildDir, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
catch { /* may not exist */ }
|
|
498
|
+
result = spawnSync(inv.command, inv.args, inv.options);
|
|
499
|
+
pristineRetry = true;
|
|
500
|
+
}
|
|
378
501
|
const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
379
502
|
const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
380
|
-
const output = stdout + stderr
|
|
503
|
+
const output = stdout + stderr + (pristineRetry
|
|
504
|
+
? '\n[cuttlefish] dependency cycle detected in the cached build dir — retried with a pristine build'
|
|
505
|
+
: '');
|
|
381
506
|
// Prefix the build log with how west was resolved, for transparency.
|
|
382
507
|
const header = `Using west via ${inv.install.source}` +
|
|
383
508
|
(inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
|
|
@@ -414,7 +539,8 @@ export const Toolchain = {
|
|
|
414
539
|
const board = targetFromOptions(o);
|
|
415
540
|
const zc = o.zephyrConfig;
|
|
416
541
|
const runner = zc?.runner;
|
|
417
|
-
const
|
|
542
|
+
const runnerArgs = zc?.runnerArgs;
|
|
543
|
+
const args = buildFlashArgs(buildDir, board, runner, o.port, runnerArgs);
|
|
418
544
|
const inv = westSpawn(args, {
|
|
419
545
|
cwd: projectRoot,
|
|
420
546
|
encoding: 'utf-8',
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/** Write a file only if the content differs from the existing file.
|
|
2
2
|
* Returns true when the file was written (content changed or file was new). */
|
|
3
3
|
export declare function writeIfChanged(filePath: string, content: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Append cuttlefish library packages' devicetree overlay fragments to the
|
|
6
|
+
* generated overlay. Library entries come from the transpiler's libraries.json
|
|
7
|
+
* sidecar (next to the emitted sources) and are already gated on the
|
|
8
|
+
* library's include token appearing in the emitted sources — no re-detection.
|
|
9
|
+
* Fragments merge after the framework overlay so library nodes (e.g.
|
|
10
|
+
* @typecad/zephyr-esp32s3-rgb's WS2812 node on I2S0) layer over it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function appendLibraryOverlayFragments(overlay: string, projectRoot: string): string;
|
|
4
13
|
/**
|
|
5
14
|
* Emit the Zephyr application skeleton around the generated src/main.cpp.
|
|
6
15
|
*
|