@kvell007/embed-labs-cli 0.1.0-alpha.11 → 0.1.0-alpha.13

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.
@@ -6,6 +6,7 @@ export interface LocalToolchainAuthContext {
6
6
  }
7
7
  export interface LocalToolchainValidationResult {
8
8
  ok: boolean;
9
+ mode: string;
9
10
  host: {
10
11
  platform: string;
11
12
  arch: string;
@@ -107,6 +108,7 @@ export interface LocalToolchainInstallOptions extends LocalToolchainLatestOption
107
108
  sourceReleaseRoot?: string;
108
109
  sourceUrl?: string;
109
110
  installRoot?: string;
111
+ mode?: string;
110
112
  force?: boolean;
111
113
  }
112
114
  export interface LocalToolchainInstallResult {
@@ -114,16 +116,18 @@ export interface LocalToolchainInstallResult {
114
116
  version: string;
115
117
  channel: string;
116
118
  host: string;
119
+ mode: string;
117
120
  install_root: string;
118
121
  release_root: string;
119
122
  registry_path: string;
120
123
  source: {
121
- kind: "directory" | "url";
124
+ kind: "directory" | "url" | "components";
122
125
  value: string;
123
126
  downloaded_path?: string;
124
127
  mirror_kind?: string;
125
128
  sha256?: string;
126
129
  size_bytes?: number;
130
+ components?: LocalToolchainInstalledComponent[];
127
131
  };
128
132
  installed_paths: string[];
129
133
  packages: LocalToolchainPackageRef[];
@@ -133,6 +137,7 @@ export interface LocalToolchainCurrentResult {
133
137
  installed: boolean;
134
138
  board_id: string;
135
139
  version?: string;
140
+ mode?: string;
136
141
  release_root?: string;
137
142
  registry_path: string;
138
143
  install_root: string;
@@ -147,15 +152,17 @@ export interface LocalToolchainDownloadPlan {
147
152
  board_id: string;
148
153
  host: string;
149
154
  toolchain: string;
150
- source_url: string;
151
- mirror_kind: string;
152
- archive: {
155
+ source_url?: string;
156
+ mirror_kind?: string;
157
+ archive?: {
153
158
  file: string;
154
159
  size_bytes: number;
155
160
  sha256: string;
156
161
  content_type?: string;
157
162
  };
158
163
  mirrors: LocalToolchainDownloadMirror[];
164
+ components?: LocalToolchainDownloadComponent[];
165
+ default_mode?: string;
159
166
  }
160
167
  export interface LocalToolchainDownloadMirror {
161
168
  kind: string;
@@ -164,10 +171,38 @@ export interface LocalToolchainDownloadMirror {
164
171
  sha256?: string;
165
172
  size_bytes?: number;
166
173
  }
174
+ export interface LocalToolchainDownloadComponent {
175
+ id: string;
176
+ version: string;
177
+ role?: string;
178
+ install_modes?: string[];
179
+ archive: {
180
+ file: string;
181
+ size_bytes: number;
182
+ sha256: string;
183
+ content_type?: string;
184
+ };
185
+ source_url?: string;
186
+ mirror_kind?: string;
187
+ mirrors: LocalToolchainDownloadMirror[];
188
+ }
189
+ export interface LocalToolchainInstalledComponent {
190
+ id: string;
191
+ version: string;
192
+ role?: string;
193
+ archive_file: string;
194
+ mirror_kind?: string;
195
+ downloaded_path: string;
196
+ size_bytes: number;
197
+ sha256: string;
198
+ }
167
199
  export declare function defaultLocalReleaseRoot(): string;
168
200
  export declare function latestLocalToolchain(options?: LocalToolchainLatestOptions): Promise<LocalToolchainLatestResult>;
169
201
  export declare function currentLocalToolchain(installRoot?: string, boardId?: string): Promise<LocalToolchainCurrentResult>;
170
202
  export declare function installLocalToolchain(options?: LocalToolchainInstallOptions): Promise<LocalToolchainInstallResult>;
171
- export declare function validateLocalToolchain(releaseRoot?: string): Promise<LocalToolchainValidationResult>;
203
+ export declare function validateLocalToolchain(input?: string | {
204
+ releaseRoot?: string;
205
+ mode?: string;
206
+ }): Promise<LocalToolchainValidationResult>;
172
207
  export declare function compileTaishanPiSingleFile(options: LocalCompileOptions): Promise<LocalCompileResult>;
173
208
  export declare function buildTaishanPiQtSmoke(options: LocalQtSmokeBuildOptions): Promise<LocalCompileResult>;
@@ -103,15 +103,23 @@ const INSTALL_COPY_PATHS = [
103
103
  "toolchain/llvm-cross",
104
104
  "toolchain/host",
105
105
  "toolchain/host-tools",
106
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
106
107
  "qt-target/qt6-rk3566-llvm-6.8.3",
108
+ "qt-host/qt6-host-macos-6.8.3",
107
109
  "tools/mac",
108
110
  "toolkit-runtime/qtquick-live-preview",
109
111
  "toolkit-runtime/rp2350-monitor",
110
112
  "toolkit-runtime/RP2350-Monitor",
111
113
  "images/current",
112
114
  "userdata/rootfs",
113
- "boot-workspace/kernel-tree"
115
+ "boot-workspace",
116
+ "README.md",
117
+ "meta",
118
+ "scripts",
119
+ "support",
120
+ "third_party"
114
121
  ];
122
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "compile", "qt", "full", "images"];
115
123
  export function defaultLocalReleaseRoot() {
116
124
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
117
125
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -144,7 +152,7 @@ export async function latestLocalToolchain(options = {}) {
144
152
  board_id: boardId,
145
153
  channel: channel.channel,
146
154
  host: hostId(),
147
- version: board.version,
155
+ version: download?.version ?? board.version,
148
156
  metadata_root: metadataRoot,
149
157
  packages,
150
158
  download,
@@ -161,6 +169,7 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
161
169
  installed: !!releaseRoot,
162
170
  board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
163
171
  version: typeof registry.version === "string" ? registry.version : undefined,
172
+ mode: typeof registry.mode === "string" ? registry.mode : undefined,
164
173
  release_root: releaseRoot,
165
174
  registry_path: registryPath,
166
175
  install_root: root,
@@ -181,30 +190,42 @@ export async function installLocalToolchain(options = {}) {
181
190
  const latest = await latestLocalToolchain(options);
182
191
  const installRoot = resolveInstallRoot(options.installRoot);
183
192
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
193
+ const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
184
194
  if (await pathExists(releaseRoot) && !options.force) {
185
- const validation = await validateLocalToolchain(releaseRoot);
195
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
186
196
  if (!validation.ok) {
187
- throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
197
+ if (latest.download?.components?.length) {
198
+ // Component installs can upgrade an existing lower-mode install by overlaying
199
+ // only the newly selected components instead of deleting the whole tree.
200
+ }
201
+ else {
202
+ throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
203
+ }
204
+ }
205
+ else {
206
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
207
+ return {
208
+ board_id: latest.board_id,
209
+ version: latest.version,
210
+ channel: latest.channel,
211
+ host: latest.host,
212
+ mode: installMode,
213
+ install_root: installRoot,
214
+ release_root: releaseRoot,
215
+ registry_path: localToolchainRegistryPath(installRoot),
216
+ source: { kind: "directory", value: releaseRoot },
217
+ installed_paths: [],
218
+ packages: latest.packages,
219
+ validation
220
+ };
188
221
  }
189
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
190
- return {
191
- board_id: latest.board_id,
192
- version: latest.version,
193
- channel: latest.channel,
194
- host: latest.host,
195
- install_root: installRoot,
196
- release_root: releaseRoot,
197
- registry_path: localToolchainRegistryPath(installRoot),
198
- source: { kind: "directory", value: releaseRoot },
199
- installed_paths: [],
200
- packages: latest.packages,
201
- validation
202
- };
203
222
  }
204
223
  const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
205
224
  try {
206
225
  const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
207
- await rm(releaseRoot, { recursive: true, force: true });
226
+ if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
227
+ await rm(releaseRoot, { recursive: true, force: true });
228
+ }
208
229
  await mkdir(releaseRoot, { recursive: true });
209
230
  const installedPaths = [];
210
231
  for (const relativePath of INSTALL_COPY_PATHS) {
@@ -222,8 +243,8 @@ export async function installLocalToolchain(options = {}) {
222
243
  });
223
244
  installedPaths.push(relativePath);
224
245
  }
225
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
226
- const validation = await validateLocalToolchain(releaseRoot);
246
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
247
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
227
248
  if (!validation.ok) {
228
249
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
229
250
  }
@@ -232,6 +253,7 @@ export async function installLocalToolchain(options = {}) {
232
253
  version: latest.version,
233
254
  channel: latest.channel,
234
255
  host: latest.host,
256
+ mode: installMode,
235
257
  install_root: installRoot,
236
258
  release_root: releaseRoot,
237
259
  registry_path: localToolchainRegistryPath(installRoot),
@@ -245,26 +267,11 @@ export async function installLocalToolchain(options = {}) {
245
267
  await rm(tempDir, { recursive: true, force: true });
246
268
  }
247
269
  }
248
- export async function validateLocalToolchain(releaseRoot) {
270
+ export async function validateLocalToolchain(input) {
271
+ const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
272
+ const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
249
273
  const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
250
- const required = [
251
- ["release root", "."],
252
- ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
253
- ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
254
- ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
255
- ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
256
- ["host GCC libraries", "toolchain/host/lib/gcc"],
257
- ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
258
- ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
259
- ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
260
- ["Rockchip mkimage", "tools/mac/mkimage"],
261
- ["Rockchip dumpimage", "tools/mac/dumpimage"],
262
- ["Rockchip resource_tool", "tools/mac/resource_tool"],
263
- ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
264
- ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"],
265
- ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
266
- ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"]
267
- ];
274
+ const required = requiredLocalToolchainChecks(mode);
268
275
  const checked_paths = [];
269
276
  for (const [label, relativePath] of required) {
270
277
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -277,6 +284,7 @@ export async function validateLocalToolchain(releaseRoot) {
277
284
  const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
278
285
  return {
279
286
  ok: missing_paths.length === 0,
287
+ mode,
280
288
  host: {
281
289
  platform: platform(),
282
290
  arch: arch()
@@ -287,10 +295,71 @@ export async function validateLocalToolchain(releaseRoot) {
287
295
  missing_paths,
288
296
  notes: [
289
297
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
290
- "This validator checks the Mac-first TaishanPi LLVM release layout; package install/update registry work is tracked separately."
298
+ `This validator checks the TaishanPi LLVM local support layout for install mode ${mode}.`
291
299
  ]
292
300
  };
293
301
  }
302
+ function normalizeLocalToolchainInstallMode(mode) {
303
+ const normalized = mode?.trim();
304
+ if (!normalized) {
305
+ return "qt";
306
+ }
307
+ if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
308
+ return normalized;
309
+ }
310
+ throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
311
+ }
312
+ function requiredLocalToolchainChecks(mode) {
313
+ const base = [
314
+ ["release root", "."],
315
+ ["Rockchip mkimage", "tools/mac/mkimage"],
316
+ ["Rockchip dumpimage", "tools/mac/dumpimage"],
317
+ ["Rockchip resource_tool", "tools/mac/resource_tool"],
318
+ ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
319
+ ["boot resource image", "boot-workspace/out/resource.img"],
320
+ ["boot image", "boot-workspace/out/boot.img"],
321
+ ["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
322
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
323
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
324
+ ["package metadata", "meta"]
325
+ ];
326
+ const compile = [
327
+ ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
328
+ ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
329
+ ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
330
+ ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
331
+ ["host GCC libraries", "toolchain/host/lib/gcc"],
332
+ ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
333
+ ["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
334
+ ];
335
+ const qt = [
336
+ ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
337
+ ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
338
+ ["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
339
+ ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
340
+ ];
341
+ const images = [
342
+ ["base boot image", "images/current/boot.img"],
343
+ ["base rootfs image", "images/current/rootfs.img"],
344
+ ["base image parameter", "images/current/parameter.txt"]
345
+ ];
346
+ const full = [
347
+ ["rootfs overlay", "userdata/rootfs"]
348
+ ];
349
+ if (mode === "minimal") {
350
+ return base;
351
+ }
352
+ if (mode === "compile") {
353
+ return [...base, ...compile];
354
+ }
355
+ if (mode === "qt") {
356
+ return [...base, ...compile, ...qt];
357
+ }
358
+ if (mode === "images") {
359
+ return [...base, ...images];
360
+ }
361
+ return [...base, ...compile, ...qt, ...images, ...full];
362
+ }
294
363
  export async function compileTaishanPiSingleFile(options) {
295
364
  assertAuthenticated(options.auth);
296
365
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
@@ -400,7 +469,7 @@ async function resolveLocalToolchainDownloadPlan(input) {
400
469
  return item.board_id === input.boardId
401
470
  && item.host === input.host
402
471
  && item.toolchain === input.toolchain
403
- && (item.kind === undefined || item.kind === "toolchain-archive");
472
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
404
473
  });
405
474
  if (!entry?.manifest) {
406
475
  return undefined;
@@ -413,20 +482,24 @@ async function resolveLocalToolchainDownloadPlan(input) {
413
482
  if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
414
483
  throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
415
484
  }
416
- if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes)) {
417
- throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive file, size, or SHA256.`);
418
- }
419
- const mirrors = orderDownloadMirrors((manifest.mirrors ?? [])
420
- .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
421
- .map((mirror) => ({
422
- kind: mirror.kind || "unknown",
423
- enabled: mirror.enabled !== false,
424
- url: mirror.url,
425
- sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
426
- size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
427
- })), manifest.download_policy?.preferred_order);
485
+ if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
486
+ && (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
487
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
488
+ }
489
+ const mirrors = manifest.archive
490
+ ? orderDownloadMirrors((manifest.mirrors ?? [])
491
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
492
+ .map((mirror) => ({
493
+ kind: mirror.kind || "unknown",
494
+ enabled: mirror.enabled !== false,
495
+ url: mirror.url,
496
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
497
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
498
+ })), manifest.download_policy?.preferred_order)
499
+ : [];
500
+ const components = (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl));
428
501
  const first = mirrors[0];
429
- if (!first) {
502
+ if (!first && components.length === 0) {
430
503
  return undefined;
431
504
  }
432
505
  return {
@@ -437,14 +510,52 @@ async function resolveLocalToolchainDownloadPlan(input) {
437
510
  board_id: input.boardId,
438
511
  host: input.host,
439
512
  toolchain: input.toolchain,
440
- source_url: first.url,
441
- mirror_kind: first.kind,
442
- archive: {
513
+ source_url: first?.url,
514
+ mirror_kind: first?.kind,
515
+ archive: manifest.archive ? {
443
516
  file: manifest.archive.file,
444
517
  size_bytes: manifest.archive.size_bytes,
445
518
  sha256: manifest.archive.sha256,
446
519
  content_type: manifest.archive.content_type
520
+ } : undefined,
521
+ mirrors,
522
+ components: components.length > 0 ? components : undefined,
523
+ default_mode: manifest.download_policy?.default_mode
524
+ };
525
+ }
526
+ function normalizeDownloadComponent(component, manifest, manifestUrl) {
527
+ if (!component.id || !component.version) {
528
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
529
+ }
530
+ if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
531
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
532
+ }
533
+ const mirrors = orderDownloadMirrors((component.mirrors ?? [])
534
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
535
+ .map((mirror) => ({
536
+ kind: mirror.kind || "unknown",
537
+ enabled: mirror.enabled !== false,
538
+ url: new URL(mirror.url, manifestUrl).toString(),
539
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
540
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
541
+ })), manifest.download_policy?.preferred_order);
542
+ const first = mirrors[0];
543
+ if (!first) {
544
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
545
+ }
546
+ return {
547
+ id: component.id,
548
+ version: component.version,
549
+ role: component.role,
550
+ install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
551
+ archive: {
552
+ file: component.archive.file,
553
+ size_bytes: component.archive.size_bytes,
554
+ sha256: component.archive.sha256,
555
+ content_type: component.archive.content_type
447
556
  },
557
+ source_url: first.url,
558
+ mirror_kind: first.kind,
448
559
  mirrors
449
560
  };
450
561
  }
@@ -528,7 +639,7 @@ function resolveInstallRoot(installRoot) {
528
639
  function localToolchainRegistryPath(installRoot) {
529
640
  return join(installRoot, "registry", "local-toolchains.json");
530
641
  }
531
- async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
642
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
532
643
  const registryPath = localToolchainRegistryPath(installRoot);
533
644
  await mkdir(dirname(registryPath), { recursive: true });
534
645
  await writeFile(registryPath, `${JSON.stringify({
@@ -537,8 +648,11 @@ async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
537
648
  version: latest.version,
538
649
  channel: latest.channel,
539
650
  host: latest.host,
651
+ mode,
540
652
  release_root: releaseRoot,
541
653
  packages: latest.packages,
654
+ source,
655
+ installed_components: source?.components,
542
656
  updated_at: new Date().toISOString()
543
657
  }, null, 2)}\n`, "utf8");
544
658
  }
@@ -576,12 +690,18 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
576
690
  source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
577
691
  };
578
692
  }
693
+ if (latest.download?.components?.length) {
694
+ return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
695
+ }
579
696
  if (latest.download) {
580
697
  const failures = [];
581
698
  for (const mirror of latest.download.mirrors) {
582
699
  if (!mirror.enabled) {
583
700
  continue;
584
701
  }
702
+ if (!latest.download.archive) {
703
+ continue;
704
+ }
585
705
  const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
586
706
  try {
587
707
  const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
@@ -622,6 +742,71 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
622
742
  }
623
743
  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.");
624
744
  }
745
+ async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
746
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
747
+ const components = selectedDownloadComponents(download.components ?? [], mode);
748
+ if (components.length === 0) {
749
+ throw new Error(`No local toolchain components selected for mode ${mode}.`);
750
+ }
751
+ const extractRoot = join(tempDir, "extract-components");
752
+ await mkdir(extractRoot, { recursive: true });
753
+ const installedComponents = [];
754
+ const failures = [];
755
+ for (const component of components) {
756
+ let installed = false;
757
+ for (const mirror of component.mirrors) {
758
+ if (!mirror.enabled) {
759
+ continue;
760
+ }
761
+ try {
762
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
763
+ sha256: mirror.sha256 ?? component.archive.sha256,
764
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes
765
+ });
766
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
767
+ if (extracted.exit_code !== 0) {
768
+ throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
769
+ }
770
+ installedComponents.push({
771
+ id: component.id,
772
+ version: component.version,
773
+ role: component.role,
774
+ archive_file: component.archive.file,
775
+ mirror_kind: mirror.kind,
776
+ downloaded_path: downloadedPath,
777
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
778
+ sha256: mirror.sha256 ?? component.archive.sha256
779
+ });
780
+ installed = true;
781
+ break;
782
+ }
783
+ catch (error) {
784
+ failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
785
+ }
786
+ }
787
+ if (!installed) {
788
+ throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
789
+ }
790
+ }
791
+ return {
792
+ path: extractRoot,
793
+ source: {
794
+ kind: "components",
795
+ value: download.manifest_url,
796
+ mirror_kind: "components",
797
+ size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
798
+ components: installedComponents
799
+ }
800
+ };
801
+ }
802
+ function selectedDownloadComponents(components, mode) {
803
+ return components.filter((component) => {
804
+ if (!component.install_modes?.length) {
805
+ return true;
806
+ }
807
+ return component.install_modes.includes(mode);
808
+ });
809
+ }
625
810
  async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
626
811
  const downloadsDir = join(installRoot, "cache", "downloads");
627
812
  await mkdir(downloadsDir, { recursive: true });