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

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