@kvell007/embed-labs-cli 0.1.0-alpha.10 → 0.1.0-alpha.12

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>;
@@ -21,6 +21,8 @@ const BUILT_IN_CHANNEL = {
21
21
  { id: "embedlabs.tools.vendor.rockchip", version: "1.0.0", manifest: "" },
22
22
  { id: "embedlabs.tools.common.llvm", version: "22.1.3", manifest: "" },
23
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: "" },
24
26
  { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
25
27
  { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" }
26
28
  ]
@@ -50,6 +52,22 @@ const BUILT_IN_MANIFESTS = {
50
52
  hosts: ["darwin-arm64", "linux-x86_64"],
51
53
  provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
52
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
+ },
53
71
  "embedlabs.family.rk356x": {
54
72
  schema: "embedlabs.package.v1",
55
73
  id: "embedlabs.family.rk356x",
@@ -74,7 +92,9 @@ const BUILT_IN_MANIFESTS = {
74
92
  { id: "embedlabs.family.rk356x", version: "^1.0.0" },
75
93
  { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
76
94
  { id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
77
- { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] }
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"] }
78
98
  ],
79
99
  build_modes: ["local-llvm"]
80
100
  }
@@ -83,12 +103,23 @@ const INSTALL_COPY_PATHS = [
83
103
  "toolchain/llvm-cross",
84
104
  "toolchain/host",
85
105
  "toolchain/host-tools",
106
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
86
107
  "qt-target/qt6-rk3566-llvm-6.8.3",
108
+ "qt-host/qt6-host-macos-6.8.3",
87
109
  "tools/mac",
110
+ "toolkit-runtime/qtquick-live-preview",
111
+ "toolkit-runtime/rp2350-monitor",
112
+ "toolkit-runtime/RP2350-Monitor",
88
113
  "images/current",
89
114
  "userdata/rootfs",
90
- "boot-workspace/kernel-tree"
115
+ "boot-workspace",
116
+ "README.md",
117
+ "meta",
118
+ "scripts",
119
+ "support",
120
+ "third_party"
91
121
  ];
122
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "compile", "qt", "full", "images"];
92
123
  export function defaultLocalReleaseRoot() {
93
124
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
94
125
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -121,7 +152,7 @@ export async function latestLocalToolchain(options = {}) {
121
152
  board_id: boardId,
122
153
  channel: channel.channel,
123
154
  host: hostId(),
124
- version: board.version,
155
+ version: download?.version ?? board.version,
125
156
  metadata_root: metadataRoot,
126
157
  packages,
127
158
  download,
@@ -138,6 +169,7 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
138
169
  installed: !!releaseRoot,
139
170
  board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
140
171
  version: typeof registry.version === "string" ? registry.version : undefined,
172
+ mode: typeof registry.mode === "string" ? registry.mode : undefined,
141
173
  release_root: releaseRoot,
142
174
  registry_path: registryPath,
143
175
  install_root: root,
@@ -158,30 +190,42 @@ export async function installLocalToolchain(options = {}) {
158
190
  const latest = await latestLocalToolchain(options);
159
191
  const installRoot = resolveInstallRoot(options.installRoot);
160
192
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
193
+ const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
161
194
  if (await pathExists(releaseRoot) && !options.force) {
162
- const validation = await validateLocalToolchain(releaseRoot);
195
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
163
196
  if (!validation.ok) {
164
- 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
+ };
165
221
  }
166
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
167
- return {
168
- board_id: latest.board_id,
169
- version: latest.version,
170
- channel: latest.channel,
171
- host: latest.host,
172
- install_root: installRoot,
173
- release_root: releaseRoot,
174
- registry_path: localToolchainRegistryPath(installRoot),
175
- source: { kind: "directory", value: releaseRoot },
176
- installed_paths: [],
177
- packages: latest.packages,
178
- validation
179
- };
180
222
  }
181
223
  const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
182
224
  try {
183
225
  const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
184
- 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
+ }
185
229
  await mkdir(releaseRoot, { recursive: true });
186
230
  const installedPaths = [];
187
231
  for (const relativePath of INSTALL_COPY_PATHS) {
@@ -199,8 +243,8 @@ export async function installLocalToolchain(options = {}) {
199
243
  });
200
244
  installedPaths.push(relativePath);
201
245
  }
202
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
203
- const validation = await validateLocalToolchain(releaseRoot);
246
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
247
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
204
248
  if (!validation.ok) {
205
249
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
206
250
  }
@@ -209,6 +253,7 @@ export async function installLocalToolchain(options = {}) {
209
253
  version: latest.version,
210
254
  channel: latest.channel,
211
255
  host: latest.host,
256
+ mode: installMode,
212
257
  install_root: installRoot,
213
258
  release_root: releaseRoot,
214
259
  registry_path: localToolchainRegistryPath(installRoot),
@@ -222,23 +267,11 @@ export async function installLocalToolchain(options = {}) {
222
267
  await rm(tempDir, { recursive: true, force: true });
223
268
  }
224
269
  }
225
- 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);
226
273
  const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
227
- const required = [
228
- ["release root", "."],
229
- ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
230
- ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
231
- ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
232
- ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
233
- ["host GCC libraries", "toolchain/host/lib/gcc"],
234
- ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
235
- ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
236
- ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
237
- ["Rockchip mkimage", "tools/mac/mkimage"],
238
- ["Rockchip dumpimage", "tools/mac/dumpimage"],
239
- ["Rockchip resource_tool", "tools/mac/resource_tool"],
240
- ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"]
241
- ];
274
+ const required = requiredLocalToolchainChecks(mode);
242
275
  const checked_paths = [];
243
276
  for (const [label, relativePath] of required) {
244
277
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -251,6 +284,7 @@ export async function validateLocalToolchain(releaseRoot) {
251
284
  const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
252
285
  return {
253
286
  ok: missing_paths.length === 0,
287
+ mode,
254
288
  host: {
255
289
  platform: platform(),
256
290
  arch: arch()
@@ -261,10 +295,71 @@ export async function validateLocalToolchain(releaseRoot) {
261
295
  missing_paths,
262
296
  notes: [
263
297
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
264
- "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}.`
265
299
  ]
266
300
  };
267
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
+ }
268
363
  export async function compileTaishanPiSingleFile(options) {
269
364
  assertAuthenticated(options.auth);
270
365
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
@@ -374,7 +469,7 @@ async function resolveLocalToolchainDownloadPlan(input) {
374
469
  return item.board_id === input.boardId
375
470
  && item.host === input.host
376
471
  && item.toolchain === input.toolchain
377
- && (item.kind === undefined || item.kind === "toolchain-archive");
472
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
378
473
  });
379
474
  if (!entry?.manifest) {
380
475
  return undefined;
@@ -387,20 +482,24 @@ async function resolveLocalToolchainDownloadPlan(input) {
387
482
  if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
388
483
  throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
389
484
  }
390
- if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes)) {
391
- throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive file, size, or SHA256.`);
392
- }
393
- const mirrors = orderDownloadMirrors((manifest.mirrors ?? [])
394
- .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
395
- .map((mirror) => ({
396
- kind: mirror.kind || "unknown",
397
- enabled: mirror.enabled !== false,
398
- url: mirror.url,
399
- sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
400
- size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
401
- })), 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));
402
501
  const first = mirrors[0];
403
- if (!first) {
502
+ if (!first && components.length === 0) {
404
503
  return undefined;
405
504
  }
406
505
  return {
@@ -411,14 +510,52 @@ async function resolveLocalToolchainDownloadPlan(input) {
411
510
  board_id: input.boardId,
412
511
  host: input.host,
413
512
  toolchain: input.toolchain,
414
- source_url: first.url,
415
- mirror_kind: first.kind,
416
- archive: {
513
+ source_url: first?.url,
514
+ mirror_kind: first?.kind,
515
+ archive: manifest.archive ? {
417
516
  file: manifest.archive.file,
418
517
  size_bytes: manifest.archive.size_bytes,
419
518
  sha256: manifest.archive.sha256,
420
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
421
556
  },
557
+ source_url: first.url,
558
+ mirror_kind: first.kind,
422
559
  mirrors
423
560
  };
424
561
  }
@@ -502,7 +639,7 @@ function resolveInstallRoot(installRoot) {
502
639
  function localToolchainRegistryPath(installRoot) {
503
640
  return join(installRoot, "registry", "local-toolchains.json");
504
641
  }
505
- async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
642
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
506
643
  const registryPath = localToolchainRegistryPath(installRoot);
507
644
  await mkdir(dirname(registryPath), { recursive: true });
508
645
  await writeFile(registryPath, `${JSON.stringify({
@@ -511,8 +648,11 @@ async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
511
648
  version: latest.version,
512
649
  channel: latest.channel,
513
650
  host: latest.host,
651
+ mode,
514
652
  release_root: releaseRoot,
515
653
  packages: latest.packages,
654
+ source,
655
+ installed_components: source?.components,
516
656
  updated_at: new Date().toISOString()
517
657
  }, null, 2)}\n`, "utf8");
518
658
  }
@@ -550,12 +690,18 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
550
690
  source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
551
691
  };
552
692
  }
693
+ if (latest.download?.components?.length) {
694
+ return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
695
+ }
553
696
  if (latest.download) {
554
697
  const failures = [];
555
698
  for (const mirror of latest.download.mirrors) {
556
699
  if (!mirror.enabled) {
557
700
  continue;
558
701
  }
702
+ if (!latest.download.archive) {
703
+ continue;
704
+ }
559
705
  const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
560
706
  try {
561
707
  const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
@@ -596,6 +742,71 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
596
742
  }
597
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.");
598
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
+ }
599
810
  async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
600
811
  const downloadsDir = join(installRoot, "cache", "downloads");
601
812
  await mkdir(downloadsDir, { recursive: true });