@typecad/framework-zephyr 1.0.0-alpha.10 → 1.0.0-alpha.11

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 (56) hide show
  1. package/dist/chips/esp32.js +12 -0
  2. package/dist/chips/types.d.ts +30 -0
  3. package/dist/chips/xiao-ble.js +6 -0
  4. package/dist/display/gfx.d.ts +12 -3
  5. package/dist/display/gfx.js +130 -17
  6. package/dist/display/profiles.js +13 -0
  7. package/dist/display/ui-adapter.js +6 -0
  8. package/dist/doctor.d.ts +3 -3
  9. package/dist/doctor.js +56 -29
  10. package/dist/dt-config/kconfig.d.ts +3 -0
  11. package/dist/dt-config/kconfig.js +18 -0
  12. package/dist/dt-config/overlay.js +5 -0
  13. package/dist/framework.manifest.js +37 -14
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +5 -0
  16. package/dist/licenses.d.ts +59 -0
  17. package/dist/licenses.js +347 -0
  18. package/dist/lowering/dac.d.ts +15 -0
  19. package/dist/lowering/dac.js +69 -0
  20. package/dist/lowering/fs.d.ts +16 -0
  21. package/dist/lowering/fs.js +121 -0
  22. package/dist/lowering/hwtimer.d.ts +15 -0
  23. package/dist/lowering/hwtimer.js +84 -0
  24. package/dist/lowering/index.d.ts +4 -1
  25. package/dist/lowering/index.js +12 -3
  26. package/dist/strategy.js +186 -14
  27. package/dist/toolchain/compat.js +10 -1
  28. package/dist/toolchain/env-check.d.ts +93 -0
  29. package/dist/toolchain/env-check.js +190 -0
  30. package/dist/toolchain/scaffold.js +3 -0
  31. package/dist/toolchain/west-discover.d.ts +11 -3
  32. package/dist/toolchain/west-discover.js +80 -6
  33. package/dist/toolchain/west-spawn.js +15 -0
  34. package/package.json +4 -4
  35. package/src/chips/esp32.ts +12 -0
  36. package/src/chips/types.ts +29 -0
  37. package/src/chips/xiao-ble.ts +6 -0
  38. package/src/display/gfx.ts +135 -19
  39. package/src/display/profiles.ts +13 -0
  40. package/src/display/ui-adapter.ts +5 -0
  41. package/src/doctor.ts +77 -56
  42. package/src/dt-config/kconfig.ts +19 -0
  43. package/src/dt-config/overlay.ts +5 -0
  44. package/src/framework.manifest.ts +38 -14
  45. package/src/index.ts +6 -0
  46. package/src/licenses.ts +425 -0
  47. package/src/lowering/dac.ts +82 -0
  48. package/src/lowering/fs.ts +127 -0
  49. package/src/lowering/hwtimer.ts +101 -0
  50. package/src/lowering/index.ts +9 -2
  51. package/src/strategy.ts +180 -14
  52. package/src/toolchain/compat.ts +154 -145
  53. package/src/toolchain/env-check.ts +285 -0
  54. package/src/toolchain/scaffold.ts +3 -0
  55. package/src/toolchain/west-discover.ts +88 -8
  56. package/src/toolchain/west-spawn.ts +15 -0
