@kvell007/embed-labs-cli 0.1.0-alpha.4 → 0.1.0-alpha.40
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 +51 -3
- package/dist/index.js +1974 -83
- package/dist/index.js.map +1 -1
- package/dist/install-progress.d.ts +2 -0
- package/dist/install-progress.js +61 -0
- package/dist/install-progress.js.map +1 -0
- package/dist/local-toolchain.d.ts +135 -2
- package/dist/local-toolchain.js +1178 -66
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +7 -3
package/dist/local-toolchain.js
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { access, cp, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants, createWriteStream } from "node:fs";
|
|
4
|
+
import { access, cp, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { arch, homedir, platform, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
7
10
|
const DEFAULT_RELEASE_ROOT = "/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal";
|
|
8
11
|
const DEFAULT_QT_SMOKE_SOURCE = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/qt-smoke";
|
|
9
|
-
const DEFAULT_METADATA_ROOT = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/embedlabs-release";
|
|
10
12
|
const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
|
|
13
|
+
const PICO2W_RP2350_BOARD_ID = "pico2w-rp2350-monitor";
|
|
14
|
+
const COLOREASYPICO2_RP2350_BOARD_ID = "coloreasypico2-rp2350-monitor";
|
|
11
15
|
const DEFAULT_CHANNEL = "stable";
|
|
16
|
+
const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
|
|
17
|
+
const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
|
|
12
18
|
const BUILT_IN_CHANNEL = {
|
|
13
19
|
schema: "embedlabs.channel.v1",
|
|
14
20
|
channel: DEFAULT_CHANNEL,
|
|
15
21
|
packages: [
|
|
16
22
|
{ id: "embedlabs.tools.vendor.rockchip", version: "1.0.0", manifest: "" },
|
|
17
23
|
{ id: "embedlabs.tools.common.llvm", version: "22.1.3", manifest: "" },
|
|
24
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.2.rel1", manifest: "" },
|
|
18
25
|
{ id: "embedlabs.tools.common.e2fsprogs", version: "1.0.0", manifest: "" },
|
|
26
|
+
{ id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.31", manifest: "" },
|
|
27
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.31", manifest: "" },
|
|
19
28
|
{ id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
|
|
20
|
-
{ id: "embedlabs.
|
|
29
|
+
{ id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
|
|
30
|
+
{ id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" },
|
|
31
|
+
{ id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.31", manifest: "" },
|
|
32
|
+
{ id: "embedlabs.board.coloreasypico2.rp2350-monitor", version: "1.0.31", manifest: "" }
|
|
21
33
|
]
|
|
22
34
|
};
|
|
23
35
|
const BUILT_IN_MANIFESTS = {
|
|
@@ -37,6 +49,14 @@ const BUILT_IN_MANIFESTS = {
|
|
|
37
49
|
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
38
50
|
provides: ["llvm.clang", "llvm.clangxx", "llvm.ld_lld", "llvm.ar", "llvm.objcopy", "llvm.readelf"]
|
|
39
51
|
},
|
|
52
|
+
"embedlabs.tools.common.arm-none-eabi": {
|
|
53
|
+
schema: "embedlabs.package.v1",
|
|
54
|
+
id: "embedlabs.tools.common.arm-none-eabi",
|
|
55
|
+
version: "15.2.rel1",
|
|
56
|
+
kind: "tools",
|
|
57
|
+
hosts: ["darwin-arm64"],
|
|
58
|
+
provides: ["arm-none-eabi.gcc", "arm-none-eabi.g++", "arm-none-eabi.objcopy", "picotool"]
|
|
59
|
+
},
|
|
40
60
|
"embedlabs.tools.common.e2fsprogs": {
|
|
41
61
|
schema: "embedlabs.package.v1",
|
|
42
62
|
id: "embedlabs.tools.common.e2fsprogs",
|
|
@@ -45,6 +65,22 @@ const BUILT_IN_MANIFESTS = {
|
|
|
45
65
|
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
46
66
|
provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
|
|
47
67
|
},
|
|
68
|
+
"embedlabs.tools.runtime.qtquick-live-preview": {
|
|
69
|
+
schema: "embedlabs.package.v1",
|
|
70
|
+
id: "embedlabs.tools.runtime.qtquick-live-preview",
|
|
71
|
+
version: "1.0.31",
|
|
72
|
+
kind: "tools",
|
|
73
|
+
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
74
|
+
provides: ["qtquick.live_preview", "qtquick.live_preview.inspector", "qtquick.live_preview.feedback"]
|
|
75
|
+
},
|
|
76
|
+
"embedlabs.tools.runtime.rp2350-monitor": {
|
|
77
|
+
schema: "embedlabs.package.v1",
|
|
78
|
+
id: "embedlabs.tools.runtime.rp2350-monitor",
|
|
79
|
+
version: "1.0.31",
|
|
80
|
+
kind: "tools",
|
|
81
|
+
hosts: ["darwin-arm64", "linux-x86_64"],
|
|
82
|
+
provides: ["rp2350.monitor.cli", "rp2350.monitor.logic_analyzer", "rp2350.monitor.logic_decode"]
|
|
83
|
+
},
|
|
48
84
|
"embedlabs.family.rk356x": {
|
|
49
85
|
schema: "embedlabs.package.v1",
|
|
50
86
|
id: "embedlabs.family.rk356x",
|
|
@@ -57,6 +93,17 @@ const BUILT_IN_MANIFESTS = {
|
|
|
57
93
|
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
|
|
58
94
|
]
|
|
59
95
|
},
|
|
96
|
+
"embedlabs.family.rp2350": {
|
|
97
|
+
schema: "embedlabs.package.v1",
|
|
98
|
+
id: "embedlabs.family.rp2350",
|
|
99
|
+
version: "1.0.0",
|
|
100
|
+
kind: "family",
|
|
101
|
+
family: "rp2350",
|
|
102
|
+
requires: [
|
|
103
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2"] },
|
|
104
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
60
107
|
"embedlabs.board.taishanpi.1m-rk3566": {
|
|
61
108
|
schema: "embedlabs.package.v1",
|
|
62
109
|
id: "embedlabs.board.taishanpi.1m-rk3566",
|
|
@@ -69,20 +116,67 @@ const BUILT_IN_MANIFESTS = {
|
|
|
69
116
|
{ id: "embedlabs.family.rk356x", version: "^1.0.0" },
|
|
70
117
|
{ id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
|
|
71
118
|
{ id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
|
|
72
|
-
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] }
|
|
119
|
+
{ id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
|
|
120
|
+
{ id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.31", roles: ["qtquick-preview"] }
|
|
73
121
|
],
|
|
74
122
|
build_modes: ["local-llvm"]
|
|
123
|
+
},
|
|
124
|
+
"embedlabs.board.pico2w.rp2350-monitor": {
|
|
125
|
+
schema: "embedlabs.package.v1",
|
|
126
|
+
id: "embedlabs.board.pico2w.rp2350-monitor",
|
|
127
|
+
version: "1.0.31",
|
|
128
|
+
kind: "board",
|
|
129
|
+
display_name: "Pico 2 W",
|
|
130
|
+
family: "rp2350",
|
|
131
|
+
board: "Pico 2 W",
|
|
132
|
+
board_id: PICO2W_RP2350_BOARD_ID,
|
|
133
|
+
requires: [
|
|
134
|
+
{ id: "embedlabs.family.rp2350", version: "^1.0.0" },
|
|
135
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
|
|
136
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
137
|
+
],
|
|
138
|
+
build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
|
|
139
|
+
},
|
|
140
|
+
"embedlabs.board.coloreasypico2.rp2350-monitor": {
|
|
141
|
+
schema: "embedlabs.package.v1",
|
|
142
|
+
id: "embedlabs.board.coloreasypico2.rp2350-monitor",
|
|
143
|
+
version: "1.0.31",
|
|
144
|
+
kind: "board",
|
|
145
|
+
display_name: "ColorEasyPICO2",
|
|
146
|
+
family: "rp2350",
|
|
147
|
+
board: "ColorEasyPICO2",
|
|
148
|
+
board_id: COLOREASYPICO2_RP2350_BOARD_ID,
|
|
149
|
+
requires: [
|
|
150
|
+
{ id: "embedlabs.family.rp2350", version: "^1.0.0" },
|
|
151
|
+
{ id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
|
|
152
|
+
{ id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
|
|
153
|
+
],
|
|
154
|
+
build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
|
|
75
155
|
}
|
|
76
156
|
};
|
|
77
157
|
const INSTALL_COPY_PATHS = [
|
|
78
158
|
"toolchain/llvm-cross",
|
|
79
|
-
"toolchain/host
|
|
159
|
+
"toolchain/host",
|
|
160
|
+
"toolchain/host-tools",
|
|
161
|
+
"toolchain/qt6-rk3566-llvm-toolchain.cmake",
|
|
80
162
|
"qt-target/qt6-rk3566-llvm-6.8.3",
|
|
163
|
+
"qt-host/qt6-host-macos-6.8.3",
|
|
81
164
|
"tools/mac",
|
|
165
|
+
"toolkit-runtime/qtquick-live-preview",
|
|
166
|
+
"toolkit-runtime/rp2350-monitor",
|
|
167
|
+
"rp2350-sdk",
|
|
168
|
+
"rp2350-initial-firmware",
|
|
169
|
+
"rp2350-examples",
|
|
82
170
|
"images/current",
|
|
83
171
|
"userdata/rootfs",
|
|
84
|
-
"boot-workspace
|
|
172
|
+
"boot-workspace",
|
|
173
|
+
"README.md",
|
|
174
|
+
"meta",
|
|
175
|
+
"scripts",
|
|
176
|
+
"support",
|
|
177
|
+
"third_party"
|
|
85
178
|
];
|
|
179
|
+
const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
|
|
86
180
|
export function defaultLocalReleaseRoot() {
|
|
87
181
|
return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
|
|
88
182
|
|| process.env.EMBEDLABS_RELEASE_ROOT?.trim()
|
|
@@ -97,14 +191,30 @@ export async function latestLocalToolchain(options = {}) {
|
|
|
97
191
|
if (!board) {
|
|
98
192
|
throw new Error(`No local toolchain board package found for ${boardId}.`);
|
|
99
193
|
}
|
|
194
|
+
const canonicalBoardId = boardIdForPackageManifest(board);
|
|
100
195
|
const packages = resolvePackageRefs(boardPackageId, channel, manifests);
|
|
196
|
+
let download;
|
|
197
|
+
let downloadError;
|
|
198
|
+
try {
|
|
199
|
+
download = await resolveLocalToolchainDownloadPlan({
|
|
200
|
+
boardId: canonicalBoardId,
|
|
201
|
+
channel: channelName,
|
|
202
|
+
host: hostId(),
|
|
203
|
+
toolchain: "llvm"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
downloadError = error instanceof Error ? error.message : String(error);
|
|
208
|
+
}
|
|
101
209
|
return {
|
|
102
|
-
board_id:
|
|
210
|
+
board_id: canonicalBoardId,
|
|
103
211
|
channel: channel.channel,
|
|
104
212
|
host: hostId(),
|
|
105
|
-
version: board.version,
|
|
213
|
+
version: download?.version ?? board.version,
|
|
106
214
|
metadata_root: metadataRoot,
|
|
107
|
-
packages
|
|
215
|
+
packages,
|
|
216
|
+
download,
|
|
217
|
+
download_error: downloadError
|
|
108
218
|
};
|
|
109
219
|
}
|
|
110
220
|
export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD_ID) {
|
|
@@ -112,11 +222,36 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
|
|
|
112
222
|
const registryPath = localToolchainRegistryPath(root);
|
|
113
223
|
try {
|
|
114
224
|
const registry = JSON.parse(await readFile(registryPath, "utf8"));
|
|
225
|
+
const environments = registry.environments;
|
|
226
|
+
const boardInstall = environments?.[normalizeBoardId(boardId)];
|
|
227
|
+
if (boardInstall?.release_root) {
|
|
228
|
+
return {
|
|
229
|
+
installed: true,
|
|
230
|
+
board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
|
|
231
|
+
version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
|
|
232
|
+
mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
|
|
233
|
+
release_root: boardInstall.release_root,
|
|
234
|
+
registry_path: registryPath,
|
|
235
|
+
install_root: root,
|
|
236
|
+
channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
|
|
237
|
+
packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
|
|
238
|
+
};
|
|
239
|
+
}
|
|
115
240
|
const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
|
|
241
|
+
const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
|
|
242
|
+
if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
|
|
243
|
+
return {
|
|
244
|
+
installed: false,
|
|
245
|
+
board_id: boardId,
|
|
246
|
+
registry_path: registryPath,
|
|
247
|
+
install_root: root
|
|
248
|
+
};
|
|
249
|
+
}
|
|
116
250
|
return {
|
|
117
251
|
installed: !!releaseRoot,
|
|
118
|
-
board_id:
|
|
252
|
+
board_id: registryBoardId,
|
|
119
253
|
version: typeof registry.version === "string" ? registry.version : undefined,
|
|
254
|
+
mode: typeof registry.mode === "string" ? registry.mode : undefined,
|
|
120
255
|
release_root: releaseRoot,
|
|
121
256
|
registry_path: registryPath,
|
|
122
257
|
install_root: root,
|
|
@@ -133,48 +268,221 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
|
|
|
133
268
|
};
|
|
134
269
|
}
|
|
135
270
|
}
|
|
271
|
+
async function discoverInstalledLocalToolchains(installRoot, current) {
|
|
272
|
+
const installed = new Map();
|
|
273
|
+
if (current.installed && current.release_root) {
|
|
274
|
+
installed.set(normalizeBoardId(current.board_id), {
|
|
275
|
+
board_id: current.board_id,
|
|
276
|
+
version: current.version,
|
|
277
|
+
channel: current.channel,
|
|
278
|
+
mode: current.mode,
|
|
279
|
+
release_root: current.release_root
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const toolchainsRoot = join(installRoot, "toolchains");
|
|
283
|
+
let boardEntries;
|
|
284
|
+
try {
|
|
285
|
+
boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return installed;
|
|
289
|
+
}
|
|
290
|
+
for (const boardEntry of boardEntries) {
|
|
291
|
+
if (!boardEntry.isDirectory()) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const boardId = normalizeBoardId(boardEntry.name);
|
|
295
|
+
if (installed.has(boardId)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const boardRoot = join(toolchainsRoot, boardEntry.name);
|
|
299
|
+
let versionEntries;
|
|
300
|
+
try {
|
|
301
|
+
versionEntries = await readdir(boardRoot, { withFileTypes: true });
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const versions = versionEntries
|
|
307
|
+
.filter((entry) => entry.isDirectory())
|
|
308
|
+
.map((entry) => entry.name)
|
|
309
|
+
.sort(compareVersionLike)
|
|
310
|
+
.reverse();
|
|
311
|
+
const version = versions[0];
|
|
312
|
+
if (!version) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
installed.set(boardId, {
|
|
316
|
+
board_id: boardId,
|
|
317
|
+
version,
|
|
318
|
+
release_root: join(boardRoot, version)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return installed;
|
|
322
|
+
}
|
|
323
|
+
export async function listLocalToolchainEnvironments(options = {}) {
|
|
324
|
+
const channelName = options.channel ?? DEFAULT_CHANNEL;
|
|
325
|
+
const host = hostId();
|
|
326
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
327
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
328
|
+
const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
|
|
329
|
+
const current = await currentLocalToolchain(installRoot);
|
|
330
|
+
const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
|
|
331
|
+
const boardManifests = [...manifests.values()]
|
|
332
|
+
.filter((manifest) => manifest.kind === "board")
|
|
333
|
+
.filter((manifest) => {
|
|
334
|
+
if (!options.boardId) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
const normalizedFilter = normalizeBoardId(options.boardId);
|
|
338
|
+
return boardIdForPackageManifest(manifest) === normalizedFilter
|
|
339
|
+
|| manifest.id === options.boardId
|
|
340
|
+
|| manifest.id === packageIdForBoardFilter(options.boardId);
|
|
341
|
+
})
|
|
342
|
+
.sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
|
|
343
|
+
const environments = [];
|
|
344
|
+
for (const board of boardManifests) {
|
|
345
|
+
const boardId = boardIdForPackageManifest(board);
|
|
346
|
+
const packages = resolvePackageRefs(board.id, channel, manifests);
|
|
347
|
+
const hostSupport = packageHostSupport(packages, manifests, host);
|
|
348
|
+
let download;
|
|
349
|
+
let downloadError;
|
|
350
|
+
try {
|
|
351
|
+
download = await resolveLocalToolchainDownloadPlan({
|
|
352
|
+
boardId,
|
|
353
|
+
channel: channelName,
|
|
354
|
+
host,
|
|
355
|
+
toolchain: "llvm"
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
downloadError = error instanceof Error ? error.message : String(error);
|
|
360
|
+
}
|
|
361
|
+
const latestVersion = download?.version ?? board.version;
|
|
362
|
+
const currentForBoard = await currentLocalToolchain(installRoot, boardId);
|
|
363
|
+
const installedCandidate = currentForBoard.installed
|
|
364
|
+
? currentForBoard
|
|
365
|
+
: installedByBoard.get(normalizeBoardId(boardId));
|
|
366
|
+
const installed = installedCandidate
|
|
367
|
+
? {
|
|
368
|
+
version: installedCandidate.version,
|
|
369
|
+
channel: installedCandidate.channel,
|
|
370
|
+
mode: installedCandidate.mode,
|
|
371
|
+
release_root: installedCandidate.release_root
|
|
372
|
+
}
|
|
373
|
+
: undefined;
|
|
374
|
+
const updateAvailable = !!installed?.version && installed.version !== latestVersion;
|
|
375
|
+
const status = !hostSupport.supported
|
|
376
|
+
? "unsupported_host"
|
|
377
|
+
: updateAvailable
|
|
378
|
+
? "update_available"
|
|
379
|
+
: installed
|
|
380
|
+
? "installed"
|
|
381
|
+
: "available";
|
|
382
|
+
const mode = download?.default_mode ?? "qt";
|
|
383
|
+
const installModes = localToolchainInstallModesForDownload(download);
|
|
384
|
+
environments.push({
|
|
385
|
+
board_id: boardId,
|
|
386
|
+
package_id: board.id,
|
|
387
|
+
display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
|
|
388
|
+
family: board.family,
|
|
389
|
+
variant: board.variant,
|
|
390
|
+
channel: channel.channel,
|
|
391
|
+
host,
|
|
392
|
+
status,
|
|
393
|
+
supported_host: hostSupport.supported,
|
|
394
|
+
unsupported_packages: hostSupport.unsupportedPackages,
|
|
395
|
+
install_modes: installModes,
|
|
396
|
+
installed,
|
|
397
|
+
latest: {
|
|
398
|
+
version: latestVersion,
|
|
399
|
+
default_mode: download?.default_mode,
|
|
400
|
+
source_url: download?.source_url,
|
|
401
|
+
manifest_url: download?.manifest_url,
|
|
402
|
+
component_count: download?.components?.length,
|
|
403
|
+
download_error: downloadError
|
|
404
|
+
},
|
|
405
|
+
packages,
|
|
406
|
+
components: download?.components?.map(localToolchainEnvironmentComponent),
|
|
407
|
+
install_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode}`,
|
|
408
|
+
update_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode} --force`,
|
|
409
|
+
notes: environmentNotes({ boardId, status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const filteredEnvironments = options.installedOnly
|
|
413
|
+
? environments.filter((environment) => !!environment.installed)
|
|
414
|
+
: environments;
|
|
415
|
+
return {
|
|
416
|
+
host,
|
|
417
|
+
channel: channel.channel,
|
|
418
|
+
metadata_source: metadataRoot ? "local_override" : "built_in",
|
|
419
|
+
metadata_root: metadataRoot,
|
|
420
|
+
install_root: installRoot,
|
|
421
|
+
registry_path: registryPath,
|
|
422
|
+
environments: filteredEnvironments
|
|
423
|
+
};
|
|
424
|
+
}
|
|
136
425
|
export async function installLocalToolchain(options = {}) {
|
|
137
426
|
const latest = await latestLocalToolchain(options);
|
|
138
427
|
const installRoot = resolveInstallRoot(options.installRoot);
|
|
139
428
|
const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
|
|
429
|
+
const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
|
|
140
430
|
if (await pathExists(releaseRoot) && !options.force) {
|
|
141
|
-
|
|
431
|
+
await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
|
|
432
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
|
|
142
433
|
if (!validation.ok) {
|
|
143
|
-
|
|
434
|
+
if (latest.download?.components?.length) {
|
|
435
|
+
// Component installs can upgrade an existing lower-mode install by overlaying
|
|
436
|
+
// only the newly selected components instead of deleting the whole tree.
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
|
|
444
|
+
return {
|
|
445
|
+
board_id: latest.board_id,
|
|
446
|
+
version: latest.version,
|
|
447
|
+
channel: latest.channel,
|
|
448
|
+
host: latest.host,
|
|
449
|
+
mode: installMode,
|
|
450
|
+
install_root: installRoot,
|
|
451
|
+
release_root: releaseRoot,
|
|
452
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
453
|
+
source: { kind: "directory", value: releaseRoot },
|
|
454
|
+
installed_paths: [],
|
|
455
|
+
packages: latest.packages,
|
|
456
|
+
validation
|
|
457
|
+
};
|
|
144
458
|
}
|
|
145
|
-
await writeCurrentRegistry(installRoot, latest, releaseRoot);
|
|
146
|
-
return {
|
|
147
|
-
board_id: latest.board_id,
|
|
148
|
-
version: latest.version,
|
|
149
|
-
channel: latest.channel,
|
|
150
|
-
host: latest.host,
|
|
151
|
-
install_root: installRoot,
|
|
152
|
-
release_root: releaseRoot,
|
|
153
|
-
registry_path: localToolchainRegistryPath(installRoot),
|
|
154
|
-
source: { kind: "directory", value: releaseRoot },
|
|
155
|
-
installed_paths: [],
|
|
156
|
-
packages: latest.packages,
|
|
157
|
-
validation
|
|
158
|
-
};
|
|
159
459
|
}
|
|
160
460
|
const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
|
|
161
461
|
try {
|
|
162
|
-
const sourceRoot = await sourceReleaseRootForInstall(options, installRoot, tempDir);
|
|
163
|
-
await
|
|
462
|
+
const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
|
|
463
|
+
if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
|
|
464
|
+
await rm(releaseRoot, { recursive: true, force: true });
|
|
465
|
+
}
|
|
164
466
|
await mkdir(releaseRoot, { recursive: true });
|
|
165
467
|
const installedPaths = [];
|
|
166
|
-
for (const relativePath of
|
|
167
|
-
const sourcePath =
|
|
468
|
+
for (const relativePath of installCopyPathsForBoard(latest.board_id)) {
|
|
469
|
+
const sourcePath = await resolveInstallSourcePathForBoard(sourceRoot.path, relativePath, latest.board_id);
|
|
168
470
|
if (!await pathExists(sourcePath)) {
|
|
169
471
|
continue;
|
|
170
472
|
}
|
|
171
473
|
const targetPath = resolve(releaseRoot, relativePath);
|
|
172
474
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
173
|
-
await cp(sourcePath, targetPath, {
|
|
475
|
+
await cp(sourcePath, targetPath, {
|
|
476
|
+
recursive: true,
|
|
477
|
+
force: true,
|
|
478
|
+
preserveTimestamps: true,
|
|
479
|
+
verbatimSymlinks: true
|
|
480
|
+
});
|
|
174
481
|
installedPaths.push(relativePath);
|
|
175
482
|
}
|
|
176
|
-
await
|
|
177
|
-
|
|
483
|
+
await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
|
|
484
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
|
|
485
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
|
|
178
486
|
if (!validation.ok) {
|
|
179
487
|
throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
|
|
180
488
|
}
|
|
@@ -183,6 +491,7 @@ export async function installLocalToolchain(options = {}) {
|
|
|
183
491
|
version: latest.version,
|
|
184
492
|
channel: latest.channel,
|
|
185
493
|
host: latest.host,
|
|
494
|
+
mode: installMode,
|
|
186
495
|
install_root: installRoot,
|
|
187
496
|
release_root: releaseRoot,
|
|
188
497
|
registry_path: localToolchainRegistryPath(installRoot),
|
|
@@ -196,19 +505,12 @@ export async function installLocalToolchain(options = {}) {
|
|
|
196
505
|
await rm(tempDir, { recursive: true, force: true });
|
|
197
506
|
}
|
|
198
507
|
}
|
|
199
|
-
export async function validateLocalToolchain(
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
|
|
206
|
-
["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
|
|
207
|
-
["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
|
|
208
|
-
["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
|
|
209
|
-
["Rockchip mkimage", "tools/mac/mkimage"],
|
|
210
|
-
["Rockchip resource_tool", "tools/mac/resource_tool"]
|
|
211
|
-
];
|
|
508
|
+
export async function validateLocalToolchain(input) {
|
|
509
|
+
const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
|
|
510
|
+
const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
|
|
511
|
+
const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
|
|
512
|
+
const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot, boardId);
|
|
513
|
+
const required = requiredLocalToolchainChecks(mode, boardId);
|
|
212
514
|
const checked_paths = [];
|
|
213
515
|
for (const [label, relativePath] of required) {
|
|
214
516
|
const absolutePath = resolve(resolvedRoot, relativePath);
|
|
@@ -219,22 +521,290 @@ export async function validateLocalToolchain(releaseRoot) {
|
|
|
219
521
|
});
|
|
220
522
|
}
|
|
221
523
|
const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
|
|
524
|
+
const path_leaks = await localToolchainPathLeaks(resolvedRoot, boardId);
|
|
525
|
+
const missing_groups = localToolchainMissingGroups(checked_paths, path_leaks);
|
|
526
|
+
const repair_command = missing_paths.length > 0 || path_leaks.length > 0
|
|
527
|
+
? `embedlabs local toolchain install --board ${boardId} --mode ${mode}`
|
|
528
|
+
: undefined;
|
|
222
529
|
return {
|
|
223
|
-
ok: missing_paths.length === 0,
|
|
530
|
+
ok: missing_paths.length === 0 && path_leaks.length === 0,
|
|
531
|
+
mode,
|
|
224
532
|
host: {
|
|
225
533
|
platform: platform(),
|
|
226
534
|
arch: arch()
|
|
227
535
|
},
|
|
228
|
-
board_id:
|
|
536
|
+
board_id: boardId,
|
|
229
537
|
release_root: resolvedRoot,
|
|
230
538
|
checked_paths,
|
|
231
539
|
missing_paths,
|
|
540
|
+
path_leaks,
|
|
541
|
+
missing_groups,
|
|
542
|
+
repair_command,
|
|
543
|
+
summary_for_user: localToolchainValidationSummary({
|
|
544
|
+
boardId,
|
|
545
|
+
mode,
|
|
546
|
+
ok: missing_paths.length === 0,
|
|
547
|
+
missingGroups: missing_groups,
|
|
548
|
+
repairCommand: repair_command
|
|
549
|
+
}),
|
|
232
550
|
notes: [
|
|
233
551
|
"Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
|
|
234
|
-
|
|
552
|
+
`This validator checks the ${boardId} local support layout for install mode ${mode}.`
|
|
235
553
|
]
|
|
236
554
|
};
|
|
237
555
|
}
|
|
556
|
+
function localToolchainMissingGroups(checkedPaths, pathLeaks = []) {
|
|
557
|
+
const groups = new Set();
|
|
558
|
+
if (pathLeaks.length > 0) {
|
|
559
|
+
groups.add("portable-paths/安装包可移植路径");
|
|
560
|
+
}
|
|
561
|
+
for (const check of checkedPaths) {
|
|
562
|
+
if (check.exists) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const text = `${check.label} ${check.path}`.toLowerCase();
|
|
566
|
+
if (text.includes("qt host") || text.includes("qtquick") || text.includes("qt cmake") || text.includes("qt target")) {
|
|
567
|
+
groups.add("qt/Qt host、target 或实时预览组件");
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (text.includes("compiler") || text.includes("readelf") || text.includes("sysroot") || text.includes("include directory") || text.includes("clang wrapper") || text.includes("gcc libraries")) {
|
|
571
|
+
groups.add("compile/交叉编译器与 sysroot");
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (text.includes("boot") || text.includes("dtb") || text.includes("resource image") || text.includes("metadata") || text.includes("/meta")) {
|
|
575
|
+
groups.add("board-resources/启动资源、DTB 与元数据");
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (text.includes("image") || text.includes("rootfs") || text.includes("parameter")) {
|
|
579
|
+
groups.add("images/基础镜像资源");
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (text.includes("rp2350") || text.includes("pico")) {
|
|
583
|
+
groups.add("rp2350/RP2350 SDK、固件或监控运行时");
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
groups.add("runtime/运行时文件");
|
|
587
|
+
}
|
|
588
|
+
return [...groups];
|
|
589
|
+
}
|
|
590
|
+
async function rewriteLocalToolchainPortablePaths(releaseRoot, boardId) {
|
|
591
|
+
if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const replacements = taishanPiPortablePathReplacements(releaseRoot);
|
|
595
|
+
const files = await taishanPiPortableTextFiles(releaseRoot);
|
|
596
|
+
for (const file of files) {
|
|
597
|
+
let content;
|
|
598
|
+
try {
|
|
599
|
+
content = await readFile(file, "utf8");
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const rewritten = rewritePortablePathContent(content, replacements);
|
|
605
|
+
if (rewritten !== content) {
|
|
606
|
+
await writeFile(file, rewritten, "utf8");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function localToolchainPathLeaks(releaseRoot, boardId) {
|
|
611
|
+
if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID || !await pathExists(releaseRoot)) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
const forbidden = taishanPiForbiddenPortablePathFragments();
|
|
615
|
+
const files = await taishanPiPortableTextFiles(releaseRoot);
|
|
616
|
+
const leaks = [];
|
|
617
|
+
for (const file of files) {
|
|
618
|
+
let content;
|
|
619
|
+
try {
|
|
620
|
+
content = await readFile(file, "utf8");
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
for (const fragment of forbidden) {
|
|
626
|
+
if (content.includes(fragment)) {
|
|
627
|
+
leaks.push({
|
|
628
|
+
label: "portable path check",
|
|
629
|
+
path: file,
|
|
630
|
+
forbidden: fragment
|
|
631
|
+
});
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return leaks;
|
|
637
|
+
}
|
|
638
|
+
async function taishanPiPortableTextFiles(releaseRoot) {
|
|
639
|
+
const candidates = [
|
|
640
|
+
"toolchain/qt6-rk3566-llvm-toolchain.cmake",
|
|
641
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/bin/target_qt.conf",
|
|
642
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/Qt6Dependencies.cmake",
|
|
643
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/qt.toolchain.cmake",
|
|
644
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
|
|
645
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qdevice.pri",
|
|
646
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qmodule.pri",
|
|
647
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_core_private.pri",
|
|
648
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_gui_private.pri",
|
|
649
|
+
"qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri",
|
|
650
|
+
"qt-host/qt6-host-macos-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
|
|
651
|
+
"qt-host/qt6-host-macos-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri"
|
|
652
|
+
];
|
|
653
|
+
const files = [];
|
|
654
|
+
for (const candidate of candidates) {
|
|
655
|
+
const file = join(releaseRoot, candidate);
|
|
656
|
+
if (await pathExists(file)) {
|
|
657
|
+
files.push(file);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return files;
|
|
661
|
+
}
|
|
662
|
+
function taishanPiPortablePathReplacements(releaseRoot) {
|
|
663
|
+
const normalizedRoot = resolve(releaseRoot);
|
|
664
|
+
return [
|
|
665
|
+
["@EMBEDLABS_RELEASE_ROOT@", normalizedRoot],
|
|
666
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/scripts/qt6-rk3566-llvm-toolchain.cmake", join(normalizedRoot, "toolchain", "qt6-rk3566-llvm-toolchain.cmake")],
|
|
667
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/.llvm-cross", join(normalizedRoot, "toolchain", "llvm-cross")],
|
|
668
|
+
["/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal", normalizedRoot],
|
|
669
|
+
["/Volumes/LLVM-TSPI/sdk-tools/buildroot/output/rockchip_rk3566/host", join(normalizedRoot, "toolchain", "host")],
|
|
670
|
+
["/Volumes/LLVM-TSPI/qt6-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3")],
|
|
671
|
+
["/Volumes/LLVM-TSPI/qt6-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3")],
|
|
672
|
+
["/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")],
|
|
673
|
+
["/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")],
|
|
674
|
+
["/Volumes/LLVM-TSPI/qt-everywhere-src-6.8.3", join(normalizedRoot, "meta", "source", "qt-everywhere-src-6.8.3")],
|
|
675
|
+
["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi", join(normalizedRoot, "meta", "source", "llvm-build-tspi")]
|
|
676
|
+
];
|
|
677
|
+
}
|
|
678
|
+
function taishanPiForbiddenPortablePathFragments() {
|
|
679
|
+
return [
|
|
680
|
+
"/Volumes/LLVM-TSPI",
|
|
681
|
+
"/Users/kvell/kk-project/DBT-Agent-Project"
|
|
682
|
+
];
|
|
683
|
+
}
|
|
684
|
+
function rewritePortablePathContent(content, replacements) {
|
|
685
|
+
let rewritten = content;
|
|
686
|
+
for (const [from, to] of replacements) {
|
|
687
|
+
rewritten = rewritten.split(from).join(to);
|
|
688
|
+
}
|
|
689
|
+
return rewritten;
|
|
690
|
+
}
|
|
691
|
+
function localToolchainValidationSummary(input) {
|
|
692
|
+
const boardName = input.boardId === DEFAULT_BOARD_ID
|
|
693
|
+
? "泰山派 1M-RK3566"
|
|
694
|
+
: input.boardId;
|
|
695
|
+
if (input.ok) {
|
|
696
|
+
if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
|
|
697
|
+
return `${boardName} Qt 本地开发环境完整,可以继续 QtQuick 原型、一键交叉编译和真机部署。`;
|
|
698
|
+
}
|
|
699
|
+
return `${boardName} 本地开发环境完整,可以继续使用 ${input.mode} 模式。`;
|
|
700
|
+
}
|
|
701
|
+
const groups = input.missingGroups.length > 0 ? input.missingGroups.join("、") : "部分文件";
|
|
702
|
+
if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
|
|
703
|
+
return `${boardName} Qt 本地开发环境不完整,缺少 ${groups}。这会影响一键交叉编译、实时预览或真机部署;请先执行 ${input.repairCommand} 补装缺失组件。`;
|
|
704
|
+
}
|
|
705
|
+
return `${boardName} 本地开发环境不完整,缺少 ${groups};请先执行 ${input.repairCommand} 补装缺失组件。`;
|
|
706
|
+
}
|
|
707
|
+
function normalizeLocalToolchainInstallMode(mode) {
|
|
708
|
+
const normalized = mode?.trim();
|
|
709
|
+
if (!normalized) {
|
|
710
|
+
return "qt";
|
|
711
|
+
}
|
|
712
|
+
if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
|
|
713
|
+
return normalized;
|
|
714
|
+
}
|
|
715
|
+
throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
|
|
716
|
+
}
|
|
717
|
+
function requiredLocalToolchainChecks(mode, boardId) {
|
|
718
|
+
if (isRp2350MonitorBoardId(boardId)) {
|
|
719
|
+
return requiredRp2350MonitorChecks(mode);
|
|
720
|
+
}
|
|
721
|
+
const base = [
|
|
722
|
+
["release root", "."],
|
|
723
|
+
["Rockchip mkimage", "tools/mac/mkimage"],
|
|
724
|
+
["Rockchip dumpimage", "tools/mac/dumpimage"],
|
|
725
|
+
["Rockchip resource_tool", "tools/mac/resource_tool"],
|
|
726
|
+
["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
|
|
727
|
+
["boot resource image", "boot-workspace/out/resource.img"],
|
|
728
|
+
["boot image", "boot-workspace/out/boot.img"],
|
|
729
|
+
["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
|
|
730
|
+
["package metadata", "meta"]
|
|
731
|
+
];
|
|
732
|
+
const compile = [
|
|
733
|
+
["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
|
|
734
|
+
["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
|
|
735
|
+
["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
|
|
736
|
+
["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
|
|
737
|
+
["host GCC libraries", "toolchain/host/lib/gcc"],
|
|
738
|
+
["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
|
|
739
|
+
["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
|
|
740
|
+
];
|
|
741
|
+
const qt = [
|
|
742
|
+
["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
|
|
743
|
+
["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
|
|
744
|
+
["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
|
|
745
|
+
["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
|
|
746
|
+
];
|
|
747
|
+
const images = [
|
|
748
|
+
["base boot image", "images/current/boot.img"],
|
|
749
|
+
["base rootfs image", "images/current/rootfs.img"],
|
|
750
|
+
["base image parameter", "images/current/parameter.txt"]
|
|
751
|
+
];
|
|
752
|
+
const full = [
|
|
753
|
+
["rootfs overlay", "userdata/rootfs"]
|
|
754
|
+
];
|
|
755
|
+
if (mode === "minimal") {
|
|
756
|
+
return base;
|
|
757
|
+
}
|
|
758
|
+
if (mode === "compile") {
|
|
759
|
+
return [...base, ...compile];
|
|
760
|
+
}
|
|
761
|
+
if (mode === "qt") {
|
|
762
|
+
return [...base, ...compile, ...qt];
|
|
763
|
+
}
|
|
764
|
+
if (mode === "images") {
|
|
765
|
+
return [...base, ...images];
|
|
766
|
+
}
|
|
767
|
+
return [...base, ...compile, ...qt, ...images, ...full];
|
|
768
|
+
}
|
|
769
|
+
function requiredRp2350MonitorChecks(mode) {
|
|
770
|
+
const base = [
|
|
771
|
+
["release root", "."],
|
|
772
|
+
];
|
|
773
|
+
const runtime = [
|
|
774
|
+
["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
|
|
775
|
+
["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
|
|
776
|
+
["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
|
|
777
|
+
["RP2350 picotool", "toolkit-runtime/rp2350-monitor/tools/picotool"],
|
|
778
|
+
["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
|
|
779
|
+
["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
|
|
780
|
+
["package metadata", "meta"]
|
|
781
|
+
];
|
|
782
|
+
const firmware = [
|
|
783
|
+
["RP2350 Monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
|
|
784
|
+
["RP2350 Monitor firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
|
|
785
|
+
];
|
|
786
|
+
const initialFirmware = [
|
|
787
|
+
["Pico 2 W initialization UF2", "rp2350-initial-firmware/pico2w/initial.uf2"],
|
|
788
|
+
["ColorEasyPICO2 initialization UF2", "rp2350-initial-firmware/coloreasypico2/initial.uf2"]
|
|
789
|
+
];
|
|
790
|
+
const compile = [
|
|
791
|
+
["Pico SDK", "rp2350-sdk/pico-sdk/pico_sdk_init.cmake"],
|
|
792
|
+
["Pico SDK CMakeLists", "rp2350-sdk/pico-sdk/CMakeLists.txt"],
|
|
793
|
+
["ARM bare-metal C compiler", "rp2350-sdk/toolchains/arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi/bin/arm-none-eabi-gcc"],
|
|
794
|
+
["ARM bare-metal C++ compiler", "rp2350-sdk/toolchains/arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi/bin/arm-none-eabi-g++"],
|
|
795
|
+
["picotool", "rp2350-sdk/picotool/install/picotool/picotool"]
|
|
796
|
+
];
|
|
797
|
+
if (mode === "compile") {
|
|
798
|
+
return [...base, ...compile, ...initialFirmware];
|
|
799
|
+
}
|
|
800
|
+
if (mode === "firmware") {
|
|
801
|
+
return [...base, ...runtime, ...firmware, ...initialFirmware];
|
|
802
|
+
}
|
|
803
|
+
if (mode === "full") {
|
|
804
|
+
return [...base, ...runtime, ...firmware, ...initialFirmware, ...compile];
|
|
805
|
+
}
|
|
806
|
+
return [...base, ...runtime];
|
|
807
|
+
}
|
|
238
808
|
export async function compileTaishanPiSingleFile(options) {
|
|
239
809
|
assertAuthenticated(options.auth);
|
|
240
810
|
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
@@ -304,15 +874,14 @@ export async function buildTaishanPiQtSmoke(options) {
|
|
|
304
874
|
}
|
|
305
875
|
async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
306
876
|
const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
|
|
307
|
-
|
|
308
|
-
if (!candidateRoot) {
|
|
877
|
+
if (!explicitRoot) {
|
|
309
878
|
return {
|
|
310
879
|
channel: BUILT_IN_CHANNEL,
|
|
311
880
|
manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
|
|
312
881
|
metadataRoot: undefined
|
|
313
882
|
};
|
|
314
883
|
}
|
|
315
|
-
const root = resolve(
|
|
884
|
+
const root = resolve(explicitRoot);
|
|
316
885
|
const channelPath = join(root, "channels", channelName, "index.json");
|
|
317
886
|
const channel = JSON.parse(await readFile(channelPath, "utf8"));
|
|
318
887
|
if (channel.schema !== "embedlabs.channel.v1") {
|
|
@@ -334,6 +903,149 @@ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
|
334
903
|
}
|
|
335
904
|
return { channel, manifests, metadataRoot: root };
|
|
336
905
|
}
|
|
906
|
+
async function resolveLocalToolchainDownloadPlan(input) {
|
|
907
|
+
const requestedBoardId = normalizeBoardId(input.boardId);
|
|
908
|
+
const compatibleBoardIds = compatibleDownloadBoardIds(requestedBoardId);
|
|
909
|
+
const channelUrl = downloadChannelUrl(input.channel);
|
|
910
|
+
const channel = await fetchJson(channelUrl);
|
|
911
|
+
if (channel.schema !== "embedlabs.download-channel.v1") {
|
|
912
|
+
throw new Error(`Unexpected download channel schema ${channel.schema}.`);
|
|
913
|
+
}
|
|
914
|
+
const entries = channel.packages ?? [];
|
|
915
|
+
let entry;
|
|
916
|
+
for (const boardId of compatibleBoardIds) {
|
|
917
|
+
entry = entries.find((item) => {
|
|
918
|
+
return normalizeBoardId(item.board_id ?? "") === boardId
|
|
919
|
+
&& item.host === input.host
|
|
920
|
+
&& item.toolchain === input.toolchain
|
|
921
|
+
&& (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
|
|
922
|
+
});
|
|
923
|
+
if (entry) {
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (!entry?.manifest) {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
const manifestUrl = new URL(entry.manifest, channelUrl).toString();
|
|
931
|
+
const manifest = await fetchJson(manifestUrl);
|
|
932
|
+
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
933
|
+
throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
934
|
+
}
|
|
935
|
+
const manifestBoardId = normalizeBoardId(manifest.board_id ?? "");
|
|
936
|
+
if (!compatibleBoardIds.includes(manifestBoardId) || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
|
|
937
|
+
throw new Error(`Download manifest does not match requested ${requestedBoardId}/${input.host}/${input.toolchain}.`);
|
|
938
|
+
}
|
|
939
|
+
if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
|
|
940
|
+
&& (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
|
|
941
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
|
|
942
|
+
}
|
|
943
|
+
const mirrors = manifest.archive
|
|
944
|
+
? orderDownloadMirrors((manifest.mirrors ?? [])
|
|
945
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
946
|
+
.map((mirror) => ({
|
|
947
|
+
kind: mirror.kind || "unknown",
|
|
948
|
+
enabled: mirror.enabled !== false,
|
|
949
|
+
url: mirror.url,
|
|
950
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
|
|
951
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
|
|
952
|
+
})), manifest.download_policy?.preferred_order)
|
|
953
|
+
: [];
|
|
954
|
+
const components = downloadComponentsForBoard(requestedBoardId, (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl)));
|
|
955
|
+
const first = mirrors[0];
|
|
956
|
+
if (!first && components.length === 0) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
channel_url: channelUrl,
|
|
961
|
+
manifest_url: manifestUrl,
|
|
962
|
+
package_id: manifest.id,
|
|
963
|
+
version: manifest.version,
|
|
964
|
+
board_id: requestedBoardId,
|
|
965
|
+
host: input.host,
|
|
966
|
+
toolchain: input.toolchain,
|
|
967
|
+
source_url: first?.url,
|
|
968
|
+
mirror_kind: first?.kind,
|
|
969
|
+
archive: manifest.archive ? {
|
|
970
|
+
file: manifest.archive.file,
|
|
971
|
+
size_bytes: manifest.archive.size_bytes,
|
|
972
|
+
sha256: manifest.archive.sha256,
|
|
973
|
+
content_type: manifest.archive.content_type
|
|
974
|
+
} : undefined,
|
|
975
|
+
mirrors,
|
|
976
|
+
components: components.length > 0 ? components : undefined,
|
|
977
|
+
default_mode: manifest.download_policy?.default_mode
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function normalizeDownloadComponent(component, manifest, manifestUrl) {
|
|
981
|
+
if (!component.id || !component.version) {
|
|
982
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
|
|
983
|
+
}
|
|
984
|
+
if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
|
|
985
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
|
|
986
|
+
}
|
|
987
|
+
const mirrors = orderDownloadMirrors((component.mirrors ?? [])
|
|
988
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
989
|
+
.map((mirror) => ({
|
|
990
|
+
kind: mirror.kind || "unknown",
|
|
991
|
+
enabled: mirror.enabled !== false,
|
|
992
|
+
url: new URL(mirror.url, manifestUrl).toString(),
|
|
993
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
|
|
994
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
|
|
995
|
+
})), manifest.download_policy?.preferred_order);
|
|
996
|
+
const first = mirrors[0];
|
|
997
|
+
if (!first) {
|
|
998
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
id: component.id,
|
|
1002
|
+
version: component.version,
|
|
1003
|
+
role: component.role,
|
|
1004
|
+
install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
|
|
1005
|
+
archive: {
|
|
1006
|
+
file: component.archive.file,
|
|
1007
|
+
size_bytes: component.archive.size_bytes,
|
|
1008
|
+
sha256: component.archive.sha256,
|
|
1009
|
+
content_type: component.archive.content_type
|
|
1010
|
+
},
|
|
1011
|
+
source_url: first.url,
|
|
1012
|
+
mirror_kind: first.kind,
|
|
1013
|
+
mirrors
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function orderDownloadMirrors(mirrors, preferredOrder) {
|
|
1017
|
+
const preference = preferredOrder?.length
|
|
1018
|
+
? preferredOrder
|
|
1019
|
+
: ["github_release", "embedlabs_cdn", "cloudfront", "embedlabs_server"];
|
|
1020
|
+
return [...mirrors].sort((left, right) => mirrorRank(left.kind, preference) - mirrorRank(right.kind, preference));
|
|
1021
|
+
}
|
|
1022
|
+
function mirrorRank(kind, preferredOrder) {
|
|
1023
|
+
const index = preferredOrder.indexOf(kind);
|
|
1024
|
+
return index >= 0 ? index : preferredOrder.length + 1;
|
|
1025
|
+
}
|
|
1026
|
+
async function fetchJson(url) {
|
|
1027
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
|
|
1028
|
+
if (!response.ok) {
|
|
1029
|
+
throw new Error(`HTTP ${response.status} while loading ${url}`);
|
|
1030
|
+
}
|
|
1031
|
+
return await response.json();
|
|
1032
|
+
}
|
|
1033
|
+
function downloadChannelUrl(channelName) {
|
|
1034
|
+
const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
|
|
1035
|
+
|| process.env.EMBEDLABS_DOWNLOAD_CHANNEL_URL?.trim();
|
|
1036
|
+
if (explicit) {
|
|
1037
|
+
return explicit;
|
|
1038
|
+
}
|
|
1039
|
+
return `${downloadBaseUrl()}/downloads/metadata/channels/${encodeURIComponent(channelName)}/index.json`;
|
|
1040
|
+
}
|
|
1041
|
+
function downloadBaseUrl() {
|
|
1042
|
+
return trimTrailingSlash(process.env.EMBED_DOWNLOAD_BASE_URL?.trim()
|
|
1043
|
+
|| process.env.EMBEDLABS_DOWNLOAD_BASE_URL?.trim()
|
|
1044
|
+
|| DEFAULT_DOWNLOAD_BASE_URL);
|
|
1045
|
+
}
|
|
1046
|
+
function trimTrailingSlash(value) {
|
|
1047
|
+
return value.replace(/\/+$/, "");
|
|
1048
|
+
}
|
|
337
1049
|
function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
|
|
338
1050
|
if (seen.has(packageId)) {
|
|
339
1051
|
return [];
|
|
@@ -356,14 +1068,153 @@ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
|
|
|
356
1068
|
return refs;
|
|
357
1069
|
}
|
|
358
1070
|
function boardPackageIdFor(boardId) {
|
|
359
|
-
|
|
1071
|
+
const normalized = normalizeBoardId(boardId);
|
|
1072
|
+
if (normalized === DEFAULT_BOARD_ID || normalized === "taishanpi" || normalized === "taishanpi-1m-rk3566") {
|
|
360
1073
|
return "embedlabs.board.taishanpi.1m-rk3566";
|
|
361
1074
|
}
|
|
362
|
-
if (
|
|
1075
|
+
if (normalized === COLOREASYPICO2_RP2350_BOARD_ID
|
|
1076
|
+
|| normalized === "coloreasypico2"
|
|
1077
|
+
|| normalized === "color-easy-pico2"
|
|
1078
|
+
|| normalized === "color-easy-pico-2"
|
|
1079
|
+
|| normalized === "color-easy-pico-2-rp2350-monitor"
|
|
1080
|
+
|| normalized === "ce-pico2") {
|
|
1081
|
+
return "embedlabs.board.coloreasypico2.rp2350-monitor";
|
|
1082
|
+
}
|
|
1083
|
+
if (normalized === PICO2W_RP2350_BOARD_ID
|
|
1084
|
+
|| normalized === "pico2w"
|
|
1085
|
+
|| normalized === "pico-2-w"
|
|
1086
|
+
|| normalized === "pico2"
|
|
1087
|
+
|| normalized === "rp2350"
|
|
1088
|
+
|| normalized === "rp2350-monitor") {
|
|
1089
|
+
return "embedlabs.board.pico2w.rp2350-monitor";
|
|
1090
|
+
}
|
|
1091
|
+
if (normalized.startsWith("embedlabs.board.")) {
|
|
363
1092
|
return boardId;
|
|
364
1093
|
}
|
|
365
1094
|
throw new Error(`Unsupported local toolchain board ${boardId}.`);
|
|
366
1095
|
}
|
|
1096
|
+
function packageIdForBoardFilter(boardId) {
|
|
1097
|
+
try {
|
|
1098
|
+
return boardPackageIdFor(boardId);
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function boardIdForPackageManifest(manifest) {
|
|
1105
|
+
const explicit = manifest.board_id;
|
|
1106
|
+
if (explicit?.trim()) {
|
|
1107
|
+
return normalizeBoardId(explicit);
|
|
1108
|
+
}
|
|
1109
|
+
if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
|
|
1110
|
+
return DEFAULT_BOARD_ID;
|
|
1111
|
+
}
|
|
1112
|
+
if (manifest.id.startsWith("embedlabs.board.")) {
|
|
1113
|
+
return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
|
|
1114
|
+
}
|
|
1115
|
+
return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
|
|
1116
|
+
}
|
|
1117
|
+
function normalizeBoardId(boardId) {
|
|
1118
|
+
return boardId.trim().toLowerCase().replaceAll("_", "-");
|
|
1119
|
+
}
|
|
1120
|
+
function isRp2350MonitorBoardId(boardId) {
|
|
1121
|
+
const normalized = normalizeBoardId(boardId);
|
|
1122
|
+
return normalized === PICO2W_RP2350_BOARD_ID
|
|
1123
|
+
|| normalized === COLOREASYPICO2_RP2350_BOARD_ID
|
|
1124
|
+
|| normalized === "pico2w"
|
|
1125
|
+
|| normalized === "pico-2-w"
|
|
1126
|
+
|| normalized === "pico2"
|
|
1127
|
+
|| normalized === "rp2350"
|
|
1128
|
+
|| normalized === "rp2350-monitor"
|
|
1129
|
+
|| normalized === "coloreasypico2"
|
|
1130
|
+
|| normalized === "color-easy-pico2"
|
|
1131
|
+
|| normalized === "color-easy-pico-2"
|
|
1132
|
+
|| normalized === "ce-pico2";
|
|
1133
|
+
}
|
|
1134
|
+
function compatibleDownloadBoardIds(boardId) {
|
|
1135
|
+
const normalized = normalizeBoardId(boardId);
|
|
1136
|
+
if (normalized === COLOREASYPICO2_RP2350_BOARD_ID) {
|
|
1137
|
+
return [COLOREASYPICO2_RP2350_BOARD_ID, PICO2W_RP2350_BOARD_ID];
|
|
1138
|
+
}
|
|
1139
|
+
return [normalized];
|
|
1140
|
+
}
|
|
1141
|
+
function downloadComponentsForBoard(boardId, components) {
|
|
1142
|
+
const normalized = normalizeBoardId(boardId);
|
|
1143
|
+
if (normalized === DEFAULT_BOARD_ID) {
|
|
1144
|
+
return components.filter((component) => !isRp2350MonitorComponent(component));
|
|
1145
|
+
}
|
|
1146
|
+
return components;
|
|
1147
|
+
}
|
|
1148
|
+
function isRp2350MonitorComponent(component) {
|
|
1149
|
+
const text = `${component.id} ${component.role ?? ""} ${component.archive.file}`.toLowerCase();
|
|
1150
|
+
return text.includes("rp2350-monitor") || text.includes("pico2w-rp2350-monitor");
|
|
1151
|
+
}
|
|
1152
|
+
function compareVersionLike(left, right) {
|
|
1153
|
+
return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
|
|
1154
|
+
}
|
|
1155
|
+
function isRecord(value) {
|
|
1156
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1157
|
+
}
|
|
1158
|
+
function packageHostSupport(packages, manifests, host) {
|
|
1159
|
+
const unsupportedPackages = [];
|
|
1160
|
+
for (const item of packages) {
|
|
1161
|
+
const manifest = manifests.get(item.id);
|
|
1162
|
+
if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
|
|
1163
|
+
unsupportedPackages.push(`${item.id}@${manifest.version}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
supported: unsupportedPackages.length === 0,
|
|
1168
|
+
unsupportedPackages
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function environmentNotes(input) {
|
|
1172
|
+
const notes = [];
|
|
1173
|
+
if (normalizeBoardId(input.boardId) === COLOREASYPICO2_RP2350_BOARD_ID) {
|
|
1174
|
+
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.");
|
|
1175
|
+
}
|
|
1176
|
+
if (isRp2350MonitorBoardId(input.boardId)) {
|
|
1177
|
+
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.");
|
|
1178
|
+
}
|
|
1179
|
+
if (input.status === "available") {
|
|
1180
|
+
notes.push("Environment is available but not installed on this computer.");
|
|
1181
|
+
}
|
|
1182
|
+
if (input.status === "update_available") {
|
|
1183
|
+
notes.push("A newer package is available; run the update command to refresh only the selected environment.");
|
|
1184
|
+
}
|
|
1185
|
+
if (input.status === "unsupported_host") {
|
|
1186
|
+
notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
|
|
1187
|
+
}
|
|
1188
|
+
if (input.downloadError) {
|
|
1189
|
+
notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
|
|
1190
|
+
}
|
|
1191
|
+
return notes;
|
|
1192
|
+
}
|
|
1193
|
+
function localToolchainInstallModesForDownload(download) {
|
|
1194
|
+
if (!download?.components?.length) {
|
|
1195
|
+
return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
|
|
1196
|
+
}
|
|
1197
|
+
const modes = new Set();
|
|
1198
|
+
for (const component of download.components) {
|
|
1199
|
+
for (const mode of component.install_modes ?? []) {
|
|
1200
|
+
modes.add(mode);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return modes.size > 0
|
|
1204
|
+
? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
|
|
1205
|
+
: [...LOCAL_TOOLCHAIN_INSTALL_MODES];
|
|
1206
|
+
}
|
|
1207
|
+
function localToolchainEnvironmentComponent(component) {
|
|
1208
|
+
return {
|
|
1209
|
+
id: component.id,
|
|
1210
|
+
version: component.version,
|
|
1211
|
+
role: component.role,
|
|
1212
|
+
install_modes: component.install_modes,
|
|
1213
|
+
file: component.archive.file,
|
|
1214
|
+
size_bytes: component.archive.size_bytes,
|
|
1215
|
+
source_url: component.source_url
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
367
1218
|
function hostId() {
|
|
368
1219
|
if (platform() === "darwin" && arch() === "arm64") {
|
|
369
1220
|
return "darwin-arm64";
|
|
@@ -381,21 +1232,44 @@ function resolveInstallRoot(installRoot) {
|
|
|
381
1232
|
function localToolchainRegistryPath(installRoot) {
|
|
382
1233
|
return join(installRoot, "registry", "local-toolchains.json");
|
|
383
1234
|
}
|
|
384
|
-
async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
|
|
1235
|
+
async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
|
|
385
1236
|
const registryPath = localToolchainRegistryPath(installRoot);
|
|
386
1237
|
await mkdir(dirname(registryPath), { recursive: true });
|
|
387
|
-
|
|
1238
|
+
let existing = {};
|
|
1239
|
+
try {
|
|
1240
|
+
existing = JSON.parse(await readFile(registryPath, "utf8"));
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
existing = {};
|
|
1244
|
+
}
|
|
1245
|
+
const entry = {
|
|
388
1246
|
installed: true,
|
|
389
1247
|
board_id: latest.board_id,
|
|
390
1248
|
version: latest.version,
|
|
391
1249
|
channel: latest.channel,
|
|
392
1250
|
host: latest.host,
|
|
1251
|
+
mode,
|
|
393
1252
|
release_root: releaseRoot,
|
|
394
1253
|
packages: latest.packages,
|
|
1254
|
+
source,
|
|
1255
|
+
installed_components: source?.components,
|
|
1256
|
+
updated_at: new Date().toISOString()
|
|
1257
|
+
};
|
|
1258
|
+
const environments = isRecord(existing.environments)
|
|
1259
|
+
? { ...existing.environments }
|
|
1260
|
+
: {};
|
|
1261
|
+
environments[normalizeBoardId(latest.board_id)] = entry;
|
|
1262
|
+
const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
|
|
1263
|
+
&& typeof existing.release_root === "string"
|
|
1264
|
+
&& normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
|
|
1265
|
+
const topLevel = preserveTopLevel ? existing : entry;
|
|
1266
|
+
await writeFile(registryPath, `${JSON.stringify({
|
|
1267
|
+
...topLevel,
|
|
1268
|
+
environments,
|
|
395
1269
|
updated_at: new Date().toISOString()
|
|
396
1270
|
}, null, 2)}\n`, "utf8");
|
|
397
1271
|
}
|
|
398
|
-
async function resolveLocalReleaseRoot(releaseRoot) {
|
|
1272
|
+
async function resolveLocalReleaseRoot(releaseRoot, boardId = DEFAULT_BOARD_ID) {
|
|
399
1273
|
if (releaseRoot?.trim()) {
|
|
400
1274
|
return resolve(releaseRoot);
|
|
401
1275
|
}
|
|
@@ -403,13 +1277,13 @@ async function resolveLocalReleaseRoot(releaseRoot) {
|
|
|
403
1277
|
if (envRoot) {
|
|
404
1278
|
return resolve(envRoot);
|
|
405
1279
|
}
|
|
406
|
-
const current = await currentLocalToolchain(undefined,
|
|
1280
|
+
const current = await currentLocalToolchain(undefined, boardId);
|
|
407
1281
|
if (current.release_root) {
|
|
408
1282
|
return resolve(current.release_root);
|
|
409
1283
|
}
|
|
410
1284
|
return resolve(DEFAULT_RELEASE_ROOT);
|
|
411
1285
|
}
|
|
412
|
-
async function sourceReleaseRootForInstall(options, installRoot, tempDir) {
|
|
1286
|
+
async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir) {
|
|
413
1287
|
if (options.sourceReleaseRoot) {
|
|
414
1288
|
return {
|
|
415
1289
|
path: resolve(options.sourceReleaseRoot),
|
|
@@ -429,32 +1303,270 @@ async function sourceReleaseRootForInstall(options, installRoot, tempDir) {
|
|
|
429
1303
|
source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
|
|
430
1304
|
};
|
|
431
1305
|
}
|
|
1306
|
+
if (latest.download?.components?.length) {
|
|
1307
|
+
return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
|
|
1308
|
+
}
|
|
1309
|
+
if (latest.download) {
|
|
1310
|
+
const failures = [];
|
|
1311
|
+
for (const mirror of latest.download.mirrors) {
|
|
1312
|
+
if (!mirror.enabled) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (!latest.download.archive) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
|
|
1319
|
+
try {
|
|
1320
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
1321
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
1322
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
1323
|
+
});
|
|
1324
|
+
await rm(extractRoot, { recursive: true, force: true });
|
|
1325
|
+
await mkdir(extractRoot, { recursive: true });
|
|
1326
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
1327
|
+
if (extracted.exit_code !== 0) {
|
|
1328
|
+
throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
path: await findReleaseRoot(extractRoot),
|
|
1332
|
+
source: {
|
|
1333
|
+
kind: "url",
|
|
1334
|
+
value: mirror.url,
|
|
1335
|
+
downloaded_path: downloadedPath,
|
|
1336
|
+
mirror_kind: mirror.kind,
|
|
1337
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
1338
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
catch (error) {
|
|
1343
|
+
failures.push(`${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (failures.length > 0 && !await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
1347
|
+
throw new Error(`Could not install local toolchain from download mirrors: ${failures.join("; ")}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
432
1350
|
if (await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
433
1351
|
return {
|
|
434
1352
|
path: resolve(DEFAULT_RELEASE_ROOT),
|
|
435
1353
|
source: { kind: "directory", value: resolve(DEFAULT_RELEASE_ROOT) }
|
|
436
1354
|
};
|
|
437
1355
|
}
|
|
438
|
-
throw new Error("Local toolchain install
|
|
1356
|
+
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.");
|
|
439
1357
|
}
|
|
440
|
-
async function
|
|
1358
|
+
async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
|
|
1359
|
+
const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
|
|
1360
|
+
const components = selectedDownloadComponents(download.components ?? [], mode);
|
|
1361
|
+
if (components.length === 0) {
|
|
1362
|
+
throw new Error(`No local toolchain components selected for mode ${mode}.`);
|
|
1363
|
+
}
|
|
1364
|
+
const extractRoot = join(tempDir, "extract-components");
|
|
1365
|
+
await mkdir(extractRoot, { recursive: true });
|
|
1366
|
+
const installedComponents = [];
|
|
1367
|
+
const failures = [];
|
|
1368
|
+
for (const component of components) {
|
|
1369
|
+
let installed = false;
|
|
1370
|
+
for (const mirror of component.mirrors) {
|
|
1371
|
+
if (!mirror.enabled) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
1376
|
+
sha256: mirror.sha256 ?? component.archive.sha256,
|
|
1377
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes
|
|
1378
|
+
});
|
|
1379
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
1380
|
+
if (extracted.exit_code !== 0) {
|
|
1381
|
+
throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
|
|
1382
|
+
}
|
|
1383
|
+
installedComponents.push({
|
|
1384
|
+
id: component.id,
|
|
1385
|
+
version: component.version,
|
|
1386
|
+
role: component.role,
|
|
1387
|
+
archive_file: component.archive.file,
|
|
1388
|
+
mirror_kind: mirror.kind,
|
|
1389
|
+
downloaded_path: downloadedPath,
|
|
1390
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
|
|
1391
|
+
sha256: mirror.sha256 ?? component.archive.sha256
|
|
1392
|
+
});
|
|
1393
|
+
installed = true;
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (!installed) {
|
|
1401
|
+
throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
path: extractRoot,
|
|
1406
|
+
source: {
|
|
1407
|
+
kind: "components",
|
|
1408
|
+
value: download.manifest_url,
|
|
1409
|
+
mirror_kind: "components",
|
|
1410
|
+
size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
|
|
1411
|
+
components: installedComponents
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function selectedDownloadComponents(components, mode) {
|
|
1416
|
+
return components.filter((component) => {
|
|
1417
|
+
if (!component.install_modes?.length) {
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
return component.install_modes.includes(mode);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
function installCopyPathsForBoard(boardId) {
|
|
1424
|
+
if (normalizeBoardId(boardId) === DEFAULT_BOARD_ID) {
|
|
1425
|
+
return INSTALL_COPY_PATHS.filter((relativePath) => {
|
|
1426
|
+
const normalized = relativePath.toLowerCase();
|
|
1427
|
+
return normalized !== "toolkit-runtime/rp2350-monitor"
|
|
1428
|
+
&& normalized !== "toolkit-runtime/rp2350-monitor/";
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return INSTALL_COPY_PATHS;
|
|
1432
|
+
}
|
|
1433
|
+
async function resolveInstallSourcePathForBoard(sourceRoot, relativePath, boardId) {
|
|
1434
|
+
const direct = resolve(sourceRoot, relativePath);
|
|
1435
|
+
if (await pathExists(direct)) {
|
|
1436
|
+
return direct;
|
|
1437
|
+
}
|
|
1438
|
+
if (!isRp2350MonitorBoardId(boardId)) {
|
|
1439
|
+
return direct;
|
|
1440
|
+
}
|
|
1441
|
+
const legacyRp2350Root = await findLegacyRp2350Root(sourceRoot);
|
|
1442
|
+
if (!legacyRp2350Root) {
|
|
1443
|
+
return direct;
|
|
1444
|
+
}
|
|
1445
|
+
if (relativePath === "rp2350-sdk") {
|
|
1446
|
+
return legacyRp2350Root;
|
|
1447
|
+
}
|
|
1448
|
+
if (relativePath === "rp2350-initial-firmware") {
|
|
1449
|
+
return resolve(legacyRp2350Root, "initial_firmware");
|
|
1450
|
+
}
|
|
1451
|
+
if (relativePath === "rp2350-examples") {
|
|
1452
|
+
return resolve(legacyRp2350Root, "validation");
|
|
1453
|
+
}
|
|
1454
|
+
return direct;
|
|
1455
|
+
}
|
|
1456
|
+
async function findLegacyRp2350Root(sourceRoot) {
|
|
1457
|
+
const direct = resolve(sourceRoot, "RP2350");
|
|
1458
|
+
if (await pathExists(resolve(direct, "pico-sdk", "pico_sdk_init.cmake"))) {
|
|
1459
|
+
return direct;
|
|
1460
|
+
}
|
|
1461
|
+
let entries;
|
|
1462
|
+
try {
|
|
1463
|
+
entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
for (const entry of entries) {
|
|
1469
|
+
if (!entry.isDirectory()) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const candidate = resolve(sourceRoot, entry.name, "RP2350");
|
|
1473
|
+
if (await pathExists(resolve(candidate, "pico-sdk", "pico_sdk_init.cmake"))) {
|
|
1474
|
+
return candidate;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return undefined;
|
|
1478
|
+
}
|
|
1479
|
+
async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
|
|
441
1480
|
const downloadsDir = join(installRoot, "cache", "downloads");
|
|
442
1481
|
await mkdir(downloadsDir, { recursive: true });
|
|
443
1482
|
const parsed = new URL(sourceUrl);
|
|
444
1483
|
const filename = basename(parsed.pathname) || `local-toolchain-${Date.now()}.tar.gz`;
|
|
445
1484
|
const outputPath = join(downloadsDir, filename);
|
|
446
1485
|
if (parsed.protocol === "file:") {
|
|
447
|
-
await cp(parsed
|
|
1486
|
+
await cp(fileURLToPath(parsed), outputPath, { force: true });
|
|
448
1487
|
return outputPath;
|
|
449
1488
|
}
|
|
450
|
-
|
|
1489
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1490
|
+
throw new Error(`Unsupported local toolchain download URL protocol: ${parsed.protocol}`);
|
|
1491
|
+
}
|
|
1492
|
+
const partialPath = `${outputPath}.part`;
|
|
1493
|
+
const expectedSize = expected?.size_bytes;
|
|
1494
|
+
const existingComplete = await fileSize(outputPath);
|
|
1495
|
+
const remoteSize = await remoteContentLength(sourceUrl);
|
|
1496
|
+
const targetSize = expectedSize ?? remoteSize;
|
|
1497
|
+
if (existingComplete > 0 && targetSize !== undefined && existingComplete === targetSize) {
|
|
1498
|
+
if (expected?.sha256) {
|
|
1499
|
+
const actual = await sha256(outputPath);
|
|
1500
|
+
if (actual === expected.sha256) {
|
|
1501
|
+
return outputPath;
|
|
1502
|
+
}
|
|
1503
|
+
await rm(outputPath, { force: true });
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
return outputPath;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
let resumeFrom = await fileSize(partialPath);
|
|
1510
|
+
const headers = new Headers();
|
|
1511
|
+
if (resumeFrom > 0) {
|
|
1512
|
+
headers.set("range", `bytes=${resumeFrom}-`);
|
|
1513
|
+
}
|
|
1514
|
+
let response = await fetch(sourceUrl, { headers });
|
|
1515
|
+
if (resumeFrom > 0 && response.status !== 206) {
|
|
1516
|
+
await rm(partialPath, { force: true });
|
|
1517
|
+
resumeFrom = 0;
|
|
1518
|
+
response = await fetch(sourceUrl);
|
|
1519
|
+
}
|
|
451
1520
|
if (!response.ok) {
|
|
452
1521
|
throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
|
|
453
1522
|
}
|
|
454
|
-
|
|
455
|
-
|
|
1523
|
+
if (!response.body) {
|
|
1524
|
+
throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
|
|
1525
|
+
}
|
|
1526
|
+
const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
|
|
1527
|
+
await pipeline(Readable.fromWeb(response.body), writeStream);
|
|
1528
|
+
const downloadedSize = await fileSize(partialPath);
|
|
1529
|
+
if (targetSize !== undefined && downloadedSize !== targetSize) {
|
|
1530
|
+
throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
|
|
1531
|
+
}
|
|
1532
|
+
await rename(partialPath, outputPath);
|
|
1533
|
+
if (expected?.sha256) {
|
|
1534
|
+
const actual = await sha256(outputPath);
|
|
1535
|
+
if (actual !== expected.sha256) {
|
|
1536
|
+
await rm(outputPath, { force: true });
|
|
1537
|
+
throw new Error(`Local toolchain download SHA256 mismatch: expected ${expected.sha256}, got ${actual}.`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
456
1540
|
return outputPath;
|
|
457
1541
|
}
|
|
1542
|
+
async function remoteContentLength(sourceUrl) {
|
|
1543
|
+
try {
|
|
1544
|
+
const response = await fetch(sourceUrl, { method: "HEAD" });
|
|
1545
|
+
if (!response.ok) {
|
|
1546
|
+
return undefined;
|
|
1547
|
+
}
|
|
1548
|
+
const length = response.headers.get("content-length");
|
|
1549
|
+
if (!length) {
|
|
1550
|
+
return undefined;
|
|
1551
|
+
}
|
|
1552
|
+
const parsed = Number.parseInt(length, 10);
|
|
1553
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
1554
|
+
}
|
|
1555
|
+
catch {
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function fileSize(filePath) {
|
|
1560
|
+
try {
|
|
1561
|
+
return (await stat(filePath)).size;
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
return 0;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function safeFileToken(value) {
|
|
1568
|
+
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_") || "mirror";
|
|
1569
|
+
}
|
|
458
1570
|
async function findReleaseRoot(extractRoot) {
|
|
459
1571
|
if (await pathExists(join(extractRoot, "toolchain"))) {
|
|
460
1572
|
return extractRoot;
|