@typecad/framework-zephyr 1.0.0-alpha.15 → 1.0.0-alpha.18
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/LICENSE +202 -21
- package/README.md +49 -87
- package/dist/as-built.d.ts +2 -2
- package/dist/as-built.js +2 -2
- package/dist/boardgen.d.ts +1 -9
- package/dist/boardgen.js +228 -45
- package/dist/chips/types.d.ts +1 -1
- package/dist/debug-codegen.js +1 -1
- package/dist/display/touch-adapter.js +1 -1
- package/dist/display/ui-adapter.js +549 -549
- package/dist/doctor.js +4 -4
- package/dist/dt-config/custom-board.js +2 -2
- package/dist/dt-config/kconfig.js +18 -12
- package/dist/dt-config/overlay.d.ts +2 -2
- package/dist/dt-config/overlay.js +2 -2
- package/dist/framework.manifest.d.ts +3 -3
- package/dist/framework.manifest.js +10 -7
- package/dist/index.js +5 -5
- package/dist/licenses.d.ts +2 -2
- package/dist/licenses.js +8 -8
- package/dist/lowering/fs.js +1 -1
- package/dist/lowering/gpio.js +0 -32
- package/dist/lowering/http.js +264 -32
- package/dist/lowering/i2c.js +0 -8
- package/dist/lowering/interrupts.js +6 -0
- package/dist/lowering/mqtt.js +110 -8
- package/dist/lowering/usb.js +11 -11
- package/dist/lowering/wdt.js +2 -29
- package/dist/sdk/board-catalog-sync.d.ts +1 -3
- package/dist/sdk/board-catalog-sync.js +4 -10
- package/dist/strategy.d.ts +22 -44
- package/dist/strategy.js +152 -155
- package/dist/tmp-probe.d.ts +2 -0
- package/dist/tmp-probe.js +9 -0
- package/dist/toolchain/debug-config.d.ts +50 -90
- package/dist/toolchain/debug-config.js +241 -502
- package/dist/toolchain/env-check.d.ts +1 -3
- package/dist/toolchain/env-check.js +2 -7
- package/dist/toolchain/index.d.ts +65 -2
- package/dist/toolchain/index.js +432 -55
- package/dist/toolchain/runners.d.ts +16 -0
- package/dist/toolchain/runners.js +75 -0
- package/dist/toolchain/scaffold.d.ts +1 -1
- package/dist/toolchain/scaffold.js +12 -12
- package/dist/toolchain/west-discover.d.ts +6 -0
- package/dist/toolchain/west-discover.js +36 -13
- package/dist/toolchain/west-spawn.js +8 -2
- package/installer/README.md +328 -328
- package/installer/install.sh +2 -2
- package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
- package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
- package/installer/templates/project/.vscode/settings.json +1 -1
- package/installer/templates/project/README.md +2 -2
- package/package.json +5 -5
- package/src/as-built.ts +206 -206
- package/src/boardgen.ts +214 -48
- package/src/chips/types.ts +567 -567
- package/src/display/touch-adapter.ts +204 -204
- package/src/display/ui-adapter.ts +781 -781
- package/src/doctor.ts +4 -4
- package/src/dt-config/custom-board.ts +2 -2
- package/src/dt-config/kconfig.ts +18 -12
- package/src/dt-config/overlay.ts +1058 -1058
- package/src/framework.manifest.ts +538 -535
- package/src/index.ts +5 -5
- package/src/licenses.ts +425 -425
- package/src/lowering/fs.ts +135 -135
- package/src/lowering/gpio.ts +0 -33
- package/src/lowering/http.ts +264 -32
- package/src/lowering/i2c.ts +0 -9
- package/src/lowering/interrupts.ts +6 -0
- package/src/lowering/mqtt.ts +109 -8
- package/src/lowering/usb.ts +221 -221
- package/src/lowering/wdt.ts +2 -25
- package/src/sdk/board-catalog-sync.ts +4 -25
- package/src/strategy.ts +2265 -2309
- package/src/toolchain/debug-config.ts +264 -514
- package/src/toolchain/env-check.ts +279 -285
- package/src/toolchain/index.ts +1703 -1286
- package/src/toolchain/runners.ts +80 -0
- package/src/toolchain/scaffold.ts +296 -296
- package/src/toolchain/west-discover.ts +35 -13
- package/src/toolchain/west-spawn.ts +174 -168
- package/dist/async/timer-polyfill.d.ts +0 -10
- package/dist/async/timer-polyfill.js +0 -95
- package/dist/chips/board-overrides.d.ts +0 -7
- package/dist/chips/board-overrides.js +0 -11
- package/dist/chips/esp32.d.ts +0 -2
- package/dist/chips/esp32.js +0 -71
- package/dist/chips/esp32s3.d.ts +0 -2
- package/dist/chips/esp32s3.js +0 -103
- package/dist/chips/soc/.d.ts +0 -2
- package/dist/chips/soc/.js +0 -129
- package/dist/chips/soc/esp32.d.ts +0 -2
- package/dist/chips/soc/esp32.js +0 -120
- package/dist/chips/soc/esp32c3.d.ts +0 -2
- package/dist/chips/soc/esp32c3.js +0 -90
- package/dist/chips/soc/esp32c6.d.ts +0 -2
- package/dist/chips/soc/esp32c6.js +0 -109
- package/dist/chips/soc/esp32s3.d.ts +0 -2
- package/dist/chips/soc/esp32s3.js +0 -189
- package/dist/chips/soc/index.d.ts +0 -2
- package/dist/chips/soc/index.js +0 -23
- package/dist/chips/soc/nrf52840.d.ts +0 -2
- package/dist/chips/soc/nrf52840.js +0 -130
- package/dist/chips/soc/rp2040.d.ts +0 -2
- package/dist/chips/soc/rp2040.js +0 -141
- package/dist/chips/soc/rp2350a.d.ts +0 -2
- package/dist/chips/soc/rp2350a.js +0 -145
- package/dist/chips/soc/samd21g18a.d.ts +0 -2
- package/dist/chips/soc/samd21g18a.js +0 -143
- package/dist/chips/soc/stm32f411xe.d.ts +0 -2
- package/dist/chips/soc/stm32f411xe.js +0 -251
- package/dist/chips/xiao-ble.d.ts +0 -2
- package/dist/chips/xiao-ble.js +0 -100
- package/dist/lowering/power.d.ts +0 -9
- package/dist/lowering/power.js +0 -60
- package/dist/lowering/pulse.d.ts +0 -7
- package/dist/lowering/pulse.js +0 -51
- package/dist/lowering/tone.d.ts +0 -10
- package/dist/lowering/tone.js +0 -63
- package/dist/lowering/worker-backing.d.ts +0 -14
- package/dist/lowering/worker-backing.js +0 -79
- package/dist/lowering/worker.d.ts +0 -6
- package/dist/lowering/worker.js +0 -14
- package/dist/sdk/board-data.generated.d.ts +0 -2
- package/dist/sdk/board-data.generated.js +0 -4
- package/dist/sdk/catalog-walker.d.ts +0 -90
- package/dist/sdk/catalog-walker.js +0 -682
- package/dist/sdk/dts-reader.d.ts +0 -83
- package/dist/sdk/dts-reader.js +0 -596
- package/src/debug-codegen.ts +0 -207
package/src/licenses.ts
CHANGED
|
@@ -1,425 +1,425 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// @typecad/framework-zephyr — Zephyr license scanner
|
|
3
|
-
//
|
|
4
|
-
// Zephyr-specific enumeration + presenter for the `
|
|
5
|
-
// subcommand. The framework-agnostic SPDX detection engine lives in the shared
|
|
6
|
-
// cuttlefish core (`@typecad/cuttlefish/api/shared`); this module owns only the
|
|
7
|
-
// Zephyr pieces: discovering the west workspace, enumerating the Zephyr kernel
|
|
8
|
-
// + west manifest projects (`west list`), and resolving each one's LICENSE.
|
|
9
|
-
//
|
|
10
|
-
// Scoping mirrors framework-arduino's `--all` distinction:
|
|
11
|
-
// - default (project scope): only the dependencies the firmware ACTUALLY
|
|
12
|
-
// links, derived from the build's `compile_commands.json` (a module is
|
|
13
|
-
// listed iff its sources were compiled). A west manifest carries every
|
|
14
|
-
// vendor HAL/library; almost none are linked by a single project, so the
|
|
15
|
-
// default filters them out. Requires a prior `
|
|
16
|
-
// one, only the kernel is reported with a hint to build first.
|
|
17
|
-
// - `--all`: every west manifest project (the whole workspace).
|
|
18
|
-
//
|
|
19
|
-
// Mirrors framework-arduino's presenter shape (runLicensesPresenter(strict,
|
|
20
|
-
// all); never calls process.exit(); sets process.exitCode under --strict).
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
import { spawnSync } from 'node:child_process';
|
|
24
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
25
|
-
import * as path from 'node:path';
|
|
26
|
-
import * as ui from '@typecad/cuttlefish/utils/ui';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
resolveLibraryLicense,
|
|
30
|
-
RISK_RANK,
|
|
31
|
-
riskBracket,
|
|
32
|
-
statusMark,
|
|
33
|
-
countByRisk,
|
|
34
|
-
type LibraryLicenseEntry,
|
|
35
|
-
type ReadFile,
|
|
36
|
-
type ReadDir,
|
|
37
|
-
} from '@typecad/cuttlefish/api/shared';
|
|
38
|
-
import { discoverWest } from './toolchain/west-discover.js';
|
|
39
|
-
import { westSpawn } from './toolchain/west-spawn.js';
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Outcome + runner types
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
export type ZephyrLicensesOutcome =
|
|
46
|
-
| { ok: true; entries: LibraryLicenseEntry[]; needsBuild?: boolean }
|
|
47
|
-
| {
|
|
48
|
-
ok: false;
|
|
49
|
-
reason: 'west-not-found' | 'no-workspace' | 'no-dependencies';
|
|
50
|
-
message: string;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Injected enumeration seams so scanZephyrLicenses is unit-testable without
|
|
55
|
-
* spawning west or touching disk. The production runner (defaultRunner) wires
|
|
56
|
-
* these to discoverWest + westSpawn + fs.
|
|
57
|
-
*/
|
|
58
|
-
export interface ZephyrLicensesRunner {
|
|
59
|
-
/** Absolute workspace topdir, or null if cwd is not inside a west workspace. */
|
|
60
|
-
topdir: () => string | null;
|
|
61
|
-
/** west manifest projects `[{ name, abspath }]`, or null if `west list` failed. */
|
|
62
|
-
listModules: () => { name: string; abspath: string }[] | null;
|
|
63
|
-
/** Discovered $ZEPHYR_BASE (absolute), or undefined. */
|
|
64
|
-
zephyrBase?: string;
|
|
65
|
-
/**
|
|
66
|
-
* The build's compile_commands.json text, or null when no build exists. The
|
|
67
|
-
* project scope uses it to determine which modules the firmware actually
|
|
68
|
-
* links (a module is "linked" iff one of its sources was compiled).
|
|
69
|
-
*/
|
|
70
|
-
compileCommands?: () => string | null;
|
|
71
|
-
readFile: ReadFile;
|
|
72
|
-
readdir: ReadDir;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// Linked-module filtering (project scope)
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/** Normalize a path for case-insensitive, separator-agnostic comparison. */
|
|
80
|
-
function norm(p: string): string {
|
|
81
|
-
return p.split('\\').join('/').toLowerCase();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Filter the west manifest to the modules whose sources were compiled in the
|
|
86
|
-
* last build. A module is "linked" iff some compiled translation unit lives
|
|
87
|
-
* under its abspath. If the build data is unparseable, no filtering is applied
|
|
88
|
-
* (degrade to listing all modules rather than reporting nothing).
|
|
89
|
-
*/
|
|
90
|
-
function filterLinkedModules(
|
|
91
|
-
ccText: string,
|
|
92
|
-
modules: { name: string; abspath: string }[],
|
|
93
|
-
): { name: string; abspath: string }[] {
|
|
94
|
-
let arr: unknown;
|
|
95
|
-
try {
|
|
96
|
-
arr = JSON.parse(ccText);
|
|
97
|
-
} catch {
|
|
98
|
-
return modules;
|
|
99
|
-
}
|
|
100
|
-
if (!Array.isArray(arr)) return modules;
|
|
101
|
-
// One normalized blob of every compiled source path; membership is then a
|
|
102
|
-
// substring check per module (O(modules) after an O(TUs) join).
|
|
103
|
-
const blob = arr
|
|
104
|
-
.map((e) => norm(typeof (e as { file?: unknown })?.file === 'string' ? (e as { file: string }).file : ''))
|
|
105
|
-
.join('\n');
|
|
106
|
-
return modules.filter((m) => blob.includes(norm(m.abspath) + '/'));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// scanZephyrLicenses — enumerate + resolve the workspace dependency set
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Resolve the Zephyr kernel + west manifest projects to license entries.
|
|
115
|
-
*
|
|
116
|
-
* The Zephyr kernel is always included (when ZEPHYR_BASE is known) — a
|
|
117
|
-
* cuttlefish app always links it via `find_package(Zephyr)`.
|
|
118
|
-
*
|
|
119
|
-
* `all === false` (default, project scope) filters the west modules to those
|
|
120
|
-
* whose sources were compiled in the last build (via `compileCommands`). When
|
|
121
|
-
* no build is available, only the kernel is returned and `needsBuild` is set so
|
|
122
|
-
* the presenter can hint the user to build first. Never throws.
|
|
123
|
-
*/
|
|
124
|
-
export function scanZephyrLicenses(
|
|
125
|
-
runner: ZephyrLicensesRunner,
|
|
126
|
-
all = false,
|
|
127
|
-
): ZephyrLicensesOutcome {
|
|
128
|
-
const { readFile, readdir } = runner;
|
|
129
|
-
const entries: LibraryLicenseEntry[] = [];
|
|
130
|
-
|
|
131
|
-
// 1. The Zephyr kernel ($ZEPHYR_BASE). Apache-2.0; resolved from its LICENSE.
|
|
132
|
-
if (runner.zephyrBase) {
|
|
133
|
-
entries.push(
|
|
134
|
-
resolveLibraryLicense(
|
|
135
|
-
{ name: 'zephyr (kernel)', installDir: runner.zephyrBase },
|
|
136
|
-
readFile,
|
|
137
|
-
readdir,
|
|
138
|
-
// Zephyr's top-level LICENSE is the authoritative source; no manifest
|
|
139
|
-
// license field to consult, and scanning its source headers is noise.
|
|
140
|
-
{ subdirs: [] },
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 2. west manifest projects.
|
|
146
|
-
const modules = runner.listModules();
|
|
147
|
-
if (modules === null) {
|
|
148
|
-
if (entries.length > 0) return { ok: true, entries };
|
|
149
|
-
return {
|
|
150
|
-
ok: false,
|
|
151
|
-
reason: 'no-workspace',
|
|
152
|
-
message: '`west list` did not return a project list (not inside a west workspace?).',
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Project scope: keep only the modules the build actually linked.
|
|
157
|
-
let projectModules = modules;
|
|
158
|
-
let needsBuild = false;
|
|
159
|
-
if (!all) {
|
|
160
|
-
const cc = runner.compileCommands ? runner.compileCommands() : null;
|
|
161
|
-
if (cc === null) {
|
|
162
|
-
// No build — can't determine the linked set. Report the kernel only and
|
|
163
|
-
// flag it so the presenter prints a "build first" hint.
|
|
164
|
-
projectModules = [];
|
|
165
|
-
needsBuild = true;
|
|
166
|
-
} else {
|
|
167
|
-
projectModules = filterLinkedModules(cc, modules);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
for (const m of projectModules) {
|
|
172
|
-
// Skip the Zephyr kernel itself (already added above by name) to avoid a
|
|
173
|
-
// duplicate row when it is also a manifest project.
|
|
174
|
-
if (runner.zephyrBase && path.resolve(m.abspath) === path.resolve(runner.zephyrBase)) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
entries.push(
|
|
178
|
-
resolveLibraryLicense({ name: m.name, installDir: m.abspath }, readFile, readdir, {
|
|
179
|
-
// Zephyr HAL modules commonly keep their LICENSE under a `zephyr/` or
|
|
180
|
-
// `src/` subdir (e.g. hal_nordic ships zephyr/LICENSE.txt). Check both
|
|
181
|
-
// alongside the root. (Source-header scanning is bounded — it only runs
|
|
182
|
-
// when no LICENSE file is found, capped at 6 files / 120 lines.)
|
|
183
|
-
subdirs: ['zephyr', 'src'],
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (entries.length === 0) {
|
|
189
|
-
return {
|
|
190
|
-
ok: false,
|
|
191
|
-
reason: 'no-dependencies',
|
|
192
|
-
message: 'No Zephyr dependencies found to scan.',
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
entries.sort((a, b) => {
|
|
197
|
-
const r = RISK_RANK[a.risk] - RISK_RANK[b.risk];
|
|
198
|
-
if (r !== 0) return r;
|
|
199
|
-
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
200
|
-
});
|
|
201
|
-
return { ok: true, entries, needsBuild };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Default runner — wires seams to discoverWest + westSpawn + fs
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Find the build's compile_commands.json under cwd. Cuttlefish's Zephyr project
|
|
210
|
-
* root is the transpile output dir (e.g. `<cwd>/src/out`), so the build dir is
|
|
211
|
-
* `<root>/build` — not necessarily `<cwd>/build`. Check the common layouts,
|
|
212
|
-
* then fall back to a bounded search (skipping node_modules/.git/dist).
|
|
213
|
-
*/
|
|
214
|
-
function findCompileCommandsText(readFile: ReadFile, readdir: ReadDir): string | null {
|
|
215
|
-
const direct = [
|
|
216
|
-
path.join(process.cwd(), 'build', 'compile_commands.json'),
|
|
217
|
-
path.join(process.cwd(), 'out', 'build', 'compile_commands.json'),
|
|
218
|
-
path.join(process.cwd(), 'src', 'out', 'build', 'compile_commands.json'),
|
|
219
|
-
];
|
|
220
|
-
for (const c of direct) {
|
|
221
|
-
const text = readFile(c);
|
|
222
|
-
if (text) return text;
|
|
223
|
-
}
|
|
224
|
-
// Bounded recursive search for a build/compile_commands.json.
|
|
225
|
-
const isCc = (p: string) => {
|
|
226
|
-
const n = norm(p);
|
|
227
|
-
return n.endsWith('/build/compile_commands.json') || n.endsWith('\\build\\compile_commands.json');
|
|
228
|
-
};
|
|
229
|
-
let found: string | undefined;
|
|
230
|
-
const seen = new Set<string>();
|
|
231
|
-
const walk = (dir: string, depth: number): void => {
|
|
232
|
-
if (found || depth > 4 || seen.has(dir)) return;
|
|
233
|
-
seen.add(dir);
|
|
234
|
-
let entries: string[];
|
|
235
|
-
try {
|
|
236
|
-
entries = readdir(dir);
|
|
237
|
-
} catch {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
for (const e of entries) {
|
|
241
|
-
const full = path.join(dir, e);
|
|
242
|
-
if (e === 'compile_commands.json' && isCc(full)) {
|
|
243
|
-
found = full;
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
for (const e of entries) {
|
|
248
|
-
if (found) return;
|
|
249
|
-
if (e === 'node_modules' || e === '.git' || e === 'dist' || e.startsWith('.')) continue;
|
|
250
|
-
walk(path.join(dir, e), depth + 1);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
walk(process.cwd(), 0);
|
|
254
|
-
return found ? (readFile(found) ?? null) : null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function defaultRunner(): ZephyrLicensesRunner | null {
|
|
258
|
-
const install = discoverWest();
|
|
259
|
-
if (!install) return null;
|
|
260
|
-
const zephyrBase = install.zephyrBase;
|
|
261
|
-
|
|
262
|
-
// `west topdir` prints the workspace root (one path line). Returns null when
|
|
263
|
-
// cwd is not inside a west workspace (west exits non-zero).
|
|
264
|
-
const topdir = (): string | null => {
|
|
265
|
-
try {
|
|
266
|
-
const inv = westSpawn(['topdir'], { encoding: 'utf8', timeout: 15_000 });
|
|
267
|
-
const r = spawnSync(inv.command, inv.args, inv.options);
|
|
268
|
-
const out = typeof r.stdout === 'string' ? r.stdout.trim() : '';
|
|
269
|
-
return r.status === 0 && out ? out : null;
|
|
270
|
-
} catch {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// `west list --format '{name}\t{abspath}'` → one project per line. Returns
|
|
276
|
-
// null on any spawn failure.
|
|
277
|
-
const listModules = (): { name: string; abspath: string }[] | null => {
|
|
278
|
-
try {
|
|
279
|
-
const inv = westSpawn(['list', '--format', '{name}\t{abspath}'], {
|
|
280
|
-
encoding: 'utf8',
|
|
281
|
-
timeout: 30_000,
|
|
282
|
-
});
|
|
283
|
-
const r = spawnSync(inv.command, inv.args, inv.options);
|
|
284
|
-
if (r.status !== 0) return null;
|
|
285
|
-
const out = typeof r.stdout === 'string' ? r.stdout : '';
|
|
286
|
-
const mods: { name: string; abspath: string }[] = [];
|
|
287
|
-
for (const line of out.split(/\r?\n/)) {
|
|
288
|
-
const trimmed = line.trim();
|
|
289
|
-
if (!trimmed) continue;
|
|
290
|
-
const [name, abspath] = trimmed.split('\t');
|
|
291
|
-
if (name && abspath) mods.push({ name, abspath });
|
|
292
|
-
}
|
|
293
|
-
return mods;
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const readFile: ReadFile = (p) => {
|
|
300
|
-
try {
|
|
301
|
-
return readFileSync(p, 'utf8');
|
|
302
|
-
} catch {
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
const readdir: ReadDir = (d) => {
|
|
307
|
-
try {
|
|
308
|
-
return readdirSync(d);
|
|
309
|
-
} catch {
|
|
310
|
-
return [];
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
topdir,
|
|
316
|
-
listModules,
|
|
317
|
-
zephyrBase,
|
|
318
|
-
compileCommands: () => findCompileCommandsText(readFile, readdir),
|
|
319
|
-
readFile,
|
|
320
|
-
readdir,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
// CLI presenter
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
|
|
328
|
-
let testRunner: ZephyrLicensesRunner | null | undefined;
|
|
329
|
-
|
|
330
|
-
/** @internal Test-only override of the default runner. Pass null to simulate west-not-found. */
|
|
331
|
-
export function __setLicensesRunnerForTest(runner: ZephyrLicensesRunner | null | undefined): void {
|
|
332
|
-
testRunner = runner;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* `
|
|
337
|
-
* west manifest projects, resolves each one's license, classifies copyleft
|
|
338
|
-
* risk, and renders a sorted table. Warns on unknown licenses; sets
|
|
339
|
-
* process.exitCode under --strict when any strong-copyleft dependency is
|
|
340
|
-
* present. Never calls process.exit().
|
|
341
|
-
*
|
|
342
|
-
* Default scope = only the modules the firmware actually links (from the last
|
|
343
|
-
* `
|
|
344
|
-
*/
|
|
345
|
-
export function runLicensesPresenter(strict: boolean, all: boolean): void {
|
|
346
|
-
ui.printHeader();
|
|
347
|
-
ui.printStep(
|
|
348
|
-
all
|
|
349
|
-
? 'Checking licenses for every west manifest module'
|
|
350
|
-
: 'Checking licenses for this Zephyr project (linked dependencies only)',
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
// Surface the config (informational). Best-effort: a missing/malformed config
|
|
354
|
-
// never blocks the license scan.
|
|
355
|
-
let buildTarget: string | undefined;
|
|
356
|
-
try {
|
|
357
|
-
buildTarget =
|
|
358
|
-
} catch {
|
|
359
|
-
/* best-effort */
|
|
360
|
-
}
|
|
361
|
-
if (buildTarget) {
|
|
362
|
-
ui.printInfo(`Board target ... ${buildTarget}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const runner = testRunner !== undefined ? testRunner : defaultRunner();
|
|
366
|
-
if (!runner) {
|
|
367
|
-
ui.printError('west ............. NOT FOUND');
|
|
368
|
-
ui.printInfo(" → install west (pip install west) or run the typeCAD Zephyr installer.");
|
|
369
|
-
process.exitCode = 1;
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = scanZephyrLicenses(runner, all);
|
|
374
|
-
if (!result.ok) {
|
|
375
|
-
if (result.reason === 'no-workspace' || result.reason === 'no-dependencies') {
|
|
376
|
-
ui.printInfo(`(${result.message})`);
|
|
377
|
-
}
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Project scope with no build: only the kernel is reported. Hint the user to
|
|
382
|
-
// build (which records which modules actually link) or use --all.
|
|
383
|
-
if (result.needsBuild) {
|
|
384
|
-
ui.printInfo(
|
|
385
|
-
'(no build found — only the Zephyr kernel is shown. Run `
|
|
386
|
-
'this report to the modules your firmware actually links, or use `
|
|
387
|
-
'for every west module.)',
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const counts = countByRisk(result.entries);
|
|
392
|
-
for (const e of result.entries) {
|
|
393
|
-
if (e.risk === 'unknown') {
|
|
394
|
-
ui.printWarning(`${e.name} .................. UNKNOWN`);
|
|
395
|
-
} else {
|
|
396
|
-
ui.printInfo(`${e.name} .................. ${e.spdx ?? 'UNKNOWN'}${riskBracket(e.risk)}${statusMark(e.risk)}`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
ui.printSuccess(
|
|
400
|
-
`${counts.permissive} permissive, ${counts['weak-copyleft']} weak copyleft, ` +
|
|
401
|
-
`${counts['strong-copyleft']} strong copyleft, ${counts.unknown} unknown`,
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
const unknowns = result.entries.filter((e) => e.risk === 'unknown');
|
|
405
|
-
if (unknowns.length > 0) {
|
|
406
|
-
ui.printWarning(
|
|
407
|
-
`License could not be determined for ${unknowns.length} ${unknowns.length === 1 ? 'dependency' : 'dependencies'}:`,
|
|
408
|
-
);
|
|
409
|
-
for (const u of unknowns) {
|
|
410
|
-
ui.printInfo(` ${u.name} (check LICENSE in ${u.path})`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const strongCopyleft = result.entries.filter((e) => e.risk === 'strong-copyleft');
|
|
415
|
-
if (strongCopyleft.length > 0) {
|
|
416
|
-
ui.printWarning(
|
|
417
|
-
`${strongCopyleft.length} ${strongCopyleft.length === 1 ? 'dependency carries' : 'dependencies carry'} strong-copyleft terms — review before shipping.`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// --strict fails the build on any unknown OR strong-copyleft dependency.
|
|
422
|
-
if ((unknowns.length > 0 || strongCopyleft.length > 0) && strict) {
|
|
423
|
-
process.exitCode = 1;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/framework-zephyr — Zephyr license scanner
|
|
3
|
+
//
|
|
4
|
+
// Zephyr-specific enumeration + presenter for the `typecad-hal licenses`
|
|
5
|
+
// subcommand. The framework-agnostic SPDX detection engine lives in the shared
|
|
6
|
+
// cuttlefish core (`@typecad/cuttlefish/api/shared`); this module owns only the
|
|
7
|
+
// Zephyr pieces: discovering the west workspace, enumerating the Zephyr kernel
|
|
8
|
+
// + west manifest projects (`west list`), and resolving each one's LICENSE.
|
|
9
|
+
//
|
|
10
|
+
// Scoping mirrors framework-arduino's `--all` distinction:
|
|
11
|
+
// - default (project scope): only the dependencies the firmware ACTUALLY
|
|
12
|
+
// links, derived from the build's `compile_commands.json` (a module is
|
|
13
|
+
// listed iff its sources were compiled). A west manifest carries every
|
|
14
|
+
// vendor HAL/library; almost none are linked by a single project, so the
|
|
15
|
+
// default filters them out. Requires a prior `typecad-hal build` — without
|
|
16
|
+
// one, only the kernel is reported with a hint to build first.
|
|
17
|
+
// - `--all`: every west manifest project (the whole workspace).
|
|
18
|
+
//
|
|
19
|
+
// Mirrors framework-arduino's presenter shape (runLicensesPresenter(strict,
|
|
20
|
+
// all); never calls process.exit(); sets process.exitCode under --strict).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import * as ui from '@typecad/cuttlefish/utils/ui';
|
|
27
|
+
import { loadTypecadConfig } from '@typecad/cuttlefish/config-loader';
|
|
28
|
+
import {
|
|
29
|
+
resolveLibraryLicense,
|
|
30
|
+
RISK_RANK,
|
|
31
|
+
riskBracket,
|
|
32
|
+
statusMark,
|
|
33
|
+
countByRisk,
|
|
34
|
+
type LibraryLicenseEntry,
|
|
35
|
+
type ReadFile,
|
|
36
|
+
type ReadDir,
|
|
37
|
+
} from '@typecad/cuttlefish/api/shared';
|
|
38
|
+
import { discoverWest } from './toolchain/west-discover.js';
|
|
39
|
+
import { westSpawn } from './toolchain/west-spawn.js';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Outcome + runner types
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export type ZephyrLicensesOutcome =
|
|
46
|
+
| { ok: true; entries: LibraryLicenseEntry[]; needsBuild?: boolean }
|
|
47
|
+
| {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: 'west-not-found' | 'no-workspace' | 'no-dependencies';
|
|
50
|
+
message: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Injected enumeration seams so scanZephyrLicenses is unit-testable without
|
|
55
|
+
* spawning west or touching disk. The production runner (defaultRunner) wires
|
|
56
|
+
* these to discoverWest + westSpawn + fs.
|
|
57
|
+
*/
|
|
58
|
+
export interface ZephyrLicensesRunner {
|
|
59
|
+
/** Absolute workspace topdir, or null if cwd is not inside a west workspace. */
|
|
60
|
+
topdir: () => string | null;
|
|
61
|
+
/** west manifest projects `[{ name, abspath }]`, or null if `west list` failed. */
|
|
62
|
+
listModules: () => { name: string; abspath: string }[] | null;
|
|
63
|
+
/** Discovered $ZEPHYR_BASE (absolute), or undefined. */
|
|
64
|
+
zephyrBase?: string;
|
|
65
|
+
/**
|
|
66
|
+
* The build's compile_commands.json text, or null when no build exists. The
|
|
67
|
+
* project scope uses it to determine which modules the firmware actually
|
|
68
|
+
* links (a module is "linked" iff one of its sources was compiled).
|
|
69
|
+
*/
|
|
70
|
+
compileCommands?: () => string | null;
|
|
71
|
+
readFile: ReadFile;
|
|
72
|
+
readdir: ReadDir;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Linked-module filtering (project scope)
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** Normalize a path for case-insensitive, separator-agnostic comparison. */
|
|
80
|
+
function norm(p: string): string {
|
|
81
|
+
return p.split('\\').join('/').toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Filter the west manifest to the modules whose sources were compiled in the
|
|
86
|
+
* last build. A module is "linked" iff some compiled translation unit lives
|
|
87
|
+
* under its abspath. If the build data is unparseable, no filtering is applied
|
|
88
|
+
* (degrade to listing all modules rather than reporting nothing).
|
|
89
|
+
*/
|
|
90
|
+
function filterLinkedModules(
|
|
91
|
+
ccText: string,
|
|
92
|
+
modules: { name: string; abspath: string }[],
|
|
93
|
+
): { name: string; abspath: string }[] {
|
|
94
|
+
let arr: unknown;
|
|
95
|
+
try {
|
|
96
|
+
arr = JSON.parse(ccText);
|
|
97
|
+
} catch {
|
|
98
|
+
return modules;
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(arr)) return modules;
|
|
101
|
+
// One normalized blob of every compiled source path; membership is then a
|
|
102
|
+
// substring check per module (O(modules) after an O(TUs) join).
|
|
103
|
+
const blob = arr
|
|
104
|
+
.map((e) => norm(typeof (e as { file?: unknown })?.file === 'string' ? (e as { file: string }).file : ''))
|
|
105
|
+
.join('\n');
|
|
106
|
+
return modules.filter((m) => blob.includes(norm(m.abspath) + '/'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// scanZephyrLicenses — enumerate + resolve the workspace dependency set
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the Zephyr kernel + west manifest projects to license entries.
|
|
115
|
+
*
|
|
116
|
+
* The Zephyr kernel is always included (when ZEPHYR_BASE is known) — a
|
|
117
|
+
* cuttlefish app always links it via `find_package(Zephyr)`.
|
|
118
|
+
*
|
|
119
|
+
* `all === false` (default, project scope) filters the west modules to those
|
|
120
|
+
* whose sources were compiled in the last build (via `compileCommands`). When
|
|
121
|
+
* no build is available, only the kernel is returned and `needsBuild` is set so
|
|
122
|
+
* the presenter can hint the user to build first. Never throws.
|
|
123
|
+
*/
|
|
124
|
+
export function scanZephyrLicenses(
|
|
125
|
+
runner: ZephyrLicensesRunner,
|
|
126
|
+
all = false,
|
|
127
|
+
): ZephyrLicensesOutcome {
|
|
128
|
+
const { readFile, readdir } = runner;
|
|
129
|
+
const entries: LibraryLicenseEntry[] = [];
|
|
130
|
+
|
|
131
|
+
// 1. The Zephyr kernel ($ZEPHYR_BASE). Apache-2.0; resolved from its LICENSE.
|
|
132
|
+
if (runner.zephyrBase) {
|
|
133
|
+
entries.push(
|
|
134
|
+
resolveLibraryLicense(
|
|
135
|
+
{ name: 'zephyr (kernel)', installDir: runner.zephyrBase },
|
|
136
|
+
readFile,
|
|
137
|
+
readdir,
|
|
138
|
+
// Zephyr's top-level LICENSE is the authoritative source; no manifest
|
|
139
|
+
// license field to consult, and scanning its source headers is noise.
|
|
140
|
+
{ subdirs: [] },
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. west manifest projects.
|
|
146
|
+
const modules = runner.listModules();
|
|
147
|
+
if (modules === null) {
|
|
148
|
+
if (entries.length > 0) return { ok: true, entries };
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: 'no-workspace',
|
|
152
|
+
message: '`west list` did not return a project list (not inside a west workspace?).',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Project scope: keep only the modules the build actually linked.
|
|
157
|
+
let projectModules = modules;
|
|
158
|
+
let needsBuild = false;
|
|
159
|
+
if (!all) {
|
|
160
|
+
const cc = runner.compileCommands ? runner.compileCommands() : null;
|
|
161
|
+
if (cc === null) {
|
|
162
|
+
// No build — can't determine the linked set. Report the kernel only and
|
|
163
|
+
// flag it so the presenter prints a "build first" hint.
|
|
164
|
+
projectModules = [];
|
|
165
|
+
needsBuild = true;
|
|
166
|
+
} else {
|
|
167
|
+
projectModules = filterLinkedModules(cc, modules);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const m of projectModules) {
|
|
172
|
+
// Skip the Zephyr kernel itself (already added above by name) to avoid a
|
|
173
|
+
// duplicate row when it is also a manifest project.
|
|
174
|
+
if (runner.zephyrBase && path.resolve(m.abspath) === path.resolve(runner.zephyrBase)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
entries.push(
|
|
178
|
+
resolveLibraryLicense({ name: m.name, installDir: m.abspath }, readFile, readdir, {
|
|
179
|
+
// Zephyr HAL modules commonly keep their LICENSE under a `zephyr/` or
|
|
180
|
+
// `src/` subdir (e.g. hal_nordic ships zephyr/LICENSE.txt). Check both
|
|
181
|
+
// alongside the root. (Source-header scanning is bounded — it only runs
|
|
182
|
+
// when no LICENSE file is found, capped at 6 files / 120 lines.)
|
|
183
|
+
subdirs: ['zephyr', 'src'],
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (entries.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
reason: 'no-dependencies',
|
|
192
|
+
message: 'No Zephyr dependencies found to scan.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
entries.sort((a, b) => {
|
|
197
|
+
const r = RISK_RANK[a.risk] - RISK_RANK[b.risk];
|
|
198
|
+
if (r !== 0) return r;
|
|
199
|
+
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
200
|
+
});
|
|
201
|
+
return { ok: true, entries, needsBuild };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Default runner — wires seams to discoverWest + westSpawn + fs
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Find the build's compile_commands.json under cwd. Cuttlefish's Zephyr project
|
|
210
|
+
* root is the transpile output dir (e.g. `<cwd>/src/out`), so the build dir is
|
|
211
|
+
* `<root>/build` — not necessarily `<cwd>/build`. Check the common layouts,
|
|
212
|
+
* then fall back to a bounded search (skipping node_modules/.git/dist).
|
|
213
|
+
*/
|
|
214
|
+
function findCompileCommandsText(readFile: ReadFile, readdir: ReadDir): string | null {
|
|
215
|
+
const direct = [
|
|
216
|
+
path.join(process.cwd(), 'build', 'compile_commands.json'),
|
|
217
|
+
path.join(process.cwd(), 'out', 'build', 'compile_commands.json'),
|
|
218
|
+
path.join(process.cwd(), 'src', 'out', 'build', 'compile_commands.json'),
|
|
219
|
+
];
|
|
220
|
+
for (const c of direct) {
|
|
221
|
+
const text = readFile(c);
|
|
222
|
+
if (text) return text;
|
|
223
|
+
}
|
|
224
|
+
// Bounded recursive search for a build/compile_commands.json.
|
|
225
|
+
const isCc = (p: string) => {
|
|
226
|
+
const n = norm(p);
|
|
227
|
+
return n.endsWith('/build/compile_commands.json') || n.endsWith('\\build\\compile_commands.json');
|
|
228
|
+
};
|
|
229
|
+
let found: string | undefined;
|
|
230
|
+
const seen = new Set<string>();
|
|
231
|
+
const walk = (dir: string, depth: number): void => {
|
|
232
|
+
if (found || depth > 4 || seen.has(dir)) return;
|
|
233
|
+
seen.add(dir);
|
|
234
|
+
let entries: string[];
|
|
235
|
+
try {
|
|
236
|
+
entries = readdir(dir);
|
|
237
|
+
} catch {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
const full = path.join(dir, e);
|
|
242
|
+
if (e === 'compile_commands.json' && isCc(full)) {
|
|
243
|
+
found = full;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const e of entries) {
|
|
248
|
+
if (found) return;
|
|
249
|
+
if (e === 'node_modules' || e === '.git' || e === 'dist' || e.startsWith('.')) continue;
|
|
250
|
+
walk(path.join(dir, e), depth + 1);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
walk(process.cwd(), 0);
|
|
254
|
+
return found ? (readFile(found) ?? null) : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function defaultRunner(): ZephyrLicensesRunner | null {
|
|
258
|
+
const install = discoverWest();
|
|
259
|
+
if (!install) return null;
|
|
260
|
+
const zephyrBase = install.zephyrBase;
|
|
261
|
+
|
|
262
|
+
// `west topdir` prints the workspace root (one path line). Returns null when
|
|
263
|
+
// cwd is not inside a west workspace (west exits non-zero).
|
|
264
|
+
const topdir = (): string | null => {
|
|
265
|
+
try {
|
|
266
|
+
const inv = westSpawn(['topdir'], { encoding: 'utf8', timeout: 15_000 });
|
|
267
|
+
const r = spawnSync(inv.command, inv.args, inv.options);
|
|
268
|
+
const out = typeof r.stdout === 'string' ? r.stdout.trim() : '';
|
|
269
|
+
return r.status === 0 && out ? out : null;
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// `west list --format '{name}\t{abspath}'` → one project per line. Returns
|
|
276
|
+
// null on any spawn failure.
|
|
277
|
+
const listModules = (): { name: string; abspath: string }[] | null => {
|
|
278
|
+
try {
|
|
279
|
+
const inv = westSpawn(['list', '--format', '{name}\t{abspath}'], {
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
timeout: 30_000,
|
|
282
|
+
});
|
|
283
|
+
const r = spawnSync(inv.command, inv.args, inv.options);
|
|
284
|
+
if (r.status !== 0) return null;
|
|
285
|
+
const out = typeof r.stdout === 'string' ? r.stdout : '';
|
|
286
|
+
const mods: { name: string; abspath: string }[] = [];
|
|
287
|
+
for (const line of out.split(/\r?\n/)) {
|
|
288
|
+
const trimmed = line.trim();
|
|
289
|
+
if (!trimmed) continue;
|
|
290
|
+
const [name, abspath] = trimmed.split('\t');
|
|
291
|
+
if (name && abspath) mods.push({ name, abspath });
|
|
292
|
+
}
|
|
293
|
+
return mods;
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const readFile: ReadFile = (p) => {
|
|
300
|
+
try {
|
|
301
|
+
return readFileSync(p, 'utf8');
|
|
302
|
+
} catch {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const readdir: ReadDir = (d) => {
|
|
307
|
+
try {
|
|
308
|
+
return readdirSync(d);
|
|
309
|
+
} catch {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
topdir,
|
|
316
|
+
listModules,
|
|
317
|
+
zephyrBase,
|
|
318
|
+
compileCommands: () => findCompileCommandsText(readFile, readdir),
|
|
319
|
+
readFile,
|
|
320
|
+
readdir,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// CLI presenter
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
let testRunner: ZephyrLicensesRunner | null | undefined;
|
|
329
|
+
|
|
330
|
+
/** @internal Test-only override of the default runner. Pass null to simulate west-not-found. */
|
|
331
|
+
export function __setLicensesRunnerForTest(runner: ZephyrLicensesRunner | null | undefined): void {
|
|
332
|
+
testRunner = runner;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* `typecad-hal licenses` (Zephyr) presenter. Enumerates the Zephyr kernel +
|
|
337
|
+
* west manifest projects, resolves each one's license, classifies copyleft
|
|
338
|
+
* risk, and renders a sorted table. Warns on unknown licenses; sets
|
|
339
|
+
* process.exitCode under --strict when any strong-copyleft dependency is
|
|
340
|
+
* present. Never calls process.exit().
|
|
341
|
+
*
|
|
342
|
+
* Default scope = only the modules the firmware actually links (from the last
|
|
343
|
+
* `typecad-hal build`); `--all` lists every west manifest module.
|
|
344
|
+
*/
|
|
345
|
+
export function runLicensesPresenter(strict: boolean, all: boolean): void {
|
|
346
|
+
ui.printHeader();
|
|
347
|
+
ui.printStep(
|
|
348
|
+
all
|
|
349
|
+
? 'Checking licenses for every west manifest module'
|
|
350
|
+
: 'Checking licenses for this Zephyr project (linked dependencies only)',
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Surface the config (informational). Best-effort: a missing/malformed config
|
|
354
|
+
// never blocks the license scan.
|
|
355
|
+
let buildTarget: string | undefined;
|
|
356
|
+
try {
|
|
357
|
+
buildTarget = loadTypecadConfig(process.cwd())?.buildTarget;
|
|
358
|
+
} catch {
|
|
359
|
+
/* best-effort */
|
|
360
|
+
}
|
|
361
|
+
if (buildTarget) {
|
|
362
|
+
ui.printInfo(`Board target ... ${buildTarget}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const runner = testRunner !== undefined ? testRunner : defaultRunner();
|
|
366
|
+
if (!runner) {
|
|
367
|
+
ui.printError('west ............. NOT FOUND');
|
|
368
|
+
ui.printInfo(" → install west (pip install west) or run the typeCAD Zephyr installer.");
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = scanZephyrLicenses(runner, all);
|
|
374
|
+
if (!result.ok) {
|
|
375
|
+
if (result.reason === 'no-workspace' || result.reason === 'no-dependencies') {
|
|
376
|
+
ui.printInfo(`(${result.message})`);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Project scope with no build: only the kernel is reported. Hint the user to
|
|
382
|
+
// build (which records which modules actually link) or use --all.
|
|
383
|
+
if (result.needsBuild) {
|
|
384
|
+
ui.printInfo(
|
|
385
|
+
'(no build found — only the Zephyr kernel is shown. Run `typecad-hal build` to scope ' +
|
|
386
|
+
'this report to the modules your firmware actually links, or use `typecad-hal licenses --all` ' +
|
|
387
|
+
'for every west module.)',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const counts = countByRisk(result.entries);
|
|
392
|
+
for (const e of result.entries) {
|
|
393
|
+
if (e.risk === 'unknown') {
|
|
394
|
+
ui.printWarning(`${e.name} .................. UNKNOWN`);
|
|
395
|
+
} else {
|
|
396
|
+
ui.printInfo(`${e.name} .................. ${e.spdx ?? 'UNKNOWN'}${riskBracket(e.risk)}${statusMark(e.risk)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
ui.printSuccess(
|
|
400
|
+
`${counts.permissive} permissive, ${counts['weak-copyleft']} weak copyleft, ` +
|
|
401
|
+
`${counts['strong-copyleft']} strong copyleft, ${counts.unknown} unknown`,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const unknowns = result.entries.filter((e) => e.risk === 'unknown');
|
|
405
|
+
if (unknowns.length > 0) {
|
|
406
|
+
ui.printWarning(
|
|
407
|
+
`License could not be determined for ${unknowns.length} ${unknowns.length === 1 ? 'dependency' : 'dependencies'}:`,
|
|
408
|
+
);
|
|
409
|
+
for (const u of unknowns) {
|
|
410
|
+
ui.printInfo(` ${u.name} (check LICENSE in ${u.path})`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const strongCopyleft = result.entries.filter((e) => e.risk === 'strong-copyleft');
|
|
415
|
+
if (strongCopyleft.length > 0) {
|
|
416
|
+
ui.printWarning(
|
|
417
|
+
`${strongCopyleft.length} ${strongCopyleft.length === 1 ? 'dependency carries' : 'dependencies carry'} strong-copyleft terms — review before shipping.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --strict fails the build on any unknown OR strong-copyleft dependency.
|
|
422
|
+
if ((unknowns.length > 0 || strongCopyleft.length > 0) && strict) {
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
}
|
|
425
|
+
}
|