@@ -1,145 +1,154 @@
1
- // Zephyr version compatibility + board-target normalization.
2
- //
3
- // Two mechanisms absorb Zephyr version churn:
4
- // 1. checkZephyrCompat() — compare the installed Zephyr version against the
5
- // declared range (manifest.compat.zephyr) so an incompatible Zephyr fails
6
- // fast with a clear message instead of a cryptic west/CMake board error.
7
- // 2. resolveBoardTarget() — normalize the board target for the installed
8
- // Zephyr version. Zephyr 4.3+ rejects bare multi-core board names
9
- // (esp32s3_devkitc) and requires a qualified target
10
- // (esp32s3_devkitc/esp32s3/procpu). This rewrites stale configs at build
11
- // time so users don't have to regenerate them after a Zephyr upgrade.
12
- //
13
- // This module is intentionally pure (no chalk/ui/console) so it unit-tests
14
- // cleanly; the doctor and the toolchain decide how to present results.
15
-
16
- import { readFileSync } from 'node:fs';
17
- import { join } from 'node:path';
18
- import manifest from '../framework.manifest.js';
19
-
20
- // ── minimal semver ──────────────────────────────────────────────────────────
21
-
22
- /** Parse "4.3.99" / "v4.3" / "4.3.99-rc1" → [4, 3, 99]. Undefined if unparseable. */
23
- export function parseVersion(v: string | undefined): [number, number, number] | undefined {
24
- if (!v) return undefined;
25
- const m = v.replace(/^v/i, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
26
- if (!m) return undefined;
27
- return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
28
- }
29
-
30
- /** Compare two version strings: -1 if a<b, 0 if equal, 1 if a>b. */
31
- export function compareVersion(a: string, b: string): number {
32
- const pa = parseVersion(a) ?? [0, 0, 0];
33
- const pb = parseVersion(b) ?? [0, 0, 0];
34
- for (let i = 0; i < 3; i++) {
35
- if (pa[i]! < pb[i]!) return -1;
36
- if (pa[i]! > pb[i]!) return 1;
37
- }
38
- return 0;
39
- }
40
-
41
- /**
42
- * Evaluate a simple range of space-separated comparators against a version.
43
- * Supported operators: >= <= > < =. Each comparator glues the operator to its
44
- * version (">=4.3", "<5.0"); spaces between comparators are AND.
45
- * satisfiesRange("4.3.99", ">=4.3 <5.0") === true
46
- */
47
- export function satisfiesRange(version: string, range: string): boolean {
48
- return range
49
- .trim()
50
- .split(/\s+/)
51
- .every((clause) => {
52
- const m = clause.replace(/\s+/g, '').match(/^(>=|<=|>|<|=)(.+)$/);
53
- if (!m) return true; // ignore anything that isn't a comparator
54
- const cmp = compareVersion(version, m[2]!);
55
- switch (m[1]) {
56
- case '>=': return cmp >= 0;
57
- case '<=': return cmp <= 0;
58
- case '>': return cmp > 0;
59
- case '<': return cmp < 0;
60
- case '=': return cmp === 0;
61
- default: return true;
62
- }
63
- });
64
- }
65
-
66
- // ── Zephyr version detection ────────────────────────────────────────────────
67
-
68
- /**
69
- * Detect the installed Zephyr version from ZEPHYR_BASE/VERSION. Returns
70
- * undefined when ZEPHYR_BASE is unset or VERSION can't be read/parsed. Handles
71
- * the CMake-style file (VERSION_MAJOR = 4 / VERSION_MINOR = 3 / PATCHLEVEL = 99)
72
- * and a bare "4.3.99".
73
- */
74
- export function detectZephyrVersion(): string | undefined {
75
- const base = process.env.ZEPHYR_BASE;
76
- if (!base) return undefined;
77
- let content: string;
78
- try {
79
- content = readFileSync(join(base, 'VERSION'), 'utf-8');
80
- } catch {
81
- return undefined;
82
- }
83
- const field = (key: string): string | undefined => {
84
- const m = content.match(new RegExp(`^\\s*${key}\\s*=\\s*(\\d+)`, 'm'));
85
- return m ? m[1] : undefined;
86
- };
87
- const major = field('VERSION_MAJOR');
88
- if (major !== undefined) {
89
- return `${major}.${field('VERSION_MINOR') ?? 0}.${field('PATCHLEVEL') ?? 0}`;
90
- }
91
- const m = content.match(/(\d+\.\d+(?:\.\d+)?)/);
92
- return m ? m[1] : undefined;
93
- }
94
-
95
- // ── compat check ────────────────────────────────────────────────────────────
96
-
97
- export type CompatStatus = 'ok' | 'undetectable' | 'out-of-range';
98
-
99
- export interface CompatResult {
100
- version: string | undefined;
101
- /** Declared supported range (manifest.compat.zephyr), if any. */
102
- range: string | undefined;
103
- status: CompatStatus;
104
- }
105
-
106
- /**
107
- * Compare the detected Zephyr version against the declared compat range. Pure:
108
- * returns a status; the caller decides whether to throw/warn. 'ok' covers both
109
- * "in range" and "no range declared".
110
- */
111
- export function checkZephyrCompat(version: string | undefined): CompatResult {
112
- const range = manifest.compat?.zephyr;
113
- if (!range) return { version, range, status: 'ok' };
114
- if (version === undefined) return { version, range, status: 'undetectable' };
115
- return { version, range, status: satisfiesRange(version, range) ? 'ok' : 'out-of-range' };
116
- }
117
-
118
- // ── board-target normalization ──────────────────────────────────────────────
119
-
120
- /**
121
- * For Zephyr >=4.3, multi-core ESP32 boards require a qualified board target
122
- * (board/<soc>/<core>) the bare id is rejected with "Board qualifiers … not
123
- * found". Map each known multi-core board id to its procpu (main app core)
124
- * qualified form. procpu is the core that runs application firmware; appcpu is
125
- * the secondary core (selected explicitly only when offloading to it).
126
- */
127
- const QUALIFIED_TARGETS_GE_4_3: Record<string, string> = {
128
- esp32_devkitc: 'esp32_devkitc/esp32/procpu',
129
- esp32s3_devkitc: 'esp32s3_devkitc/esp32s3/procpu',
130
- };
131
-
132
- /**
133
- * Normalize a board target for the installed Zephyr version. Rewrites a stale
134
- * bare id (esp32s3_devkitc) to the qualified form on Zephyr >=4.3; idempotent
135
- * if the target is already qualified. Older Zephyr, single-core boards
136
- * (xiao_ble), and unknown boards pass through unchanged.
137
- */
138
- export function resolveBoardTarget(boardTarget: string, version: string | undefined): string {
139
- const boardId = boardTarget.split('/')[0]!;
140
- if (version !== undefined && satisfiesRange(version, '>=4.3')) {
141
- const qualified = QUALIFIED_TARGETS_GE_4_3[boardId];
142
- if (qualified) return qualified;
143
- }
144
- return boardTarget;
145
- }
1
+ // Zephyr version compatibility + board-target normalization.
2
+ //
3
+ // Two mechanisms absorb Zephyr version churn:
4
+ // 1. checkZephyrCompat() — compare the installed Zephyr version against the
5
+ // declared range (manifest.compat.zephyr) so an incompatible Zephyr fails
6
+ // fast with a clear message instead of a cryptic west/CMake board error.
7
+ // 2. resolveBoardTarget() — normalize the board target for the installed
8
+ // Zephyr version. Zephyr 4.3+ rejects bare multi-core board names
9
+ // (esp32s3_devkitc) and requires a qualified target
10
+ // (esp32s3_devkitc/esp32s3/procpu). This rewrites stale configs at build
11
+ // time so users don't have to regenerate them after a Zephyr upgrade.
12
+ //
13
+ // This module is intentionally pure (no chalk/ui/console) so it unit-tests
14
+ // cleanly; the doctor and the toolchain decide how to present results.
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import manifest from '../framework.manifest.js';
19
+ import { discoverWest } from './west-discover.js';
20
+
21
+ // ── minimal semver ──────────────────────────────────────────────────────────
22
+
23
+ /** Parse "4.3.99" / "v4.3" / "4.3.99-rc1" → [4, 3, 99]. Undefined if unparseable. */
24
+ export function parseVersion(v: string | undefined): [number, number, number] | undefined {
25
+ if (!v) return undefined;
26
+ const m = v.replace(/^v/i, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
27
+ if (!m) return undefined;
28
+ return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
29
+ }
30
+
31
+ /** Compare two version strings: -1 if a<b, 0 if equal, 1 if a>b. */
32
+ export function compareVersion(a: string, b: string): number {
33
+ const pa = parseVersion(a) ?? [0, 0, 0];
34
+ const pb = parseVersion(b) ?? [0, 0, 0];
35
+ for (let i = 0; i < 3; i++) {
36
+ if (pa[i]! < pb[i]!) return -1;
37
+ if (pa[i]! > pb[i]!) return 1;
38
+ }
39
+ return 0;
40
+ }
41
+
42
+ /**
43
+ * Evaluate a simple range of space-separated comparators against a version.
44
+ * Supported operators: >= <= > < =. Each comparator glues the operator to its
45
+ * version (">=4.3", "<5.0"); spaces between comparators are AND.
46
+ * satisfiesRange("4.3.99", ">=4.3 <5.0") === true
47
+ */
48
+ export function satisfiesRange(version: string, range: string): boolean {
49
+ return range
50
+ .trim()
51
+ .split(/\s+/)
52
+ .every((clause) => {
53
+ const m = clause.replace(/\s+/g, '').match(/^(>=|<=|>|<|=)(.+)$/);
54
+ if (!m) return true; // ignore anything that isn't a comparator
55
+ const cmp = compareVersion(version, m[2]!);
56
+ switch (m[1]) {
57
+ case '>=': return cmp >= 0;
58
+ case '<=': return cmp <= 0;
59
+ case '>': return cmp > 0;
60
+ case '<': return cmp < 0;
61
+ case '=': return cmp === 0;
62
+ default: return true;
63
+ }
64
+ });
65
+ }
66
+
67
+ // ── Zephyr version detection ────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Detect the installed Zephyr version from ZEPHYR_BASE/VERSION. Returns
71
+ * undefined when ZEPHYR_BASE is unset or VERSION can't be read/parsed. Handles
72
+ * the CMake-style file (VERSION_MAJOR = 4 / VERSION_MINOR = 3 / PATCHLEVEL = 99)
73
+ * and a bare "4.3.99".
74
+ */
75
+ export function detectZephyrVersion(): string | undefined {
76
+ let base = process.env.ZEPHYR_BASE;
77
+ if (!base) {
78
+ // Fall back to the discovered west install's zephyrBase — covers the
79
+ // micromamba env from @typecad/zephyr-installer WITHOUT activation.
80
+ // (micromamba run sets ZEPHYR_BASE only inside the west subprocess; this
81
+ // makes the compat check work in the parent cuttlefish process too.)
82
+ const install = discoverWest();
83
+ base = install?.zephyrBase;
84
+ }
85
+ if (!base) return undefined;
86
+ let content: string;
87
+ try {
88
+ content = readFileSync(join(base, 'VERSION'), 'utf-8');
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ const field = (key: string): string | undefined => {
93
+ const m = content.match(new RegExp(`^\\s*${key}\\s*=\\s*(\\d+)`, 'm'));
94
+ return m ? m[1] : undefined;
95
+ };
96
+ const major = field('VERSION_MAJOR');
97
+ if (major !== undefined) {
98
+ return `${major}.${field('VERSION_MINOR') ?? 0}.${field('PATCHLEVEL') ?? 0}`;
99
+ }
100
+ const m = content.match(/(\d+\.\d+(?:\.\d+)?)/);
101
+ return m ? m[1] : undefined;
102
+ }
103
+
104
+ // ── compat check ────────────────────────────────────────────────────────────
105
+
106
+ export type CompatStatus = 'ok' | 'undetectable' | 'out-of-range';
107
+
108
+ export interface CompatResult {
109
+ version: string | undefined;
110
+ /** Declared supported range (manifest.compat.zephyr), if any. */
111
+ range: string | undefined;
112
+ status: CompatStatus;
113
+ }
114
+
115
+ /**
116
+ * Compare the detected Zephyr version against the declared compat range. Pure:
117
+ * returns a status; the caller decides whether to throw/warn. 'ok' covers both
118
+ * "in range" and "no range declared".
119
+ */
120
+ export function checkZephyrCompat(version: string | undefined): CompatResult {
121
+ const range = manifest.compat?.zephyr;
122
+ if (!range) return { version, range, status: 'ok' };
123
+ if (version === undefined) return { version, range, status: 'undetectable' };
124
+ return { version, range, status: satisfiesRange(version, range) ? 'ok' : 'out-of-range' };
125
+ }
126
+
127
+ // ── board-target normalization ──────────────────────────────────────────────
128
+
129
+ /**
130
+ * For Zephyr >=4.3, multi-core ESP32 boards require a qualified board target
131
+ * (board/<soc>/<core>) — the bare id is rejected with "Board qualifiers … not
132
+ * found". Map each known multi-core board id to its procpu (main app core)
133
+ * qualified form. procpu is the core that runs application firmware; appcpu is
134
+ * the secondary core (selected explicitly only when offloading to it).
135
+ */
136
+ const QUALIFIED_TARGETS_GE_4_3: Record<string, string> = {
137
+ esp32_devkitc: 'esp32_devkitc/esp32/procpu',
138
+ esp32s3_devkitc: 'esp32s3_devkitc/esp32s3/procpu',
139
+ };
140
+
141
+ /**
142
+ * Normalize a board target for the installed Zephyr version. Rewrites a stale
143
+ * bare id (esp32s3_devkitc) to the qualified form on Zephyr >=4.3; idempotent
144
+ * if the target is already qualified. Older Zephyr, single-core boards
145
+ * (xiao_ble), and unknown boards pass through unchanged.
146
+ */
147
+ export function resolveBoardTarget(boardTarget: string, version: string | undefined): string {
148
+ const boardId = boardTarget.split('/')[0]!;
149
+ if (version !== undefined && satisfiesRange(version, '>=4.3')) {
150
+ const qualified = QUALIFIED_TARGETS_GE_4_3[boardId];
151
+ if (qualified) return qualified;
152
+ }
153
+ return boardTarget;
154
+ }
@@ -0,0 +1,285 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Zephyr environment check — the shared detection behind `cuttlefish doctor`.
3
+ //
4
+ // Mirrors @typecad/arduino-cli's checkArduinoEnv(): gather the impure
5
+ // environment facts once (west presence + version, Zephyr version, board
6
+ // existence), then reduce them to a structured result the doctor (and, later,
7
+ // the build/test gates) can present uniformly. The check is side-effect-free
8
+ // and never throws — it never installs or mutates anything.
9
+ //
10
+ // Two parity checks vs. framework-arduino's doctor:
11
+ // 1. west (the Zephyr build tool) is discoverable + responsive — the direct
12
+ // analog of "arduino-cli is installed". discoverWest() already confirms
13
+ // responsiveness via `west --version`; we additionally capture the version
14
+ // string to report it.
15
+ // 2. the configured board target exists in the Zephyr checkout
16
+ // ($ZEPHYR_BASE/boards/) — the analog of "the required core is installed".
17
+ //
18
+ // The existing compat-range check (compat.ts) is folded in as a third check so
19
+ // the doctor reports everything through one entry point.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { existsSync, readdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+
26
+ import { type WestInstall, discoverWest, resetWestDiscoveryCache } from './west-discover.js';
27
+ import { westSpawn } from './west-spawn.js';
28
+ import {
29
+ detectZephyrVersion,
30
+ checkZephyrCompat,
31
+ resolveBoardTarget,
32
+ type CompatStatus,
33
+ } from './compat.js';
34
+
35
+ // ---- probe data (impure facts, injectable for tests) -----------------------
36
+
37
+ /**
38
+ * Raw west facts gathered from discovery + a `west --version` probe. Mirrors
39
+ * ArduinoCliProbeData: `westFound` is true when a usable west install was
40
+ * discovered (discovery itself probes responsiveness).
41
+ */
42
+ export interface WestProbeData {
43
+ /** A usable west install was discovered. */
44
+ westFound: boolean;
45
+ /** west version string if the `--version` probe parsed one, e.g. "1.3.0". */
46
+ westVersion: string | undefined;
47
+ /** Which discovery strategy found west, for surfacing to the user. */
48
+ source: WestInstall['source'] | undefined;
49
+ /** Effective ZEPHYR_BASE (env var, else a base discovery surfaced). */
50
+ zephyrBase: string | undefined;
51
+ }
52
+
53
+ /**
54
+ * Test-injection seam for checkZephyrEnv. Mirrors Arduino's
55
+ * CheckArduinoEnvOptions.fakeProbe so tests never spawn a real west/python.
56
+ */
57
+ export interface CheckZephyrEnvOptions {
58
+ /** FOR TESTS ONLY: skip the real probe and use this data directly. */
59
+ fakeWestProbe?: WestProbeData;
60
+ /** FOR TESTS ONLY: override the board-existence lookup. */
61
+ fakeBoardExists?: (boardId: string, zephyrBase: string | undefined) => boolean | undefined;
62
+ }
63
+
64
+ // ---- result types (mirror ArduinoEnvResult's shape) ------------------------
65
+
66
+ export interface ZephyrEnvCheck {
67
+ /** west (the Zephyr build tool) was discovered and responsive. */
68
+ westFound: boolean;
69
+ /** west version string if known, e.g. "1.3.0". */
70
+ westVersion: string | undefined;
71
+ /** Discovery strategy that found west, for display. */
72
+ westSource: WestInstall['source'] | undefined;
73
+ /** Effective ZEPHYR_BASE (env var, else a discovered base). */
74
+ zephyrBase: string | undefined;
75
+ /** Detected Zephyr RTOS version from $ZEPHYR_BASE/VERSION, if readable. */
76
+ zephyrVersion: string | undefined;
77
+ /** Declared supported range (manifest.compat.zephyr), if any. */
78
+ compatRange: string | undefined;
79
+ /** Result of the compat-range check against the detected version. */
80
+ compatStatus: CompatStatus;
81
+ /** Raw board target from cuttlefish.config.ts, if configured. */
82
+ buildTarget: string | undefined;
83
+ /** buildTarget normalized for the installed Zephyr version (may equal it). */
84
+ resolvedBoardTarget: string | undefined;
85
+ /** Does the resolved board exist in the checkout? undefined = undetermined. */
86
+ boardTargetSupported: boolean | undefined;
87
+ }
88
+
89
+ export type ZephyrEnvOk = { ok: true; check: ZephyrEnvCheck };
90
+
91
+ export type ZephyrEnvFailure = {
92
+ ok: false;
93
+ reason: 'west-not-found' | 'zephyr-out-of-range' | 'board-not-supported';
94
+ check: ZephyrEnvCheck;
95
+ /** Human-readable lines ready to print. */
96
+ messages: string[];
97
+ /** Exact remediation hint, when applicable. */
98
+ fixCommand: string | undefined;
99
+ };
100
+
101
+ export type ZephyrEnvResult = ZephyrEnvOk | ZephyrEnvFailure;
102
+
103
+ // ---- west probe (impure; isolated + cached + overridable) ------------------
104
+
105
+ let cachedProbe: WestProbeData | undefined;
106
+
107
+ /** Clear the west-probe cache (for tests). Also resets discovery cache. */
108
+ export function resetWestProbeCacheForTest(): void {
109
+ cachedProbe = undefined;
110
+ resetWestDiscoveryCache();
111
+ }
112
+
113
+ /**
114
+ * Gather west facts: discover a usable install, then run `west --version`
115
+ * through it to capture the version. Memoized for the process lifetime (west
116
+ * installs don't move). Never throws — returns westFound:false on any failure.
117
+ */
118
+ export function probeWestEnv(): WestProbeData {
119
+ if (cachedProbe) return cachedProbe;
120
+
121
+ const install = discoverWest();
122
+ const envBase = process.env.ZEPHYR_BASE || undefined;
123
+ if (!install) {
124
+ const data: WestProbeData = {
125
+ westFound: false,
126
+ westVersion: undefined,
127
+ source: undefined,
128
+ zephyrBase: envBase,
129
+ };
130
+ cachedProbe = data;
131
+ return data;
132
+ }
133
+
134
+ // Run `west --version` through the discovered install to capture the version.
135
+ // discoverWest() already confirmed responsiveness, so a parse failure here is
136
+ // not "unresponsive" — it just means we couldn't read a version token.
137
+ let westVersion: string | undefined;
138
+ try {
139
+ const inv = westSpawn(['--version'], {
140
+ encoding: 'utf8',
141
+ timeout: 15_000,
142
+ windowsHide: true,
143
+ });
144
+ const r = spawnSync(inv.command, inv.args, inv.options);
145
+ if (r.status === 0) {
146
+ // inv.options is a generic SpawnSyncOptions (no encoding literal), so
147
+ // coerce stdout to a string before matching.
148
+ const out = typeof r.stdout === 'string' ? r.stdout : '';
149
+ const m = out.match(/v?(\d+\.\d+\.\d+)/);
150
+ westVersion = m ? m[1] : undefined;
151
+ }
152
+ } catch {
153
+ // westSpawn throws only when discovery fails — but discovery already
154
+ // succeeded (install is non-null). Defensive: treat as no version read.
155
+ westVersion = undefined;
156
+ }
157
+
158
+ const data: WestProbeData = {
159
+ westFound: true,
160
+ westVersion,
161
+ source: install.source,
162
+ zephyrBase: envBase ?? install.zephyrBase,
163
+ };
164
+ cachedProbe = data;
165
+ return data;
166
+ }
167
+
168
+ // ---- board existence (pure-ish fs probe) -----------------------------------
169
+
170
+ /**
171
+ * Does `boardId` exist as a board directory in the Zephyr checkout? Checks the
172
+ * HWMv2 vendor layout used by Zephyr 4.x: $ZEPHYR_BASE/boards/<vendor>/<boardId>.
173
+ * Returns true/false when determinable; undefined when the base is unknown or
174
+ * the boards/ tree can't be read (so callers never fail on an inconclusive
175
+ * lookup — they just skip the board check).
176
+ */
177
+ export function boardExistsInCheckout(
178
+ boardId: string,
179
+ zephyrBase: string | undefined,
180
+ ): boolean | undefined {
181
+ if (!zephyrBase) return undefined;
182
+ const boards = join(zephyrBase, 'boards');
183
+ try {
184
+ const entries = readdirSync(boards, { withFileTypes: true });
185
+ for (const entry of entries) {
186
+ if (entry.isDirectory() && existsSync(join(boards, entry.name, boardId))) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ }
195
+
196
+ // ---- main entry point -------------------------------------------------------
197
+
198
+ /**
199
+ * Verify the environment can build for `buildTarget`. Cheap and
200
+ * side-effect-free: discovers west, reads the Zephyr version, checks the compat
201
+ * range, and — when a target is configured — verifies the board exists in the
202
+ * checkout. Reports what (if anything) is wrong.
203
+ *
204
+ * - If `buildTarget` is undefined/empty, the board check is skipped (not a
205
+ * failure), mirroring Arduino's no-FQBN path.
206
+ * - Never installs anything. Never mutates the user environment.
207
+ * - Never throws — always returns a result. Callers decide how to react.
208
+ *
209
+ * `options` is for-test only (injects fake probe data / board lookup).
210
+ */
211
+ export function checkZephyrEnv(
212
+ buildTarget?: string,
213
+ options?: CheckZephyrEnvOptions,
214
+ ): ZephyrEnvResult {
215
+ const probe = options?.fakeWestProbe ?? probeWestEnv();
216
+ const boardLookup = options?.fakeBoardExists ?? boardExistsInCheckout;
217
+
218
+ const zephyrVersion = detectZephyrVersion();
219
+ const compat = checkZephyrCompat(zephyrVersion);
220
+ const resolvedBoardTarget = buildTarget ? resolveBoardTarget(buildTarget, zephyrVersion) : undefined;
221
+ const boardId = resolvedBoardTarget ? resolvedBoardTarget.split('/')[0]! : undefined;
222
+ const boardTargetSupported =
223
+ boardId !== undefined ? boardLookup(boardId, probe.zephyrBase) : undefined;
224
+
225
+ const check: ZephyrEnvCheck = {
226
+ westFound: probe.westFound,
227
+ westVersion: probe.westVersion,
228
+ westSource: probe.source,
229
+ zephyrBase: probe.zephyrBase,
230
+ zephyrVersion,
231
+ compatRange: compat.range,
232
+ compatStatus: compat.status,
233
+ buildTarget,
234
+ resolvedBoardTarget,
235
+ boardTargetSupported,
236
+ };
237
+
238
+ // 1. west (the build tool) missing entirely — nothing else can run.
239
+ if (!probe.westFound) {
240
+ return {
241
+ ok: false,
242
+ reason: 'west-not-found',
243
+ check,
244
+ messages: [
245
+ "west (the Zephyr build tool) was not found.",
246
+ " Run the typeCAD Zephyr installer, activate an existing Zephyr venv,",
247
+ " set ZEPHYR_BASE to a Zephyr SDK root, or `pip install west`.",
248
+ ],
249
+ fixCommand: undefined,
250
+ };
251
+ }
252
+
253
+ // 2. west healthy but the Zephyr RTOS is outside the supported range.
254
+ if (compat.status === 'out-of-range') {
255
+ return {
256
+ ok: false,
257
+ reason: 'zephyr-out-of-range',
258
+ check,
259
+ messages: [
260
+ `Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr.`,
261
+ " Set ZEPHYR_BASE to a compatible Zephyr checkout, or install one via '@typecad/zephyr-installer'.",
262
+ ],
263
+ fixCommand: undefined,
264
+ };
265
+ }
266
+
267
+ // 3. west + version OK — only check the board when a target is configured and
268
+ // the lookup was able to answer. A missing/absent target is not a board
269
+ // problem; an inconclusive lookup (no base) is reported as a skip, not a fail.
270
+ if (buildTarget && boardTargetSupported === false) {
271
+ return {
272
+ ok: false,
273
+ reason: 'board-not-supported',
274
+ check,
275
+ messages: [
276
+ `Board target '${resolvedBoardTarget}' was not found in this Zephyr checkout` +
277
+ (probe.zephyrBase ? ` (${join(probe.zephyrBase, 'boards')}).` : '.'),
278
+ " Check the board id, or run `west boards` to list boards in this checkout.",
279
+ ],
280
+ fixCommand: 'west boards',
281
+ };
282
+ }
283
+
284
+ return { ok: true, check };
285
+ }
@@ -85,6 +85,9 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
85
85
  const usage: KconfigUsage = {
86
86
  usesAdc: uses('adc_'),
87
87
  usesPwm: uses('pwm_'),
88
+ usesDac: uses('dac_') || uses('__tc_dac'),
89
+ usesFS: uses('__tc_fs'),
90
+ usesHwtimer: uses('counter_') || uses('__tc_hw'),
88
91
  usesI2c: uses('i2c_'),
89
92
  usesSpi: uses('spi_'),
90
93
  usesUart: uses('uart_'),