@kvell007/embed-labs-cli 0.1.0-alpha.6 → 0.1.0-alpha.61
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 +87 -23
- package/dist/embed-labs-mcp-bridge.mjs +3104 -0
- package/dist/index.js +2395 -184
- package/dist/index.js.map +1 -1
- package/dist/install-progress.d.ts +2 -0
- package/dist/install-progress.js +75 -0
- package/dist/install-progress.js.map +1 -0
- package/dist/local-toolchain.d.ts +190 -5
- package/dist/local-toolchain.js +1480 -87
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +3 -3
package/dist/local-toolchain.js
CHANGED
|
@@ -8,9 +8,9 @@ import { Readable } from "node:stream";
|
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
const DEFAULT_RELEASE_ROOT = "/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal";
|
|
11
|
-
const DEFAULT_QT_SMOKE_SOURCE = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/qt-smoke";
|
|
12
|
-
const DEFAULT_METADATA_ROOT = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/embedlabs-release";
|
|
13
11
|
const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
|
|
12
|
+
const PICO2W_RP2350_BOARD_ID = "pico2w-rp2350-monitor";
|
|
13
|
+
const COLOREASYPICO2_RP2350_BOARD_ID = "coloreasypico2-rp2350-monitor";
|
|
14
14
|
const DEFAULT_CHANNEL = "stable";
|
|
15
15
|
const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
|
|
16
16
|
const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
|
|
@@ -20,9 +20,15 @@ const BUILT_IN_CHANNEL = {
|
|
|
20
20
|
packages: [
|
|
21
21
|
{ id: "embedlabs.tools.vendor.rockchip", version: "1.0.0", manifest: "" },
|
|
22
22
|
{ id: "embedlabs.tools.common.llvm", version: "22.1.3", manifest: "" },
|
|
23
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.2.rel1", manifest: "" },
|
|
23
24
|
{ id: "embedlabs.tools.common.e2fsprogs", version: "1.0.0", manifest: "" },
|
|
25
|
+
{ id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.33", manifest: "" },
|
|
26
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.33", manifest: "" },
|
|
24
27
|
{ id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
|
|
25
|
-
{ id: "embedlabs.
|
|
28
|
+
{ id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
|
|
29
|
+
{ id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.33", manifest: "" },
|
|
30
|
+
{ id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.33", manifest: "" },
|
|
31
|
+
{ id: "embedlabs.board.coloreasypico2.rp2350-monitor", version: "1.0.33", manifest: "" }
|
|
26
32
|
]
|
|
27
33
|
};
|
|
28
34
|
const BUILT_IN_MANIFESTS = {
|
|
@@ -42,6 +48,14 @@ const BUILT_IN_MANIFESTS = {
|
|
|
42
48
|
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
43
49
|
provides: ["llvm.clang", "llvm.clangxx", "llvm.ld_lld", "llvm.ar", "llvm.objcopy", "llvm.readelf"]
|
|
44
50
|
},
|
|
51
|
+
"embedlabs.tools.common.arm-none-eabi": {
|
|
52
|
+
schema: "embedlabs.package.v1",
|
|
53
|
+
id: "embedlabs.tools.common.arm-none-eabi",
|
|
54
|
+
version: "15.2.rel1",
|
|
55
|
+
kind: "tools",
|
|
56
|
+
hosts: ["darwin-arm64"],
|
|
57
|
+
provides: ["arm-none-eabi.gcc", "arm-none-eabi.g++", "arm-none-eabi.objcopy", "picotool"]
|
|
58
|
+
},
|
|
45
59
|
"embedlabs.tools.common.e2fsprogs": {
|
|
46
60
|
schema: "embedlabs.package.v1",
|
|
47
61
|
id: "embedlabs.tools.common.e2fsprogs",
|
|
@@ -50,6 +64,22 @@ const BUILT_IN_MANIFESTS = {
|
|
|
50
64
|
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
51
65
|
provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
|
|
52
66
|
},
|
|
67
|
+
"embedlabs.tools.runtime.qtquick-live-preview": {
|
|
68
|
+
schema: "embedlabs.package.v1",
|
|
69
|
+
id: "embedlabs.tools.runtime.qtquick-live-preview",
|
|
70
|
+
version: "1.0.33",
|
|
71
|
+
kind: "tools",
|
|
72
|
+
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
73
|
+
provides: ["qtquick.live_preview", "qtquick.live_preview.inspector", "qtquick.live_preview.feedback"]
|
|
74
|
+
},
|
|
75
|
+
"embedlabs.tools.runtime.rp2350-monitor": {
|
|
76
|
+
schema: "embedlabs.package.v1",
|
|
77
|
+
id: "embedlabs.tools.runtime.rp2350-monitor",
|
|
78
|
+
version: "1.0.33",
|
|
79
|
+
kind: "tools",
|
|
80
|
+
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
81
|
+
provides: ["rp2350.monitor.cli", "rp2350.monitor.logic_analyzer", "rp2350.monitor.logic_decode"]
|
|
82
|
+
},
|
|
53
83
|
"embedlabs.family.rk356x": {
|
|
54
84
|
schema: "embedlabs.package.v1",
|
|
55
85
|
id: "embedlabs.family.rk356x",
|
|
@@ -62,10 +92,21 @@ const BUILT_IN_MANIFESTS = {
|
|
|
62
92
|
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
|
|
63
93
|
]
|
|
64
94
|
},
|
|
95
|
+
"embedlabs.family.rp2350": {
|
|
96
|
+
schema: "embedlabs.package.v1",
|
|
97
|
+
id: "embedlabs.family.rp2350",
|
|
98
|
+
version: "1.0.0",
|
|
99
|
+
kind: "family",
|
|
100
|
+
family: "rp2350",
|
|
101
|
+
requires: [
|
|
102
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2"] },
|
|
103
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
104
|
+
]
|
|
105
|
+
},
|
|
65
106
|
"embedlabs.board.taishanpi.1m-rk3566": {
|
|
66
107
|
schema: "embedlabs.package.v1",
|
|
67
108
|
id: "embedlabs.board.taishanpi.1m-rk3566",
|
|
68
|
-
version: "1.0.
|
|
109
|
+
version: "1.0.33",
|
|
69
110
|
kind: "board",
|
|
70
111
|
family: "rk356x",
|
|
71
112
|
board: "TaishanPi",
|
|
@@ -74,20 +115,67 @@ const BUILT_IN_MANIFESTS = {
|
|
|
74
115
|
{ id: "embedlabs.family.rk356x", version: "^1.0.0" },
|
|
75
116
|
{ id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
|
|
76
117
|
{ id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
|
|
77
|
-
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] }
|
|
118
|
+
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
|
|
119
|
+
{ id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.33", roles: ["qtquick-preview"] }
|
|
78
120
|
],
|
|
79
121
|
build_modes: ["local-llvm"]
|
|
122
|
+
},
|
|
123
|
+
"embedlabs.board.pico2w.rp2350-monitor": {
|
|
124
|
+
schema: "embedlabs.package.v1",
|
|
125
|
+
id: "embedlabs.board.pico2w.rp2350-monitor",
|
|
126
|
+
version: "1.0.33",
|
|
127
|
+
kind: "board",
|
|
128
|
+
display_name: "Pico 2 W",
|
|
129
|
+
family: "rp2350",
|
|
130
|
+
board: "Pico 2 W",
|
|
131
|
+
board_id: PICO2W_RP2350_BOARD_ID,
|
|
132
|
+
requires: [
|
|
133
|
+
{ id: "embedlabs.family.rp2350", version: "^1.0.0" },
|
|
134
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
|
|
135
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
136
|
+
],
|
|
137
|
+
build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
|
|
138
|
+
},
|
|
139
|
+
"embedlabs.board.coloreasypico2.rp2350-monitor": {
|
|
140
|
+
schema: "embedlabs.package.v1",
|
|
141
|
+
id: "embedlabs.board.coloreasypico2.rp2350-monitor",
|
|
142
|
+
version: "1.0.33",
|
|
143
|
+
kind: "board",
|
|
144
|
+
display_name: "ColorEasyPICO2",
|
|
145
|
+
family: "rp2350",
|
|
146
|
+
board: "ColorEasyPICO2",
|
|
147
|
+
board_id: COLOREASYPICO2_RP2350_BOARD_ID,
|
|
148
|
+
requires: [
|
|
149
|
+
{ id: "embedlabs.family.rp2350", version: "^1.0.0" },
|
|
150
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
|
|
151
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
152
|
+
],
|
|
153
|
+
build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
|
|
80
154
|
}
|
|
81
155
|
};
|
|
82
156
|
const INSTALL_COPY_PATHS = [
|
|
83
157
|
"toolchain/llvm-cross",
|
|
84
|
-
"toolchain/host
|
|
158
|
+
"toolchain/host",
|
|
159
|
+
"toolchain/host-tools",
|
|
160
|
+
"toolchain/qt6-rk3566-llvm-toolchain.cmake",
|
|
85
161
|
"qt-target/qt6-rk3566-llvm-6.8.3",
|
|
162
|
+
"qt-host/qt6-host-macos-6.8.3",
|
|
86
163
|
"tools/mac",
|
|
164
|
+
"toolkit-runtime/qtquick-live-preview",
|
|
165
|
+
"toolkit-runtime/rp2350-monitor",
|
|
166
|
+
"rp2350-sdk",
|
|
167
|
+
"rp2350-initial-firmware",
|
|
168
|
+
"rp2350-examples",
|
|
87
169
|
"images/current",
|
|
88
170
|
"userdata/rootfs",
|
|
89
|
-
"boot-workspace
|
|
171
|
+
"boot-workspace",
|
|
172
|
+
"README.md",
|
|
173
|
+
"meta",
|
|
174
|
+
"scripts",
|
|
175
|
+
"support",
|
|
176
|
+
"third_party"
|
|
90
177
|
];
|
|
178
|
+
const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
|
|
91
179
|
export function defaultLocalReleaseRoot() {
|
|
92
180
|
return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
|
|
93
181
|
|| process.env.EMBEDLABS_RELEASE_ROOT?.trim()
|
|
@@ -96,31 +184,36 @@ export function defaultLocalReleaseRoot() {
|
|
|
96
184
|
export async function latestLocalToolchain(options = {}) {
|
|
97
185
|
const boardId = options.boardId ?? DEFAULT_BOARD_ID;
|
|
98
186
|
const channelName = options.channel ?? DEFAULT_CHANNEL;
|
|
187
|
+
const host = localToolchainHostId();
|
|
99
188
|
const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
|
|
100
189
|
const boardPackageId = boardPackageIdFor(boardId);
|
|
101
190
|
const board = manifests.get(boardPackageId);
|
|
102
191
|
if (!board) {
|
|
103
192
|
throw new Error(`No local toolchain board package found for ${boardId}.`);
|
|
104
193
|
}
|
|
194
|
+
const canonicalBoardId = boardIdForPackageManifest(board);
|
|
105
195
|
const packages = resolvePackageRefs(boardPackageId, channel, manifests);
|
|
106
196
|
let download;
|
|
107
197
|
let downloadError;
|
|
108
198
|
try {
|
|
109
199
|
download = await resolveLocalToolchainDownloadPlan({
|
|
110
|
-
boardId,
|
|
200
|
+
boardId: canonicalBoardId,
|
|
111
201
|
channel: channelName,
|
|
112
|
-
host
|
|
202
|
+
host,
|
|
113
203
|
toolchain: "llvm"
|
|
114
204
|
});
|
|
115
205
|
}
|
|
116
206
|
catch (error) {
|
|
117
207
|
downloadError = error instanceof Error ? error.message : String(error);
|
|
118
208
|
}
|
|
209
|
+
if (!download && isNativeWindowsTaishanPiHost(canonicalBoardId, host)) {
|
|
210
|
+
downloadError = taishanPiWindowsRequirementMessage(host);
|
|
211
|
+
}
|
|
119
212
|
return {
|
|
120
|
-
board_id:
|
|
121
|
-
channel:
|
|
122
|
-
host
|
|
123
|
-
version: board.version,
|
|
213
|
+
board_id: canonicalBoardId,
|
|
214
|
+
channel: channelName,
|
|
215
|
+
host,
|
|
216
|
+
version: download?.version ?? board.version,
|
|
124
217
|
metadata_root: metadataRoot,
|
|
125
218
|
packages,
|
|
126
219
|
download,
|
|
@@ -132,11 +225,36 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
|
|
|
132
225
|
const registryPath = localToolchainRegistryPath(root);
|
|
133
226
|
try {
|
|
134
227
|
const registry = JSON.parse(await readFile(registryPath, "utf8"));
|
|
228
|
+
const environments = registry.environments;
|
|
229
|
+
const boardInstall = environments?.[normalizeBoardId(boardId)];
|
|
230
|
+
if (boardInstall?.release_root) {
|
|
231
|
+
return {
|
|
232
|
+
installed: true,
|
|
233
|
+
board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
|
|
234
|
+
version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
|
|
235
|
+
mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
|
|
236
|
+
release_root: boardInstall.release_root,
|
|
237
|
+
registry_path: registryPath,
|
|
238
|
+
install_root: root,
|
|
239
|
+
channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
|
|
240
|
+
packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
|
|
241
|
+
};
|
|
242
|
+
}
|
|
135
243
|
const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
|
|
244
|
+
const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
|
|
245
|
+
if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
|
|
246
|
+
return {
|
|
247
|
+
installed: false,
|
|
248
|
+
board_id: boardId,
|
|
249
|
+
registry_path: registryPath,
|
|
250
|
+
install_root: root
|
|
251
|
+
};
|
|
252
|
+
}
|
|
136
253
|
return {
|
|
137
254
|
installed: !!releaseRoot,
|
|
138
|
-
board_id:
|
|
255
|
+
board_id: registryBoardId,
|
|
139
256
|
version: typeof registry.version === "string" ? registry.version : undefined,
|
|
257
|
+
mode: typeof registry.mode === "string" ? registry.mode : undefined,
|
|
140
258
|
release_root: releaseRoot,
|
|
141
259
|
registry_path: registryPath,
|
|
142
260
|
install_root: root,
|
|
@@ -153,48 +271,248 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
|
|
|
153
271
|
};
|
|
154
272
|
}
|
|
155
273
|
}
|
|
274
|
+
async function discoverInstalledLocalToolchains(installRoot, current) {
|
|
275
|
+
const installed = new Map();
|
|
276
|
+
if (current.installed && current.release_root) {
|
|
277
|
+
installed.set(normalizeBoardId(current.board_id), {
|
|
278
|
+
board_id: current.board_id,
|
|
279
|
+
version: current.version,
|
|
280
|
+
channel: current.channel,
|
|
281
|
+
mode: current.mode,
|
|
282
|
+
release_root: current.release_root
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const toolchainsRoot = join(installRoot, "toolchains");
|
|
286
|
+
let boardEntries;
|
|
287
|
+
try {
|
|
288
|
+
boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return installed;
|
|
292
|
+
}
|
|
293
|
+
for (const boardEntry of boardEntries) {
|
|
294
|
+
if (!boardEntry.isDirectory()) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const boardId = normalizeBoardId(boardEntry.name);
|
|
298
|
+
if (installed.has(boardId)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const boardRoot = join(toolchainsRoot, boardEntry.name);
|
|
302
|
+
let versionEntries;
|
|
303
|
+
try {
|
|
304
|
+
versionEntries = await readdir(boardRoot, { withFileTypes: true });
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const versions = versionEntries
|
|
310
|
+
.filter((entry) => entry.isDirectory())
|
|
311
|
+
.map((entry) => entry.name)
|
|
312
|
+
.sort(compareVersionLike)
|
|
313
|
+
.reverse();
|
|
314
|
+
const version = versions[0];
|
|
315
|
+
if (!version) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
installed.set(boardId, {
|
|
319
|
+
board_id: boardId,
|
|
320
|
+
version,
|
|
321
|
+
release_root: join(boardRoot, version)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return installed;
|
|
325
|
+
}
|
|
326
|
+
export async function listLocalToolchainEnvironments(options = {}) {
|
|
327
|
+
const channelName = options.channel ?? DEFAULT_CHANNEL;
|
|
328
|
+
const host = localToolchainHostId();
|
|
329
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
330
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
331
|
+
const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
|
|
332
|
+
const current = await currentLocalToolchain(installRoot);
|
|
333
|
+
const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
|
|
334
|
+
const boardManifests = [...manifests.values()]
|
|
335
|
+
.filter((manifest) => manifest.kind === "board")
|
|
336
|
+
.filter((manifest) => {
|
|
337
|
+
if (!options.boardId) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
const normalizedFilter = normalizeBoardId(options.boardId);
|
|
341
|
+
return boardIdForPackageManifest(manifest) === normalizedFilter
|
|
342
|
+
|| manifest.id === options.boardId
|
|
343
|
+
|| manifest.id === packageIdForBoardFilter(options.boardId);
|
|
344
|
+
})
|
|
345
|
+
.sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
|
|
346
|
+
const environments = [];
|
|
347
|
+
const wslStatus = host.startsWith("win32-")
|
|
348
|
+
? await windowsWslStatus()
|
|
349
|
+
: undefined;
|
|
350
|
+
for (const board of boardManifests) {
|
|
351
|
+
const boardId = boardIdForPackageManifest(board);
|
|
352
|
+
const packages = resolvePackageRefs(board.id, channel, manifests);
|
|
353
|
+
const hostSupport = packageHostSupport(packages, manifests, host);
|
|
354
|
+
let download;
|
|
355
|
+
let downloadError;
|
|
356
|
+
try {
|
|
357
|
+
download = await resolveLocalToolchainDownloadPlan({
|
|
358
|
+
boardId,
|
|
359
|
+
channel: channelName,
|
|
360
|
+
host,
|
|
361
|
+
toolchain: "llvm"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
downloadError = error instanceof Error ? error.message : String(error);
|
|
366
|
+
}
|
|
367
|
+
if (!download && isNativeWindowsTaishanPiHost(boardId, host)) {
|
|
368
|
+
downloadError = taishanPiWindowsRequirementMessage(host);
|
|
369
|
+
}
|
|
370
|
+
const latestVersion = download?.version ?? board.version;
|
|
371
|
+
const currentForBoard = await currentLocalToolchain(installRoot, boardId);
|
|
372
|
+
const installedCandidate = currentForBoard.installed
|
|
373
|
+
? currentForBoard
|
|
374
|
+
: installedByBoard.get(normalizeBoardId(boardId));
|
|
375
|
+
const installed = installedCandidate
|
|
376
|
+
? {
|
|
377
|
+
version: installedCandidate.version,
|
|
378
|
+
channel: installedCandidate.channel,
|
|
379
|
+
mode: installedCandidate.mode,
|
|
380
|
+
release_root: installedCandidate.release_root
|
|
381
|
+
}
|
|
382
|
+
: undefined;
|
|
383
|
+
const updateAvailable = !!installed?.version && installed.version !== latestVersion;
|
|
384
|
+
const nativeWindowsTaishanPi = isNativeWindowsTaishanPiHost(boardId, host);
|
|
385
|
+
const execution = nativeWindowsTaishanPi
|
|
386
|
+
? taishanPiWindowsExecutionRoute(host, wslStatus)
|
|
387
|
+
: {
|
|
388
|
+
kind: "native",
|
|
389
|
+
supported: !!download || !!installed || hostSupport.supported,
|
|
390
|
+
actual_host: host
|
|
391
|
+
};
|
|
392
|
+
const effectiveHostSupport = nativeWindowsTaishanPi
|
|
393
|
+
? { supported: false, unsupportedPackages: ["taishanpi-1m-rk3566 native Windows toolchain"] }
|
|
394
|
+
: download || installed
|
|
395
|
+
? { supported: true, unsupportedPackages: [] }
|
|
396
|
+
: hostSupport;
|
|
397
|
+
const status = !effectiveHostSupport.supported
|
|
398
|
+
? "unsupported_host"
|
|
399
|
+
: updateAvailable
|
|
400
|
+
? "update_available"
|
|
401
|
+
: installed
|
|
402
|
+
? "installed"
|
|
403
|
+
: "available";
|
|
404
|
+
const mode = download?.default_mode ?? "qt";
|
|
405
|
+
const installModes = download
|
|
406
|
+
? localToolchainInstallModesForDownload(download)
|
|
407
|
+
: installed?.mode
|
|
408
|
+
? [installed.mode]
|
|
409
|
+
: localToolchainInstallModesForDownload(download);
|
|
410
|
+
environments.push({
|
|
411
|
+
board_id: boardId,
|
|
412
|
+
package_id: board.id,
|
|
413
|
+
display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
|
|
414
|
+
family: board.family,
|
|
415
|
+
variant: board.variant,
|
|
416
|
+
channel: channelName,
|
|
417
|
+
host,
|
|
418
|
+
status,
|
|
419
|
+
supported_host: effectiveHostSupport.supported,
|
|
420
|
+
unsupported_packages: effectiveHostSupport.unsupportedPackages,
|
|
421
|
+
install_modes: installModes,
|
|
422
|
+
installed,
|
|
423
|
+
latest: {
|
|
424
|
+
version: latestVersion,
|
|
425
|
+
default_mode: download?.default_mode,
|
|
426
|
+
source_url: download?.source_url,
|
|
427
|
+
manifest_url: download?.manifest_url,
|
|
428
|
+
component_count: download?.components?.length,
|
|
429
|
+
download_error: downloadError
|
|
430
|
+
},
|
|
431
|
+
packages,
|
|
432
|
+
components: download?.components?.map(localToolchainEnvironmentComponent),
|
|
433
|
+
execution,
|
|
434
|
+
install_command: nativeWindowsTaishanPi ? "embedlabs local wsl status" : `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode}`,
|
|
435
|
+
update_command: nativeWindowsTaishanPi ? "embedlabs local wsl status" : `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode} --force`,
|
|
436
|
+
notes: environmentNotes({ boardId, host, status, downloadError, unsupportedPackages: effectiveHostSupport.unsupportedPackages })
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const filteredEnvironments = options.installedOnly
|
|
440
|
+
? environments.filter((environment) => !!environment.installed)
|
|
441
|
+
: environments;
|
|
442
|
+
return {
|
|
443
|
+
host,
|
|
444
|
+
channel: channelName,
|
|
445
|
+
metadata_source: metadataRoot ? "local_override" : "built_in",
|
|
446
|
+
metadata_root: metadataRoot,
|
|
447
|
+
install_root: installRoot,
|
|
448
|
+
registry_path: registryPath,
|
|
449
|
+
environments: filteredEnvironments
|
|
450
|
+
};
|
|
451
|
+
}
|
|
156
452
|
export async function installLocalToolchain(options = {}) {
|
|
157
453
|
const latest = await latestLocalToolchain(options);
|
|
454
|
+
if (isNativeWindowsTaishanPiHost(latest.board_id, latest.host)) {
|
|
455
|
+
throw new Error(taishanPiWindowsRequirementMessage(latest.host));
|
|
456
|
+
}
|
|
158
457
|
const installRoot = resolveInstallRoot(options.installRoot);
|
|
159
458
|
const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
|
|
459
|
+
const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
|
|
160
460
|
if (await pathExists(releaseRoot) && !options.force) {
|
|
161
|
-
|
|
461
|
+
await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
|
|
462
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
|
|
162
463
|
if (!validation.ok) {
|
|
163
|
-
|
|
464
|
+
if (latest.download?.components?.length) {
|
|
465
|
+
// Component installs can upgrade an existing lower-mode install by overlaying
|
|
466
|
+
// only the newly selected components instead of deleting the whole tree.
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
|
|
474
|
+
return {
|
|
475
|
+
board_id: latest.board_id,
|
|
476
|
+
version: latest.version,
|
|
477
|
+
channel: latest.channel,
|
|
478
|
+
host: latest.host,
|
|
479
|
+
mode: installMode,
|
|
480
|
+
install_root: installRoot,
|
|
481
|
+
release_root: releaseRoot,
|
|
482
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
483
|
+
source: { kind: "directory", value: releaseRoot },
|
|
484
|
+
installed_paths: [],
|
|
485
|
+
packages: latest.packages,
|
|
486
|
+
validation
|
|
487
|
+
};
|
|
164
488
|
}
|
|
165
|
-
await writeCurrentRegistry(installRoot, latest, releaseRoot);
|
|
166
|
-
return {
|
|
167
|
-
board_id: latest.board_id,
|
|
168
|
-
version: latest.version,
|
|
169
|
-
channel: latest.channel,
|
|
170
|
-
host: latest.host,
|
|
171
|
-
install_root: installRoot,
|
|
172
|
-
release_root: releaseRoot,
|
|
173
|
-
registry_path: localToolchainRegistryPath(installRoot),
|
|
174
|
-
source: { kind: "directory", value: releaseRoot },
|
|
175
|
-
installed_paths: [],
|
|
176
|
-
packages: latest.packages,
|
|
177
|
-
validation
|
|
178
|
-
};
|
|
179
489
|
}
|
|
180
490
|
const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
|
|
181
491
|
try {
|
|
182
492
|
const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
|
|
183
|
-
await
|
|
493
|
+
if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
|
|
494
|
+
await rm(releaseRoot, { recursive: true, force: true });
|
|
495
|
+
}
|
|
184
496
|
await mkdir(releaseRoot, { recursive: true });
|
|
185
497
|
const installedPaths = [];
|
|
186
|
-
for (const relativePath of
|
|
187
|
-
const sourcePath =
|
|
498
|
+
for (const relativePath of installCopyPathsForBoard(latest.board_id)) {
|
|
499
|
+
const sourcePath = await resolveInstallSourcePathForBoard(sourceRoot.path, relativePath, latest.board_id);
|
|
188
500
|
if (!await pathExists(sourcePath)) {
|
|
189
501
|
continue;
|
|
190
502
|
}
|
|
191
503
|
const targetPath = resolve(releaseRoot, relativePath);
|
|
192
504
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
193
|
-
await cp(sourcePath, targetPath, {
|
|
505
|
+
await cp(sourcePath, targetPath, {
|
|
506
|
+
recursive: true,
|
|
507
|
+
force: true,
|
|
508
|
+
preserveTimestamps: true,
|
|
509
|
+
verbatimSymlinks: true
|
|
510
|
+
});
|
|
194
511
|
installedPaths.push(relativePath);
|
|
195
512
|
}
|
|
196
|
-
await
|
|
197
|
-
|
|
513
|
+
await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
|
|
514
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
|
|
515
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
|
|
198
516
|
if (!validation.ok) {
|
|
199
517
|
throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
|
|
200
518
|
}
|
|
@@ -203,6 +521,7 @@ export async function installLocalToolchain(options = {}) {
|
|
|
203
521
|
version: latest.version,
|
|
204
522
|
channel: latest.channel,
|
|
205
523
|
host: latest.host,
|
|
524
|
+
mode: installMode,
|
|
206
525
|
install_root: installRoot,
|
|
207
526
|
release_root: releaseRoot,
|
|
208
527
|
registry_path: localToolchainRegistryPath(installRoot),
|
|
@@ -216,19 +535,183 @@ export async function installLocalToolchain(options = {}) {
|
|
|
216
535
|
await rm(tempDir, { recursive: true, force: true });
|
|
217
536
|
}
|
|
218
537
|
}
|
|
219
|
-
export async function
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
[
|
|
229
|
-
[
|
|
230
|
-
|
|
231
|
-
|
|
538
|
+
export async function windowsWslStatus() {
|
|
539
|
+
const result = {
|
|
540
|
+
host: hostId(),
|
|
541
|
+
platform: platform(),
|
|
542
|
+
arch: arch(),
|
|
543
|
+
checked_at: new Date().toISOString(),
|
|
544
|
+
applicable: platform() === "win32",
|
|
545
|
+
wsl_available: false,
|
|
546
|
+
usable: false,
|
|
547
|
+
distributions: [],
|
|
548
|
+
online_distributions: [],
|
|
549
|
+
taishanpi_execution: {
|
|
550
|
+
supported: false,
|
|
551
|
+
required_host: "linux-x86_64",
|
|
552
|
+
windows_host: hostId(),
|
|
553
|
+
route: "wsl2",
|
|
554
|
+
status: platform() === "win32" ? "wsl_missing" : "wsl_not_applicable",
|
|
555
|
+
install_command: "embedlabs local wsl status",
|
|
556
|
+
reason: platform() === "win32"
|
|
557
|
+
? "WSL status has not been checked yet."
|
|
558
|
+
: "WSL2 is only applicable on Windows hosts."
|
|
559
|
+
},
|
|
560
|
+
commands: {
|
|
561
|
+
status: "wsl.exe --status",
|
|
562
|
+
list: "wsl.exe -l -v",
|
|
563
|
+
list_online: "wsl.exe --list --online",
|
|
564
|
+
install_ubuntu: "wsl.exe --install -d Ubuntu --web-download"
|
|
565
|
+
},
|
|
566
|
+
notes: []
|
|
567
|
+
};
|
|
568
|
+
if (!result.applicable) {
|
|
569
|
+
result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
|
|
570
|
+
result.notes.push("WSL2 is only required for TaishanPi local development on Windows hosts.");
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
const whereWsl = await runCommand(["cmd", "/c", "where wsl.exe"], homedir());
|
|
574
|
+
result.wsl_available = whereWsl.exit_code === 0;
|
|
575
|
+
if (!result.wsl_available) {
|
|
576
|
+
result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
|
|
577
|
+
result.notes.push("wsl.exe was not found. Enable Windows Subsystem for Linux before installing TaishanPi local tools.");
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
const list = await runCommand(["wsl.exe", "-l", "-v"], homedir());
|
|
581
|
+
const normalized = normalizeWindowsCommandText([...list.stdout_tail, ...list.stderr_tail].join("\n"));
|
|
582
|
+
result.distributions = list.exit_code === 0 ? parseWslDistributionList(normalized) : [];
|
|
583
|
+
const online = await runCommand(["wsl.exe", "--list", "--online"], homedir());
|
|
584
|
+
const normalizedOnline = normalizeWindowsCommandText([...online.stdout_tail, ...online.stderr_tail].join("\n"));
|
|
585
|
+
result.online_distributions = online.exit_code === 0 ? parseWslOnlineDistributionList(normalizedOnline) : [];
|
|
586
|
+
result.usable = list.exit_code === 0 && result.distributions.length > 0;
|
|
587
|
+
result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
|
|
588
|
+
if (!result.usable) {
|
|
589
|
+
result.notes.push("No usable WSL2 distribution is configured yet. Install Ubuntu, restart Windows if requested, then run Embed Labs TaishanPi tools inside WSL2.");
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
result.notes.push("Run TaishanPi local compile, Qt, and image tooling inside the listed WSL2 distribution. Native Windows TaishanPi toolchain packages are intentionally not published.");
|
|
593
|
+
}
|
|
594
|
+
if (result.host === "win32-arm64") {
|
|
595
|
+
result.notes.push("This Windows ARM64 host maps WSL2 to linux-arm64. TaishanPi local compile/Qt/image parity still requires a published linux-arm64/native Windows package, or a separate Windows x64 + WSL2/linux-x86_64 package and validation.");
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
export async function windowsWslInstall(options = {}) {
|
|
600
|
+
const distro = options.distribution?.trim() || "Ubuntu";
|
|
601
|
+
const command = ["wsl.exe", "--install", "-d", distro];
|
|
602
|
+
if (options.noLaunch !== false) {
|
|
603
|
+
command.push("--no-launch");
|
|
604
|
+
}
|
|
605
|
+
if (options.webDownload !== false) {
|
|
606
|
+
command.push("--web-download");
|
|
607
|
+
}
|
|
608
|
+
const notes = [];
|
|
609
|
+
if (platform() !== "win32") {
|
|
610
|
+
notes.push("WSL installation can only run on Windows hosts.");
|
|
611
|
+
return {
|
|
612
|
+
host: hostId(),
|
|
613
|
+
platform: platform(),
|
|
614
|
+
arch: arch(),
|
|
615
|
+
command,
|
|
616
|
+
exit_code: 1,
|
|
617
|
+
stdout_tail: [],
|
|
618
|
+
stderr_tail: [],
|
|
619
|
+
status_after: await windowsWslStatus(),
|
|
620
|
+
notes
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const result = await runCommandWithTimeout(command, homedir(), options.timeoutMs ?? 600_000);
|
|
624
|
+
const statusAfter = await windowsWslStatus();
|
|
625
|
+
if (result.exit_code === 0) {
|
|
626
|
+
notes.push("WSL install command completed. If Windows requests a restart, restart before running TaishanPi local tools.");
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
notes.push("WSL install command did not complete successfully. Run the same command in an elevated Windows terminal if the error indicates permissions or reboot requirements.");
|
|
630
|
+
}
|
|
631
|
+
if (!statusAfter.usable) {
|
|
632
|
+
notes.push("WSL2 is still not usable after this command. Complete first-launch setup for the distribution, then rerun embedlabs local wsl status.");
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
host: hostId(),
|
|
636
|
+
platform: platform(),
|
|
637
|
+
arch: arch(),
|
|
638
|
+
command,
|
|
639
|
+
exit_code: result.exit_code,
|
|
640
|
+
stdout_tail: result.stdout_tail,
|
|
641
|
+
stderr_tail: result.stderr_tail,
|
|
642
|
+
status_after: statusAfter,
|
|
643
|
+
notes
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
export async function uninstallLocalToolchain(options = {}) {
|
|
647
|
+
if (!options.boardId?.trim()) {
|
|
648
|
+
throw new Error("board_id is required for local toolchain uninstall.");
|
|
649
|
+
}
|
|
650
|
+
const boardId = normalizeBoardId(options.boardId);
|
|
651
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
652
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
653
|
+
const boardRoot = resolve(installRoot, "toolchains", boardId);
|
|
654
|
+
const removedPaths = [];
|
|
655
|
+
if (await pathExists(boardRoot)) {
|
|
656
|
+
await rm(boardRoot, { recursive: true, force: true });
|
|
657
|
+
removedPaths.push(boardRoot);
|
|
658
|
+
}
|
|
659
|
+
let removedRegistryEntry = false;
|
|
660
|
+
let existing = {};
|
|
661
|
+
try {
|
|
662
|
+
existing = JSON.parse(await readFile(registryPath, "utf8"));
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
existing = {};
|
|
666
|
+
}
|
|
667
|
+
const environments = isRecord(existing.environments)
|
|
668
|
+
? { ...existing.environments }
|
|
669
|
+
: {};
|
|
670
|
+
if (Object.prototype.hasOwnProperty.call(environments, boardId)) {
|
|
671
|
+
delete environments[boardId];
|
|
672
|
+
removedRegistryEntry = true;
|
|
673
|
+
}
|
|
674
|
+
const topLevelBoard = normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID));
|
|
675
|
+
const topLevelPointsToBoard = topLevelBoard === boardId && typeof existing.release_root === "string";
|
|
676
|
+
const cleaned = { ...existing, environments, updated_at: new Date().toISOString() };
|
|
677
|
+
if (topLevelPointsToBoard) {
|
|
678
|
+
for (const key of [
|
|
679
|
+
"installed",
|
|
680
|
+
"board_id",
|
|
681
|
+
"version",
|
|
682
|
+
"channel",
|
|
683
|
+
"host",
|
|
684
|
+
"mode",
|
|
685
|
+
"release_root",
|
|
686
|
+
"packages",
|
|
687
|
+
"source",
|
|
688
|
+
"installed_components"
|
|
689
|
+
]) {
|
|
690
|
+
delete cleaned[key];
|
|
691
|
+
}
|
|
692
|
+
removedRegistryEntry = true;
|
|
693
|
+
}
|
|
694
|
+
if (removedRegistryEntry || Object.keys(environments).length > 0 || Object.keys(existing).length > 0) {
|
|
695
|
+
await mkdir(dirname(registryPath), { recursive: true });
|
|
696
|
+
await writeFile(registryPath, `${JSON.stringify(cleaned, null, 2)}\n`, "utf8");
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
board_id: boardId,
|
|
700
|
+
install_root: installRoot,
|
|
701
|
+
registry_path: registryPath,
|
|
702
|
+
removed: removedPaths.length > 0 || removedRegistryEntry,
|
|
703
|
+
removed_paths: removedPaths,
|
|
704
|
+
removed_registry_entry: removedRegistryEntry,
|
|
705
|
+
remaining_installed_boards: Object.keys(environments).sort(),
|
|
706
|
+
observed_at: new Date().toISOString()
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
export async function validateLocalToolchain(input) {
|
|
710
|
+
const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
|
|
711
|
+
const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
|
|
712
|
+
const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
|
|
713
|
+
const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot, boardId);
|
|
714
|
+
const required = requiredLocalToolchainChecks(mode, boardId);
|
|
232
715
|
const checked_paths = [];
|
|
233
716
|
for (const [label, relativePath] of required) {
|
|
234
717
|
const absolutePath = resolve(resolvedRoot, relativePath);
|
|
@@ -239,22 +722,353 @@ export async function validateLocalToolchain(releaseRoot) {
|
|
|
239
722
|
});
|
|
240
723
|
}
|
|
241
724
|
const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
|
|
725
|
+
const path_leaks = await localToolchainPathLeaks(resolvedRoot, boardId);
|
|
726
|
+
const missing_groups = localToolchainMissingGroups(checked_paths, path_leaks);
|
|
727
|
+
const repair_command = missing_paths.length > 0 || path_leaks.length > 0
|
|
728
|
+
? `embedlabs local toolchain install --board ${boardId} --mode ${mode}`
|
|
729
|
+
: undefined;
|
|
242
730
|
return {
|
|
243
|
-
ok: missing_paths.length === 0,
|
|
731
|
+
ok: missing_paths.length === 0 && path_leaks.length === 0,
|
|
732
|
+
mode,
|
|
244
733
|
host: {
|
|
245
734
|
platform: platform(),
|
|
246
735
|
arch: arch()
|
|
247
736
|
},
|
|
248
|
-
board_id:
|
|
737
|
+
board_id: boardId,
|
|
249
738
|
release_root: resolvedRoot,
|
|
250
739
|
checked_paths,
|
|
251
740
|
missing_paths,
|
|
741
|
+
path_leaks,
|
|
742
|
+
missing_groups,
|
|
743
|
+
repair_command,
|
|
744
|
+
summary_for_user: localToolchainValidationSummary({
|
|
745
|
+
boardId,
|
|
746
|
+
mode,
|
|
747
|
+
ok: missing_paths.length === 0,
|
|
748
|
+
missingGroups: missing_groups,
|
|
749
|
+
repairCommand: repair_command
|
|
750
|
+
}),
|
|
252
751
|
notes: [
|
|
253
752
|
"Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
|
|
254
|
-
|
|
753
|
+
`This validator checks the ${boardId} local support layout for install mode ${mode}.`
|
|
255
754
|
]
|
|
256
755
|
};
|
|
257
756
|
}
|
|
757
|
+
function localToolchainMissingGroups(checkedPaths, pathLeaks = []) {
|
|
758
|
+
const groups = new Set();
|
|
759
|
+
if (pathLeaks.length > 0) {
|
|
760
|
+
groups.add("portable-paths/安装包可移植路径");
|
|
761
|
+
}
|
|
762
|
+
for (const check of checkedPaths) {
|
|
763
|
+
if (check.exists) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const text = `${check.label} ${check.path}`.toLowerCase();
|
|
767
|
+
if (text.includes("qt host") || text.includes("qtquick") || text.includes("qt cmake") || text.includes("qt target")) {
|
|
768
|
+
groups.add("qt/Qt host、target 或实时预览组件");
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (text.includes("compiler") || text.includes("readelf") || text.includes("sysroot") || text.includes("include directory") || text.includes("clang wrapper") || text.includes("gcc libraries")) {
|
|
772
|
+
groups.add("compile/交叉编译器与 sysroot");
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (text.includes("boot") || text.includes("dtb") || text.includes("resource image") || text.includes("metadata") || text.includes("/meta")) {
|
|
776
|
+
groups.add("board-resources/启动资源、DTB 与元数据");
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (text.includes("image") || text.includes("rootfs") || text.includes("parameter")) {
|
|
780
|
+
groups.add("images/基础镜像资源");
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (text.includes("rp2350") || text.includes("pico")) {
|
|
784
|
+
groups.add("rp2350/RP2350 SDK、固件或监控运行时");
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
groups.add("runtime/运行时文件");
|
|
788
|
+
}
|
|
789
|
+
return [...groups];
|
|
790
|
+
}
|
|
791
|
+
async function rewriteLocalToolchainPortablePaths(releaseRoot, boardId) {
|
|
792
|
+
if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const replacements = taishanPiPortablePathReplacements(releaseRoot);
|
|
796
|
+
const files = await taishanPiPortableTextFiles(releaseRoot);
|
|
797
|
+
for (const file of files) {
|
|
798
|
+
let content;
|
|
799
|
+
try {
|
|
800
|
+
content = await readFile(file, "utf8");
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const rewritten = rewritePortablePathContent(content, replacements);
|
|
806
|
+
if (rewritten !== content) {
|
|
807
|
+
await writeFile(file, rewritten, "utf8");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function localToolchainPathLeaks(releaseRoot, boardId) {
|
|
812
|
+
if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID || !await pathExists(releaseRoot)) {
|
|
813
|
+
return [];
|
|
814
|
+
}
|
|
815
|
+
const forbidden = taishanPiForbiddenPortablePathFragments();
|
|
816
|
+
const files = await taishanPiPortableTextFiles(releaseRoot);
|
|
817
|
+
const leaks = [];
|
|
818
|
+
for (const file of files) {
|
|
819
|
+
let content;
|
|
820
|
+
try {
|
|
821
|
+
content = await readFile(file, "utf8");
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
for (const fragment of forbidden) {
|
|
827
|
+
if (content.includes(fragment)) {
|
|
828
|
+
leaks.push({
|
|
829
|
+
label: "portable path check",
|
|
830
|
+
path: file,
|
|
831
|
+
forbidden: fragment
|
|
832
|
+
});
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return leaks;
|
|
838
|
+
}
|
|
839
|
+
async function taishanPiPortableTextFiles(releaseRoot) {
|
|
840
|
+
const candidates = [
|
|
841
|
+
"toolchain/qt6-rk3566-llvm-toolchain.cmake",
|
|
842
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/bin/target_qt.conf",
|
|
843
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/Qt6Dependencies.cmake",
|
|
844
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/qt.toolchain.cmake",
|
|
845
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
|
|
846
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qdevice.pri",
|
|
847
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qmodule.pri",
|
|
848
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_core_private.pri",
|
|
849
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_gui_private.pri",
|
|
850
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport.pri",
|
|
851
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport_private.pri",
|
|
852
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus.pri",
|
|
853
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus_private.pri",
|
|
854
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialPort.pc",
|
|
855
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialBus.pc",
|
|
856
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri",
|
|
857
|
+
"qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
|
|
858
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_lib_serialport.pri",
|
|
859
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_lib_serialport_private.pri",
|
|
860
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_lib_serialbus.pri",
|
|
861
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_lib_serialbus_private.pri",
|
|
862
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri"
|
|
863
|
+
];
|
|
864
|
+
const files = [];
|
|
865
|
+
for (const candidate of candidates) {
|
|
866
|
+
const file = join(releaseRoot, candidate);
|
|
867
|
+
if (await pathExists(file)) {
|
|
868
|
+
files.push(file);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const relativeDir of [
|
|
872
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort",
|
|
873
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus",
|
|
874
|
+
"qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6SerialPort",
|
|
875
|
+
"qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6SerialBus"
|
|
876
|
+
]) {
|
|
877
|
+
const dir = join(releaseRoot, relativeDir);
|
|
878
|
+
if (!await pathExists(dir)) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
882
|
+
if (!entry.isFile()) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const file = join(dir, entry.name);
|
|
886
|
+
if ([".cmake", ".pri", ".pc", ".json"].includes(extname(file))) {
|
|
887
|
+
files.push(file);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return files;
|
|
892
|
+
}
|
|
893
|
+
function taishanPiPortablePathReplacements(releaseRoot) {
|
|
894
|
+
const normalizedRoot = resolve(releaseRoot);
|
|
895
|
+
// Legacy development paths are recognized only so older staged packages can
|
|
896
|
+
// be rewritten into a portable installed layout during validation/repair.
|
|
897
|
+
return [
|
|
898
|
+
["@EMBEDLABS_RELEASE_ROOT@", normalizedRoot],
|
|
899
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/scripts/qt6-rk3566-llvm-toolchain.cmake", join(normalizedRoot, "toolchain", "qt6-rk3566-llvm-toolchain.cmake")],
|
|
900
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/.llvm-cross", join(normalizedRoot, "toolchain", "llvm-cross")],
|
|
901
|
+
["/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal", normalizedRoot],
|
|
902
|
+
["/Volumes/LLVM-TSPI/sdk-tools/buildroot/output/rockchip_rk3566/host", join(normalizedRoot, "toolchain", "host")],
|
|
903
|
+
["/Volumes/LLVM-TSPI/qt6-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3")],
|
|
904
|
+
["/Volumes/LLVM-TSPI/qt6-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3")],
|
|
905
|
+
["/Volumes/LLVM-TSPI/qt-build-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3", ".build-info", "qt-build-host-macos-6.8.3")],
|
|
906
|
+
["/Volumes/LLVM-TSPI/qt-build-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", ".build-info", "qt-build-rk3566-llvm-6.8.3")],
|
|
907
|
+
["/Volumes/LLVM-TSPI/qt-everywhere-src-6.8.3", join(normalizedRoot, "meta", "source", "qt-everywhere-src-6.8.3")],
|
|
908
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi", join(normalizedRoot, "meta", "source", "llvm-build-tspi")]
|
|
909
|
+
];
|
|
910
|
+
}
|
|
911
|
+
function taishanPiForbiddenPortablePathFragments() {
|
|
912
|
+
return [
|
|
913
|
+
"/Volumes/LLVM-TSPI",
|
|
914
|
+
"/Users/kvell/kk-project/DBT-Agent-Project"
|
|
915
|
+
];
|
|
916
|
+
}
|
|
917
|
+
function rewritePortablePathContent(content, replacements) {
|
|
918
|
+
let rewritten = content;
|
|
919
|
+
for (const [from, to] of replacements) {
|
|
920
|
+
rewritten = rewritten.split(from).join(to);
|
|
921
|
+
}
|
|
922
|
+
return rewritten;
|
|
923
|
+
}
|
|
924
|
+
function localToolchainValidationSummary(input) {
|
|
925
|
+
const boardName = input.boardId === DEFAULT_BOARD_ID
|
|
926
|
+
? "泰山派 1M-RK3566"
|
|
927
|
+
: input.boardId;
|
|
928
|
+
if (input.ok) {
|
|
929
|
+
if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
|
|
930
|
+
return `${boardName} Qt 本地开发环境完整,可以继续 QtQuick 原型、一键交叉编译和真机部署。`;
|
|
931
|
+
}
|
|
932
|
+
return `${boardName} 本地开发环境完整,可以继续使用 ${input.mode} 模式。`;
|
|
933
|
+
}
|
|
934
|
+
const groups = input.missingGroups.length > 0 ? input.missingGroups.join("、") : "部分文件";
|
|
935
|
+
if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
|
|
936
|
+
return `${boardName} Qt 本地开发环境不完整,缺少 ${groups}。这会影响一键交叉编译、实时预览或真机部署;请先执行 ${input.repairCommand} 补装缺失组件。`;
|
|
937
|
+
}
|
|
938
|
+
return `${boardName} 本地开发环境不完整,缺少 ${groups};请先执行 ${input.repairCommand} 补装缺失组件。`;
|
|
939
|
+
}
|
|
940
|
+
function normalizeLocalToolchainInstallMode(mode) {
|
|
941
|
+
const normalized = mode?.trim();
|
|
942
|
+
if (!normalized) {
|
|
943
|
+
return "qt";
|
|
944
|
+
}
|
|
945
|
+
if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
|
|
946
|
+
return normalized;
|
|
947
|
+
}
|
|
948
|
+
throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
|
|
949
|
+
}
|
|
950
|
+
function requiredLocalToolchainChecks(mode, boardId) {
|
|
951
|
+
if (isRp2350MonitorBoardId(boardId)) {
|
|
952
|
+
return requiredRp2350MonitorChecks(mode);
|
|
953
|
+
}
|
|
954
|
+
const base = [
|
|
955
|
+
["release root", "."],
|
|
956
|
+
["boot resource image", "boot-workspace/out/resource.img"],
|
|
957
|
+
["boot image", "boot-workspace/out/boot.img"],
|
|
958
|
+
["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
|
|
959
|
+
["package metadata", "meta"]
|
|
960
|
+
];
|
|
961
|
+
const rockchipTools = [
|
|
962
|
+
["Rockchip mkimage", "tools/mac/mkimage"],
|
|
963
|
+
["Rockchip dumpimage", "tools/mac/dumpimage"],
|
|
964
|
+
["Rockchip resource_tool", "tools/mac/resource_tool"],
|
|
965
|
+
["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"]
|
|
966
|
+
];
|
|
967
|
+
const compile = [
|
|
968
|
+
["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
|
|
969
|
+
["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
|
|
970
|
+
["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
|
|
971
|
+
["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
|
|
972
|
+
["host GCC libraries", "toolchain/host/lib/gcc"],
|
|
973
|
+
["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
|
|
974
|
+
["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
|
|
975
|
+
];
|
|
976
|
+
const qt = [
|
|
977
|
+
["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
|
|
978
|
+
["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
|
|
979
|
+
["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
|
|
980
|
+
["Qt host SerialPort CMake package", "qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6SerialPort/Qt6SerialPortConfig.cmake"],
|
|
981
|
+
["Qt host SerialBus CMake package", "qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6SerialBus/Qt6SerialBusConfig.cmake"],
|
|
982
|
+
["Qt target SerialPort library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialPort.so.6"],
|
|
983
|
+
["Qt target SerialPort CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort/Qt6SerialPortConfig.cmake"],
|
|
984
|
+
["Qt target SerialBus library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialBus.so.6"],
|
|
985
|
+
["Qt target SerialBus CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus/Qt6SerialBusConfig.cmake"],
|
|
986
|
+
["Qt board runtime SerialPort library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialPort.so.6"],
|
|
987
|
+
["Qt board runtime SerialBus library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialBus.so.6"],
|
|
988
|
+
["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
|
|
989
|
+
];
|
|
990
|
+
const images = [
|
|
991
|
+
["base boot image", "images/current/boot.img"],
|
|
992
|
+
["base rootfs image", "images/current/rootfs.img"],
|
|
993
|
+
["base image parameter", "images/current/parameter.txt"]
|
|
994
|
+
];
|
|
995
|
+
const full = [
|
|
996
|
+
["rootfs overlay", "userdata/rootfs"]
|
|
997
|
+
];
|
|
998
|
+
if (mode === "minimal") {
|
|
999
|
+
return [...base, ...rockchipTools];
|
|
1000
|
+
}
|
|
1001
|
+
if (mode === "compile") {
|
|
1002
|
+
return [...base, ...compile];
|
|
1003
|
+
}
|
|
1004
|
+
if (mode === "qt") {
|
|
1005
|
+
return [...base, ...compile, ...qt];
|
|
1006
|
+
}
|
|
1007
|
+
if (mode === "images") {
|
|
1008
|
+
return [...base, ...rockchipTools, ...images];
|
|
1009
|
+
}
|
|
1010
|
+
return [...base, ...rockchipTools, ...compile, ...qt, ...images, ...full];
|
|
1011
|
+
}
|
|
1012
|
+
function requiredRp2350MonitorChecks(mode) {
|
|
1013
|
+
const armToolchainDir = rp2350ArmToolchainDirName();
|
|
1014
|
+
const executableSuffix = platform() === "win32" ? ".exe" : "";
|
|
1015
|
+
const monitorLauncher = platform() === "win32"
|
|
1016
|
+
? "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer.cmd"
|
|
1017
|
+
: "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer";
|
|
1018
|
+
const base = [
|
|
1019
|
+
["release root", "."],
|
|
1020
|
+
];
|
|
1021
|
+
const runtime = [
|
|
1022
|
+
["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
|
|
1023
|
+
["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
|
|
1024
|
+
["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
|
|
1025
|
+
["RP2350 picotool", `toolkit-runtime/rp2350-monitor/tools/picotool${executableSuffix}`],
|
|
1026
|
+
["RP2350 Monitor logic analyzer", monitorLauncher],
|
|
1027
|
+
["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
|
|
1028
|
+
["package metadata", "meta"]
|
|
1029
|
+
];
|
|
1030
|
+
if (platform() === "win32") {
|
|
1031
|
+
runtime.splice(4, 0, ["RP2350 Monitor bundled Python", "toolkit-runtime/rp2350-monitor/tools/python/python.exe"]);
|
|
1032
|
+
runtime.splice(5, 0, ["RP2350 Monitor pyserial", "toolkit-runtime/rp2350-monitor/tools/python/Lib/site-packages/serial/__init__.py"]);
|
|
1033
|
+
}
|
|
1034
|
+
const firmware = [
|
|
1035
|
+
["RP2350 Monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
|
|
1036
|
+
["RP2350 Monitor firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
|
|
1037
|
+
];
|
|
1038
|
+
const initialFirmware = [
|
|
1039
|
+
["Pico 2 W initialization UF2", "rp2350-initial-firmware/pico2w/initial.uf2"],
|
|
1040
|
+
["ColorEasyPICO2 initialization UF2", "rp2350-initial-firmware/coloreasypico2/initial.uf2"]
|
|
1041
|
+
];
|
|
1042
|
+
const compile = [
|
|
1043
|
+
["Pico SDK", "rp2350-sdk/pico-sdk/pico_sdk_init.cmake"],
|
|
1044
|
+
["Pico SDK CMakeLists", "rp2350-sdk/pico-sdk/CMakeLists.txt"],
|
|
1045
|
+
["ARM bare-metal C compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-gcc${executableSuffix}`],
|
|
1046
|
+
["ARM bare-metal C++ compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-g++${executableSuffix}`],
|
|
1047
|
+
["picotool", `rp2350-sdk/picotool/install/picotool/picotool${executableSuffix}`]
|
|
1048
|
+
];
|
|
1049
|
+
if (mode === "compile") {
|
|
1050
|
+
return [...base, ...compile, ...initialFirmware];
|
|
1051
|
+
}
|
|
1052
|
+
if (mode === "firmware") {
|
|
1053
|
+
return [...base, ...runtime, ...firmware, ...initialFirmware];
|
|
1054
|
+
}
|
|
1055
|
+
if (mode === "full") {
|
|
1056
|
+
return [...base, ...runtime, ...firmware, ...initialFirmware, ...compile];
|
|
1057
|
+
}
|
|
1058
|
+
return [...base, ...runtime];
|
|
1059
|
+
}
|
|
1060
|
+
function rp2350ArmToolchainDirName() {
|
|
1061
|
+
if (platform() === "win32") {
|
|
1062
|
+
return "arm-gnu-toolchain-15.2.rel1-mingw-w64-x86_64-arm-none-eabi";
|
|
1063
|
+
}
|
|
1064
|
+
if (platform() === "linux" && arch() === "x64") {
|
|
1065
|
+
return "arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi";
|
|
1066
|
+
}
|
|
1067
|
+
if (platform() === "linux" && arch() === "arm64") {
|
|
1068
|
+
return "arm-gnu-toolchain-15.2.rel1-aarch64-arm-none-eabi";
|
|
1069
|
+
}
|
|
1070
|
+
return "arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi";
|
|
1071
|
+
}
|
|
258
1072
|
export async function compileTaishanPiSingleFile(options) {
|
|
259
1073
|
assertAuthenticated(options.auth);
|
|
260
1074
|
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
@@ -285,7 +1099,7 @@ export async function compileTaishanPiSingleFile(options) {
|
|
|
285
1099
|
export async function buildTaishanPiQtSmoke(options) {
|
|
286
1100
|
assertAuthenticated(options.auth);
|
|
287
1101
|
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
288
|
-
const sourceDir = resolve(options.sourceDir ??
|
|
1102
|
+
const sourceDir = resolve(options.sourceDir ?? join(releaseRoot, "examples", "qt-smoke"));
|
|
289
1103
|
const buildDir = resolve(options.buildDir);
|
|
290
1104
|
const targetName = options.targetName ?? "qt_llvm_smoke";
|
|
291
1105
|
const qtCmake = join(releaseRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", "bin", "qt-cmake");
|
|
@@ -324,15 +1138,14 @@ export async function buildTaishanPiQtSmoke(options) {
|
|
|
324
1138
|
}
|
|
325
1139
|
async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
326
1140
|
const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
|
|
327
|
-
|
|
328
|
-
if (!candidateRoot) {
|
|
1141
|
+
if (!explicitRoot) {
|
|
329
1142
|
return {
|
|
330
1143
|
channel: BUILT_IN_CHANNEL,
|
|
331
1144
|
manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
|
|
332
1145
|
metadataRoot: undefined
|
|
333
1146
|
};
|
|
334
1147
|
}
|
|
335
|
-
const root = resolve(
|
|
1148
|
+
const root = resolve(explicitRoot);
|
|
336
1149
|
const channelPath = join(root, "channels", channelName, "index.json");
|
|
337
1150
|
const channel = JSON.parse(await readFile(channelPath, "utf8"));
|
|
338
1151
|
if (channel.schema !== "embedlabs.channel.v1") {
|
|
@@ -355,17 +1168,26 @@ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
|
355
1168
|
return { channel, manifests, metadataRoot: root };
|
|
356
1169
|
}
|
|
357
1170
|
async function resolveLocalToolchainDownloadPlan(input) {
|
|
1171
|
+
const requestedBoardId = normalizeBoardId(input.boardId);
|
|
1172
|
+
const compatibleBoardIds = compatibleDownloadBoardIds(requestedBoardId);
|
|
358
1173
|
const channelUrl = downloadChannelUrl(input.channel);
|
|
359
1174
|
const channel = await fetchJson(channelUrl);
|
|
360
1175
|
if (channel.schema !== "embedlabs.download-channel.v1") {
|
|
361
1176
|
throw new Error(`Unexpected download channel schema ${channel.schema}.`);
|
|
362
1177
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1178
|
+
const entries = channel.packages ?? [];
|
|
1179
|
+
let entry;
|
|
1180
|
+
for (const boardId of compatibleBoardIds) {
|
|
1181
|
+
entry = entries.find((item) => {
|
|
1182
|
+
return normalizeBoardId(item.board_id ?? "") === boardId
|
|
1183
|
+
&& item.host === input.host
|
|
1184
|
+
&& item.toolchain === input.toolchain
|
|
1185
|
+
&& (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
|
|
1186
|
+
});
|
|
1187
|
+
if (entry) {
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
369
1191
|
if (!entry?.manifest) {
|
|
370
1192
|
return undefined;
|
|
371
1193
|
}
|
|
@@ -374,23 +1196,28 @@ async function resolveLocalToolchainDownloadPlan(input) {
|
|
|
374
1196
|
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
375
1197
|
throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
376
1198
|
}
|
|
377
|
-
|
|
378
|
-
|
|
1199
|
+
const manifestBoardId = normalizeBoardId(manifest.board_id ?? "");
|
|
1200
|
+
if (!compatibleBoardIds.includes(manifestBoardId) || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
|
|
1201
|
+
throw new Error(`Download manifest does not match requested ${requestedBoardId}/${input.host}/${input.toolchain}.`);
|
|
379
1202
|
}
|
|
380
|
-
if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
|
|
381
|
-
|
|
1203
|
+
if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
|
|
1204
|
+
&& (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
|
|
1205
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
|
|
382
1206
|
}
|
|
383
|
-
const mirrors =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1207
|
+
const mirrors = manifest.archive
|
|
1208
|
+
? orderDownloadMirrors((manifest.mirrors ?? [])
|
|
1209
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
1210
|
+
.map((mirror) => ({
|
|
1211
|
+
kind: mirror.kind || "unknown",
|
|
1212
|
+
enabled: mirror.enabled !== false,
|
|
1213
|
+
url: mirror.url,
|
|
1214
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
|
|
1215
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
|
|
1216
|
+
})), manifest.download_policy?.preferred_order)
|
|
1217
|
+
: [];
|
|
1218
|
+
const components = downloadComponentsForBoard(requestedBoardId, (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl)));
|
|
392
1219
|
const first = mirrors[0];
|
|
393
|
-
if (!first) {
|
|
1220
|
+
if (!first && components.length === 0) {
|
|
394
1221
|
return undefined;
|
|
395
1222
|
}
|
|
396
1223
|
return {
|
|
@@ -398,17 +1225,55 @@ async function resolveLocalToolchainDownloadPlan(input) {
|
|
|
398
1225
|
manifest_url: manifestUrl,
|
|
399
1226
|
package_id: manifest.id,
|
|
400
1227
|
version: manifest.version,
|
|
401
|
-
board_id:
|
|
1228
|
+
board_id: requestedBoardId,
|
|
402
1229
|
host: input.host,
|
|
403
1230
|
toolchain: input.toolchain,
|
|
404
|
-
source_url: first
|
|
405
|
-
mirror_kind: first
|
|
406
|
-
archive: {
|
|
1231
|
+
source_url: first?.url,
|
|
1232
|
+
mirror_kind: first?.kind,
|
|
1233
|
+
archive: manifest.archive ? {
|
|
407
1234
|
file: manifest.archive.file,
|
|
408
1235
|
size_bytes: manifest.archive.size_bytes,
|
|
409
1236
|
sha256: manifest.archive.sha256,
|
|
410
1237
|
content_type: manifest.archive.content_type
|
|
1238
|
+
} : undefined,
|
|
1239
|
+
mirrors,
|
|
1240
|
+
components: components.length > 0 ? components : undefined,
|
|
1241
|
+
default_mode: manifest.download_policy?.default_mode
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function normalizeDownloadComponent(component, manifest, manifestUrl) {
|
|
1245
|
+
if (!component.id || !component.version) {
|
|
1246
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
|
|
1247
|
+
}
|
|
1248
|
+
if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
|
|
1249
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
|
|
1250
|
+
}
|
|
1251
|
+
const mirrors = orderDownloadMirrors((component.mirrors ?? [])
|
|
1252
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
1253
|
+
.map((mirror) => ({
|
|
1254
|
+
kind: mirror.kind || "unknown",
|
|
1255
|
+
enabled: mirror.enabled !== false,
|
|
1256
|
+
url: new URL(mirror.url, manifestUrl).toString(),
|
|
1257
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
|
|
1258
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
|
|
1259
|
+
})), manifest.download_policy?.preferred_order);
|
|
1260
|
+
const first = mirrors[0];
|
|
1261
|
+
if (!first) {
|
|
1262
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
id: component.id,
|
|
1266
|
+
version: component.version,
|
|
1267
|
+
role: component.role,
|
|
1268
|
+
install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
|
|
1269
|
+
archive: {
|
|
1270
|
+
file: component.archive.file,
|
|
1271
|
+
size_bytes: component.archive.size_bytes,
|
|
1272
|
+
sha256: component.archive.sha256,
|
|
1273
|
+
content_type: component.archive.content_type
|
|
411
1274
|
},
|
|
1275
|
+
source_url: first.url,
|
|
1276
|
+
mirror_kind: first.kind,
|
|
412
1277
|
mirrors
|
|
413
1278
|
};
|
|
414
1279
|
}
|
|
@@ -423,7 +1288,12 @@ function mirrorRank(kind, preferredOrder) {
|
|
|
423
1288
|
return index >= 0 ? index : preferredOrder.length + 1;
|
|
424
1289
|
}
|
|
425
1290
|
async function fetchJson(url) {
|
|
426
|
-
const response = await fetch(url, {
|
|
1291
|
+
const response = await fetch(url, {
|
|
1292
|
+
headers: {
|
|
1293
|
+
"accept-encoding": "identity"
|
|
1294
|
+
},
|
|
1295
|
+
signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS)
|
|
1296
|
+
});
|
|
427
1297
|
if (!response.ok) {
|
|
428
1298
|
throw new Error(`HTTP ${response.status} while loading ${url}`);
|
|
429
1299
|
}
|
|
@@ -467,14 +1337,160 @@ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
|
|
|
467
1337
|
return refs;
|
|
468
1338
|
}
|
|
469
1339
|
function boardPackageIdFor(boardId) {
|
|
470
|
-
|
|
1340
|
+
const normalized = normalizeBoardId(boardId);
|
|
1341
|
+
if (normalized === DEFAULT_BOARD_ID || normalized === "taishanpi" || normalized === "taishanpi-1m-rk3566") {
|
|
471
1342
|
return "embedlabs.board.taishanpi.1m-rk3566";
|
|
472
1343
|
}
|
|
473
|
-
if (
|
|
1344
|
+
if (normalized === COLOREASYPICO2_RP2350_BOARD_ID
|
|
1345
|
+
|| normalized === "coloreasypico2"
|
|
1346
|
+
|| normalized === "color-easy-pico2"
|
|
1347
|
+
|| normalized === "color-easy-pico-2"
|
|
1348
|
+
|| normalized === "color-easy-pico-2-rp2350-monitor"
|
|
1349
|
+
|| normalized === "ce-pico2") {
|
|
1350
|
+
return "embedlabs.board.coloreasypico2.rp2350-monitor";
|
|
1351
|
+
}
|
|
1352
|
+
if (normalized === PICO2W_RP2350_BOARD_ID
|
|
1353
|
+
|| normalized === "pico2w"
|
|
1354
|
+
|| normalized === "pico-2-w"
|
|
1355
|
+
|| normalized === "pico2"
|
|
1356
|
+
|| normalized === "rp2350"
|
|
1357
|
+
|| normalized === "rp2350-monitor") {
|
|
1358
|
+
return "embedlabs.board.pico2w.rp2350-monitor";
|
|
1359
|
+
}
|
|
1360
|
+
if (normalized.startsWith("embedlabs.board.")) {
|
|
474
1361
|
return boardId;
|
|
475
1362
|
}
|
|
476
1363
|
throw new Error(`Unsupported local toolchain board ${boardId}.`);
|
|
477
1364
|
}
|
|
1365
|
+
function packageIdForBoardFilter(boardId) {
|
|
1366
|
+
try {
|
|
1367
|
+
return boardPackageIdFor(boardId);
|
|
1368
|
+
}
|
|
1369
|
+
catch {
|
|
1370
|
+
return undefined;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function boardIdForPackageManifest(manifest) {
|
|
1374
|
+
const explicit = manifest.board_id;
|
|
1375
|
+
if (explicit?.trim()) {
|
|
1376
|
+
return normalizeBoardId(explicit);
|
|
1377
|
+
}
|
|
1378
|
+
if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
|
|
1379
|
+
return DEFAULT_BOARD_ID;
|
|
1380
|
+
}
|
|
1381
|
+
if (manifest.id.startsWith("embedlabs.board.")) {
|
|
1382
|
+
return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
|
|
1383
|
+
}
|
|
1384
|
+
return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
|
|
1385
|
+
}
|
|
1386
|
+
function normalizeBoardId(boardId) {
|
|
1387
|
+
return boardId.trim().toLowerCase().replaceAll("_", "-");
|
|
1388
|
+
}
|
|
1389
|
+
function isRp2350MonitorBoardId(boardId) {
|
|
1390
|
+
const normalized = normalizeBoardId(boardId);
|
|
1391
|
+
return normalized === PICO2W_RP2350_BOARD_ID
|
|
1392
|
+
|| normalized === COLOREASYPICO2_RP2350_BOARD_ID
|
|
1393
|
+
|| normalized === "pico2w"
|
|
1394
|
+
|| normalized === "pico-2-w"
|
|
1395
|
+
|| normalized === "pico2"
|
|
1396
|
+
|| normalized === "rp2350"
|
|
1397
|
+
|| normalized === "rp2350-monitor"
|
|
1398
|
+
|| normalized === "coloreasypico2"
|
|
1399
|
+
|| normalized === "color-easy-pico2"
|
|
1400
|
+
|| normalized === "color-easy-pico-2"
|
|
1401
|
+
|| normalized === "ce-pico2";
|
|
1402
|
+
}
|
|
1403
|
+
function compatibleDownloadBoardIds(boardId) {
|
|
1404
|
+
const normalized = normalizeBoardId(boardId);
|
|
1405
|
+
if (normalized === COLOREASYPICO2_RP2350_BOARD_ID) {
|
|
1406
|
+
return [COLOREASYPICO2_RP2350_BOARD_ID, PICO2W_RP2350_BOARD_ID];
|
|
1407
|
+
}
|
|
1408
|
+
return [normalized];
|
|
1409
|
+
}
|
|
1410
|
+
function downloadComponentsForBoard(boardId, components) {
|
|
1411
|
+
const normalized = normalizeBoardId(boardId);
|
|
1412
|
+
if (normalized === DEFAULT_BOARD_ID) {
|
|
1413
|
+
return components.filter((component) => !isRp2350MonitorComponent(component));
|
|
1414
|
+
}
|
|
1415
|
+
return components;
|
|
1416
|
+
}
|
|
1417
|
+
function isRp2350MonitorComponent(component) {
|
|
1418
|
+
const text = `${component.id} ${component.role ?? ""} ${component.archive.file}`.toLowerCase();
|
|
1419
|
+
return text.includes("rp2350-monitor") || text.includes("pico2w-rp2350-monitor");
|
|
1420
|
+
}
|
|
1421
|
+
function compareVersionLike(left, right) {
|
|
1422
|
+
return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
|
|
1423
|
+
}
|
|
1424
|
+
function isRecord(value) {
|
|
1425
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1426
|
+
}
|
|
1427
|
+
function packageHostSupport(packages, manifests, host) {
|
|
1428
|
+
const unsupportedPackages = [];
|
|
1429
|
+
for (const item of packages) {
|
|
1430
|
+
const manifest = manifests.get(item.id);
|
|
1431
|
+
if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
|
|
1432
|
+
unsupportedPackages.push(`${item.id}@${manifest.version}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
supported: unsupportedPackages.length === 0,
|
|
1437
|
+
unsupportedPackages
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function environmentNotes(input) {
|
|
1441
|
+
const notes = [];
|
|
1442
|
+
if (normalizeBoardId(input.boardId) === COLOREASYPICO2_RP2350_BOARD_ID) {
|
|
1443
|
+
notes.push("ColorEasyPICO2 uses the same RP2350/Pico SDK, ARM bare-metal toolchain, picotool, and optional RP2350 Monitor image package as Pico 2 W; Wi-Fi operations are not declared for this no-Wi-Fi board.");
|
|
1444
|
+
}
|
|
1445
|
+
if (isRp2350MonitorBoardId(input.boardId)) {
|
|
1446
|
+
notes.push("RP2350 Monitor is an optional hardware-control firmware image; the board environment itself is the Pico 2 W or ColorEasyPICO2 C/C++ SDK environment.");
|
|
1447
|
+
}
|
|
1448
|
+
if (isNativeWindowsTaishanPiHost(input.boardId, input.host)) {
|
|
1449
|
+
notes.push(taishanPiWindowsRequirementMessage(input.host));
|
|
1450
|
+
notes.push("Check this computer with: embedlabs local wsl status");
|
|
1451
|
+
}
|
|
1452
|
+
if (input.status === "available") {
|
|
1453
|
+
notes.push("Environment is available but not installed on this computer.");
|
|
1454
|
+
}
|
|
1455
|
+
if (input.status === "update_available") {
|
|
1456
|
+
notes.push("A newer package is available; run the update command to refresh only the selected environment.");
|
|
1457
|
+
}
|
|
1458
|
+
if (input.status === "unsupported_host") {
|
|
1459
|
+
notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (input.downloadError) {
|
|
1462
|
+
notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
|
|
1463
|
+
}
|
|
1464
|
+
return notes;
|
|
1465
|
+
}
|
|
1466
|
+
function localToolchainInstallModesForDownload(download) {
|
|
1467
|
+
if (!download?.components?.length) {
|
|
1468
|
+
return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
|
|
1469
|
+
}
|
|
1470
|
+
const modes = new Set();
|
|
1471
|
+
for (const component of download.components) {
|
|
1472
|
+
for (const mode of component.install_modes ?? []) {
|
|
1473
|
+
modes.add(mode);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return modes.size > 0
|
|
1477
|
+
? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
|
|
1478
|
+
: [...LOCAL_TOOLCHAIN_INSTALL_MODES];
|
|
1479
|
+
}
|
|
1480
|
+
function localToolchainChannelFlag(channelName) {
|
|
1481
|
+
return channelName === DEFAULT_CHANNEL ? "" : ` --channel ${channelName}`;
|
|
1482
|
+
}
|
|
1483
|
+
function localToolchainEnvironmentComponent(component) {
|
|
1484
|
+
return {
|
|
1485
|
+
id: component.id,
|
|
1486
|
+
version: component.version,
|
|
1487
|
+
role: component.role,
|
|
1488
|
+
install_modes: component.install_modes,
|
|
1489
|
+
file: component.archive.file,
|
|
1490
|
+
size_bytes: component.archive.size_bytes,
|
|
1491
|
+
source_url: component.source_url
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
478
1494
|
function hostId() {
|
|
479
1495
|
if (platform() === "darwin" && arch() === "arm64") {
|
|
480
1496
|
return "darwin-arm64";
|
|
@@ -484,6 +1500,171 @@ function hostId() {
|
|
|
484
1500
|
}
|
|
485
1501
|
return `${platform()}-${arch()}`;
|
|
486
1502
|
}
|
|
1503
|
+
const LOCAL_TOOLCHAIN_HOST_IDS = new Set([
|
|
1504
|
+
"darwin-arm64",
|
|
1505
|
+
"linux-x86_64",
|
|
1506
|
+
"linux-arm64",
|
|
1507
|
+
"win32-x64",
|
|
1508
|
+
"win32-arm64"
|
|
1509
|
+
]);
|
|
1510
|
+
function localToolchainHostId() {
|
|
1511
|
+
const override = process.env.EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST?.trim();
|
|
1512
|
+
if (override) {
|
|
1513
|
+
if (!LOCAL_TOOLCHAIN_HOST_IDS.has(override)) {
|
|
1514
|
+
throw new Error(`Unsupported EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST value: ${override}`);
|
|
1515
|
+
}
|
|
1516
|
+
return override;
|
|
1517
|
+
}
|
|
1518
|
+
return hostId();
|
|
1519
|
+
}
|
|
1520
|
+
function isNativeWindowsTaishanPiHost(boardId, host) {
|
|
1521
|
+
return normalizeBoardId(boardId) === DEFAULT_BOARD_ID && host.startsWith("win32-");
|
|
1522
|
+
}
|
|
1523
|
+
function taishanPiWindowsExecutionRoute(host, status) {
|
|
1524
|
+
const actualHost = wslHostForWindowsHost(host);
|
|
1525
|
+
if (!host.startsWith("win32-")) {
|
|
1526
|
+
return {
|
|
1527
|
+
kind: "native",
|
|
1528
|
+
supported: false,
|
|
1529
|
+
required_host: "linux-x86_64",
|
|
1530
|
+
actual_host: host,
|
|
1531
|
+
windows_host: host,
|
|
1532
|
+
route: "wsl2",
|
|
1533
|
+
status: "wsl_not_applicable",
|
|
1534
|
+
status_command: "embedlabs local wsl status",
|
|
1535
|
+
install_command: "embedlabs local wsl status",
|
|
1536
|
+
reason: "WSL2 routing is only used from Windows hosts."
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
if (!status?.wsl_available) {
|
|
1540
|
+
return {
|
|
1541
|
+
kind: "wsl2",
|
|
1542
|
+
supported: false,
|
|
1543
|
+
required_host: "linux-x86_64",
|
|
1544
|
+
actual_host: actualHost,
|
|
1545
|
+
windows_host: host,
|
|
1546
|
+
route: "wsl2",
|
|
1547
|
+
status: "wsl_missing",
|
|
1548
|
+
status_command: "embedlabs local wsl status",
|
|
1549
|
+
install_command: "embedlabs local wsl install --distribution Ubuntu",
|
|
1550
|
+
reason: "wsl.exe is not available on this Windows host."
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
if (actualHost !== "linux-x86_64") {
|
|
1554
|
+
return {
|
|
1555
|
+
kind: "wsl2",
|
|
1556
|
+
supported: false,
|
|
1557
|
+
required_host: "linux-x86_64",
|
|
1558
|
+
actual_host: actualHost,
|
|
1559
|
+
windows_host: host,
|
|
1560
|
+
route: "wsl2",
|
|
1561
|
+
status: "architecture_mismatch",
|
|
1562
|
+
status_command: "embedlabs local wsl status",
|
|
1563
|
+
install_command: "embedlabs local wsl status",
|
|
1564
|
+
reason: `This Windows host maps WSL2 to ${actualHost}, but the current TaishanPi package requires linux-x86_64.`
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
if (!status.usable) {
|
|
1568
|
+
return {
|
|
1569
|
+
kind: "wsl2",
|
|
1570
|
+
supported: false,
|
|
1571
|
+
required_host: "linux-x86_64",
|
|
1572
|
+
actual_host: actualHost,
|
|
1573
|
+
windows_host: host,
|
|
1574
|
+
route: "wsl2",
|
|
1575
|
+
status: "wsl_not_configured",
|
|
1576
|
+
status_command: "embedlabs local wsl status",
|
|
1577
|
+
install_command: "embedlabs local wsl install --distribution Ubuntu",
|
|
1578
|
+
reason: "No usable WSL2 distribution is configured."
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
kind: "wsl2",
|
|
1583
|
+
supported: false,
|
|
1584
|
+
required_host: "linux-x86_64",
|
|
1585
|
+
actual_host: actualHost,
|
|
1586
|
+
windows_host: host,
|
|
1587
|
+
route: "wsl2",
|
|
1588
|
+
status: "package_missing",
|
|
1589
|
+
status_command: "embedlabs local wsl status",
|
|
1590
|
+
install_command: "embedlabs local wsl status",
|
|
1591
|
+
reason: "The Windows x64 WSL2 route is structurally compatible, but no TaishanPi linux-x86_64 package is published in the current stable channel."
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function wslHostForWindowsHost(host) {
|
|
1595
|
+
if (host === "win32-x64") {
|
|
1596
|
+
return "linux-x86_64";
|
|
1597
|
+
}
|
|
1598
|
+
if (host === "win32-arm64") {
|
|
1599
|
+
return "linux-arm64";
|
|
1600
|
+
}
|
|
1601
|
+
return undefined;
|
|
1602
|
+
}
|
|
1603
|
+
function taishanPiWindowsRequirementMessage(host) {
|
|
1604
|
+
if (host === "win32-arm64") {
|
|
1605
|
+
return "TaishanPi local compile, Qt, image, and Rockchip tooling is not available on native Windows ARM64 yet. Full parity requires a published linux-arm64/native Windows TaishanPi package, or a separately validated Windows x64 + WSL2/linux-x86_64 package.";
|
|
1606
|
+
}
|
|
1607
|
+
return "TaishanPi local compile, Qt, image, and Rockchip tooling is not available on native Windows yet. Windows x64 support requires WSL2/Linux x86_64 plus a published and validated TaishanPi linux-x86_64 package.";
|
|
1608
|
+
}
|
|
1609
|
+
function normalizeWindowsCommandText(value) {
|
|
1610
|
+
return value.replace(/\0/g, "").replace(/\r/g, "\n");
|
|
1611
|
+
}
|
|
1612
|
+
function parseWslDistributionList(value) {
|
|
1613
|
+
const distributions = [];
|
|
1614
|
+
const lines = value.split(/\n+/)
|
|
1615
|
+
.map((line) => line.trim())
|
|
1616
|
+
.filter(Boolean);
|
|
1617
|
+
for (const rawLine of lines) {
|
|
1618
|
+
if (/^(NAME|Windows Subsystem|Usage|Copyright|\-|用法|选项|命令)/i.test(rawLine)) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const isDefault = rawLine.startsWith("*");
|
|
1622
|
+
const line = rawLine.replace(/^\*\s*/, "").trim();
|
|
1623
|
+
const parts = line.split(/\s+/).filter(Boolean);
|
|
1624
|
+
if (parts.length === 0 || parts[0].includes(":")) {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const maybeVersion = parts.at(-1);
|
|
1628
|
+
const version = maybeVersion && /^[12]$/.test(maybeVersion) ? maybeVersion : undefined;
|
|
1629
|
+
const state = version && parts.length >= 3 ? parts.at(-2) : parts.length >= 2 ? parts.at(-1) : undefined;
|
|
1630
|
+
distributions.push({
|
|
1631
|
+
name: parts[0],
|
|
1632
|
+
state,
|
|
1633
|
+
version,
|
|
1634
|
+
default: isDefault
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
return distributions;
|
|
1638
|
+
}
|
|
1639
|
+
function parseWslOnlineDistributionList(value) {
|
|
1640
|
+
const distributions = [];
|
|
1641
|
+
const lines = value.split(/\n+/)
|
|
1642
|
+
.map((line) => line.trim())
|
|
1643
|
+
.filter(Boolean);
|
|
1644
|
+
let inTable = false;
|
|
1645
|
+
for (const rawLine of lines) {
|
|
1646
|
+
if (/^NAME\s+FRIENDLY NAME/i.test(rawLine)) {
|
|
1647
|
+
inTable = true;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (!inTable) {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const isDefault = rawLine.startsWith("*");
|
|
1654
|
+
const line = rawLine.replace(/^\*\s*/, "").trim();
|
|
1655
|
+
const parts = line.split(/\s{2,}/).filter(Boolean);
|
|
1656
|
+
const name = parts[0]?.trim();
|
|
1657
|
+
if (!name || name.includes(":")) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
distributions.push({
|
|
1661
|
+
name,
|
|
1662
|
+
friendly_name: parts.slice(1).join(" ").trim() || undefined,
|
|
1663
|
+
default: isDefault
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return distributions;
|
|
1667
|
+
}
|
|
487
1668
|
function resolveInstallRoot(installRoot) {
|
|
488
1669
|
return resolve(installRoot
|
|
489
1670
|
|| process.env.EMBEDLABS_HOME?.trim()
|
|
@@ -492,21 +1673,44 @@ function resolveInstallRoot(installRoot) {
|
|
|
492
1673
|
function localToolchainRegistryPath(installRoot) {
|
|
493
1674
|
return join(installRoot, "registry", "local-toolchains.json");
|
|
494
1675
|
}
|
|
495
|
-
async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
|
|
1676
|
+
async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
|
|
496
1677
|
const registryPath = localToolchainRegistryPath(installRoot);
|
|
497
1678
|
await mkdir(dirname(registryPath), { recursive: true });
|
|
498
|
-
|
|
1679
|
+
let existing = {};
|
|
1680
|
+
try {
|
|
1681
|
+
existing = JSON.parse(await readFile(registryPath, "utf8"));
|
|
1682
|
+
}
|
|
1683
|
+
catch {
|
|
1684
|
+
existing = {};
|
|
1685
|
+
}
|
|
1686
|
+
const entry = {
|
|
499
1687
|
installed: true,
|
|
500
1688
|
board_id: latest.board_id,
|
|
501
1689
|
version: latest.version,
|
|
502
1690
|
channel: latest.channel,
|
|
503
1691
|
host: latest.host,
|
|
1692
|
+
mode,
|
|
504
1693
|
release_root: releaseRoot,
|
|
505
1694
|
packages: latest.packages,
|
|
1695
|
+
source,
|
|
1696
|
+
installed_components: source?.components,
|
|
1697
|
+
updated_at: new Date().toISOString()
|
|
1698
|
+
};
|
|
1699
|
+
const environments = isRecord(existing.environments)
|
|
1700
|
+
? { ...existing.environments }
|
|
1701
|
+
: {};
|
|
1702
|
+
environments[normalizeBoardId(latest.board_id)] = entry;
|
|
1703
|
+
const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
|
|
1704
|
+
&& typeof existing.release_root === "string"
|
|
1705
|
+
&& normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
|
|
1706
|
+
const topLevel = preserveTopLevel ? existing : entry;
|
|
1707
|
+
await writeFile(registryPath, `${JSON.stringify({
|
|
1708
|
+
...topLevel,
|
|
1709
|
+
environments,
|
|
506
1710
|
updated_at: new Date().toISOString()
|
|
507
1711
|
}, null, 2)}\n`, "utf8");
|
|
508
1712
|
}
|
|
509
|
-
async function resolveLocalReleaseRoot(releaseRoot) {
|
|
1713
|
+
async function resolveLocalReleaseRoot(releaseRoot, boardId = DEFAULT_BOARD_ID) {
|
|
510
1714
|
if (releaseRoot?.trim()) {
|
|
511
1715
|
return resolve(releaseRoot);
|
|
512
1716
|
}
|
|
@@ -514,7 +1718,7 @@ async function resolveLocalReleaseRoot(releaseRoot) {
|
|
|
514
1718
|
if (envRoot) {
|
|
515
1719
|
return resolve(envRoot);
|
|
516
1720
|
}
|
|
517
|
-
const current = await currentLocalToolchain(undefined,
|
|
1721
|
+
const current = await currentLocalToolchain(undefined, boardId);
|
|
518
1722
|
if (current.release_root) {
|
|
519
1723
|
return resolve(current.release_root);
|
|
520
1724
|
}
|
|
@@ -540,12 +1744,18 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
|
|
|
540
1744
|
source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
|
|
541
1745
|
};
|
|
542
1746
|
}
|
|
1747
|
+
if (latest.download?.components?.length) {
|
|
1748
|
+
return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
|
|
1749
|
+
}
|
|
543
1750
|
if (latest.download) {
|
|
544
1751
|
const failures = [];
|
|
545
1752
|
for (const mirror of latest.download.mirrors) {
|
|
546
1753
|
if (!mirror.enabled) {
|
|
547
1754
|
continue;
|
|
548
1755
|
}
|
|
1756
|
+
if (!latest.download.archive) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
549
1759
|
const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
|
|
550
1760
|
try {
|
|
551
1761
|
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
@@ -586,6 +1796,127 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
|
|
|
586
1796
|
}
|
|
587
1797
|
throw new Error("Local toolchain install could not resolve a source. Pass --source-url <tar.gz>, --source-release-root <path>, or configure EMBED_DOWNLOAD_BASE_URL.");
|
|
588
1798
|
}
|
|
1799
|
+
async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
|
|
1800
|
+
const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
|
|
1801
|
+
const components = selectedDownloadComponents(download.components ?? [], mode);
|
|
1802
|
+
if (components.length === 0) {
|
|
1803
|
+
throw new Error(`No local toolchain components selected for mode ${mode}.`);
|
|
1804
|
+
}
|
|
1805
|
+
const extractRoot = join(tempDir, "extract-components");
|
|
1806
|
+
await mkdir(extractRoot, { recursive: true });
|
|
1807
|
+
const installedComponents = [];
|
|
1808
|
+
const failures = [];
|
|
1809
|
+
for (const component of components) {
|
|
1810
|
+
let installed = false;
|
|
1811
|
+
for (const mirror of component.mirrors) {
|
|
1812
|
+
if (!mirror.enabled) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
1817
|
+
sha256: mirror.sha256 ?? component.archive.sha256,
|
|
1818
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes
|
|
1819
|
+
});
|
|
1820
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
1821
|
+
if (extracted.exit_code !== 0) {
|
|
1822
|
+
throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
|
|
1823
|
+
}
|
|
1824
|
+
installedComponents.push({
|
|
1825
|
+
id: component.id,
|
|
1826
|
+
version: component.version,
|
|
1827
|
+
role: component.role,
|
|
1828
|
+
archive_file: component.archive.file,
|
|
1829
|
+
mirror_kind: mirror.kind,
|
|
1830
|
+
downloaded_path: downloadedPath,
|
|
1831
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
|
|
1832
|
+
sha256: mirror.sha256 ?? component.archive.sha256
|
|
1833
|
+
});
|
|
1834
|
+
installed = true;
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
catch (error) {
|
|
1838
|
+
failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (!installed) {
|
|
1842
|
+
throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
path: extractRoot,
|
|
1847
|
+
source: {
|
|
1848
|
+
kind: "components",
|
|
1849
|
+
value: download.manifest_url,
|
|
1850
|
+
mirror_kind: "components",
|
|
1851
|
+
size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
|
|
1852
|
+
components: installedComponents
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function selectedDownloadComponents(components, mode) {
|
|
1857
|
+
return components.filter((component) => {
|
|
1858
|
+
if (!component.install_modes?.length) {
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
return component.install_modes.includes(mode);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
function installCopyPathsForBoard(boardId) {
|
|
1865
|
+
if (normalizeBoardId(boardId) === DEFAULT_BOARD_ID) {
|
|
1866
|
+
return INSTALL_COPY_PATHS.filter((relativePath) => {
|
|
1867
|
+
const normalized = relativePath.toLowerCase();
|
|
1868
|
+
return normalized !== "toolkit-runtime/rp2350-monitor"
|
|
1869
|
+
&& normalized !== "toolkit-runtime/rp2350-monitor/";
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
return INSTALL_COPY_PATHS;
|
|
1873
|
+
}
|
|
1874
|
+
async function resolveInstallSourcePathForBoard(sourceRoot, relativePath, boardId) {
|
|
1875
|
+
const direct = resolve(sourceRoot, relativePath);
|
|
1876
|
+
if (await pathExists(direct)) {
|
|
1877
|
+
return direct;
|
|
1878
|
+
}
|
|
1879
|
+
if (!isRp2350MonitorBoardId(boardId)) {
|
|
1880
|
+
return direct;
|
|
1881
|
+
}
|
|
1882
|
+
const legacyRp2350Root = await findLegacyRp2350Root(sourceRoot);
|
|
1883
|
+
if (!legacyRp2350Root) {
|
|
1884
|
+
return direct;
|
|
1885
|
+
}
|
|
1886
|
+
if (relativePath === "rp2350-sdk") {
|
|
1887
|
+
return legacyRp2350Root;
|
|
1888
|
+
}
|
|
1889
|
+
if (relativePath === "rp2350-initial-firmware") {
|
|
1890
|
+
return resolve(legacyRp2350Root, "initial_firmware");
|
|
1891
|
+
}
|
|
1892
|
+
if (relativePath === "rp2350-examples") {
|
|
1893
|
+
return resolve(legacyRp2350Root, "validation");
|
|
1894
|
+
}
|
|
1895
|
+
return direct;
|
|
1896
|
+
}
|
|
1897
|
+
async function findLegacyRp2350Root(sourceRoot) {
|
|
1898
|
+
const direct = resolve(sourceRoot, "RP2350");
|
|
1899
|
+
if (await pathExists(resolve(direct, "pico-sdk", "pico_sdk_init.cmake"))) {
|
|
1900
|
+
return direct;
|
|
1901
|
+
}
|
|
1902
|
+
let entries;
|
|
1903
|
+
try {
|
|
1904
|
+
entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
1905
|
+
}
|
|
1906
|
+
catch {
|
|
1907
|
+
return undefined;
|
|
1908
|
+
}
|
|
1909
|
+
for (const entry of entries) {
|
|
1910
|
+
if (!entry.isDirectory()) {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const candidate = resolve(sourceRoot, entry.name, "RP2350");
|
|
1914
|
+
if (await pathExists(resolve(candidate, "pico-sdk", "pico_sdk_init.cmake"))) {
|
|
1915
|
+
return candidate;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return undefined;
|
|
1919
|
+
}
|
|
589
1920
|
async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
|
|
590
1921
|
const downloadsDir = join(installRoot, "cache", "downloads");
|
|
591
1922
|
await mkdir(downloadsDir, { recursive: true });
|
|
@@ -775,6 +2106,68 @@ async function runCommand(command, cwd) {
|
|
|
775
2106
|
});
|
|
776
2107
|
});
|
|
777
2108
|
}
|
|
2109
|
+
async function runCommandWithTimeout(command, cwd, timeoutMs) {
|
|
2110
|
+
return await new Promise((resolve) => {
|
|
2111
|
+
const child = spawn(command[0], command.slice(1), {
|
|
2112
|
+
cwd,
|
|
2113
|
+
env: process.env,
|
|
2114
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2115
|
+
});
|
|
2116
|
+
let stdout = "";
|
|
2117
|
+
let stderr = "";
|
|
2118
|
+
let settled = false;
|
|
2119
|
+
const timer = setTimeout(() => {
|
|
2120
|
+
if (settled) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
stderr += `Command timed out after ${timeoutMs} ms.\n`;
|
|
2124
|
+
child.kill("SIGTERM");
|
|
2125
|
+
setTimeout(() => {
|
|
2126
|
+
if (!settled) {
|
|
2127
|
+
child.kill("SIGKILL");
|
|
2128
|
+
}
|
|
2129
|
+
}, 2_000).unref();
|
|
2130
|
+
}, timeoutMs);
|
|
2131
|
+
child.stdout.setEncoding("utf8");
|
|
2132
|
+
child.stderr.setEncoding("utf8");
|
|
2133
|
+
child.stdout.on("data", (chunk) => {
|
|
2134
|
+
stdout += chunk;
|
|
2135
|
+
});
|
|
2136
|
+
child.stderr.on("data", (chunk) => {
|
|
2137
|
+
stderr += chunk;
|
|
2138
|
+
});
|
|
2139
|
+
child.on("error", (error) => {
|
|
2140
|
+
if (settled) {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
settled = true;
|
|
2144
|
+
clearTimeout(timer);
|
|
2145
|
+
stderr += `${error.message}\n`;
|
|
2146
|
+
resolve({
|
|
2147
|
+
command,
|
|
2148
|
+
cwd,
|
|
2149
|
+
exit_code: 127,
|
|
2150
|
+
stdout_tail: tailLines(stdout),
|
|
2151
|
+
stderr_tail: tailLines(stderr)
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
child.on("close", (code) => {
|
|
2155
|
+
if (settled) {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
settled = true;
|
|
2159
|
+
clearTimeout(timer);
|
|
2160
|
+
const timedOut = stderr.includes(`Command timed out after ${timeoutMs} ms.`);
|
|
2161
|
+
resolve({
|
|
2162
|
+
command,
|
|
2163
|
+
cwd,
|
|
2164
|
+
exit_code: timedOut ? 124 : code ?? 1,
|
|
2165
|
+
stdout_tail: tailLines(stdout),
|
|
2166
|
+
stderr_tail: tailLines(stderr)
|
|
2167
|
+
});
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
778
2171
|
async function fileInfoFor(filePath) {
|
|
779
2172
|
const result = await runCommand(["file", filePath], dirname(filePath));
|
|
780
2173
|
return result.exit_code === 0 ? result.stdout_tail.join("\n") : undefined;
|