@kvell007/embed-labs-cli 0.1.0-alpha.3 → 0.1.0-alpha.31

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.
@@ -1,27 +1,485 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { constants } from "node:fs";
4
- import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
- import { arch, platform } from "node:os";
3
+ import { constants, createWriteStream } from "node:fs";
4
+ import { access, cp, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
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";
12
+ const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
13
+ const PICO2W_RP2350_BOARD_ID = "pico2w-rp2350-monitor";
14
+ const DEFAULT_CHANNEL = "stable";
15
+ const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
16
+ const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
17
+ const BUILT_IN_CHANNEL = {
18
+ schema: "embedlabs.channel.v1",
19
+ channel: DEFAULT_CHANNEL,
20
+ packages: [
21
+ { id: "embedlabs.tools.vendor.rockchip", version: "1.0.0", manifest: "" },
22
+ { id: "embedlabs.tools.common.llvm", version: "22.1.3", manifest: "" },
23
+ { id: "embedlabs.tools.common.e2fsprogs", version: "1.0.0", manifest: "" },
24
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.31", manifest: "" },
25
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.31", manifest: "" },
26
+ { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
27
+ { id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
28
+ { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" },
29
+ { id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.31", manifest: "" }
30
+ ]
31
+ };
32
+ const BUILT_IN_MANIFESTS = {
33
+ "embedlabs.tools.vendor.rockchip": {
34
+ schema: "embedlabs.package.v1",
35
+ id: "embedlabs.tools.vendor.rockchip",
36
+ version: "1.0.0",
37
+ kind: "tools",
38
+ hosts: ["darwin-arm64", "linux-x86_64"],
39
+ provides: ["rockchip.mkimage", "rockchip.dumpimage", "rockchip.resource_tool", "rockchip.rkdeveloptool"]
40
+ },
41
+ "embedlabs.tools.common.llvm": {
42
+ schema: "embedlabs.package.v1",
43
+ id: "embedlabs.tools.common.llvm",
44
+ version: "22.1.3",
45
+ kind: "tools",
46
+ hosts: ["darwin-arm64", "linux-x86_64"],
47
+ provides: ["llvm.clang", "llvm.clangxx", "llvm.ld_lld", "llvm.ar", "llvm.objcopy", "llvm.readelf"]
48
+ },
49
+ "embedlabs.tools.common.e2fsprogs": {
50
+ schema: "embedlabs.package.v1",
51
+ id: "embedlabs.tools.common.e2fsprogs",
52
+ version: "1.0.0",
53
+ kind: "tools",
54
+ hosts: ["darwin-arm64", "linux-x86_64"],
55
+ provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
56
+ },
57
+ "embedlabs.tools.runtime.qtquick-live-preview": {
58
+ schema: "embedlabs.package.v1",
59
+ id: "embedlabs.tools.runtime.qtquick-live-preview",
60
+ version: "1.0.31",
61
+ kind: "tools",
62
+ hosts: ["darwin-arm64", "linux-x86_64"],
63
+ provides: ["qtquick.live_preview", "qtquick.live_preview.inspector", "qtquick.live_preview.feedback"]
64
+ },
65
+ "embedlabs.tools.runtime.rp2350-monitor": {
66
+ schema: "embedlabs.package.v1",
67
+ id: "embedlabs.tools.runtime.rp2350-monitor",
68
+ version: "1.0.31",
69
+ kind: "tools",
70
+ hosts: ["darwin-arm64", "linux-x86_64"],
71
+ provides: ["rp2350.monitor.cli", "rp2350.monitor.logic_analyzer", "rp2350.monitor.logic_decode"]
72
+ },
73
+ "embedlabs.family.rk356x": {
74
+ schema: "embedlabs.package.v1",
75
+ id: "embedlabs.family.rk356x",
76
+ version: "1.0.0",
77
+ kind: "family",
78
+ family: "rk356x",
79
+ requires: [
80
+ { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0" },
81
+ { id: "embedlabs.tools.common.llvm", version: "22.x" },
82
+ { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
83
+ ]
84
+ },
85
+ "embedlabs.family.rp2350": {
86
+ schema: "embedlabs.package.v1",
87
+ id: "embedlabs.family.rp2350",
88
+ version: "1.0.0",
89
+ kind: "family",
90
+ family: "rp2350",
91
+ requires: [
92
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31" }
93
+ ]
94
+ },
95
+ "embedlabs.board.taishanpi.1m-rk3566": {
96
+ schema: "embedlabs.package.v1",
97
+ id: "embedlabs.board.taishanpi.1m-rk3566",
98
+ version: "1.0.31",
99
+ kind: "board",
100
+ family: "rk356x",
101
+ board: "TaishanPi",
102
+ variant: "1M-RK3566",
103
+ requires: [
104
+ { id: "embedlabs.family.rk356x", version: "^1.0.0" },
105
+ { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
106
+ { id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
107
+ { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
108
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.31", roles: ["qtquick-preview"] },
109
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["rp2350-monitor"] }
110
+ ],
111
+ build_modes: ["local-llvm"]
112
+ },
113
+ "embedlabs.board.pico2w.rp2350-monitor": {
114
+ schema: "embedlabs.package.v1",
115
+ id: "embedlabs.board.pico2w.rp2350-monitor",
116
+ version: "1.0.31",
117
+ kind: "board",
118
+ display_name: "Pico 2 W / RP2350 Monitor",
119
+ family: "rp2350",
120
+ board: "Pico 2 W",
121
+ variant: "RP2350 Monitor",
122
+ board_id: PICO2W_RP2350_BOARD_ID,
123
+ requires: [
124
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
125
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
126
+ ],
127
+ build_modes: ["local-monitor"]
128
+ }
129
+ };
130
+ const INSTALL_COPY_PATHS = [
131
+ "toolchain/llvm-cross",
132
+ "toolchain/host",
133
+ "toolchain/host-tools",
134
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
135
+ "qt-target/qt6-rk3566-llvm-6.8.3",
136
+ "qt-host/qt6-host-macos-6.8.3",
137
+ "tools/mac",
138
+ "toolkit-runtime/qtquick-live-preview",
139
+ "toolkit-runtime/rp2350-monitor",
140
+ "toolkit-runtime/RP2350-Monitor",
141
+ "images/current",
142
+ "userdata/rootfs",
143
+ "boot-workspace",
144
+ "README.md",
145
+ "meta",
146
+ "scripts",
147
+ "support",
148
+ "third_party"
149
+ ];
150
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
9
151
  export function defaultLocalReleaseRoot() {
10
- return DEFAULT_RELEASE_ROOT;
152
+ return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
153
+ || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
154
+ || DEFAULT_RELEASE_ROOT;
11
155
  }
12
- export async function validateLocalToolchain(releaseRoot = DEFAULT_RELEASE_ROOT) {
13
- const resolvedRoot = resolve(releaseRoot);
14
- const required = [
15
- ["release root", "."],
16
- ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
17
- ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
18
- ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
19
- ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
20
- ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
21
- ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
22
- ["Rockchip mkimage", "tools/mac/mkimage"],
23
- ["Rockchip resource_tool", "tools/mac/resource_tool"]
24
- ];
156
+ export async function latestLocalToolchain(options = {}) {
157
+ const boardId = options.boardId ?? DEFAULT_BOARD_ID;
158
+ const channelName = options.channel ?? DEFAULT_CHANNEL;
159
+ const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
160
+ const boardPackageId = boardPackageIdFor(boardId);
161
+ const board = manifests.get(boardPackageId);
162
+ if (!board) {
163
+ throw new Error(`No local toolchain board package found for ${boardId}.`);
164
+ }
165
+ const canonicalBoardId = boardIdForPackageManifest(board);
166
+ const packages = resolvePackageRefs(boardPackageId, channel, manifests);
167
+ let download;
168
+ let downloadError;
169
+ try {
170
+ download = await resolveLocalToolchainDownloadPlan({
171
+ boardId: canonicalBoardId,
172
+ channel: channelName,
173
+ host: hostId(),
174
+ toolchain: "llvm"
175
+ });
176
+ }
177
+ catch (error) {
178
+ downloadError = error instanceof Error ? error.message : String(error);
179
+ }
180
+ return {
181
+ board_id: canonicalBoardId,
182
+ channel: channel.channel,
183
+ host: hostId(),
184
+ version: download?.version ?? board.version,
185
+ metadata_root: metadataRoot,
186
+ packages,
187
+ download,
188
+ download_error: downloadError
189
+ };
190
+ }
191
+ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD_ID) {
192
+ const root = resolveInstallRoot(installRoot);
193
+ const registryPath = localToolchainRegistryPath(root);
194
+ try {
195
+ const registry = JSON.parse(await readFile(registryPath, "utf8"));
196
+ const environments = registry.environments;
197
+ const boardInstall = environments?.[normalizeBoardId(boardId)];
198
+ if (boardInstall?.release_root) {
199
+ return {
200
+ installed: true,
201
+ board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
202
+ version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
203
+ mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
204
+ release_root: boardInstall.release_root,
205
+ registry_path: registryPath,
206
+ install_root: root,
207
+ channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
208
+ packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
209
+ };
210
+ }
211
+ const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
212
+ const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
213
+ if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
214
+ return {
215
+ installed: false,
216
+ board_id: boardId,
217
+ registry_path: registryPath,
218
+ install_root: root
219
+ };
220
+ }
221
+ return {
222
+ installed: !!releaseRoot,
223
+ board_id: registryBoardId,
224
+ version: typeof registry.version === "string" ? registry.version : undefined,
225
+ mode: typeof registry.mode === "string" ? registry.mode : undefined,
226
+ release_root: releaseRoot,
227
+ registry_path: registryPath,
228
+ install_root: root,
229
+ channel: typeof registry.channel === "string" ? registry.channel : undefined,
230
+ packages: Array.isArray(registry.packages) ? registry.packages : undefined
231
+ };
232
+ }
233
+ catch {
234
+ return {
235
+ installed: false,
236
+ board_id: boardId,
237
+ registry_path: registryPath,
238
+ install_root: root
239
+ };
240
+ }
241
+ }
242
+ async function discoverInstalledLocalToolchains(installRoot, current) {
243
+ const installed = new Map();
244
+ if (current.installed && current.release_root) {
245
+ installed.set(normalizeBoardId(current.board_id), {
246
+ board_id: current.board_id,
247
+ version: current.version,
248
+ channel: current.channel,
249
+ mode: current.mode,
250
+ release_root: current.release_root
251
+ });
252
+ }
253
+ const toolchainsRoot = join(installRoot, "toolchains");
254
+ let boardEntries;
255
+ try {
256
+ boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
257
+ }
258
+ catch {
259
+ return installed;
260
+ }
261
+ for (const boardEntry of boardEntries) {
262
+ if (!boardEntry.isDirectory()) {
263
+ continue;
264
+ }
265
+ const boardId = normalizeBoardId(boardEntry.name);
266
+ if (installed.has(boardId)) {
267
+ continue;
268
+ }
269
+ const boardRoot = join(toolchainsRoot, boardEntry.name);
270
+ let versionEntries;
271
+ try {
272
+ versionEntries = await readdir(boardRoot, { withFileTypes: true });
273
+ }
274
+ catch {
275
+ continue;
276
+ }
277
+ const versions = versionEntries
278
+ .filter((entry) => entry.isDirectory())
279
+ .map((entry) => entry.name)
280
+ .sort(compareVersionLike)
281
+ .reverse();
282
+ const version = versions[0];
283
+ if (!version) {
284
+ continue;
285
+ }
286
+ installed.set(boardId, {
287
+ board_id: boardId,
288
+ version,
289
+ release_root: join(boardRoot, version)
290
+ });
291
+ }
292
+ return installed;
293
+ }
294
+ export async function listLocalToolchainEnvironments(options = {}) {
295
+ const channelName = options.channel ?? DEFAULT_CHANNEL;
296
+ const host = hostId();
297
+ const installRoot = resolveInstallRoot(options.installRoot);
298
+ const registryPath = localToolchainRegistryPath(installRoot);
299
+ const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
300
+ const current = await currentLocalToolchain(installRoot);
301
+ const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
302
+ const boardManifests = [...manifests.values()]
303
+ .filter((manifest) => manifest.kind === "board")
304
+ .filter((manifest) => {
305
+ if (!options.boardId) {
306
+ return true;
307
+ }
308
+ const normalizedFilter = normalizeBoardId(options.boardId);
309
+ return boardIdForPackageManifest(manifest) === normalizedFilter
310
+ || manifest.id === options.boardId
311
+ || manifest.id === packageIdForBoardFilter(options.boardId);
312
+ })
313
+ .sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
314
+ const environments = [];
315
+ for (const board of boardManifests) {
316
+ const boardId = boardIdForPackageManifest(board);
317
+ const packages = resolvePackageRefs(board.id, channel, manifests);
318
+ const hostSupport = packageHostSupport(packages, manifests, host);
319
+ let download;
320
+ let downloadError;
321
+ try {
322
+ download = await resolveLocalToolchainDownloadPlan({
323
+ boardId,
324
+ channel: channelName,
325
+ host,
326
+ toolchain: "llvm"
327
+ });
328
+ }
329
+ catch (error) {
330
+ downloadError = error instanceof Error ? error.message : String(error);
331
+ }
332
+ const latestVersion = download?.version ?? board.version;
333
+ const currentForBoard = await currentLocalToolchain(installRoot, boardId);
334
+ const installedCandidate = currentForBoard.installed
335
+ ? currentForBoard
336
+ : installedByBoard.get(normalizeBoardId(boardId));
337
+ const installed = installedCandidate
338
+ ? {
339
+ version: installedCandidate.version,
340
+ channel: installedCandidate.channel,
341
+ mode: installedCandidate.mode,
342
+ release_root: installedCandidate.release_root
343
+ }
344
+ : undefined;
345
+ const updateAvailable = !!installed?.version && installed.version !== latestVersion;
346
+ const status = !hostSupport.supported
347
+ ? "unsupported_host"
348
+ : updateAvailable
349
+ ? "update_available"
350
+ : installed
351
+ ? "installed"
352
+ : "available";
353
+ const mode = download?.default_mode ?? "qt";
354
+ const installModes = localToolchainInstallModesForDownload(download);
355
+ environments.push({
356
+ board_id: boardId,
357
+ package_id: board.id,
358
+ display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
359
+ family: board.family,
360
+ variant: board.variant,
361
+ channel: channel.channel,
362
+ host,
363
+ status,
364
+ supported_host: hostSupport.supported,
365
+ unsupported_packages: hostSupport.unsupportedPackages,
366
+ install_modes: installModes,
367
+ installed,
368
+ latest: {
369
+ version: latestVersion,
370
+ default_mode: download?.default_mode,
371
+ source_url: download?.source_url,
372
+ manifest_url: download?.manifest_url,
373
+ component_count: download?.components?.length,
374
+ download_error: downloadError
375
+ },
376
+ packages,
377
+ components: download?.components?.map(localToolchainEnvironmentComponent),
378
+ install_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode}`,
379
+ update_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode} --force`,
380
+ notes: environmentNotes({ status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
381
+ });
382
+ }
383
+ const filteredEnvironments = options.installedOnly
384
+ ? environments.filter((environment) => !!environment.installed)
385
+ : environments;
386
+ return {
387
+ host,
388
+ channel: channel.channel,
389
+ metadata_source: metadataRoot ? "local_override" : "built_in",
390
+ metadata_root: metadataRoot,
391
+ install_root: installRoot,
392
+ registry_path: registryPath,
393
+ environments: filteredEnvironments
394
+ };
395
+ }
396
+ export async function installLocalToolchain(options = {}) {
397
+ const latest = await latestLocalToolchain(options);
398
+ const installRoot = resolveInstallRoot(options.installRoot);
399
+ const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
400
+ const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
401
+ if (await pathExists(releaseRoot) && !options.force) {
402
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
403
+ if (!validation.ok) {
404
+ if (latest.download?.components?.length) {
405
+ // Component installs can upgrade an existing lower-mode install by overlaying
406
+ // only the newly selected components instead of deleting the whole tree.
407
+ }
408
+ else {
409
+ throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
410
+ }
411
+ }
412
+ else {
413
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
414
+ return {
415
+ board_id: latest.board_id,
416
+ version: latest.version,
417
+ channel: latest.channel,
418
+ host: latest.host,
419
+ mode: installMode,
420
+ install_root: installRoot,
421
+ release_root: releaseRoot,
422
+ registry_path: localToolchainRegistryPath(installRoot),
423
+ source: { kind: "directory", value: releaseRoot },
424
+ installed_paths: [],
425
+ packages: latest.packages,
426
+ validation
427
+ };
428
+ }
429
+ }
430
+ const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
431
+ try {
432
+ const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
433
+ if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
434
+ await rm(releaseRoot, { recursive: true, force: true });
435
+ }
436
+ await mkdir(releaseRoot, { recursive: true });
437
+ const installedPaths = [];
438
+ for (const relativePath of INSTALL_COPY_PATHS) {
439
+ const sourcePath = resolve(sourceRoot.path, relativePath);
440
+ if (!await pathExists(sourcePath)) {
441
+ continue;
442
+ }
443
+ const targetPath = resolve(releaseRoot, relativePath);
444
+ await mkdir(dirname(targetPath), { recursive: true });
445
+ await cp(sourcePath, targetPath, {
446
+ recursive: true,
447
+ force: true,
448
+ preserveTimestamps: true,
449
+ verbatimSymlinks: true
450
+ });
451
+ installedPaths.push(relativePath);
452
+ }
453
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
454
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
455
+ if (!validation.ok) {
456
+ throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
457
+ }
458
+ return {
459
+ board_id: latest.board_id,
460
+ version: latest.version,
461
+ channel: latest.channel,
462
+ host: latest.host,
463
+ mode: installMode,
464
+ install_root: installRoot,
465
+ release_root: releaseRoot,
466
+ registry_path: localToolchainRegistryPath(installRoot),
467
+ source: sourceRoot.source,
468
+ installed_paths: installedPaths,
469
+ packages: latest.packages,
470
+ validation
471
+ };
472
+ }
473
+ finally {
474
+ await rm(tempDir, { recursive: true, force: true });
475
+ }
476
+ }
477
+ export async function validateLocalToolchain(input) {
478
+ const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
479
+ const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
480
+ const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
481
+ const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
482
+ const required = requiredLocalToolchainChecks(mode, boardId);
25
483
  const checked_paths = [];
26
484
  for (const [label, relativePath] of required) {
27
485
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -34,23 +492,107 @@ export async function validateLocalToolchain(releaseRoot = DEFAULT_RELEASE_ROOT)
34
492
  const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
35
493
  return {
36
494
  ok: missing_paths.length === 0,
495
+ mode,
37
496
  host: {
38
497
  platform: platform(),
39
498
  arch: arch()
40
499
  },
41
- board_id: "taishanpi-1m-rk3566",
500
+ board_id: boardId,
42
501
  release_root: resolvedRoot,
43
502
  checked_paths,
44
503
  missing_paths,
45
504
  notes: [
46
505
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
47
- "This validator checks the Mac-first TaishanPi LLVM release layout; package install/update registry work is tracked separately."
506
+ `This validator checks the ${boardId} local support layout for install mode ${mode}.`
48
507
  ]
49
508
  };
50
509
  }
510
+ function normalizeLocalToolchainInstallMode(mode) {
511
+ const normalized = mode?.trim();
512
+ if (!normalized) {
513
+ return "qt";
514
+ }
515
+ if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
516
+ return normalized;
517
+ }
518
+ throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
519
+ }
520
+ function requiredLocalToolchainChecks(mode, boardId) {
521
+ if (boardId === PICO2W_RP2350_BOARD_ID || boardId === "rp2350" || boardId === "rp2350-monitor") {
522
+ return requiredRp2350MonitorChecks(mode);
523
+ }
524
+ const base = [
525
+ ["release root", "."],
526
+ ["Rockchip mkimage", "tools/mac/mkimage"],
527
+ ["Rockchip dumpimage", "tools/mac/dumpimage"],
528
+ ["Rockchip resource_tool", "tools/mac/resource_tool"],
529
+ ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
530
+ ["boot resource image", "boot-workspace/out/resource.img"],
531
+ ["boot image", "boot-workspace/out/boot.img"],
532
+ ["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
533
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
534
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
535
+ ["package metadata", "meta"]
536
+ ];
537
+ const compile = [
538
+ ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
539
+ ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
540
+ ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
541
+ ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
542
+ ["host GCC libraries", "toolchain/host/lib/gcc"],
543
+ ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
544
+ ["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
545
+ ];
546
+ const qt = [
547
+ ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
548
+ ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
549
+ ["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
550
+ ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
551
+ ];
552
+ const images = [
553
+ ["base boot image", "images/current/boot.img"],
554
+ ["base rootfs image", "images/current/rootfs.img"],
555
+ ["base image parameter", "images/current/parameter.txt"]
556
+ ];
557
+ const full = [
558
+ ["rootfs overlay", "userdata/rootfs"]
559
+ ];
560
+ if (mode === "minimal") {
561
+ return base;
562
+ }
563
+ if (mode === "compile") {
564
+ return [...base, ...compile];
565
+ }
566
+ if (mode === "qt") {
567
+ return [...base, ...compile, ...qt];
568
+ }
569
+ if (mode === "images") {
570
+ return [...base, ...images];
571
+ }
572
+ return [...base, ...compile, ...qt, ...images, ...full];
573
+ }
574
+ function requiredRp2350MonitorChecks(mode) {
575
+ const runtime = [
576
+ ["release root", "."],
577
+ ["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
578
+ ["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
579
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
580
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
581
+ ["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
582
+ ["package metadata", "meta"]
583
+ ];
584
+ const firmware = [
585
+ ["Pico 2 W monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
586
+ ["Pico 2 W firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
587
+ ];
588
+ if (mode === "firmware" || mode === "full") {
589
+ return [...runtime, ...firmware];
590
+ }
591
+ return runtime;
592
+ }
51
593
  export async function compileTaishanPiSingleFile(options) {
52
594
  assertAuthenticated(options.auth);
53
- const releaseRoot = resolve(options.releaseRoot ?? DEFAULT_RELEASE_ROOT);
595
+ const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
54
596
  const sourcePath = resolve(options.sourcePath);
55
597
  const outputPath = resolve(options.outputPath);
56
598
  await access(sourcePath, constants.R_OK);
@@ -77,7 +619,7 @@ export async function compileTaishanPiSingleFile(options) {
77
619
  }
78
620
  export async function buildTaishanPiQtSmoke(options) {
79
621
  assertAuthenticated(options.auth);
80
- const releaseRoot = resolve(options.releaseRoot ?? DEFAULT_RELEASE_ROOT);
622
+ const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
81
623
  const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
82
624
  const buildDir = resolve(options.buildDir);
83
625
  const targetName = options.targetName ?? "qt_llvm_smoke";
@@ -104,7 +646,7 @@ export async function buildTaishanPiQtSmoke(options) {
104
646
  }
105
647
  const artifactPath = join(buildDir, targetName);
106
648
  return await localCompileResult({
107
- boardId: "taishanpi-1m-rk3566",
649
+ boardId: DEFAULT_BOARD_ID,
108
650
  operation: "local.build.qt_smoke",
109
651
  releaseRoot,
110
652
  accountId: options.accountId,
@@ -115,6 +657,607 @@ export async function buildTaishanPiQtSmoke(options) {
115
657
  commands: [configure, build]
116
658
  });
117
659
  }
660
+ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
661
+ const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
662
+ if (!explicitRoot) {
663
+ return {
664
+ channel: BUILT_IN_CHANNEL,
665
+ manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
666
+ metadataRoot: undefined
667
+ };
668
+ }
669
+ const root = resolve(explicitRoot);
670
+ const channelPath = join(root, "channels", channelName, "index.json");
671
+ const channel = JSON.parse(await readFile(channelPath, "utf8"));
672
+ if (channel.schema !== "embedlabs.channel.v1") {
673
+ throw new Error(`Unexpected local toolchain channel schema ${channel.schema}.`);
674
+ }
675
+ const manifests = new Map();
676
+ for (const entry of channel.packages) {
677
+ const manifestPath = entry.manifest
678
+ ? resolve(dirname(channelPath), entry.manifest)
679
+ : join(root, "manifests", entry.id, entry.version, "manifest.json");
680
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
681
+ if (manifest.schema !== "embedlabs.package.v1") {
682
+ throw new Error(`Unexpected local toolchain manifest schema for ${entry.id}: ${manifest.schema}.`);
683
+ }
684
+ if (manifest.id !== entry.id || manifest.version !== entry.version) {
685
+ throw new Error(`Local toolchain manifest mismatch for ${entry.id}@${entry.version}.`);
686
+ }
687
+ manifests.set(manifest.id, manifest);
688
+ }
689
+ return { channel, manifests, metadataRoot: root };
690
+ }
691
+ async function resolveLocalToolchainDownloadPlan(input) {
692
+ const channelUrl = downloadChannelUrl(input.channel);
693
+ const channel = await fetchJson(channelUrl);
694
+ if (channel.schema !== "embedlabs.download-channel.v1") {
695
+ throw new Error(`Unexpected download channel schema ${channel.schema}.`);
696
+ }
697
+ const entry = (channel.packages ?? []).find((item) => {
698
+ return item.board_id === input.boardId
699
+ && item.host === input.host
700
+ && item.toolchain === input.toolchain
701
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
702
+ });
703
+ if (!entry?.manifest) {
704
+ return undefined;
705
+ }
706
+ const manifestUrl = new URL(entry.manifest, channelUrl).toString();
707
+ const manifest = await fetchJson(manifestUrl);
708
+ if (manifest.id !== entry.id || manifest.version !== entry.version) {
709
+ throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
710
+ }
711
+ if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
712
+ throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
713
+ }
714
+ if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
715
+ && (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
716
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
717
+ }
718
+ const mirrors = manifest.archive
719
+ ? orderDownloadMirrors((manifest.mirrors ?? [])
720
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
721
+ .map((mirror) => ({
722
+ kind: mirror.kind || "unknown",
723
+ enabled: mirror.enabled !== false,
724
+ url: mirror.url,
725
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
726
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
727
+ })), manifest.download_policy?.preferred_order)
728
+ : [];
729
+ const components = (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl));
730
+ const first = mirrors[0];
731
+ if (!first && components.length === 0) {
732
+ return undefined;
733
+ }
734
+ return {
735
+ channel_url: channelUrl,
736
+ manifest_url: manifestUrl,
737
+ package_id: manifest.id,
738
+ version: manifest.version,
739
+ board_id: input.boardId,
740
+ host: input.host,
741
+ toolchain: input.toolchain,
742
+ source_url: first?.url,
743
+ mirror_kind: first?.kind,
744
+ archive: manifest.archive ? {
745
+ file: manifest.archive.file,
746
+ size_bytes: manifest.archive.size_bytes,
747
+ sha256: manifest.archive.sha256,
748
+ content_type: manifest.archive.content_type
749
+ } : undefined,
750
+ mirrors,
751
+ components: components.length > 0 ? components : undefined,
752
+ default_mode: manifest.download_policy?.default_mode
753
+ };
754
+ }
755
+ function normalizeDownloadComponent(component, manifest, manifestUrl) {
756
+ if (!component.id || !component.version) {
757
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
758
+ }
759
+ if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
760
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
761
+ }
762
+ const mirrors = orderDownloadMirrors((component.mirrors ?? [])
763
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
764
+ .map((mirror) => ({
765
+ kind: mirror.kind || "unknown",
766
+ enabled: mirror.enabled !== false,
767
+ url: new URL(mirror.url, manifestUrl).toString(),
768
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
769
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
770
+ })), manifest.download_policy?.preferred_order);
771
+ const first = mirrors[0];
772
+ if (!first) {
773
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
774
+ }
775
+ return {
776
+ id: component.id,
777
+ version: component.version,
778
+ role: component.role,
779
+ install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
780
+ archive: {
781
+ file: component.archive.file,
782
+ size_bytes: component.archive.size_bytes,
783
+ sha256: component.archive.sha256,
784
+ content_type: component.archive.content_type
785
+ },
786
+ source_url: first.url,
787
+ mirror_kind: first.kind,
788
+ mirrors
789
+ };
790
+ }
791
+ function orderDownloadMirrors(mirrors, preferredOrder) {
792
+ const preference = preferredOrder?.length
793
+ ? preferredOrder
794
+ : ["github_release", "embedlabs_cdn", "cloudfront", "embedlabs_server"];
795
+ return [...mirrors].sort((left, right) => mirrorRank(left.kind, preference) - mirrorRank(right.kind, preference));
796
+ }
797
+ function mirrorRank(kind, preferredOrder) {
798
+ const index = preferredOrder.indexOf(kind);
799
+ return index >= 0 ? index : preferredOrder.length + 1;
800
+ }
801
+ async function fetchJson(url) {
802
+ const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
803
+ if (!response.ok) {
804
+ throw new Error(`HTTP ${response.status} while loading ${url}`);
805
+ }
806
+ return await response.json();
807
+ }
808
+ function downloadChannelUrl(channelName) {
809
+ const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
810
+ || process.env.EMBEDLABS_DOWNLOAD_CHANNEL_URL?.trim();
811
+ if (explicit) {
812
+ return explicit;
813
+ }
814
+ return `${downloadBaseUrl()}/downloads/metadata/channels/${encodeURIComponent(channelName)}/index.json`;
815
+ }
816
+ function downloadBaseUrl() {
817
+ return trimTrailingSlash(process.env.EMBED_DOWNLOAD_BASE_URL?.trim()
818
+ || process.env.EMBEDLABS_DOWNLOAD_BASE_URL?.trim()
819
+ || DEFAULT_DOWNLOAD_BASE_URL);
820
+ }
821
+ function trimTrailingSlash(value) {
822
+ return value.replace(/\/+$/, "");
823
+ }
824
+ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
825
+ if (seen.has(packageId)) {
826
+ return [];
827
+ }
828
+ seen.add(packageId);
829
+ const manifest = manifests.get(packageId);
830
+ if (!manifest) {
831
+ throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
832
+ }
833
+ const refs = [];
834
+ for (const requirement of manifest.requires ?? []) {
835
+ refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
836
+ }
837
+ const channelEntry = channel.packages.find((item) => item.id === packageId);
838
+ refs.push({
839
+ id: packageId,
840
+ version: manifest.version,
841
+ manifest: channelEntry?.manifest
842
+ });
843
+ return refs;
844
+ }
845
+ function boardPackageIdFor(boardId) {
846
+ if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
847
+ return "embedlabs.board.taishanpi.1m-rk3566";
848
+ }
849
+ if (boardId === PICO2W_RP2350_BOARD_ID
850
+ || boardId === "pico2w"
851
+ || boardId === "pico-2-w"
852
+ || boardId === "pico2"
853
+ || boardId === "rp2350"
854
+ || boardId === "rp2350-monitor") {
855
+ return "embedlabs.board.pico2w.rp2350-monitor";
856
+ }
857
+ if (boardId.startsWith("embedlabs.board.")) {
858
+ return boardId;
859
+ }
860
+ throw new Error(`Unsupported local toolchain board ${boardId}.`);
861
+ }
862
+ function packageIdForBoardFilter(boardId) {
863
+ try {
864
+ return boardPackageIdFor(boardId);
865
+ }
866
+ catch {
867
+ return undefined;
868
+ }
869
+ }
870
+ function boardIdForPackageManifest(manifest) {
871
+ const explicit = manifest.board_id;
872
+ if (explicit?.trim()) {
873
+ return normalizeBoardId(explicit);
874
+ }
875
+ if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
876
+ return DEFAULT_BOARD_ID;
877
+ }
878
+ if (manifest.id.startsWith("embedlabs.board.")) {
879
+ return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
880
+ }
881
+ return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
882
+ }
883
+ function normalizeBoardId(boardId) {
884
+ return boardId.trim().toLowerCase().replaceAll("_", "-");
885
+ }
886
+ function compareVersionLike(left, right) {
887
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
888
+ }
889
+ function isRecord(value) {
890
+ return !!value && typeof value === "object" && !Array.isArray(value);
891
+ }
892
+ function packageHostSupport(packages, manifests, host) {
893
+ const unsupportedPackages = [];
894
+ for (const item of packages) {
895
+ const manifest = manifests.get(item.id);
896
+ if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
897
+ unsupportedPackages.push(`${item.id}@${manifest.version}`);
898
+ }
899
+ }
900
+ return {
901
+ supported: unsupportedPackages.length === 0,
902
+ unsupportedPackages
903
+ };
904
+ }
905
+ function environmentNotes(input) {
906
+ const notes = [];
907
+ if (input.status === "available") {
908
+ notes.push("Environment is available but not installed on this computer.");
909
+ }
910
+ if (input.status === "update_available") {
911
+ notes.push("A newer package is available; run the update command to refresh only the selected environment.");
912
+ }
913
+ if (input.status === "unsupported_host") {
914
+ notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
915
+ }
916
+ if (input.downloadError) {
917
+ notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
918
+ }
919
+ return notes;
920
+ }
921
+ function localToolchainInstallModesForDownload(download) {
922
+ if (!download?.components?.length) {
923
+ return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
924
+ }
925
+ const modes = new Set();
926
+ for (const component of download.components) {
927
+ for (const mode of component.install_modes ?? []) {
928
+ modes.add(mode);
929
+ }
930
+ }
931
+ return modes.size > 0
932
+ ? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
933
+ : [...LOCAL_TOOLCHAIN_INSTALL_MODES];
934
+ }
935
+ function localToolchainEnvironmentComponent(component) {
936
+ return {
937
+ id: component.id,
938
+ version: component.version,
939
+ role: component.role,
940
+ install_modes: component.install_modes,
941
+ file: component.archive.file,
942
+ size_bytes: component.archive.size_bytes,
943
+ source_url: component.source_url
944
+ };
945
+ }
946
+ function hostId() {
947
+ if (platform() === "darwin" && arch() === "arm64") {
948
+ return "darwin-arm64";
949
+ }
950
+ if (platform() === "linux" && arch() === "x64") {
951
+ return "linux-x86_64";
952
+ }
953
+ return `${platform()}-${arch()}`;
954
+ }
955
+ function resolveInstallRoot(installRoot) {
956
+ return resolve(installRoot
957
+ || process.env.EMBEDLABS_HOME?.trim()
958
+ || join(homedir(), ".embedlabs"));
959
+ }
960
+ function localToolchainRegistryPath(installRoot) {
961
+ return join(installRoot, "registry", "local-toolchains.json");
962
+ }
963
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
964
+ const registryPath = localToolchainRegistryPath(installRoot);
965
+ await mkdir(dirname(registryPath), { recursive: true });
966
+ let existing = {};
967
+ try {
968
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
969
+ }
970
+ catch {
971
+ existing = {};
972
+ }
973
+ const entry = {
974
+ installed: true,
975
+ board_id: latest.board_id,
976
+ version: latest.version,
977
+ channel: latest.channel,
978
+ host: latest.host,
979
+ mode,
980
+ release_root: releaseRoot,
981
+ packages: latest.packages,
982
+ source,
983
+ installed_components: source?.components,
984
+ updated_at: new Date().toISOString()
985
+ };
986
+ const environments = isRecord(existing.environments)
987
+ ? { ...existing.environments }
988
+ : {};
989
+ environments[normalizeBoardId(latest.board_id)] = entry;
990
+ const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
991
+ && typeof existing.release_root === "string"
992
+ && normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
993
+ const topLevel = preserveTopLevel ? existing : entry;
994
+ await writeFile(registryPath, `${JSON.stringify({
995
+ ...topLevel,
996
+ environments,
997
+ updated_at: new Date().toISOString()
998
+ }, null, 2)}\n`, "utf8");
999
+ }
1000
+ async function resolveLocalReleaseRoot(releaseRoot) {
1001
+ if (releaseRoot?.trim()) {
1002
+ return resolve(releaseRoot);
1003
+ }
1004
+ const envRoot = process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim() || process.env.EMBEDLABS_RELEASE_ROOT?.trim();
1005
+ if (envRoot) {
1006
+ return resolve(envRoot);
1007
+ }
1008
+ const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
1009
+ if (current.release_root) {
1010
+ return resolve(current.release_root);
1011
+ }
1012
+ return resolve(DEFAULT_RELEASE_ROOT);
1013
+ }
1014
+ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir) {
1015
+ if (options.sourceReleaseRoot) {
1016
+ return {
1017
+ path: resolve(options.sourceReleaseRoot),
1018
+ source: { kind: "directory", value: resolve(options.sourceReleaseRoot) }
1019
+ };
1020
+ }
1021
+ if (options.sourceUrl) {
1022
+ const downloadedPath = await downloadToolchainArchive(options.sourceUrl, installRoot);
1023
+ const extractRoot = join(tempDir, "extract");
1024
+ await mkdir(extractRoot, { recursive: true });
1025
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
1026
+ if (extracted.exit_code !== 0) {
1027
+ throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
1028
+ }
1029
+ return {
1030
+ path: await findReleaseRoot(extractRoot),
1031
+ source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
1032
+ };
1033
+ }
1034
+ if (latest.download?.components?.length) {
1035
+ return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
1036
+ }
1037
+ if (latest.download) {
1038
+ const failures = [];
1039
+ for (const mirror of latest.download.mirrors) {
1040
+ if (!mirror.enabled) {
1041
+ continue;
1042
+ }
1043
+ if (!latest.download.archive) {
1044
+ continue;
1045
+ }
1046
+ const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
1047
+ try {
1048
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
1049
+ sha256: mirror.sha256 ?? latest.download.archive.sha256,
1050
+ size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
1051
+ });
1052
+ await rm(extractRoot, { recursive: true, force: true });
1053
+ await mkdir(extractRoot, { recursive: true });
1054
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
1055
+ if (extracted.exit_code !== 0) {
1056
+ throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
1057
+ }
1058
+ return {
1059
+ path: await findReleaseRoot(extractRoot),
1060
+ source: {
1061
+ kind: "url",
1062
+ value: mirror.url,
1063
+ downloaded_path: downloadedPath,
1064
+ mirror_kind: mirror.kind,
1065
+ sha256: mirror.sha256 ?? latest.download.archive.sha256,
1066
+ size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
1067
+ }
1068
+ };
1069
+ }
1070
+ catch (error) {
1071
+ failures.push(`${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
1072
+ }
1073
+ }
1074
+ if (failures.length > 0 && !await pathExists(DEFAULT_RELEASE_ROOT)) {
1075
+ throw new Error(`Could not install local toolchain from download mirrors: ${failures.join("; ")}`);
1076
+ }
1077
+ }
1078
+ if (await pathExists(DEFAULT_RELEASE_ROOT)) {
1079
+ return {
1080
+ path: resolve(DEFAULT_RELEASE_ROOT),
1081
+ source: { kind: "directory", value: resolve(DEFAULT_RELEASE_ROOT) }
1082
+ };
1083
+ }
1084
+ 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.");
1085
+ }
1086
+ async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
1087
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
1088
+ const components = selectedDownloadComponents(download.components ?? [], mode);
1089
+ if (components.length === 0) {
1090
+ throw new Error(`No local toolchain components selected for mode ${mode}.`);
1091
+ }
1092
+ const extractRoot = join(tempDir, "extract-components");
1093
+ await mkdir(extractRoot, { recursive: true });
1094
+ const installedComponents = [];
1095
+ const failures = [];
1096
+ for (const component of components) {
1097
+ let installed = false;
1098
+ for (const mirror of component.mirrors) {
1099
+ if (!mirror.enabled) {
1100
+ continue;
1101
+ }
1102
+ try {
1103
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
1104
+ sha256: mirror.sha256 ?? component.archive.sha256,
1105
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes
1106
+ });
1107
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
1108
+ if (extracted.exit_code !== 0) {
1109
+ throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
1110
+ }
1111
+ installedComponents.push({
1112
+ id: component.id,
1113
+ version: component.version,
1114
+ role: component.role,
1115
+ archive_file: component.archive.file,
1116
+ mirror_kind: mirror.kind,
1117
+ downloaded_path: downloadedPath,
1118
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
1119
+ sha256: mirror.sha256 ?? component.archive.sha256
1120
+ });
1121
+ installed = true;
1122
+ break;
1123
+ }
1124
+ catch (error) {
1125
+ failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
1126
+ }
1127
+ }
1128
+ if (!installed) {
1129
+ throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
1130
+ }
1131
+ }
1132
+ return {
1133
+ path: extractRoot,
1134
+ source: {
1135
+ kind: "components",
1136
+ value: download.manifest_url,
1137
+ mirror_kind: "components",
1138
+ size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
1139
+ components: installedComponents
1140
+ }
1141
+ };
1142
+ }
1143
+ function selectedDownloadComponents(components, mode) {
1144
+ return components.filter((component) => {
1145
+ if (!component.install_modes?.length) {
1146
+ return true;
1147
+ }
1148
+ return component.install_modes.includes(mode);
1149
+ });
1150
+ }
1151
+ async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
1152
+ const downloadsDir = join(installRoot, "cache", "downloads");
1153
+ await mkdir(downloadsDir, { recursive: true });
1154
+ const parsed = new URL(sourceUrl);
1155
+ const filename = basename(parsed.pathname) || `local-toolchain-${Date.now()}.tar.gz`;
1156
+ const outputPath = join(downloadsDir, filename);
1157
+ if (parsed.protocol === "file:") {
1158
+ await cp(fileURLToPath(parsed), outputPath, { force: true });
1159
+ return outputPath;
1160
+ }
1161
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1162
+ throw new Error(`Unsupported local toolchain download URL protocol: ${parsed.protocol}`);
1163
+ }
1164
+ const partialPath = `${outputPath}.part`;
1165
+ const expectedSize = expected?.size_bytes;
1166
+ const existingComplete = await fileSize(outputPath);
1167
+ const remoteSize = await remoteContentLength(sourceUrl);
1168
+ const targetSize = expectedSize ?? remoteSize;
1169
+ if (existingComplete > 0 && targetSize !== undefined && existingComplete === targetSize) {
1170
+ if (expected?.sha256) {
1171
+ const actual = await sha256(outputPath);
1172
+ if (actual === expected.sha256) {
1173
+ return outputPath;
1174
+ }
1175
+ await rm(outputPath, { force: true });
1176
+ }
1177
+ else {
1178
+ return outputPath;
1179
+ }
1180
+ }
1181
+ let resumeFrom = await fileSize(partialPath);
1182
+ const headers = new Headers();
1183
+ if (resumeFrom > 0) {
1184
+ headers.set("range", `bytes=${resumeFrom}-`);
1185
+ }
1186
+ let response = await fetch(sourceUrl, { headers });
1187
+ if (resumeFrom > 0 && response.status !== 206) {
1188
+ await rm(partialPath, { force: true });
1189
+ resumeFrom = 0;
1190
+ response = await fetch(sourceUrl);
1191
+ }
1192
+ if (!response.ok) {
1193
+ throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
1194
+ }
1195
+ if (!response.body) {
1196
+ throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
1197
+ }
1198
+ const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
1199
+ await pipeline(Readable.fromWeb(response.body), writeStream);
1200
+ const downloadedSize = await fileSize(partialPath);
1201
+ if (targetSize !== undefined && downloadedSize !== targetSize) {
1202
+ throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
1203
+ }
1204
+ await rename(partialPath, outputPath);
1205
+ if (expected?.sha256) {
1206
+ const actual = await sha256(outputPath);
1207
+ if (actual !== expected.sha256) {
1208
+ await rm(outputPath, { force: true });
1209
+ throw new Error(`Local toolchain download SHA256 mismatch: expected ${expected.sha256}, got ${actual}.`);
1210
+ }
1211
+ }
1212
+ return outputPath;
1213
+ }
1214
+ async function remoteContentLength(sourceUrl) {
1215
+ try {
1216
+ const response = await fetch(sourceUrl, { method: "HEAD" });
1217
+ if (!response.ok) {
1218
+ return undefined;
1219
+ }
1220
+ const length = response.headers.get("content-length");
1221
+ if (!length) {
1222
+ return undefined;
1223
+ }
1224
+ const parsed = Number.parseInt(length, 10);
1225
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
1226
+ }
1227
+ catch {
1228
+ return undefined;
1229
+ }
1230
+ }
1231
+ async function fileSize(filePath) {
1232
+ try {
1233
+ return (await stat(filePath)).size;
1234
+ }
1235
+ catch {
1236
+ return 0;
1237
+ }
1238
+ }
1239
+ function safeFileToken(value) {
1240
+ return value.replace(/[^a-zA-Z0-9_.-]+/g, "_") || "mirror";
1241
+ }
1242
+ async function findReleaseRoot(extractRoot) {
1243
+ if (await pathExists(join(extractRoot, "toolchain"))) {
1244
+ return extractRoot;
1245
+ }
1246
+ const entries = await readdir(extractRoot);
1247
+ for (const entry of entries) {
1248
+ const candidate = join(extractRoot, entry);
1249
+ try {
1250
+ const info = await stat(candidate);
1251
+ if (info.isDirectory() && await pathExists(join(candidate, "toolchain"))) {
1252
+ return candidate;
1253
+ }
1254
+ }
1255
+ catch {
1256
+ // Ignore entries that disappear during extraction cleanup.
1257
+ }
1258
+ }
1259
+ throw new Error("Downloaded local toolchain archive did not contain a release root with toolchain/.");
1260
+ }
118
1261
  function compilerForSource(releaseRoot, sourcePath) {
119
1262
  const extension = extname(sourcePath).toLowerCase();
120
1263
  const binDir = join(releaseRoot, "toolchain", "llvm-cross", "bin");