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