@typecad/expect 1.0.0-alpha.13 → 1.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +417 -345
- package/dist/host/cli.js +80 -36
- package/dist/host/compiler.d.ts +11 -11
- package/dist/host/compiler.js +87 -135
- package/dist/host/config.d.ts +0 -3
- package/dist/host/config.js +26 -24
- package/dist/host/finder.d.ts +1 -1
- package/dist/host/finder.js +1 -1
- package/dist/host/port-discovery.d.ts +55 -0
- package/dist/host/port-discovery.js +122 -0
- package/dist/host/preprocessor.d.ts +37 -18
- package/dist/host/preprocessor.js +63 -33
- package/dist/host/project-root.d.ts +8 -0
- package/dist/host/project-root.js +24 -0
- package/dist/host/protocol-emitter.js +7 -7
- package/dist/host/reporter.js +1 -1
- package/dist/host/runner.d.ts +0 -5
- package/dist/host/runner.js +139 -31
- package/dist/host/serial.js +30 -9
- package/dist/host/test-pins.d.ts +37 -0
- package/dist/host/test-pins.js +105 -0
- package/dist/host/types.d.ts +12 -3
- package/dist/index.d.ts +1 -1
- package/package.json +7 -8
package/dist/host/cli.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
import { loadConfig } from './config.js';
|
|
22
22
|
import { run } from './runner.js';
|
|
23
|
+
import { boardTestPins } from './test-pins.js';
|
|
24
|
+
import { listUsbSerialPorts, matchUsbPorts, formatUsbIdentity } from './port-discovery.js';
|
|
25
|
+
import { resolveProjectRoot } from './project-root.js';
|
|
23
26
|
import path from 'node:path';
|
|
24
27
|
function parseArgs(argv) {
|
|
25
28
|
const result = { files: [] };
|
|
@@ -78,6 +81,9 @@ function parseArgs(argv) {
|
|
|
78
81
|
case '-v':
|
|
79
82
|
result.verbose = true;
|
|
80
83
|
break;
|
|
84
|
+
case '--discover':
|
|
85
|
+
result.discover = true;
|
|
86
|
+
break;
|
|
81
87
|
case '--dry-run':
|
|
82
88
|
result.dryRun = true;
|
|
83
89
|
break;
|
|
@@ -103,41 +109,44 @@ function parseArgs(argv) {
|
|
|
103
109
|
// ---------------------------------------------------------------------------
|
|
104
110
|
// Help text
|
|
105
111
|
// ---------------------------------------------------------------------------
|
|
106
|
-
const HELP = `
|
|
107
|
-
\x1b[1m\x1b[36m cuttlefish-test\x1b[0m — Hardware test runner for TypeCAD
|
|
108
|
-
|
|
109
|
-
\x1b[1mUsage:\x1b[0m
|
|
110
|
-
cuttlefish-test [options] [files...]
|
|
111
|
-
|
|
112
|
-
\x1b[1mOptions:\x1b[0m
|
|
113
|
-
--port, -p <port> Serial port (e.g. COM4, /dev/ttyACM0)
|
|
114
|
-
--board, -b <board> Board package override
|
|
115
|
-
--build-target <id> Framework-specific build target override
|
|
116
|
-
--baud <rate> Serial baud rate (default: 115200)
|
|
117
|
-
--timeout, -t <ms> Serial read timeout (default: 30000)
|
|
118
|
-
--include, -i <glob> Test file pattern (repeatable)
|
|
119
|
-
--exclude, -x <glob> Test file pattern to skip (repeatable)
|
|
120
|
-
--verbose, -v Show debug serial output
|
|
121
|
-
--
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
112
|
+
const HELP = `
|
|
113
|
+
\x1b[1m\x1b[36m cuttlefish-test\x1b[0m — Hardware test runner for TypeCAD
|
|
114
|
+
|
|
115
|
+
\x1b[1mUsage:\x1b[0m
|
|
116
|
+
cuttlefish-test [options] [files...]
|
|
117
|
+
|
|
118
|
+
\x1b[1mOptions:\x1b[0m
|
|
119
|
+
--port, -p <port> Serial port (e.g. COM4, /dev/ttyACM0)
|
|
120
|
+
--board, -b <board> Board package override
|
|
121
|
+
--build-target <id> Framework-specific build target override
|
|
122
|
+
--baud <rate> Serial baud rate (default: 115200)
|
|
123
|
+
--timeout, -t <ms> Serial read timeout (default: 30000)
|
|
124
|
+
--include, -i <glob> Test file pattern (repeatable)
|
|
125
|
+
--exclude, -x <glob> Test file pattern to skip (repeatable)
|
|
126
|
+
--verbose, -v Show debug serial output
|
|
127
|
+
--discover List attached USB serial ports (VID:PID, serial,
|
|
128
|
+
manufacturer) and which one the current config/board
|
|
129
|
+
identity matches, then exit — test-box bring-up aid
|
|
130
|
+
--help, -h Show this help
|
|
131
|
+
|
|
132
|
+
\x1b[1mExamples:\x1b[0m
|
|
133
|
+
cuttlefish-test --port COM4
|
|
134
|
+
cuttlefish-test --port /dev/ttyACM0 tests/my-test.test.ts
|
|
135
|
+
cuttlefish-test -p COM4 -v
|
|
136
|
+
|
|
137
|
+
\x1b[1mConfiguration:\x1b[0m
|
|
138
|
+
Add a \`test\` section to your cuttlefish.config.ts:
|
|
139
|
+
|
|
140
|
+
const config = {
|
|
141
|
+
board: 'xiao_ble/nrf52840',
|
|
142
|
+
test: {
|
|
143
|
+
buildTarget: 'blackpill/stm32f411ce',
|
|
144
|
+
port: 'COM4',
|
|
145
|
+
include: ['tests/**/*.test.ts'],
|
|
146
|
+
baudRate: 115200,
|
|
147
|
+
timeout: 30000,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
141
150
|
`.trim();
|
|
142
151
|
// ---------------------------------------------------------------------------
|
|
143
152
|
// Main
|
|
@@ -148,7 +157,11 @@ async function main() {
|
|
|
148
157
|
console.log(HELP);
|
|
149
158
|
process.exit(0);
|
|
150
159
|
}
|
|
151
|
-
|
|
160
|
+
// npm exec / npm run reset the child cwd to the npm local prefix (the
|
|
161
|
+
// nearest package.json ancestor), stashing the real invocation dir in
|
|
162
|
+
// INIT_CWD — resolve through it so nested suite dirs work under npx.
|
|
163
|
+
// See host/project-root.ts for the full rationale.
|
|
164
|
+
const projectRoot = resolveProjectRoot();
|
|
152
165
|
// Build config overrides from CLI args
|
|
153
166
|
const overrides = {};
|
|
154
167
|
if (args.port)
|
|
@@ -190,6 +203,37 @@ async function main() {
|
|
|
190
203
|
// CLI mode flags (not config-file settings).
|
|
191
204
|
config.dryRun = args.dryRun;
|
|
192
205
|
config.bail = args.bail;
|
|
206
|
+
// --discover: list attached USB serial ports and annotate the match for
|
|
207
|
+
// the active board identity (config test.usb, else the board's
|
|
208
|
+
// test-pins.json usb block). Bring-up aid for multi-board test boxes.
|
|
209
|
+
if (args.discover) {
|
|
210
|
+
const ports = await listUsbSerialPorts();
|
|
211
|
+
const identity = config.test.usb ?? boardTestPins(config.board, config.projectRoot, config.configPath)?.usb;
|
|
212
|
+
const matches = identity ? matchUsbPorts(ports, identity) : [];
|
|
213
|
+
console.log('USB serial ports:');
|
|
214
|
+
if (ports.length === 0) {
|
|
215
|
+
console.log(' (none found)');
|
|
216
|
+
}
|
|
217
|
+
for (const p of ports) {
|
|
218
|
+
const isMatch = identity ? matchUsbPorts([p], identity).length > 0 : false;
|
|
219
|
+
const id = `${p.vid.toUpperCase()}:${p.pid.toUpperCase()}`;
|
|
220
|
+
const serial = p.serialNumber ? ` serial ${p.serialNumber}` : '';
|
|
221
|
+
const mfr = p.manufacturer ? ` [${p.manufacturer}]` : '';
|
|
222
|
+
console.log(` ${p.path} ${id}${serial}${mfr}${isMatch ? ' <-- matches this config' : ''}`);
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
if (identity) {
|
|
226
|
+
console.log(matches.length === 1
|
|
227
|
+
? `config identity ${formatUsbIdentity(identity)} -> ${matches[0].path}`
|
|
228
|
+
: `config identity ${formatUsbIdentity(identity)} -> no unique match (${matches.length} found)`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.log('(no USB identity on this config or board — set test.usb or a test-pins.json usb block)');
|
|
232
|
+
}
|
|
233
|
+
// Exit 1 when an identity is set but has no unique match, so scripts
|
|
234
|
+
// can gate on discovery success.
|
|
235
|
+
process.exit(identity ? (matches.length === 1 ? 0 : 1) : 0);
|
|
236
|
+
}
|
|
193
237
|
// Run tests
|
|
194
238
|
const exitCode = await run(config);
|
|
195
239
|
process.exit(exitCode);
|
package/dist/host/compiler.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface CompileResult {
|
|
2
2
|
success: boolean;
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
projectDir: string;
|
|
4
|
+
sourcePath: string;
|
|
5
5
|
output: string;
|
|
6
6
|
error?: string;
|
|
7
7
|
}
|
|
@@ -11,17 +11,17 @@ export interface UploadResult {
|
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Transpile preprocessed TypeScript source to a C++
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (Zephyr) entry file.
|
|
14
|
+
* Transpile preprocessed TypeScript source to a C++ Zephyr project. Writes
|
|
15
|
+
* the preprocessed source to a temp file, invokes the cuttlefish transpiler,
|
|
16
|
+
* and returns the path to the generated .cpp entry file.
|
|
18
17
|
*/
|
|
19
|
-
export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string,
|
|
18
|
+
export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string, configPath?: string): CompileResult;
|
|
20
19
|
/**
|
|
21
|
-
* Compile the
|
|
20
|
+
* Compile the Zephyr project via `west build` (through the Zephyr Toolchain).
|
|
22
21
|
*/
|
|
23
|
-
export declare function
|
|
22
|
+
export declare function compileProgram(projectDir: string, buildTarget: string, zephyrConfig?: Record<string, unknown>): CompileResult;
|
|
24
23
|
/**
|
|
25
|
-
* Upload the compiled
|
|
24
|
+
* Upload the compiled project to the board via `west flash` (through the
|
|
25
|
+
* Zephyr Toolchain).
|
|
26
26
|
*/
|
|
27
|
-
export declare function
|
|
27
|
+
export declare function uploadProgram(projectDir: string, buildTarget: string, port: string, zephyrConfig?: Record<string, unknown>): UploadResult;
|
package/dist/host/compiler.js
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// @typecad/expect — Compiler
|
|
3
3
|
//
|
|
4
|
-
// Wraps the cuttlefish transpiler +
|
|
5
|
-
//
|
|
4
|
+
// Wraps the cuttlefish transpiler + west compile/flash cycle. Takes
|
|
5
|
+
// preprocessed TypeScript source, transpiles to C++, compiles, uploads.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { parseConfigAST } from './config.js';
|
|
12
|
-
import { checkArduinoEnv } from '@typecad/arduino-cli';
|
|
13
12
|
// createRequire lets us use require() in an ESM module for the optional
|
|
14
|
-
// framework-zephyr dynamic import
|
|
13
|
+
// framework-zephyr dynamic import.
|
|
15
14
|
const require = createRequire(import.meta.url);
|
|
16
|
-
/** Format a check failure into the `error` field used by CompileResult/UploadResult. */
|
|
17
|
-
function formatEnvFailure(failure) {
|
|
18
|
-
const lines = [...failure.messages];
|
|
19
|
-
if (failure.fixCommand)
|
|
20
|
-
lines.push(` Fix: ${failure.fixCommand}`);
|
|
21
|
-
return lines.join('\n');
|
|
22
|
-
}
|
|
23
15
|
/**
|
|
24
|
-
* Transpile preprocessed TypeScript source to a C++
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (Zephyr) entry file.
|
|
16
|
+
* Transpile preprocessed TypeScript source to a C++ Zephyr project. Writes
|
|
17
|
+
* the preprocessed source to a temp file, invokes the cuttlefish transpiler,
|
|
18
|
+
* and returns the path to the generated .cpp entry file.
|
|
28
19
|
*/
|
|
29
|
-
export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget,
|
|
20
|
+
export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget, configPath) {
|
|
30
21
|
// Create a build directory for this test file. Per-file directories are
|
|
31
|
-
// used —
|
|
22
|
+
// used — west builds have no incremental benefit from a shared dir.
|
|
32
23
|
const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
|
|
33
24
|
const buildDir = path.join(projectRoot, '.build', 'expect', baseName);
|
|
34
25
|
try {
|
|
@@ -37,7 +28,7 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
37
28
|
fs.mkdirSync(buildDir, { recursive: true });
|
|
38
29
|
}
|
|
39
30
|
catch {
|
|
40
|
-
return { success: false,
|
|
31
|
+
return { success: false, projectDir: buildDir, sourcePath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
|
|
41
32
|
}
|
|
42
33
|
const rewrittenSource = rewriteRelativeImports(preprocessedSource, originalFilePath, buildDir);
|
|
43
34
|
// Write the preprocessed source as a .ts file
|
|
@@ -51,7 +42,7 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
51
42
|
// OR when an explicit configPath was passed. The latter matters because
|
|
52
43
|
// direct-file mode discovers cuttlefish.config.ts from cwd (projectRoot),
|
|
53
44
|
// which is the DEFAULT config — so a project with several target-specific
|
|
54
|
-
// configs (e.g. tests/
|
|
45
|
+
// configs (e.g. packages/hal/tests/network/cuttlefish.config.ts vs ble-demo.config.ts)
|
|
55
46
|
// would always transpile against the default. Build mode writes a config
|
|
56
47
|
// derived from the chosen configPath into the build dir, so the right
|
|
57
48
|
// board/MCU/target is used.
|
|
@@ -59,99 +50,70 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
59
50
|
if (useBuildMode) {
|
|
60
51
|
writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget, configPath);
|
|
61
52
|
}
|
|
62
|
-
const result = spawnSync(process.execPath,
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
const result = spawnSync(process.execPath,
|
|
54
|
+
// Explicit heap headroom for the transpile child. The steady-state
|
|
55
|
+
// transpile peaks well under 1 GB, but Node's default old-space cap
|
|
56
|
+
// (~4 GB on large-RAM machines) has been hit transiently — a GC storm
|
|
57
|
+
// then kills the child with "JavaScript heap out of memory" and fails
|
|
58
|
+
// the whole test file. Dedicated headroom makes a spike recoverable.
|
|
59
|
+
[
|
|
60
|
+
'--max-old-space-size=6144',
|
|
61
|
+
cuttlefishCmd,
|
|
62
|
+
...(useBuildMode
|
|
63
|
+
? ['build', '--skip-type-check', '--force']
|
|
64
|
+
: [tsPath, '--skip-type-check', '--force']),
|
|
65
|
+
], {
|
|
65
66
|
encoding: 'utf8',
|
|
66
67
|
cwd: useBuildMode ? buildDir : projectRoot,
|
|
67
|
-
timeout:
|
|
68
|
+
timeout: 120000,
|
|
68
69
|
env: { ...process.env },
|
|
69
70
|
});
|
|
70
71
|
const transpileOutput = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
71
72
|
if (result.status !== 0) {
|
|
73
|
+
// Surface spawn-level errors (EMFILE/ENOENT from a degraded runner —
|
|
74
|
+
// e.g. after serial-handle leaks): spawnSync reports them on .error
|
|
75
|
+
// with a null status and EMPTY stdout/stderr, which previously masked
|
|
76
|
+
// the cause as a bare "Transpilation failed:".
|
|
77
|
+
const spawnErr = result.error ? ` (spawn error: ${result.error.message})` : '';
|
|
72
78
|
return {
|
|
73
79
|
success: false,
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
projectDir: buildDir,
|
|
81
|
+
sourcePath: '',
|
|
76
82
|
output: transpileOutput,
|
|
77
|
-
error: `Transpilation failed:\n${transpileOutput}`,
|
|
83
|
+
error: `Transpilation failed${spawnErr}:\n${transpileOutput}`,
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
|
-
// Find the generated entry file (.
|
|
81
|
-
const outDir = findOutputDir(buildDir, baseName, projectRoot
|
|
82
|
-
const entryPath = findEntryFile(outDir
|
|
86
|
+
// Find the generated entry file (.cpp for Zephyr)
|
|
87
|
+
const outDir = findOutputDir(buildDir, baseName, projectRoot);
|
|
88
|
+
const entryPath = findEntryFile(outDir);
|
|
83
89
|
if (!entryPath) {
|
|
84
|
-
const ext = toolchainType === 'west' ? '.cpp' : '.ino';
|
|
85
90
|
return {
|
|
86
91
|
success: false,
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
projectDir: outDir,
|
|
93
|
+
sourcePath: '',
|
|
89
94
|
output: transpileOutput,
|
|
90
|
-
error: `No
|
|
95
|
+
error: `No .cpp file found in ${outDir} after transpilation`,
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
return {
|
|
94
99
|
success: true,
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
projectDir: path.dirname(entryPath),
|
|
101
|
+
sourcePath: entryPath,
|
|
97
102
|
output: transpileOutput,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
101
|
-
* Compile the
|
|
106
|
+
* Compile the Zephyr project via `west build` (through the Zephyr Toolchain).
|
|
102
107
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
return compileWestProject(sketchDir, buildTarget, zephyrConfig);
|
|
106
|
-
}
|
|
107
|
-
return compileArduinoSketch(sketchDir, buildTarget);
|
|
108
|
-
}
|
|
109
|
-
/** Compile via arduino-cli. */
|
|
110
|
-
function compileArduinoSketch(sketchDir, buildTarget) {
|
|
111
|
-
// Hard gate: verify arduino-cli + core before spawning.
|
|
112
|
-
{
|
|
113
|
-
const gate = checkArduinoEnv(buildTarget);
|
|
114
|
-
if (!gate.ok) {
|
|
115
|
-
const message = formatEnvFailure(gate);
|
|
116
|
-
return { success: false, sketchDir, sketchPath: '', output: message, error: message };
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const result = spawnSync('arduino-cli', ['compile', '--fqbn', buildTarget, sketchDir], { encoding: 'utf8', timeout: 120000 });
|
|
120
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
121
|
-
return {
|
|
122
|
-
success: result.status === 0,
|
|
123
|
-
sketchDir,
|
|
124
|
-
sketchPath: '',
|
|
125
|
-
output,
|
|
126
|
-
error: result.status !== 0 ? `Compilation failed:\n${output}` : undefined,
|
|
127
|
-
};
|
|
108
|
+
export function compileProgram(projectDir, buildTarget, zephyrConfig) {
|
|
109
|
+
return compileWestProject(projectDir, buildTarget, zephyrConfig);
|
|
128
110
|
}
|
|
129
111
|
/**
|
|
130
|
-
* Upload the compiled
|
|
112
|
+
* Upload the compiled project to the board via `west flash` (through the
|
|
113
|
+
* Zephyr Toolchain).
|
|
131
114
|
*/
|
|
132
|
-
export function
|
|
133
|
-
|
|
134
|
-
return uploadWestProject(sketchDir, buildTarget, port, zephyrConfig);
|
|
135
|
-
}
|
|
136
|
-
return uploadArduinoSketch(sketchDir, buildTarget, port);
|
|
137
|
-
}
|
|
138
|
-
/** Upload via arduino-cli. */
|
|
139
|
-
function uploadArduinoSketch(sketchDir, buildTarget, port) {
|
|
140
|
-
// Hard gate: verify arduino-cli + core before spawning.
|
|
141
|
-
{
|
|
142
|
-
const gate = checkArduinoEnv(buildTarget);
|
|
143
|
-
if (!gate.ok) {
|
|
144
|
-
const message = formatEnvFailure(gate);
|
|
145
|
-
return { success: false, output: message, error: message };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
|
|
149
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
150
|
-
return {
|
|
151
|
-
success: result.status === 0,
|
|
152
|
-
output,
|
|
153
|
-
error: result.status !== 0 ? `Upload failed:\n${output}` : undefined,
|
|
154
|
-
};
|
|
115
|
+
export function uploadProgram(projectDir, buildTarget, port, zephyrConfig) {
|
|
116
|
+
return uploadWestProject(projectDir, buildTarget, port, zephyrConfig);
|
|
155
117
|
}
|
|
156
118
|
// ---------------------------------------------------------------------------
|
|
157
119
|
// Zephyr (west) toolchain
|
|
@@ -159,25 +121,25 @@ function uploadArduinoSketch(sketchDir, buildTarget, port) {
|
|
|
159
121
|
/**
|
|
160
122
|
* Compile a Zephyr project via `west build`. The Zephyr Toolchain (from
|
|
161
123
|
* @typecad/framework-zephyr) handles west discovery, ZEPHYR_BASE, scaffolding,
|
|
162
|
-
* and the board target. We call it via dynamic import
|
|
163
|
-
* dependency
|
|
124
|
+
* and the board target. We call it via dynamic import so expect only gains
|
|
125
|
+
* the dependency when framework-zephyr is installed.
|
|
164
126
|
*/
|
|
165
|
-
function compileWestProject(
|
|
166
|
-
//
|
|
127
|
+
function compileWestProject(projectDir, buildTarget, zephyrConfig) {
|
|
128
|
+
// projectDir for Zephyr is the project root containing src/, app/, build/.
|
|
167
129
|
// The transpiler emits src/src.cpp; the west project root is the parent of src/.
|
|
168
|
-
const srcDir = path.join(
|
|
169
|
-
const projectRoot = fs.existsSync(srcDir) ?
|
|
130
|
+
const srcDir = path.join(projectDir, 'src');
|
|
131
|
+
const projectRoot = fs.existsSync(srcDir) ? projectDir : path.dirname(projectDir);
|
|
170
132
|
const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
|
|
171
133
|
? path.join(srcDir, 'src.cpp')
|
|
172
|
-
: path.join(
|
|
173
|
-
const outputDir = fs.existsSync(srcDir) ? srcDir :
|
|
134
|
+
: path.join(projectDir, 'src.cpp');
|
|
135
|
+
const outputDir = fs.existsSync(srcDir) ? srcDir : projectDir;
|
|
174
136
|
try {
|
|
175
137
|
// Dynamic import — framework-zephyr is an optional dependency (only present
|
|
176
138
|
// for Zephyr projects). The Toolchain object has compile()/upload().
|
|
177
139
|
const mod = require('@typecad/framework-zephyr');
|
|
178
140
|
const Toolchain = mod.Toolchain;
|
|
179
141
|
if (!Toolchain || typeof Toolchain.compile !== 'function') {
|
|
180
|
-
return { success: false,
|
|
142
|
+
return { success: false, projectDir, sourcePath: '', output: '', error: '@typecad/framework-zephyr did not export a usable Toolchain.compile().' };
|
|
181
143
|
}
|
|
182
144
|
const result = Toolchain.compile({
|
|
183
145
|
outputDir,
|
|
@@ -187,27 +149,27 @@ function compileWestProject(sketchDir, buildTarget, zephyrConfig) {
|
|
|
187
149
|
});
|
|
188
150
|
return {
|
|
189
151
|
success: result.success,
|
|
190
|
-
|
|
191
|
-
|
|
152
|
+
projectDir: projectRoot,
|
|
153
|
+
sourcePath: sourcePath,
|
|
192
154
|
output: result.output,
|
|
193
155
|
error: result.success ? undefined : `west build failed:\n${result.output}`,
|
|
194
156
|
};
|
|
195
157
|
}
|
|
196
158
|
catch (e) {
|
|
197
|
-
return { success: false,
|
|
159
|
+
return { success: false, projectDir, sourcePath: '', output: '', error: `Failed to compile via west: ${e.message}` };
|
|
198
160
|
}
|
|
199
161
|
}
|
|
200
162
|
/**
|
|
201
163
|
* Upload (flash) a Zephyr project via `west flash`. For ESP32 boards, west
|
|
202
164
|
* uses the esptool runner; for nRF boards, nrfjprog. The port is forwarded.
|
|
203
165
|
*/
|
|
204
|
-
function uploadWestProject(
|
|
205
|
-
const srcDir = path.join(
|
|
206
|
-
const projectRoot = fs.existsSync(srcDir) ?
|
|
166
|
+
function uploadWestProject(projectDir, buildTarget, port, zephyrConfig) {
|
|
167
|
+
const srcDir = path.join(projectDir, 'src');
|
|
168
|
+
const projectRoot = fs.existsSync(srcDir) ? projectDir : path.dirname(projectDir);
|
|
207
169
|
const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
|
|
208
170
|
? path.join(srcDir, 'src.cpp')
|
|
209
|
-
: path.join(
|
|
210
|
-
const outputDir = fs.existsSync(srcDir) ? srcDir :
|
|
171
|
+
: path.join(projectDir, 'src.cpp');
|
|
172
|
+
const outputDir = fs.existsSync(srcDir) ? srcDir : projectDir;
|
|
211
173
|
try {
|
|
212
174
|
const mod = require('@typecad/framework-zephyr');
|
|
213
175
|
const Toolchain = mod.Toolchain;
|
|
@@ -258,7 +220,7 @@ function resolveCuttlefishCmd(projectRoot) {
|
|
|
258
220
|
// Fallback: assume it's on PATH
|
|
259
221
|
return 'cuttlefish';
|
|
260
222
|
}
|
|
261
|
-
function findOutputDir(buildDir, baseName, projectRoot
|
|
223
|
+
function findOutputDir(buildDir, baseName, projectRoot) {
|
|
262
224
|
// The cuttlefish transpiler writes output next to the source by default,
|
|
263
225
|
// or to the configured outDir. Check common locations.
|
|
264
226
|
const candidates = [
|
|
@@ -270,59 +232,54 @@ function findOutputDir(buildDir, baseName, projectRoot, toolchainType = 'arduino
|
|
|
270
232
|
path.join(projectRoot, '.build', 'expect', baseName, baseName),
|
|
271
233
|
];
|
|
272
234
|
for (const c of candidates) {
|
|
273
|
-
if (fs.existsSync(c) && hasEntryFile(c
|
|
235
|
+
if (fs.existsSync(c) && hasEntryFile(c))
|
|
274
236
|
return c;
|
|
275
237
|
}
|
|
276
238
|
// Last resort: walk the buildDir tree recursively to find any entry file
|
|
277
|
-
const found = findEntryFileRecursive(buildDir
|
|
239
|
+
const found = findEntryFileRecursive(buildDir);
|
|
278
240
|
if (found)
|
|
279
241
|
return path.dirname(found);
|
|
280
242
|
return buildDir;
|
|
281
243
|
}
|
|
282
|
-
/** Check for a .
|
|
283
|
-
function hasEntryFile(dir
|
|
244
|
+
/** Check for a .cpp entry file in a directory. */
|
|
245
|
+
function hasEntryFile(dir) {
|
|
284
246
|
if (!fs.existsSync(dir))
|
|
285
247
|
return false;
|
|
286
|
-
return fs.readdirSync(dir).some(f => isEntryFileName(f
|
|
248
|
+
return fs.readdirSync(dir).some(f => isEntryFileName(f));
|
|
287
249
|
}
|
|
288
|
-
/** True if the filename is a valid entry file
|
|
289
|
-
function isEntryFileName(name
|
|
290
|
-
|
|
291
|
-
return name.endsWith('.cpp') || name.endsWith('.cc');
|
|
292
|
-
}
|
|
293
|
-
return name.endsWith('.ino') || name.endsWith('.cc');
|
|
250
|
+
/** True if the filename is a valid entry file (.cpp/.cc). */
|
|
251
|
+
function isEntryFileName(name) {
|
|
252
|
+
return name.endsWith('.cpp') || name.endsWith('.cc');
|
|
294
253
|
}
|
|
295
|
-
/** Find the entry
|
|
296
|
-
function findEntryFile(dir
|
|
254
|
+
/** Find the entry .cpp file in a directory (or its src/ subdirectory). */
|
|
255
|
+
function findEntryFile(dir) {
|
|
297
256
|
if (!fs.existsSync(dir))
|
|
298
257
|
return undefined;
|
|
299
258
|
for (const entry of fs.readdirSync(dir)) {
|
|
300
|
-
if (isEntryFileName(entry
|
|
259
|
+
if (isEntryFileName(entry)) {
|
|
301
260
|
return path.join(dir, entry);
|
|
302
261
|
}
|
|
303
262
|
}
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return path.join(srcDir, entry);
|
|
311
|
-
}
|
|
263
|
+
// The entry may be in a src/ subdirectory
|
|
264
|
+
const srcDir = path.join(dir, 'src');
|
|
265
|
+
if (fs.existsSync(srcDir)) {
|
|
266
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
267
|
+
if (isEntryFileName(entry)) {
|
|
268
|
+
return path.join(srcDir, entry);
|
|
312
269
|
}
|
|
313
270
|
}
|
|
314
271
|
}
|
|
315
272
|
return undefined;
|
|
316
273
|
}
|
|
317
|
-
function findEntryFileRecursive(dir
|
|
274
|
+
function findEntryFileRecursive(dir) {
|
|
318
275
|
if (!fs.existsSync(dir))
|
|
319
276
|
return undefined;
|
|
320
277
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
321
|
-
if (isEntryFileName(entry.name
|
|
278
|
+
if (isEntryFileName(entry.name)) {
|
|
322
279
|
return path.join(dir, entry.name);
|
|
323
280
|
}
|
|
324
281
|
if (entry.isDirectory()) {
|
|
325
|
-
const result = findEntryFileRecursive(path.join(dir, entry.name)
|
|
282
|
+
const result = findEntryFileRecursive(path.join(dir, entry.name));
|
|
326
283
|
if (result)
|
|
327
284
|
return result;
|
|
328
285
|
}
|
|
@@ -379,11 +336,6 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget, con
|
|
|
379
336
|
lines.push(` framework: '${baseValues.output?.framework}',`);
|
|
380
337
|
lines.push(` outDir: './out',`);
|
|
381
338
|
lines.push(' },');
|
|
382
|
-
if (baseValues.console?.baudRate) {
|
|
383
|
-
lines.push(' console: {');
|
|
384
|
-
lines.push(` baudRate: ${baseValues.console?.baudRate},`);
|
|
385
|
-
lines.push(' },');
|
|
386
|
-
}
|
|
387
339
|
// Pass the zephyr section (kconfig, runner) through verbatim — the Zephyr
|
|
388
340
|
// toolchain reads runner from it (e.g. zephyr.runner: 'uf2'). Serialize the
|
|
389
341
|
// parsed object as a minimal object literal (string values only; sufficient
|
|
@@ -406,12 +358,12 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget, con
|
|
|
406
358
|
'',
|
|
407
359
|
'const config: CuttlefishConfig = {',
|
|
408
360
|
` entry: './${entryFileName}',`,
|
|
409
|
-
`
|
|
361
|
+
` framework: '@typecad/framework-zephyr',`,
|
|
410
362
|
` frameworkData: { buildTarget: '${buildTarget}' },`,
|
|
411
363
|
' output: {',
|
|
412
|
-
` framework: 'arduino',`,
|
|
413
364
|
` outDir: './out',`,
|
|
414
365
|
' },',
|
|
366
|
+
' toolchain: { type: \'west\' },',
|
|
415
367
|
'};',
|
|
416
368
|
'',
|
|
417
369
|
'export default config;',
|