@kvell007/embed-labs-cli 0.1.0-alpha.7 → 0.1.0-alpha.70

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.
@@ -8,9 +8,9 @@ import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { fileURLToPath } from "node:url";
10
10
  const DEFAULT_RELEASE_ROOT = "/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal";
11
- const DEFAULT_QT_SMOKE_SOURCE = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/qt-smoke";
12
- const DEFAULT_METADATA_ROOT = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/embedlabs-release";
13
11
  const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
12
+ const PICO2W_RP2350_BOARD_ID = "pico2w-rp2350-monitor";
13
+ const COLOREASYPICO2_RP2350_BOARD_ID = "coloreasypico2-rp2350-monitor";
14
14
  const DEFAULT_CHANNEL = "stable";
15
15
  const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
16
16
  const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
@@ -20,9 +20,15 @@ const BUILT_IN_CHANNEL = {
20
20
  packages: [
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
+ { id: "embedlabs.tools.common.arm-none-eabi", version: "15.2.rel1", manifest: "" },
23
24
  { id: "embedlabs.tools.common.e2fsprogs", version: "1.0.0", manifest: "" },
25
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.33", manifest: "" },
26
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.33", manifest: "" },
24
27
  { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
25
- { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" }
28
+ { id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
29
+ { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.33", manifest: "" },
30
+ { id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.33", manifest: "" },
31
+ { id: "embedlabs.board.coloreasypico2.rp2350-monitor", version: "1.0.33", manifest: "" }
26
32
  ]
27
33
  };
28
34
  const BUILT_IN_MANIFESTS = {
@@ -42,6 +48,14 @@ const BUILT_IN_MANIFESTS = {
42
48
  hosts: ["darwin-arm64", "linux-x86_64"],
43
49
  provides: ["llvm.clang", "llvm.clangxx", "llvm.ld_lld", "llvm.ar", "llvm.objcopy", "llvm.readelf"]
44
50
  },
51
+ "embedlabs.tools.common.arm-none-eabi": {
52
+ schema: "embedlabs.package.v1",
53
+ id: "embedlabs.tools.common.arm-none-eabi",
54
+ version: "15.2.rel1",
55
+ kind: "tools",
56
+ hosts: ["darwin-arm64"],
57
+ provides: ["arm-none-eabi.gcc", "arm-none-eabi.g++", "arm-none-eabi.objcopy", "picotool"]
58
+ },
45
59
  "embedlabs.tools.common.e2fsprogs": {
46
60
  schema: "embedlabs.package.v1",
47
61
  id: "embedlabs.tools.common.e2fsprogs",
@@ -50,6 +64,22 @@ const BUILT_IN_MANIFESTS = {
50
64
  hosts: ["darwin-arm64", "linux-x86_64"],
51
65
  provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
52
66
  },
67
+ "embedlabs.tools.runtime.qtquick-live-preview": {
68
+ schema: "embedlabs.package.v1",
69
+ id: "embedlabs.tools.runtime.qtquick-live-preview",
70
+ version: "1.0.33",
71
+ kind: "tools",
72
+ hosts: ["darwin-arm64", "linux-x86_64"],
73
+ provides: ["qtquick.live_preview", "qtquick.live_preview.inspector", "qtquick.live_preview.feedback"]
74
+ },
75
+ "embedlabs.tools.runtime.rp2350-monitor": {
76
+ schema: "embedlabs.package.v1",
77
+ id: "embedlabs.tools.runtime.rp2350-monitor",
78
+ version: "1.0.33",
79
+ kind: "tools",
80
+ hosts: ["darwin-arm64", "linux-x86_64"],
81
+ provides: ["rp2350.monitor.cli", "rp2350.monitor.logic_analyzer", "rp2350.monitor.logic_decode"]
82
+ },
53
83
  "embedlabs.family.rk356x": {
54
84
  schema: "embedlabs.package.v1",
55
85
  id: "embedlabs.family.rk356x",
@@ -62,10 +92,21 @@ const BUILT_IN_MANIFESTS = {
62
92
  { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
63
93
  ]
64
94
  },
95
+ "embedlabs.family.rp2350": {
96
+ schema: "embedlabs.package.v1",
97
+ id: "embedlabs.family.rp2350",
98
+ version: "1.0.0",
99
+ kind: "family",
100
+ family: "rp2350",
101
+ requires: [
102
+ { id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2"] },
103
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
104
+ ]
105
+ },
65
106
  "embedlabs.board.taishanpi.1m-rk3566": {
66
107
  schema: "embedlabs.package.v1",
67
108
  id: "embedlabs.board.taishanpi.1m-rk3566",
68
- version: "1.0.31",
109
+ version: "1.0.33",
69
110
  kind: "board",
70
111
  family: "rk356x",
71
112
  board: "TaishanPi",
@@ -74,20 +115,68 @@ const BUILT_IN_MANIFESTS = {
74
115
  { id: "embedlabs.family.rk356x", version: "^1.0.0" },
75
116
  { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
76
117
  { id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
77
- { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] }
118
+ { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
119
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.33", roles: ["qtquick-preview"] }
78
120
  ],
79
121
  build_modes: ["local-llvm"]
122
+ },
123
+ "embedlabs.board.pico2w.rp2350-monitor": {
124
+ schema: "embedlabs.package.v1",
125
+ id: "embedlabs.board.pico2w.rp2350-monitor",
126
+ version: "1.0.33",
127
+ kind: "board",
128
+ display_name: "Pico 2 W",
129
+ family: "rp2350",
130
+ board: "Pico 2 W",
131
+ board_id: PICO2W_RP2350_BOARD_ID,
132
+ requires: [
133
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
134
+ { id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
135
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
136
+ ],
137
+ build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
138
+ },
139
+ "embedlabs.board.coloreasypico2.rp2350-monitor": {
140
+ schema: "embedlabs.package.v1",
141
+ id: "embedlabs.board.coloreasypico2.rp2350-monitor",
142
+ version: "1.0.33",
143
+ kind: "board",
144
+ display_name: "ColorEasyPICO2",
145
+ family: "rp2350",
146
+ board: "ColorEasyPICO2",
147
+ board_id: COLOREASYPICO2_RP2350_BOARD_ID,
148
+ requires: [
149
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
150
+ { id: "embedlabs.tools.common.arm-none-eabi", version: "15.x", roles: ["compile", "uf2", "picotool"] },
151
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.33", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
152
+ ],
153
+ build_modes: ["local-pico-sdk", "optional-rp2350-monitor"]
80
154
  }
81
155
  };
82
156
  const INSTALL_COPY_PATHS = [
83
157
  "toolchain/llvm-cross",
84
- "toolchain/host/aarch64-buildroot-linux-gnu/sysroot",
158
+ "toolchain/host",
159
+ "toolchain/host-tools",
160
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
85
161
  "qt-target/qt6-rk3566-llvm-6.8.3",
162
+ "qt-host/qt6-host-macos-6.8.3",
163
+ "qt-host/qt6-host-linux-x86_64-6.8.3",
86
164
  "tools/mac",
165
+ "toolkit-runtime/qtquick-live-preview",
166
+ "toolkit-runtime/rp2350-monitor",
167
+ "rp2350-sdk",
168
+ "rp2350-initial-firmware",
169
+ "rp2350-examples",
87
170
  "images/current",
88
171
  "userdata/rootfs",
89
- "boot-workspace/kernel-tree"
172
+ "boot-workspace",
173
+ "README.md",
174
+ "meta",
175
+ "scripts",
176
+ "support",
177
+ "third_party"
90
178
  ];
179
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
91
180
  export function defaultLocalReleaseRoot() {
92
181
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
93
182
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -96,31 +185,39 @@ export function defaultLocalReleaseRoot() {
96
185
  export async function latestLocalToolchain(options = {}) {
97
186
  const boardId = options.boardId ?? DEFAULT_BOARD_ID;
98
187
  const channelName = options.channel ?? DEFAULT_CHANNEL;
188
+ const host = localToolchainHostId();
99
189
  const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
100
190
  const boardPackageId = boardPackageIdFor(boardId);
101
191
  const board = manifests.get(boardPackageId);
102
192
  if (!board) {
103
193
  throw new Error(`No local toolchain board package found for ${boardId}.`);
104
194
  }
195
+ const canonicalBoardId = boardIdForPackageManifest(board);
105
196
  const packages = resolvePackageRefs(boardPackageId, channel, manifests);
197
+ const downloadHost = isNativeWindowsTaishanPiHost(canonicalBoardId, host)
198
+ ? wslHostForWindowsHost(host) ?? host
199
+ : host;
106
200
  let download;
107
201
  let downloadError;
108
202
  try {
109
203
  download = await resolveLocalToolchainDownloadPlan({
110
- boardId,
204
+ boardId: canonicalBoardId,
111
205
  channel: channelName,
112
- host: hostId(),
206
+ host: downloadHost,
113
207
  toolchain: "llvm"
114
208
  });
115
209
  }
116
210
  catch (error) {
117
211
  downloadError = error instanceof Error ? error.message : String(error);
118
212
  }
213
+ if (!download && isNativeWindowsTaishanPiHost(canonicalBoardId, host)) {
214
+ downloadError = taishanPiWindowsRequirementMessage(host);
215
+ }
119
216
  return {
120
- board_id: boardId,
121
- channel: channel.channel,
122
- host: hostId(),
123
- version: board.version,
217
+ board_id: canonicalBoardId,
218
+ channel: channelName,
219
+ host,
220
+ version: download?.version ?? board.version,
124
221
  metadata_root: metadataRoot,
125
222
  packages,
126
223
  download,
@@ -132,11 +229,36 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
132
229
  const registryPath = localToolchainRegistryPath(root);
133
230
  try {
134
231
  const registry = JSON.parse(await readFile(registryPath, "utf8"));
232
+ const environments = registry.environments;
233
+ const boardInstall = environments?.[normalizeBoardId(boardId)];
234
+ if (boardInstall?.release_root) {
235
+ return {
236
+ installed: true,
237
+ board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
238
+ version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
239
+ mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
240
+ release_root: boardInstall.release_root,
241
+ registry_path: registryPath,
242
+ install_root: root,
243
+ channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
244
+ packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
245
+ };
246
+ }
135
247
  const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
248
+ const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
249
+ if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
250
+ return {
251
+ installed: false,
252
+ board_id: boardId,
253
+ registry_path: registryPath,
254
+ install_root: root
255
+ };
256
+ }
136
257
  return {
137
258
  installed: !!releaseRoot,
138
- board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
259
+ board_id: registryBoardId,
139
260
  version: typeof registry.version === "string" ? registry.version : undefined,
261
+ mode: typeof registry.mode === "string" ? registry.mode : undefined,
140
262
  release_root: releaseRoot,
141
263
  registry_path: registryPath,
142
264
  install_root: root,
@@ -153,48 +275,251 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
153
275
  };
154
276
  }
155
277
  }
278
+ async function discoverInstalledLocalToolchains(installRoot, current) {
279
+ const installed = new Map();
280
+ if (current.installed && current.release_root) {
281
+ installed.set(normalizeBoardId(current.board_id), {
282
+ board_id: current.board_id,
283
+ version: current.version,
284
+ channel: current.channel,
285
+ mode: current.mode,
286
+ release_root: current.release_root
287
+ });
288
+ }
289
+ const toolchainsRoot = join(installRoot, "toolchains");
290
+ let boardEntries;
291
+ try {
292
+ boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
293
+ }
294
+ catch {
295
+ return installed;
296
+ }
297
+ for (const boardEntry of boardEntries) {
298
+ if (!boardEntry.isDirectory()) {
299
+ continue;
300
+ }
301
+ const boardId = normalizeBoardId(boardEntry.name);
302
+ if (installed.has(boardId)) {
303
+ continue;
304
+ }
305
+ const boardRoot = join(toolchainsRoot, boardEntry.name);
306
+ let versionEntries;
307
+ try {
308
+ versionEntries = await readdir(boardRoot, { withFileTypes: true });
309
+ }
310
+ catch {
311
+ continue;
312
+ }
313
+ const versions = versionEntries
314
+ .filter((entry) => entry.isDirectory())
315
+ .map((entry) => entry.name)
316
+ .sort(compareVersionLike)
317
+ .reverse();
318
+ const version = versions[0];
319
+ if (!version) {
320
+ continue;
321
+ }
322
+ installed.set(boardId, {
323
+ board_id: boardId,
324
+ version,
325
+ release_root: join(boardRoot, version)
326
+ });
327
+ }
328
+ return installed;
329
+ }
330
+ export async function listLocalToolchainEnvironments(options = {}) {
331
+ const channelName = options.channel ?? DEFAULT_CHANNEL;
332
+ const host = localToolchainHostId();
333
+ const installRoot = resolveInstallRoot(options.installRoot);
334
+ const registryPath = localToolchainRegistryPath(installRoot);
335
+ const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
336
+ const current = await currentLocalToolchain(installRoot);
337
+ const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
338
+ const boardManifests = [...manifests.values()]
339
+ .filter((manifest) => manifest.kind === "board")
340
+ .filter((manifest) => {
341
+ if (!options.boardId) {
342
+ return true;
343
+ }
344
+ const normalizedFilter = normalizeBoardId(options.boardId);
345
+ return boardIdForPackageManifest(manifest) === normalizedFilter
346
+ || manifest.id === options.boardId
347
+ || manifest.id === packageIdForBoardFilter(options.boardId);
348
+ })
349
+ .sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
350
+ const environments = [];
351
+ const wslStatus = host.startsWith("win32-")
352
+ ? await windowsWslStatus()
353
+ : undefined;
354
+ for (const board of boardManifests) {
355
+ const boardId = boardIdForPackageManifest(board);
356
+ const packages = resolvePackageRefs(board.id, channel, manifests);
357
+ const hostSupport = packageHostSupport(packages, manifests, host);
358
+ let download;
359
+ let downloadError;
360
+ const downloadHost = isNativeWindowsTaishanPiHost(boardId, host)
361
+ ? wslHostForWindowsHost(host) ?? host
362
+ : host;
363
+ try {
364
+ download = await resolveLocalToolchainDownloadPlan({
365
+ boardId,
366
+ channel: channelName,
367
+ host: downloadHost,
368
+ toolchain: "llvm"
369
+ });
370
+ }
371
+ catch (error) {
372
+ downloadError = error instanceof Error ? error.message : String(error);
373
+ }
374
+ if (!download && isNativeWindowsTaishanPiHost(boardId, host)) {
375
+ downloadError = taishanPiWindowsRequirementMessage(host);
376
+ }
377
+ const latestVersion = download?.version ?? board.version;
378
+ const currentForBoard = await currentLocalToolchain(installRoot, boardId);
379
+ const installedCandidate = currentForBoard.installed
380
+ ? currentForBoard
381
+ : installedByBoard.get(normalizeBoardId(boardId));
382
+ const installed = installedCandidate
383
+ ? {
384
+ version: installedCandidate.version,
385
+ channel: installedCandidate.channel,
386
+ mode: installedCandidate.mode,
387
+ release_root: installedCandidate.release_root
388
+ }
389
+ : undefined;
390
+ const updateAvailable = !!installed?.version && installed.version !== latestVersion;
391
+ const nativeWindowsTaishanPi = isNativeWindowsTaishanPiHost(boardId, host);
392
+ const execution = nativeWindowsTaishanPi
393
+ ? taishanPiWindowsExecutionRoute(host, wslStatus, !!download)
394
+ : {
395
+ kind: "native",
396
+ supported: !!download || !!installed || hostSupport.supported,
397
+ actual_host: host
398
+ };
399
+ const effectiveHostSupport = nativeWindowsTaishanPi
400
+ ? { supported: !!download && execution.supported, unsupportedPackages: download ? [] : ["taishanpi-1m-rk3566 native Windows toolchain"] }
401
+ : download || installed
402
+ ? { supported: true, unsupportedPackages: [] }
403
+ : hostSupport;
404
+ const status = !effectiveHostSupport.supported
405
+ ? "unsupported_host"
406
+ : updateAvailable
407
+ ? "update_available"
408
+ : installed
409
+ ? "installed"
410
+ : "available";
411
+ const mode = download?.default_mode ?? "qt";
412
+ const installModes = download
413
+ ? localToolchainInstallModesForDownload(download)
414
+ : installed?.mode
415
+ ? [installed.mode]
416
+ : localToolchainInstallModesForDownload(download);
417
+ environments.push({
418
+ board_id: boardId,
419
+ package_id: board.id,
420
+ display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
421
+ family: board.family,
422
+ variant: board.variant,
423
+ channel: channelName,
424
+ host,
425
+ status,
426
+ supported_host: effectiveHostSupport.supported,
427
+ unsupported_packages: effectiveHostSupport.unsupportedPackages,
428
+ install_modes: installModes,
429
+ installed,
430
+ latest: {
431
+ version: latestVersion,
432
+ default_mode: download?.default_mode,
433
+ source_url: download?.source_url,
434
+ manifest_url: download?.manifest_url,
435
+ component_count: download?.components?.length,
436
+ download_error: downloadError
437
+ },
438
+ packages,
439
+ components: download?.components?.map(localToolchainEnvironmentComponent),
440
+ execution,
441
+ install_command: nativeWindowsTaishanPi ? `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode}` : `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode}`,
442
+ update_command: nativeWindowsTaishanPi ? `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode} --force` : `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode} --force`,
443
+ notes: environmentNotes({ boardId, host, status, downloadError, unsupportedPackages: effectiveHostSupport.unsupportedPackages })
444
+ });
445
+ }
446
+ const filteredEnvironments = options.installedOnly
447
+ ? environments.filter((environment) => !!environment.installed)
448
+ : environments;
449
+ return {
450
+ host,
451
+ channel: channelName,
452
+ metadata_source: metadataRoot ? "local_override" : "built_in",
453
+ metadata_root: metadataRoot,
454
+ install_root: installRoot,
455
+ registry_path: registryPath,
456
+ environments: filteredEnvironments
457
+ };
458
+ }
156
459
  export async function installLocalToolchain(options = {}) {
157
460
  const latest = await latestLocalToolchain(options);
461
+ if (isNativeWindowsTaishanPiHost(latest.board_id, latest.host)) {
462
+ return await installLocalToolchainWindowsWsl(options, latest);
463
+ }
158
464
  const installRoot = resolveInstallRoot(options.installRoot);
159
465
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
466
+ const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
160
467
  if (await pathExists(releaseRoot) && !options.force) {
161
- const validation = await validateLocalToolchain(releaseRoot);
468
+ await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
469
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
162
470
  if (!validation.ok) {
163
- throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
471
+ if (latest.download?.components?.length) {
472
+ // Component installs can upgrade an existing lower-mode install by overlaying
473
+ // only the newly selected components instead of deleting the whole tree.
474
+ }
475
+ else {
476
+ throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
477
+ }
478
+ }
479
+ else {
480
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
481
+ return {
482
+ board_id: latest.board_id,
483
+ version: latest.version,
484
+ channel: latest.channel,
485
+ host: latest.host,
486
+ mode: installMode,
487
+ install_root: installRoot,
488
+ release_root: releaseRoot,
489
+ registry_path: localToolchainRegistryPath(installRoot),
490
+ source: { kind: "directory", value: releaseRoot },
491
+ installed_paths: [],
492
+ packages: latest.packages,
493
+ validation
494
+ };
164
495
  }
165
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
166
- return {
167
- board_id: latest.board_id,
168
- version: latest.version,
169
- channel: latest.channel,
170
- host: latest.host,
171
- install_root: installRoot,
172
- release_root: releaseRoot,
173
- registry_path: localToolchainRegistryPath(installRoot),
174
- source: { kind: "directory", value: releaseRoot },
175
- installed_paths: [],
176
- packages: latest.packages,
177
- validation
178
- };
179
496
  }
180
497
  const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
181
498
  try {
182
499
  const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
183
- await rm(releaseRoot, { recursive: true, force: true });
500
+ if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
501
+ await rm(releaseRoot, { recursive: true, force: true });
502
+ }
184
503
  await mkdir(releaseRoot, { recursive: true });
185
504
  const installedPaths = [];
186
- for (const relativePath of INSTALL_COPY_PATHS) {
187
- const sourcePath = resolve(sourceRoot.path, relativePath);
505
+ for (const relativePath of installCopyPathsForBoard(latest.board_id)) {
506
+ const sourcePath = await resolveInstallSourcePathForBoard(sourceRoot.path, relativePath, latest.board_id);
188
507
  if (!await pathExists(sourcePath)) {
189
508
  continue;
190
509
  }
191
510
  const targetPath = resolve(releaseRoot, relativePath);
192
511
  await mkdir(dirname(targetPath), { recursive: true });
193
- await cp(sourcePath, targetPath, { recursive: true, force: true, preserveTimestamps: true });
512
+ await cp(sourcePath, targetPath, {
513
+ recursive: true,
514
+ force: true,
515
+ preserveTimestamps: true,
516
+ verbatimSymlinks: true
517
+ });
194
518
  installedPaths.push(relativePath);
195
519
  }
196
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
197
- const validation = await validateLocalToolchain(releaseRoot);
520
+ await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
521
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
522
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
198
523
  if (!validation.ok) {
199
524
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
200
525
  }
@@ -203,6 +528,7 @@ export async function installLocalToolchain(options = {}) {
203
528
  version: latest.version,
204
529
  channel: latest.channel,
205
530
  host: latest.host,
531
+ mode: installMode,
206
532
  install_root: installRoot,
207
533
  release_root: releaseRoot,
208
534
  registry_path: localToolchainRegistryPath(installRoot),
@@ -216,19 +542,230 @@ export async function installLocalToolchain(options = {}) {
216
542
  await rm(tempDir, { recursive: true, force: true });
217
543
  }
218
544
  }
219
- export async function validateLocalToolchain(releaseRoot) {
220
- const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
221
- const required = [
222
- ["release root", "."],
223
- ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
224
- ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
225
- ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
226
- ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
227
- ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
228
- ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
229
- ["Rockchip mkimage", "tools/mac/mkimage"],
230
- ["Rockchip resource_tool", "tools/mac/resource_tool"]
231
- ];
545
+ async function installLocalToolchainWindowsWsl(options, latest) {
546
+ const route = taishanPiWindowsExecutionRoute(latest.host, await windowsWslStatus());
547
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
548
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
549
+ }
550
+ const installRoot = normalizeWindowsWslInstallRoot(options.installRoot);
551
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
552
+ const cli = process.env.EMBEDLABS_WINDOWS_WSL_CLI?.trim() || process.env.EMBEDLABS_WSL_CLI?.trim() || "npx -y embedlabs@latest";
553
+ const command = [
554
+ cli,
555
+ "local toolchain install",
556
+ `--board ${sh(latest.board_id)}`,
557
+ `--channel ${sh(latest.channel)}`,
558
+ `--mode ${sh(mode)}`,
559
+ `--install-root ${shWslPath(installRoot)}`,
560
+ options.force ? "--force" : "",
561
+ options.sourceUrl ? `--source-url ${sh(options.sourceUrl)}` : "",
562
+ options.sourceReleaseRoot ? `--source-release-root ${shWslPath(normalizeWindowsWslMaybePath(options.sourceReleaseRoot, "source-release-root"))}` : "",
563
+ "--json"
564
+ ].filter(Boolean).join(" ");
565
+ const script = [
566
+ "set -euo pipefail",
567
+ `RESULT_FILE="$(mktemp)"`,
568
+ `EMBED_DOWNLOAD_BASE_URL=${sh(downloadBaseUrl())} ${command} > "$RESULT_FILE"`,
569
+ "node -e 'const fs=require(\"fs\"); const payload=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); if (!payload.ok) { console.error(JSON.stringify(payload)); process.exit(2); } console.log(JSON.stringify(payload.data));' \"$RESULT_FILE\""
570
+ ].join("\n");
571
+ const result = await runWindowsWslBash(script);
572
+ if (result.exit_code !== 0 || result.stdout_tail.length < 1) {
573
+ throw new Error(`Windows WSL TaishanPi toolchain install failed: ${result.stderr_tail.join("\n") || result.stdout_tail.join("\n")}`);
574
+ }
575
+ const payload = JSON.parse(result.stdout_tail[result.stdout_tail.length - 1]);
576
+ return {
577
+ ...payload,
578
+ host: latest.host,
579
+ source: {
580
+ ...payload.source,
581
+ value: `wsl:${payload.source.value}`
582
+ },
583
+ validation: {
584
+ ...payload.validation,
585
+ notes: [
586
+ ...payload.validation.notes,
587
+ "Installed through Windows CLI by delegating TaishanPi linux-x86_64 tools to WSL2."
588
+ ]
589
+ }
590
+ };
591
+ }
592
+ export async function windowsWslStatus() {
593
+ const result = {
594
+ host: hostId(),
595
+ platform: platform(),
596
+ arch: arch(),
597
+ checked_at: new Date().toISOString(),
598
+ applicable: platform() === "win32",
599
+ wsl_available: false,
600
+ usable: false,
601
+ distributions: [],
602
+ online_distributions: [],
603
+ taishanpi_execution: {
604
+ supported: false,
605
+ required_host: "linux-x86_64",
606
+ windows_host: hostId(),
607
+ route: "wsl2",
608
+ status: platform() === "win32" ? "wsl_missing" : "wsl_not_applicable",
609
+ install_command: "embedlabs local wsl status",
610
+ reason: platform() === "win32"
611
+ ? "WSL status has not been checked yet."
612
+ : "WSL2 is only applicable on Windows hosts."
613
+ },
614
+ commands: {
615
+ status: "wsl.exe --status",
616
+ list: "wsl.exe -l -v",
617
+ list_online: "wsl.exe --list --online",
618
+ install_ubuntu: "wsl.exe --install -d Ubuntu --web-download"
619
+ },
620
+ notes: []
621
+ };
622
+ if (!result.applicable) {
623
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
624
+ result.notes.push("WSL2 is only required for TaishanPi local development on Windows hosts.");
625
+ return result;
626
+ }
627
+ const whereWsl = await runCommand(["cmd", "/c", "where wsl.exe"], homedir());
628
+ result.wsl_available = whereWsl.exit_code === 0;
629
+ if (!result.wsl_available) {
630
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
631
+ result.notes.push("wsl.exe was not found. Enable Windows Subsystem for Linux before installing TaishanPi local tools.");
632
+ return result;
633
+ }
634
+ const list = await runCommand(["wsl.exe", "-l", "-v"], homedir());
635
+ const normalized = normalizeWindowsCommandText([...list.stdout_tail, ...list.stderr_tail].join("\n"));
636
+ result.distributions = list.exit_code === 0 ? parseWslDistributionList(normalized) : [];
637
+ const online = await runCommand(["wsl.exe", "--list", "--online"], homedir());
638
+ const normalizedOnline = normalizeWindowsCommandText([...online.stdout_tail, ...online.stderr_tail].join("\n"));
639
+ result.online_distributions = online.exit_code === 0 ? parseWslOnlineDistributionList(normalizedOnline) : [];
640
+ result.usable = list.exit_code === 0 && result.distributions.length > 0;
641
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
642
+ if (!result.usable) {
643
+ result.notes.push("No usable WSL2 distribution is configured yet. Install Ubuntu, restart Windows if requested, then run Embed Labs TaishanPi tools inside WSL2.");
644
+ }
645
+ else {
646
+ result.notes.push("Run TaishanPi local compile, Qt, and image tooling inside the listed WSL2 distribution. Native Windows TaishanPi toolchain packages are intentionally not published.");
647
+ }
648
+ if (result.host === "win32-arm64") {
649
+ result.notes.push("This Windows ARM64 host maps WSL2 to linux-arm64. TaishanPi local compile/Qt/image parity still requires a published linux-arm64/native Windows package, or a separate Windows x64 + WSL2/linux-x86_64 package and validation.");
650
+ }
651
+ return result;
652
+ }
653
+ export async function windowsWslInstall(options = {}) {
654
+ const distro = options.distribution?.trim() || "Ubuntu";
655
+ const command = ["wsl.exe", "--install", "-d", distro];
656
+ if (options.noLaunch !== false) {
657
+ command.push("--no-launch");
658
+ }
659
+ if (options.webDownload !== false) {
660
+ command.push("--web-download");
661
+ }
662
+ const notes = [];
663
+ if (platform() !== "win32") {
664
+ notes.push("WSL installation can only run on Windows hosts.");
665
+ return {
666
+ host: hostId(),
667
+ platform: platform(),
668
+ arch: arch(),
669
+ command,
670
+ exit_code: 1,
671
+ stdout_tail: [],
672
+ stderr_tail: [],
673
+ status_after: await windowsWslStatus(),
674
+ notes
675
+ };
676
+ }
677
+ const result = await runCommandWithTimeout(command, homedir(), options.timeoutMs ?? 600_000);
678
+ const statusAfter = await windowsWslStatus();
679
+ if (result.exit_code === 0) {
680
+ notes.push("WSL install command completed. If Windows requests a restart, restart before running TaishanPi local tools.");
681
+ }
682
+ else {
683
+ notes.push("WSL install command did not complete successfully. Run the same command in an elevated Windows terminal if the error indicates permissions or reboot requirements.");
684
+ }
685
+ if (!statusAfter.usable) {
686
+ notes.push("WSL2 is still not usable after this command. Complete first-launch setup for the distribution, then rerun embedlabs local wsl status.");
687
+ }
688
+ return {
689
+ host: hostId(),
690
+ platform: platform(),
691
+ arch: arch(),
692
+ command,
693
+ exit_code: result.exit_code,
694
+ stdout_tail: result.stdout_tail,
695
+ stderr_tail: result.stderr_tail,
696
+ status_after: statusAfter,
697
+ notes
698
+ };
699
+ }
700
+ export async function uninstallLocalToolchain(options = {}) {
701
+ if (!options.boardId?.trim()) {
702
+ throw new Error("board_id is required for local toolchain uninstall.");
703
+ }
704
+ const boardId = normalizeBoardId(options.boardId);
705
+ const installRoot = resolveInstallRoot(options.installRoot);
706
+ const registryPath = localToolchainRegistryPath(installRoot);
707
+ const boardRoot = resolve(installRoot, "toolchains", boardId);
708
+ const removedPaths = [];
709
+ if (await pathExists(boardRoot)) {
710
+ await rm(boardRoot, { recursive: true, force: true });
711
+ removedPaths.push(boardRoot);
712
+ }
713
+ let removedRegistryEntry = false;
714
+ let existing = {};
715
+ try {
716
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
717
+ }
718
+ catch {
719
+ existing = {};
720
+ }
721
+ const environments = isRecord(existing.environments)
722
+ ? { ...existing.environments }
723
+ : {};
724
+ if (Object.prototype.hasOwnProperty.call(environments, boardId)) {
725
+ delete environments[boardId];
726
+ removedRegistryEntry = true;
727
+ }
728
+ const topLevelBoard = normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID));
729
+ const topLevelPointsToBoard = topLevelBoard === boardId && typeof existing.release_root === "string";
730
+ const cleaned = { ...existing, environments, updated_at: new Date().toISOString() };
731
+ if (topLevelPointsToBoard) {
732
+ for (const key of [
733
+ "installed",
734
+ "board_id",
735
+ "version",
736
+ "channel",
737
+ "host",
738
+ "mode",
739
+ "release_root",
740
+ "packages",
741
+ "source",
742
+ "installed_components"
743
+ ]) {
744
+ delete cleaned[key];
745
+ }
746
+ removedRegistryEntry = true;
747
+ }
748
+ if (removedRegistryEntry || Object.keys(environments).length > 0 || Object.keys(existing).length > 0) {
749
+ await mkdir(dirname(registryPath), { recursive: true });
750
+ await writeFile(registryPath, `${JSON.stringify(cleaned, null, 2)}\n`, "utf8");
751
+ }
752
+ return {
753
+ board_id: boardId,
754
+ install_root: installRoot,
755
+ registry_path: registryPath,
756
+ removed: removedPaths.length > 0 || removedRegistryEntry,
757
+ removed_paths: removedPaths,
758
+ removed_registry_entry: removedRegistryEntry,
759
+ remaining_installed_boards: Object.keys(environments).sort(),
760
+ observed_at: new Date().toISOString()
761
+ };
762
+ }
763
+ export async function validateLocalToolchain(input) {
764
+ const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
765
+ const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
766
+ const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
767
+ const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot, boardId);
768
+ const required = requiredLocalToolchainChecks(mode, boardId);
232
769
  const checked_paths = [];
233
770
  for (const [label, relativePath] of required) {
234
771
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -239,24 +776,369 @@ export async function validateLocalToolchain(releaseRoot) {
239
776
  });
240
777
  }
241
778
  const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
779
+ const path_leaks = await localToolchainPathLeaks(resolvedRoot, boardId);
780
+ const missing_groups = localToolchainMissingGroups(checked_paths, path_leaks);
781
+ const repair_command = missing_paths.length > 0 || path_leaks.length > 0
782
+ ? `embedlabs local toolchain install --board ${boardId} --mode ${mode}`
783
+ : undefined;
242
784
  return {
243
- ok: missing_paths.length === 0,
785
+ ok: missing_paths.length === 0 && path_leaks.length === 0,
786
+ mode,
244
787
  host: {
245
788
  platform: platform(),
246
789
  arch: arch()
247
790
  },
248
- board_id: DEFAULT_BOARD_ID,
791
+ board_id: boardId,
249
792
  release_root: resolvedRoot,
250
793
  checked_paths,
251
794
  missing_paths,
795
+ path_leaks,
796
+ missing_groups,
797
+ repair_command,
798
+ summary_for_user: localToolchainValidationSummary({
799
+ boardId,
800
+ mode,
801
+ ok: missing_paths.length === 0,
802
+ missingGroups: missing_groups,
803
+ repairCommand: repair_command
804
+ }),
252
805
  notes: [
253
806
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
254
- "This validator checks the Mac-first TaishanPi LLVM release layout; package install/update registry work is tracked separately."
807
+ `This validator checks the ${boardId} local support layout for install mode ${mode}.`
255
808
  ]
256
809
  };
257
810
  }
811
+ function localToolchainMissingGroups(checkedPaths, pathLeaks = []) {
812
+ const groups = new Set();
813
+ if (pathLeaks.length > 0) {
814
+ groups.add("portable-paths/安装包可移植路径");
815
+ }
816
+ for (const check of checkedPaths) {
817
+ if (check.exists) {
818
+ continue;
819
+ }
820
+ const text = `${check.label} ${check.path}`.toLowerCase();
821
+ if (text.includes("qt host") || text.includes("qtquick") || text.includes("qt cmake") || text.includes("qt target")) {
822
+ groups.add("qt/Qt host、target 或实时预览组件");
823
+ continue;
824
+ }
825
+ if (text.includes("compiler") || text.includes("readelf") || text.includes("sysroot") || text.includes("include directory") || text.includes("clang wrapper") || text.includes("gcc libraries")) {
826
+ groups.add("compile/交叉编译器与 sysroot");
827
+ continue;
828
+ }
829
+ if (text.includes("boot") || text.includes("dtb") || text.includes("resource image") || text.includes("metadata") || text.includes("/meta")) {
830
+ groups.add("board-resources/启动资源、DTB 与元数据");
831
+ continue;
832
+ }
833
+ if (text.includes("image") || text.includes("rootfs") || text.includes("parameter")) {
834
+ groups.add("images/基础镜像资源");
835
+ continue;
836
+ }
837
+ if (text.includes("rp2350") || text.includes("pico")) {
838
+ groups.add("rp2350/RP2350 SDK、固件或监控运行时");
839
+ continue;
840
+ }
841
+ groups.add("runtime/运行时文件");
842
+ }
843
+ return [...groups];
844
+ }
845
+ async function rewriteLocalToolchainPortablePaths(releaseRoot, boardId) {
846
+ if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID) {
847
+ return;
848
+ }
849
+ const replacements = taishanPiPortablePathReplacements(releaseRoot);
850
+ const files = await taishanPiPortableTextFiles(releaseRoot);
851
+ for (const file of files) {
852
+ let content;
853
+ try {
854
+ content = await readFile(file, "utf8");
855
+ }
856
+ catch {
857
+ continue;
858
+ }
859
+ const rewritten = rewritePortablePathContent(content, replacements);
860
+ if (rewritten !== content) {
861
+ await writeFile(file, rewritten, "utf8");
862
+ }
863
+ }
864
+ }
865
+ async function localToolchainPathLeaks(releaseRoot, boardId) {
866
+ if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID || !await pathExists(releaseRoot)) {
867
+ return [];
868
+ }
869
+ const forbidden = taishanPiForbiddenPortablePathFragments();
870
+ const files = await taishanPiPortableTextFiles(releaseRoot);
871
+ const leaks = [];
872
+ for (const file of files) {
873
+ let content;
874
+ try {
875
+ content = await readFile(file, "utf8");
876
+ }
877
+ catch {
878
+ continue;
879
+ }
880
+ for (const fragment of forbidden) {
881
+ if (content.includes(fragment)) {
882
+ leaks.push({
883
+ label: "portable path check",
884
+ path: file,
885
+ forbidden: fragment
886
+ });
887
+ break;
888
+ }
889
+ }
890
+ }
891
+ return leaks;
892
+ }
893
+ async function taishanPiPortableTextFiles(releaseRoot) {
894
+ const qtHostDirs = [
895
+ "qt-host/qt6-host-macos-6.8.3",
896
+ "qt-host/qt6-host-linux-x86_64-6.8.3"
897
+ ];
898
+ const candidates = [
899
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
900
+ "qt-target/qt6-rk3566-llvm-6.8.3/bin/target_qt.conf",
901
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/Qt6Dependencies.cmake",
902
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/qt.toolchain.cmake",
903
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
904
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qdevice.pri",
905
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qmodule.pri",
906
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_core_private.pri",
907
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_gui_private.pri",
908
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport.pri",
909
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport_private.pri",
910
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus.pri",
911
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus_private.pri",
912
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialPort.pc",
913
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialBus.pc",
914
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri",
915
+ ...qtHostDirs.flatMap((qtHostDir) => [
916
+ `${qtHostDir}/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake`,
917
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialport.pri`,
918
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialport_private.pri`,
919
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialbus.pri`,
920
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialbus_private.pri`,
921
+ `${qtHostDir}/mkspecs/modules/qt_ext_openxr_loader.pri`
922
+ ])
923
+ ];
924
+ const files = [];
925
+ for (const candidate of candidates) {
926
+ const file = join(releaseRoot, candidate);
927
+ if (await pathExists(file)) {
928
+ files.push(file);
929
+ }
930
+ }
931
+ for (const relativeDir of [
932
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort",
933
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus",
934
+ ...qtHostDirs.flatMap((qtHostDir) => [
935
+ `${qtHostDir}/lib/cmake/Qt6SerialPort`,
936
+ `${qtHostDir}/lib/cmake/Qt6SerialBus`
937
+ ])
938
+ ]) {
939
+ const dir = join(releaseRoot, relativeDir);
940
+ if (!await pathExists(dir)) {
941
+ continue;
942
+ }
943
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
944
+ if (!entry.isFile()) {
945
+ continue;
946
+ }
947
+ const file = join(dir, entry.name);
948
+ if ([".cmake", ".pri", ".pc", ".json"].includes(extname(file))) {
949
+ files.push(file);
950
+ }
951
+ }
952
+ }
953
+ return files;
954
+ }
955
+ function taishanPiPortablePathReplacements(releaseRoot) {
956
+ const normalizedRoot = resolve(releaseRoot);
957
+ // Legacy development paths are recognized only so older staged packages can
958
+ // be rewritten into a portable installed layout during validation/repair.
959
+ return [
960
+ ["@EMBEDLABS_RELEASE_ROOT@", normalizedRoot],
961
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/scripts/qt6-rk3566-llvm-toolchain.cmake", join(normalizedRoot, "toolchain", "qt6-rk3566-llvm-toolchain.cmake")],
962
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/.llvm-cross", join(normalizedRoot, "toolchain", "llvm-cross")],
963
+ ["/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal", normalizedRoot],
964
+ ["/Volumes/LLVM-TSPI/sdk-tools/buildroot/output/rockchip_rk3566/host", join(normalizedRoot, "toolchain", "host")],
965
+ ["/Volumes/LLVM-TSPI/qt6-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3")],
966
+ ["/home/ubuntu/embedlabs-qt/qt6-host-linux-x86_64-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-linux-x86_64-6.8.3")],
967
+ ["/Volumes/LLVM-TSPI/qt6-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3")],
968
+ ["/Volumes/LLVM-TSPI/qt-build-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3", ".build-info", "qt-build-host-macos-6.8.3")],
969
+ ["/home/ubuntu/embedlabs-qt/qt-build-host-linux-x86_64-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-linux-x86_64-6.8.3", ".build-info", "qt-build-host-linux-x86_64-6.8.3")],
970
+ ["/Volumes/LLVM-TSPI/qt-build-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", ".build-info", "qt-build-rk3566-llvm-6.8.3")],
971
+ ["/Volumes/LLVM-TSPI/qt-everywhere-src-6.8.3", join(normalizedRoot, "meta", "source", "qt-everywhere-src-6.8.3")],
972
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi", join(normalizedRoot, "meta", "source", "llvm-build-tspi")]
973
+ ];
974
+ }
975
+ function taishanPiForbiddenPortablePathFragments() {
976
+ return [
977
+ "/Volumes/LLVM-TSPI",
978
+ "/Users/kvell/kk-project/DBT-Agent-Project"
979
+ ];
980
+ }
981
+ function rewritePortablePathContent(content, replacements) {
982
+ let rewritten = content;
983
+ for (const [from, to] of replacements) {
984
+ rewritten = rewritten.split(from).join(to);
985
+ }
986
+ return rewritten;
987
+ }
988
+ function localToolchainValidationSummary(input) {
989
+ const boardName = input.boardId === DEFAULT_BOARD_ID
990
+ ? "泰山派 1M-RK3566"
991
+ : input.boardId;
992
+ if (input.ok) {
993
+ if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
994
+ return `${boardName} Qt 本地开发环境完整,可以继续 QtQuick 原型、一键交叉编译和真机部署。`;
995
+ }
996
+ return `${boardName} 本地开发环境完整,可以继续使用 ${input.mode} 模式。`;
997
+ }
998
+ const groups = input.missingGroups.length > 0 ? input.missingGroups.join("、") : "部分文件";
999
+ if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
1000
+ return `${boardName} Qt 本地开发环境不完整,缺少 ${groups}。这会影响一键交叉编译、实时预览或真机部署;请先执行 ${input.repairCommand} 补装缺失组件。`;
1001
+ }
1002
+ return `${boardName} 本地开发环境不完整,缺少 ${groups};请先执行 ${input.repairCommand} 补装缺失组件。`;
1003
+ }
1004
+ function normalizeLocalToolchainInstallMode(mode) {
1005
+ const normalized = mode?.trim();
1006
+ if (!normalized) {
1007
+ return "qt";
1008
+ }
1009
+ if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
1010
+ return normalized;
1011
+ }
1012
+ throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
1013
+ }
1014
+ function requiredLocalToolchainChecks(mode, boardId) {
1015
+ if (isRp2350MonitorBoardId(boardId)) {
1016
+ return requiredRp2350MonitorChecks(mode);
1017
+ }
1018
+ const qtHostDir = taishanPiQtHostDirName();
1019
+ const base = [
1020
+ ["release root", "."],
1021
+ ["boot resource image", "boot-workspace/out/resource.img"],
1022
+ ["boot image", "boot-workspace/out/boot.img"],
1023
+ ["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
1024
+ ["package metadata", "meta"]
1025
+ ];
1026
+ const rockchipTools = [
1027
+ ["Rockchip mkimage", "tools/mac/mkimage"],
1028
+ ["Rockchip dumpimage", "tools/mac/dumpimage"],
1029
+ ["Rockchip resource_tool", "tools/mac/resource_tool"],
1030
+ ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"]
1031
+ ];
1032
+ const compile = [
1033
+ ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
1034
+ ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
1035
+ ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
1036
+ ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
1037
+ ["host GCC libraries", "toolchain/host/lib/gcc"],
1038
+ ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
1039
+ ["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
1040
+ ];
1041
+ const qt = [
1042
+ ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
1043
+ ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
1044
+ ["Qt host tools", `qt-host/${qtHostDir}`],
1045
+ ["Qt host SerialPort CMake package", `qt-host/${qtHostDir}/lib/cmake/Qt6SerialPort/Qt6SerialPortConfig.cmake`],
1046
+ ["Qt host SerialBus CMake package", `qt-host/${qtHostDir}/lib/cmake/Qt6SerialBus/Qt6SerialBusConfig.cmake`],
1047
+ ["Qt target SerialPort library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialPort.so.6"],
1048
+ ["Qt target SerialPort CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort/Qt6SerialPortConfig.cmake"],
1049
+ ["Qt target SerialBus library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialBus.so.6"],
1050
+ ["Qt target SerialBus CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus/Qt6SerialBusConfig.cmake"],
1051
+ ["Qt board runtime SerialPort library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialPort.so.6"],
1052
+ ["Qt board runtime SerialBus library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialBus.so.6"],
1053
+ ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
1054
+ ];
1055
+ const images = [
1056
+ ["base boot image", "images/current/boot.img"],
1057
+ ["base rootfs image", "images/current/rootfs.img"],
1058
+ ["base image parameter", "images/current/parameter.txt"]
1059
+ ];
1060
+ const full = [
1061
+ ["rootfs overlay", "userdata/rootfs"]
1062
+ ];
1063
+ if (mode === "minimal") {
1064
+ return [...base, ...rockchipTools];
1065
+ }
1066
+ if (mode === "compile") {
1067
+ return [...base, ...compile];
1068
+ }
1069
+ if (mode === "qt") {
1070
+ return [...base, ...compile, ...qt];
1071
+ }
1072
+ if (mode === "images") {
1073
+ return [...base, ...rockchipTools, ...images];
1074
+ }
1075
+ return [...base, ...rockchipTools, ...compile, ...qt, ...images, ...full];
1076
+ }
1077
+ function requiredRp2350MonitorChecks(mode) {
1078
+ const armToolchainDir = rp2350ArmToolchainDirName();
1079
+ const executableSuffix = platform() === "win32" ? ".exe" : "";
1080
+ const monitorLauncher = platform() === "win32"
1081
+ ? "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer.cmd"
1082
+ : "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer";
1083
+ const base = [
1084
+ ["release root", "."],
1085
+ ];
1086
+ const runtime = [
1087
+ ["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
1088
+ ["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
1089
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
1090
+ ["RP2350 picotool", `toolkit-runtime/rp2350-monitor/tools/picotool${executableSuffix}`],
1091
+ ["RP2350 Monitor logic analyzer", monitorLauncher],
1092
+ ["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
1093
+ ["package metadata", "meta"]
1094
+ ];
1095
+ if (platform() === "win32") {
1096
+ runtime.splice(4, 0, ["RP2350 Monitor bundled Python", "toolkit-runtime/rp2350-monitor/tools/python/python.exe"]);
1097
+ runtime.splice(5, 0, ["RP2350 Monitor pyserial", "toolkit-runtime/rp2350-monitor/tools/python/Lib/site-packages/serial/__init__.py"]);
1098
+ }
1099
+ const firmware = [
1100
+ ["RP2350 Monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
1101
+ ["RP2350 Monitor firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
1102
+ ];
1103
+ const initialFirmware = [
1104
+ ["Pico 2 W initialization UF2", "rp2350-initial-firmware/pico2w/initial.uf2"],
1105
+ ["ColorEasyPICO2 initialization UF2", "rp2350-initial-firmware/coloreasypico2/initial.uf2"]
1106
+ ];
1107
+ const compile = [
1108
+ ["Pico SDK", "rp2350-sdk/pico-sdk/pico_sdk_init.cmake"],
1109
+ ["Pico SDK CMakeLists", "rp2350-sdk/pico-sdk/CMakeLists.txt"],
1110
+ ["ARM bare-metal C compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-gcc${executableSuffix}`],
1111
+ ["ARM bare-metal C++ compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-g++${executableSuffix}`],
1112
+ ["picotool", `rp2350-sdk/picotool/install/picotool/picotool${executableSuffix}`]
1113
+ ];
1114
+ if (mode === "compile") {
1115
+ return [...base, ...compile, ...initialFirmware];
1116
+ }
1117
+ if (mode === "firmware") {
1118
+ return [...base, ...runtime, ...firmware, ...initialFirmware];
1119
+ }
1120
+ if (mode === "full") {
1121
+ return [...base, ...runtime, ...firmware, ...initialFirmware, ...compile];
1122
+ }
1123
+ return [...base, ...runtime];
1124
+ }
1125
+ function rp2350ArmToolchainDirName() {
1126
+ if (platform() === "win32") {
1127
+ return "arm-gnu-toolchain-15.2.rel1-mingw-w64-x86_64-arm-none-eabi";
1128
+ }
1129
+ if (platform() === "linux" && arch() === "x64") {
1130
+ return "arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi";
1131
+ }
1132
+ if (platform() === "linux" && arch() === "arm64") {
1133
+ return "arm-gnu-toolchain-15.2.rel1-aarch64-arm-none-eabi";
1134
+ }
1135
+ return "arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi";
1136
+ }
258
1137
  export async function compileTaishanPiSingleFile(options) {
259
1138
  assertAuthenticated(options.auth);
1139
+ if (platform() === "win32" && normalizeBoardId(options.boardId) === DEFAULT_BOARD_ID) {
1140
+ return await compileTaishanPiSingleFileWindowsWsl(options);
1141
+ }
260
1142
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
261
1143
  const sourcePath = resolve(options.sourcePath);
262
1144
  const outputPath = resolve(options.outputPath);
@@ -282,10 +1164,51 @@ export async function compileTaishanPiSingleFile(options) {
282
1164
  commands: [buildResult]
283
1165
  });
284
1166
  }
1167
+ async function compileTaishanPiSingleFileWindowsWsl(options) {
1168
+ const route = taishanPiWindowsExecutionRoute(hostId(), await windowsWslStatus());
1169
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
1170
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
1171
+ }
1172
+ const sourcePath = resolve(options.sourcePath);
1173
+ const outputPath = resolve(options.outputPath);
1174
+ await access(sourcePath, constants.R_OK);
1175
+ await mkdir(dirname(outputPath), { recursive: true });
1176
+ const releaseRoot = options.releaseRoot?.trim()
1177
+ ? normalizeWindowsWslReleaseRoot(options.releaseRoot)
1178
+ : await resolveWindowsWslTaishanPiReleaseRoot();
1179
+ const sourceWslPath = windowsPathToWslPath(sourcePath, "source");
1180
+ const outputWslPath = windowsPathToWslPath(outputPath, "output");
1181
+ const compiler = wslCompilerForSource(releaseRoot, sourcePath);
1182
+ const sysroot = `${trimTrailingSlash(releaseRoot)}/toolchain/host/aarch64-buildroot-linux-gnu/sysroot`;
1183
+ const commandScript = [
1184
+ "set -euo pipefail",
1185
+ `test -x ${sh(compiler)}`,
1186
+ `test -r ${sh(sysroot)}`,
1187
+ `mkdir -p ${sh(posixDirname(outputWslPath))}`,
1188
+ `${sh(compiler)} --sysroot=${sh(sysroot)} -O2 ${sh(sourceWslPath)} -o ${sh(outputWslPath)}`
1189
+ ].join("\n");
1190
+ const buildResult = await runWindowsWslBash(commandScript);
1191
+ if (buildResult.exit_code !== 0) {
1192
+ throw new Error(`Windows WSL TaishanPi compile failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
1193
+ }
1194
+ return await localCompileResult({
1195
+ boardId: options.boardId,
1196
+ operation: "local.compile.single_file",
1197
+ releaseRoot,
1198
+ accountId: options.accountId,
1199
+ auth: options.auth,
1200
+ sourcePath,
1201
+ artifactPath: outputPath,
1202
+ commands: [buildResult]
1203
+ });
1204
+ }
285
1205
  export async function buildTaishanPiQtSmoke(options) {
286
1206
  assertAuthenticated(options.auth);
1207
+ if (platform() === "win32") {
1208
+ return await buildTaishanPiQtSmokeWindowsWsl(options);
1209
+ }
287
1210
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
288
- const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
1211
+ const sourceDir = resolve(options.sourceDir ?? join(releaseRoot, "examples", "qt-smoke"));
289
1212
  const buildDir = resolve(options.buildDir);
290
1213
  const targetName = options.targetName ?? "qt_llvm_smoke";
291
1214
  const qtCmake = join(releaseRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", "bin", "qt-cmake");
@@ -305,34 +1228,82 @@ export async function buildTaishanPiQtSmoke(options) {
305
1228
  if (configure.exit_code !== 0) {
306
1229
  throw new Error(`Qt smoke configure failed with exit code ${configure.exit_code}: ${configure.stderr_tail.join("\n")}`);
307
1230
  }
308
- const build = await runCommand(["cmake", "--build", buildDir, "--parallel"], sourceDir);
309
- if (build.exit_code !== 0) {
310
- throw new Error(`Qt smoke build failed with exit code ${build.exit_code}: ${build.stderr_tail.join("\n")}`);
1231
+ const build = await runCommand(["cmake", "--build", buildDir, "--parallel"], sourceDir);
1232
+ if (build.exit_code !== 0) {
1233
+ throw new Error(`Qt smoke build failed with exit code ${build.exit_code}: ${build.stderr_tail.join("\n")}`);
1234
+ }
1235
+ const artifactPath = join(buildDir, targetName);
1236
+ return await localCompileResult({
1237
+ boardId: DEFAULT_BOARD_ID,
1238
+ operation: "local.build.qt_smoke",
1239
+ releaseRoot,
1240
+ accountId: options.accountId,
1241
+ auth: options.auth,
1242
+ sourcePath: sourceDir,
1243
+ buildDir,
1244
+ artifactPath,
1245
+ commands: [configure, build]
1246
+ });
1247
+ }
1248
+ async function buildTaishanPiQtSmokeWindowsWsl(options) {
1249
+ const route = taishanPiWindowsExecutionRoute(hostId(), await windowsWslStatus());
1250
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
1251
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
1252
+ }
1253
+ const releaseRoot = options.releaseRoot?.trim()
1254
+ ? normalizeWindowsWslReleaseRoot(options.releaseRoot)
1255
+ : await resolveWindowsWslTaishanPiReleaseRoot("qt");
1256
+ const targetName = options.targetName ?? "qt_llvm_smoke";
1257
+ const buildDir = resolve(options.buildDir);
1258
+ const buildWslDir = windowsPathToWslPath(buildDir, "build-dir");
1259
+ const sourceDir = options.sourceDir?.trim()
1260
+ ? resolve(options.sourceDir)
1261
+ : "";
1262
+ if (sourceDir) {
1263
+ await access(join(sourceDir, "CMakeLists.txt"), constants.R_OK);
311
1264
  }
1265
+ await mkdir(buildDir, { recursive: true });
1266
+ const sourceWslDir = sourceDir
1267
+ ? windowsPathToWslPath(sourceDir, "source")
1268
+ : `${trimTrailingSlash(releaseRoot)}/examples/qt-smoke`;
1269
+ const qtCmake = `${trimTrailingSlash(releaseRoot)}/qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake`;
312
1270
  const artifactPath = join(buildDir, targetName);
1271
+ const commandScript = [
1272
+ "set -euo pipefail",
1273
+ `test -x ${sh(qtCmake)} || { echo "Missing Qt CMake at ${qtCmake}. Install the TaishanPi Qt package with: embedlabs local toolchain install --board taishanpi-1m-rk3566 --channel windows-test --mode qt" >&2; exit 2; }`,
1274
+ `test -r ${sh(`${sourceWslDir}/CMakeLists.txt`)} || { echo "Missing CMakeLists.txt in ${sourceWslDir}" >&2; exit 2; }`,
1275
+ "command -v cmake >/dev/null 2>&1 || { echo \"Missing WSL tool: cmake\" >&2; exit 2; }",
1276
+ "command -v ninja >/dev/null 2>&1 || { echo \"Missing WSL tool: ninja\" >&2; exit 2; }",
1277
+ `mkdir -p ${sh(buildWslDir)}`,
1278
+ `${sh(qtCmake)} -S ${sh(sourceWslDir)} -B ${sh(buildWslDir)} -G Ninja -DCMAKE_BUILD_TYPE=Release`,
1279
+ `cmake --build ${sh(buildWslDir)} --parallel`
1280
+ ].join("\n");
1281
+ const buildResult = await runWindowsWslBash(commandScript);
1282
+ if (buildResult.exit_code !== 0) {
1283
+ throw new Error(`Windows WSL TaishanPi Qt smoke build failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
1284
+ }
313
1285
  return await localCompileResult({
314
1286
  boardId: DEFAULT_BOARD_ID,
315
1287
  operation: "local.build.qt_smoke",
316
1288
  releaseRoot,
317
1289
  accountId: options.accountId,
318
1290
  auth: options.auth,
319
- sourcePath: sourceDir,
1291
+ sourcePath: sourceDir || sourceWslDir,
320
1292
  buildDir,
321
1293
  artifactPath,
322
- commands: [configure, build]
1294
+ commands: [buildResult]
323
1295
  });
324
1296
  }
325
1297
  async function loadLocalToolchainMetadata(metadataRoot, channelName) {
326
1298
  const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
327
- const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
328
- if (!candidateRoot) {
1299
+ if (!explicitRoot) {
329
1300
  return {
330
1301
  channel: BUILT_IN_CHANNEL,
331
1302
  manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
332
1303
  metadataRoot: undefined
333
1304
  };
334
1305
  }
335
- const root = resolve(candidateRoot);
1306
+ const root = resolve(explicitRoot);
336
1307
  const channelPath = join(root, "channels", channelName, "index.json");
337
1308
  const channel = JSON.parse(await readFile(channelPath, "utf8"));
338
1309
  if (channel.schema !== "embedlabs.channel.v1") {
@@ -355,17 +1326,26 @@ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
355
1326
  return { channel, manifests, metadataRoot: root };
356
1327
  }
357
1328
  async function resolveLocalToolchainDownloadPlan(input) {
1329
+ const requestedBoardId = normalizeBoardId(input.boardId);
1330
+ const compatibleBoardIds = compatibleDownloadBoardIds(requestedBoardId);
358
1331
  const channelUrl = downloadChannelUrl(input.channel);
359
1332
  const channel = await fetchJson(channelUrl);
360
1333
  if (channel.schema !== "embedlabs.download-channel.v1") {
361
1334
  throw new Error(`Unexpected download channel schema ${channel.schema}.`);
362
1335
  }
363
- const entry = (channel.packages ?? []).find((item) => {
364
- return item.board_id === input.boardId
365
- && item.host === input.host
366
- && item.toolchain === input.toolchain
367
- && (item.kind === undefined || item.kind === "toolchain-archive");
368
- });
1336
+ const entries = channel.packages ?? [];
1337
+ let entry;
1338
+ for (const boardId of compatibleBoardIds) {
1339
+ entry = entries.find((item) => {
1340
+ return normalizeBoardId(item.board_id ?? "") === boardId
1341
+ && item.host === input.host
1342
+ && item.toolchain === input.toolchain
1343
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
1344
+ });
1345
+ if (entry) {
1346
+ break;
1347
+ }
1348
+ }
369
1349
  if (!entry?.manifest) {
370
1350
  return undefined;
371
1351
  }
@@ -374,23 +1354,28 @@ async function resolveLocalToolchainDownloadPlan(input) {
374
1354
  if (manifest.id !== entry.id || manifest.version !== entry.version) {
375
1355
  throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
376
1356
  }
377
- if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
378
- throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
1357
+ const manifestBoardId = normalizeBoardId(manifest.board_id ?? "");
1358
+ if (!compatibleBoardIds.includes(manifestBoardId) || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
1359
+ throw new Error(`Download manifest does not match requested ${requestedBoardId}/${input.host}/${input.toolchain}.`);
379
1360
  }
380
- if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes)) {
381
- throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive file, size, or SHA256.`);
1361
+ if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
1362
+ && (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
1363
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
382
1364
  }
383
- const mirrors = orderDownloadMirrors((manifest.mirrors ?? [])
384
- .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
385
- .map((mirror) => ({
386
- kind: mirror.kind || "unknown",
387
- enabled: mirror.enabled !== false,
388
- url: mirror.url,
389
- sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
390
- size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
391
- })), manifest.download_policy?.preferred_order);
1365
+ const mirrors = manifest.archive
1366
+ ? orderDownloadMirrors((manifest.mirrors ?? [])
1367
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
1368
+ .map((mirror) => ({
1369
+ kind: mirror.kind || "unknown",
1370
+ enabled: mirror.enabled !== false,
1371
+ url: mirror.url,
1372
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
1373
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
1374
+ })), manifest.download_policy?.preferred_order)
1375
+ : [];
1376
+ const components = downloadComponentsForBoard(requestedBoardId, (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl)));
392
1377
  const first = mirrors[0];
393
- if (!first) {
1378
+ if (!first && components.length === 0) {
394
1379
  return undefined;
395
1380
  }
396
1381
  return {
@@ -398,17 +1383,55 @@ async function resolveLocalToolchainDownloadPlan(input) {
398
1383
  manifest_url: manifestUrl,
399
1384
  package_id: manifest.id,
400
1385
  version: manifest.version,
401
- board_id: input.boardId,
1386
+ board_id: requestedBoardId,
402
1387
  host: input.host,
403
1388
  toolchain: input.toolchain,
404
- source_url: first.url,
405
- mirror_kind: first.kind,
406
- archive: {
1389
+ source_url: first?.url,
1390
+ mirror_kind: first?.kind,
1391
+ archive: manifest.archive ? {
407
1392
  file: manifest.archive.file,
408
1393
  size_bytes: manifest.archive.size_bytes,
409
1394
  sha256: manifest.archive.sha256,
410
1395
  content_type: manifest.archive.content_type
1396
+ } : undefined,
1397
+ mirrors,
1398
+ components: components.length > 0 ? components : undefined,
1399
+ default_mode: manifest.download_policy?.default_mode
1400
+ };
1401
+ }
1402
+ function normalizeDownloadComponent(component, manifest, manifestUrl) {
1403
+ if (!component.id || !component.version) {
1404
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
1405
+ }
1406
+ if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
1407
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
1408
+ }
1409
+ const mirrors = orderDownloadMirrors((component.mirrors ?? [])
1410
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
1411
+ .map((mirror) => ({
1412
+ kind: mirror.kind || "unknown",
1413
+ enabled: mirror.enabled !== false,
1414
+ url: new URL(mirror.url, manifestUrl).toString(),
1415
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
1416
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
1417
+ })), manifest.download_policy?.preferred_order);
1418
+ const first = mirrors[0];
1419
+ if (!first) {
1420
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
1421
+ }
1422
+ return {
1423
+ id: component.id,
1424
+ version: component.version,
1425
+ role: component.role,
1426
+ install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
1427
+ archive: {
1428
+ file: component.archive.file,
1429
+ size_bytes: component.archive.size_bytes,
1430
+ sha256: component.archive.sha256,
1431
+ content_type: component.archive.content_type
411
1432
  },
1433
+ source_url: first.url,
1434
+ mirror_kind: first.kind,
412
1435
  mirrors
413
1436
  };
414
1437
  }
@@ -423,7 +1446,12 @@ function mirrorRank(kind, preferredOrder) {
423
1446
  return index >= 0 ? index : preferredOrder.length + 1;
424
1447
  }
425
1448
  async function fetchJson(url) {
426
- const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
1449
+ const response = await fetch(url, {
1450
+ headers: {
1451
+ "accept-encoding": "identity"
1452
+ },
1453
+ signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS)
1454
+ });
427
1455
  if (!response.ok) {
428
1456
  throw new Error(`HTTP ${response.status} while loading ${url}`);
429
1457
  }
@@ -467,14 +1495,162 @@ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
467
1495
  return refs;
468
1496
  }
469
1497
  function boardPackageIdFor(boardId) {
470
- if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
1498
+ const normalized = normalizeBoardId(boardId);
1499
+ if (normalized === DEFAULT_BOARD_ID || normalized === "taishanpi" || normalized === "taishanpi-1m-rk3566") {
471
1500
  return "embedlabs.board.taishanpi.1m-rk3566";
472
1501
  }
473
- if (boardId.startsWith("embedlabs.board.")) {
1502
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID
1503
+ || normalized === "coloreasypico2"
1504
+ || normalized === "color-easy-pico2"
1505
+ || normalized === "color-easy-pico-2"
1506
+ || normalized === "color-easy-pico-2-rp2350-monitor"
1507
+ || normalized === "ce-pico2") {
1508
+ return "embedlabs.board.coloreasypico2.rp2350-monitor";
1509
+ }
1510
+ if (normalized === PICO2W_RP2350_BOARD_ID
1511
+ || normalized === "pico2w"
1512
+ || normalized === "pico-2-w"
1513
+ || normalized === "pico2"
1514
+ || normalized === "rp2350"
1515
+ || normalized === "rp2350-monitor") {
1516
+ return "embedlabs.board.pico2w.rp2350-monitor";
1517
+ }
1518
+ if (normalized.startsWith("embedlabs.board.")) {
474
1519
  return boardId;
475
1520
  }
476
1521
  throw new Error(`Unsupported local toolchain board ${boardId}.`);
477
1522
  }
1523
+ function packageIdForBoardFilter(boardId) {
1524
+ try {
1525
+ return boardPackageIdFor(boardId);
1526
+ }
1527
+ catch {
1528
+ return undefined;
1529
+ }
1530
+ }
1531
+ function boardIdForPackageManifest(manifest) {
1532
+ const explicit = manifest.board_id;
1533
+ if (explicit?.trim()) {
1534
+ return normalizeBoardId(explicit);
1535
+ }
1536
+ if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
1537
+ return DEFAULT_BOARD_ID;
1538
+ }
1539
+ if (manifest.id.startsWith("embedlabs.board.")) {
1540
+ return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
1541
+ }
1542
+ return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
1543
+ }
1544
+ function normalizeBoardId(boardId) {
1545
+ return boardId.trim().toLowerCase().replaceAll("_", "-");
1546
+ }
1547
+ function isRp2350MonitorBoardId(boardId) {
1548
+ const normalized = normalizeBoardId(boardId);
1549
+ return normalized === PICO2W_RP2350_BOARD_ID
1550
+ || normalized === COLOREASYPICO2_RP2350_BOARD_ID
1551
+ || normalized === "pico2w"
1552
+ || normalized === "pico-2-w"
1553
+ || normalized === "pico2"
1554
+ || normalized === "rp2350"
1555
+ || normalized === "rp2350-monitor"
1556
+ || normalized === "coloreasypico2"
1557
+ || normalized === "color-easy-pico2"
1558
+ || normalized === "color-easy-pico-2"
1559
+ || normalized === "ce-pico2";
1560
+ }
1561
+ function compatibleDownloadBoardIds(boardId) {
1562
+ const normalized = normalizeBoardId(boardId);
1563
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID) {
1564
+ return [COLOREASYPICO2_RP2350_BOARD_ID, PICO2W_RP2350_BOARD_ID];
1565
+ }
1566
+ return [normalized];
1567
+ }
1568
+ function downloadComponentsForBoard(boardId, components) {
1569
+ const normalized = normalizeBoardId(boardId);
1570
+ if (normalized === DEFAULT_BOARD_ID) {
1571
+ return components.filter((component) => !isRp2350MonitorComponent(component));
1572
+ }
1573
+ return components;
1574
+ }
1575
+ function isRp2350MonitorComponent(component) {
1576
+ const text = `${component.id} ${component.role ?? ""} ${component.archive.file}`.toLowerCase();
1577
+ return text.includes("rp2350-monitor") || text.includes("pico2w-rp2350-monitor");
1578
+ }
1579
+ function compareVersionLike(left, right) {
1580
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
1581
+ }
1582
+ function isRecord(value) {
1583
+ return !!value && typeof value === "object" && !Array.isArray(value);
1584
+ }
1585
+ function packageHostSupport(packages, manifests, host) {
1586
+ const unsupportedPackages = [];
1587
+ for (const item of packages) {
1588
+ const manifest = manifests.get(item.id);
1589
+ if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
1590
+ unsupportedPackages.push(`${item.id}@${manifest.version}`);
1591
+ }
1592
+ }
1593
+ return {
1594
+ supported: unsupportedPackages.length === 0,
1595
+ unsupportedPackages
1596
+ };
1597
+ }
1598
+ function environmentNotes(input) {
1599
+ const notes = [];
1600
+ if (normalizeBoardId(input.boardId) === COLOREASYPICO2_RP2350_BOARD_ID) {
1601
+ notes.push("ColorEasyPICO2 uses the same RP2350/Pico SDK, ARM bare-metal toolchain, picotool, and optional RP2350 Monitor image package as Pico 2 W; Wi-Fi operations are not declared for this no-Wi-Fi board.");
1602
+ }
1603
+ if (isRp2350MonitorBoardId(input.boardId)) {
1604
+ notes.push("RP2350 Monitor is an optional hardware-control firmware image; the board environment itself is the Pico 2 W or ColorEasyPICO2 C/C++ SDK environment.");
1605
+ }
1606
+ if (isNativeWindowsTaishanPiHost(input.boardId, input.host)) {
1607
+ notes.push(taishanPiWindowsRequirementMessage(input.host));
1608
+ notes.push("Check this computer with: embedlabs local wsl status");
1609
+ }
1610
+ if (input.status === "available") {
1611
+ notes.push("Environment is available but not installed on this computer.");
1612
+ }
1613
+ if (input.status === "update_available") {
1614
+ notes.push("A newer package is available; run the update command to refresh only the selected environment.");
1615
+ }
1616
+ if (input.status === "unsupported_host") {
1617
+ if (input.unsupportedPackages.length > 0) {
1618
+ notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
1619
+ }
1620
+ }
1621
+ if (input.downloadError) {
1622
+ notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
1623
+ }
1624
+ return notes;
1625
+ }
1626
+ function localToolchainInstallModesForDownload(download) {
1627
+ if (!download?.components?.length) {
1628
+ return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
1629
+ }
1630
+ const modes = new Set();
1631
+ for (const component of download.components) {
1632
+ for (const mode of component.install_modes ?? []) {
1633
+ modes.add(mode);
1634
+ }
1635
+ }
1636
+ return modes.size > 0
1637
+ ? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
1638
+ : [...LOCAL_TOOLCHAIN_INSTALL_MODES];
1639
+ }
1640
+ function localToolchainChannelFlag(channelName) {
1641
+ return channelName === DEFAULT_CHANNEL ? "" : ` --channel ${channelName}`;
1642
+ }
1643
+ function localToolchainEnvironmentComponent(component) {
1644
+ return {
1645
+ id: component.id,
1646
+ version: component.version,
1647
+ role: component.role,
1648
+ install_modes: component.install_modes,
1649
+ file: component.archive.file,
1650
+ size_bytes: component.archive.size_bytes,
1651
+ source_url: component.source_url
1652
+ };
1653
+ }
478
1654
  function hostId() {
479
1655
  if (platform() === "darwin" && arch() === "arm64") {
480
1656
  return "darwin-arm64";
@@ -484,6 +1660,191 @@ function hostId() {
484
1660
  }
485
1661
  return `${platform()}-${arch()}`;
486
1662
  }
1663
+ function taishanPiQtHostDirName(host = localToolchainHostId()) {
1664
+ if (host === "linux-x86_64") {
1665
+ return "qt6-host-linux-x86_64-6.8.3";
1666
+ }
1667
+ return "qt6-host-macos-6.8.3";
1668
+ }
1669
+ const LOCAL_TOOLCHAIN_HOST_IDS = new Set([
1670
+ "darwin-arm64",
1671
+ "linux-x86_64",
1672
+ "linux-arm64",
1673
+ "win32-x64",
1674
+ "win32-arm64"
1675
+ ]);
1676
+ function localToolchainHostId() {
1677
+ const override = process.env.EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST?.trim();
1678
+ if (override) {
1679
+ if (!LOCAL_TOOLCHAIN_HOST_IDS.has(override)) {
1680
+ throw new Error(`Unsupported EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST value: ${override}`);
1681
+ }
1682
+ return override;
1683
+ }
1684
+ return hostId();
1685
+ }
1686
+ function isNativeWindowsTaishanPiHost(boardId, host) {
1687
+ return normalizeBoardId(boardId) === DEFAULT_BOARD_ID && host.startsWith("win32-");
1688
+ }
1689
+ function taishanPiWindowsExecutionRoute(host, status, packageAvailable = false) {
1690
+ const actualHost = wslHostForWindowsHost(host);
1691
+ if (!host.startsWith("win32-")) {
1692
+ return {
1693
+ kind: "native",
1694
+ supported: false,
1695
+ required_host: "linux-x86_64",
1696
+ actual_host: host,
1697
+ windows_host: host,
1698
+ route: "wsl2",
1699
+ status: "wsl_not_applicable",
1700
+ status_command: "embedlabs local wsl status",
1701
+ install_command: "embedlabs local wsl status",
1702
+ reason: "WSL2 routing is only used from Windows hosts."
1703
+ };
1704
+ }
1705
+ if (!status?.wsl_available) {
1706
+ return {
1707
+ kind: "wsl2",
1708
+ supported: false,
1709
+ required_host: "linux-x86_64",
1710
+ actual_host: actualHost,
1711
+ windows_host: host,
1712
+ route: "wsl2",
1713
+ status: "wsl_missing",
1714
+ status_command: "embedlabs local wsl status",
1715
+ install_command: "embedlabs local wsl install --distribution Ubuntu",
1716
+ reason: "wsl.exe is not available on this Windows host."
1717
+ };
1718
+ }
1719
+ if (actualHost !== "linux-x86_64") {
1720
+ return {
1721
+ kind: "wsl2",
1722
+ supported: false,
1723
+ required_host: "linux-x86_64",
1724
+ actual_host: actualHost,
1725
+ windows_host: host,
1726
+ route: "wsl2",
1727
+ status: "architecture_mismatch",
1728
+ status_command: "embedlabs local wsl status",
1729
+ install_command: "embedlabs local wsl status",
1730
+ reason: `This Windows host maps WSL2 to ${actualHost}, but the current TaishanPi package requires linux-x86_64.`
1731
+ };
1732
+ }
1733
+ if (!status.usable) {
1734
+ return {
1735
+ kind: "wsl2",
1736
+ supported: false,
1737
+ required_host: "linux-x86_64",
1738
+ actual_host: actualHost,
1739
+ windows_host: host,
1740
+ route: "wsl2",
1741
+ status: "wsl_not_configured",
1742
+ status_command: "embedlabs local wsl status",
1743
+ install_command: "embedlabs local wsl install --distribution Ubuntu",
1744
+ reason: "No usable WSL2 distribution is configured."
1745
+ };
1746
+ }
1747
+ if (packageAvailable) {
1748
+ return {
1749
+ kind: "wsl2",
1750
+ supported: true,
1751
+ required_host: "linux-x86_64",
1752
+ actual_host: actualHost,
1753
+ windows_host: host,
1754
+ route: "wsl2",
1755
+ status: "ready",
1756
+ status_command: "embedlabs local wsl status",
1757
+ install_command: "embedlabs local toolchain install --board taishanpi-1m-rk3566 --channel windows-test --mode compile",
1758
+ reason: "Windows x64 can run the TaishanPi linux-x86_64 package through WSL2."
1759
+ };
1760
+ }
1761
+ return {
1762
+ kind: "wsl2",
1763
+ supported: false,
1764
+ required_host: "linux-x86_64",
1765
+ actual_host: actualHost,
1766
+ windows_host: host,
1767
+ route: "wsl2",
1768
+ status: "package_missing",
1769
+ status_command: "embedlabs local wsl status",
1770
+ install_command: "embedlabs local wsl status",
1771
+ reason: "The Windows x64 WSL2 route is structurally compatible, but no TaishanPi linux-x86_64 package is published in the current stable channel."
1772
+ };
1773
+ }
1774
+ function wslHostForWindowsHost(host) {
1775
+ if (host === "win32-x64") {
1776
+ return "linux-x86_64";
1777
+ }
1778
+ if (host === "win32-arm64") {
1779
+ return "linux-arm64";
1780
+ }
1781
+ return undefined;
1782
+ }
1783
+ function taishanPiWindowsRequirementMessage(host) {
1784
+ if (host === "win32-arm64") {
1785
+ return "TaishanPi local compile, Qt, image, and Rockchip tooling is not available on native Windows ARM64 yet. Full parity requires a published linux-arm64/native Windows TaishanPi package, or a separately validated Windows x64 + WSL2/linux-x86_64 package.";
1786
+ }
1787
+ return "TaishanPi local compile, Qt, image, and Rockchip tooling is not available on native Windows yet. Windows x64 support requires WSL2/Linux x86_64 plus a published and validated TaishanPi linux-x86_64 package.";
1788
+ }
1789
+ function normalizeWindowsCommandText(value) {
1790
+ return value.replace(/\0/g, "").replace(/\r/g, "\n");
1791
+ }
1792
+ function parseWslDistributionList(value) {
1793
+ const distributions = [];
1794
+ const lines = value.split(/\n+/)
1795
+ .map((line) => line.trim())
1796
+ .filter(Boolean);
1797
+ for (const rawLine of lines) {
1798
+ if (/^(NAME|Windows Subsystem|Usage|Copyright|\-|用法|选项|命令)/i.test(rawLine)) {
1799
+ continue;
1800
+ }
1801
+ const isDefault = rawLine.startsWith("*");
1802
+ const line = rawLine.replace(/^\*\s*/, "").trim();
1803
+ const parts = line.split(/\s+/).filter(Boolean);
1804
+ if (parts.length === 0 || parts[0].includes(":")) {
1805
+ continue;
1806
+ }
1807
+ const maybeVersion = parts.at(-1);
1808
+ const version = maybeVersion && /^[12]$/.test(maybeVersion) ? maybeVersion : undefined;
1809
+ const state = version && parts.length >= 3 ? parts.at(-2) : parts.length >= 2 ? parts.at(-1) : undefined;
1810
+ distributions.push({
1811
+ name: parts[0],
1812
+ state,
1813
+ version,
1814
+ default: isDefault
1815
+ });
1816
+ }
1817
+ return distributions;
1818
+ }
1819
+ function parseWslOnlineDistributionList(value) {
1820
+ const distributions = [];
1821
+ const lines = value.split(/\n+/)
1822
+ .map((line) => line.trim())
1823
+ .filter(Boolean);
1824
+ let inTable = false;
1825
+ for (const rawLine of lines) {
1826
+ if (/^NAME\s+FRIENDLY NAME/i.test(rawLine)) {
1827
+ inTable = true;
1828
+ continue;
1829
+ }
1830
+ if (!inTable) {
1831
+ continue;
1832
+ }
1833
+ const isDefault = rawLine.startsWith("*");
1834
+ const line = rawLine.replace(/^\*\s*/, "").trim();
1835
+ const parts = line.split(/\s{2,}/).filter(Boolean);
1836
+ const name = parts[0]?.trim();
1837
+ if (!name || name.includes(":")) {
1838
+ continue;
1839
+ }
1840
+ distributions.push({
1841
+ name,
1842
+ friendly_name: parts.slice(1).join(" ").trim() || undefined,
1843
+ default: isDefault
1844
+ });
1845
+ }
1846
+ return distributions;
1847
+ }
487
1848
  function resolveInstallRoot(installRoot) {
488
1849
  return resolve(installRoot
489
1850
  || process.env.EMBEDLABS_HOME?.trim()
@@ -492,21 +1853,44 @@ function resolveInstallRoot(installRoot) {
492
1853
  function localToolchainRegistryPath(installRoot) {
493
1854
  return join(installRoot, "registry", "local-toolchains.json");
494
1855
  }
495
- async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
1856
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
496
1857
  const registryPath = localToolchainRegistryPath(installRoot);
497
1858
  await mkdir(dirname(registryPath), { recursive: true });
498
- await writeFile(registryPath, `${JSON.stringify({
1859
+ let existing = {};
1860
+ try {
1861
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
1862
+ }
1863
+ catch {
1864
+ existing = {};
1865
+ }
1866
+ const entry = {
499
1867
  installed: true,
500
1868
  board_id: latest.board_id,
501
1869
  version: latest.version,
502
1870
  channel: latest.channel,
503
1871
  host: latest.host,
1872
+ mode,
504
1873
  release_root: releaseRoot,
505
1874
  packages: latest.packages,
1875
+ source,
1876
+ installed_components: source?.components,
1877
+ updated_at: new Date().toISOString()
1878
+ };
1879
+ const environments = isRecord(existing.environments)
1880
+ ? { ...existing.environments }
1881
+ : {};
1882
+ environments[normalizeBoardId(latest.board_id)] = entry;
1883
+ const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
1884
+ && typeof existing.release_root === "string"
1885
+ && normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
1886
+ const topLevel = preserveTopLevel ? existing : entry;
1887
+ await writeFile(registryPath, `${JSON.stringify({
1888
+ ...topLevel,
1889
+ environments,
506
1890
  updated_at: new Date().toISOString()
507
1891
  }, null, 2)}\n`, "utf8");
508
1892
  }
509
- async function resolveLocalReleaseRoot(releaseRoot) {
1893
+ async function resolveLocalReleaseRoot(releaseRoot, boardId = DEFAULT_BOARD_ID) {
510
1894
  if (releaseRoot?.trim()) {
511
1895
  return resolve(releaseRoot);
512
1896
  }
@@ -514,7 +1898,7 @@ async function resolveLocalReleaseRoot(releaseRoot) {
514
1898
  if (envRoot) {
515
1899
  return resolve(envRoot);
516
1900
  }
517
- const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
1901
+ const current = await currentLocalToolchain(undefined, boardId);
518
1902
  if (current.release_root) {
519
1903
  return resolve(current.release_root);
520
1904
  }
@@ -540,12 +1924,18 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
540
1924
  source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
541
1925
  };
542
1926
  }
1927
+ if (latest.download?.components?.length) {
1928
+ return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
1929
+ }
543
1930
  if (latest.download) {
544
1931
  const failures = [];
545
1932
  for (const mirror of latest.download.mirrors) {
546
1933
  if (!mirror.enabled) {
547
1934
  continue;
548
1935
  }
1936
+ if (!latest.download.archive) {
1937
+ continue;
1938
+ }
549
1939
  const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
550
1940
  try {
551
1941
  const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
@@ -586,6 +1976,127 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
586
1976
  }
587
1977
  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.");
588
1978
  }
1979
+ async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
1980
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
1981
+ const components = selectedDownloadComponents(download.components ?? [], mode);
1982
+ if (components.length === 0) {
1983
+ throw new Error(`No local toolchain components selected for mode ${mode}.`);
1984
+ }
1985
+ const extractRoot = join(tempDir, "extract-components");
1986
+ await mkdir(extractRoot, { recursive: true });
1987
+ const installedComponents = [];
1988
+ const failures = [];
1989
+ for (const component of components) {
1990
+ let installed = false;
1991
+ for (const mirror of component.mirrors) {
1992
+ if (!mirror.enabled) {
1993
+ continue;
1994
+ }
1995
+ try {
1996
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
1997
+ sha256: mirror.sha256 ?? component.archive.sha256,
1998
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes
1999
+ });
2000
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
2001
+ if (extracted.exit_code !== 0) {
2002
+ throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
2003
+ }
2004
+ installedComponents.push({
2005
+ id: component.id,
2006
+ version: component.version,
2007
+ role: component.role,
2008
+ archive_file: component.archive.file,
2009
+ mirror_kind: mirror.kind,
2010
+ downloaded_path: downloadedPath,
2011
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
2012
+ sha256: mirror.sha256 ?? component.archive.sha256
2013
+ });
2014
+ installed = true;
2015
+ break;
2016
+ }
2017
+ catch (error) {
2018
+ failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
2019
+ }
2020
+ }
2021
+ if (!installed) {
2022
+ throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
2023
+ }
2024
+ }
2025
+ return {
2026
+ path: extractRoot,
2027
+ source: {
2028
+ kind: "components",
2029
+ value: download.manifest_url,
2030
+ mirror_kind: "components",
2031
+ size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
2032
+ components: installedComponents
2033
+ }
2034
+ };
2035
+ }
2036
+ function selectedDownloadComponents(components, mode) {
2037
+ return components.filter((component) => {
2038
+ if (!component.install_modes?.length) {
2039
+ return true;
2040
+ }
2041
+ return component.install_modes.includes(mode);
2042
+ });
2043
+ }
2044
+ function installCopyPathsForBoard(boardId) {
2045
+ if (normalizeBoardId(boardId) === DEFAULT_BOARD_ID) {
2046
+ return INSTALL_COPY_PATHS.filter((relativePath) => {
2047
+ const normalized = relativePath.toLowerCase();
2048
+ return normalized !== "toolkit-runtime/rp2350-monitor"
2049
+ && normalized !== "toolkit-runtime/rp2350-monitor/";
2050
+ });
2051
+ }
2052
+ return INSTALL_COPY_PATHS;
2053
+ }
2054
+ async function resolveInstallSourcePathForBoard(sourceRoot, relativePath, boardId) {
2055
+ const direct = resolve(sourceRoot, relativePath);
2056
+ if (await pathExists(direct)) {
2057
+ return direct;
2058
+ }
2059
+ if (!isRp2350MonitorBoardId(boardId)) {
2060
+ return direct;
2061
+ }
2062
+ const legacyRp2350Root = await findLegacyRp2350Root(sourceRoot);
2063
+ if (!legacyRp2350Root) {
2064
+ return direct;
2065
+ }
2066
+ if (relativePath === "rp2350-sdk") {
2067
+ return legacyRp2350Root;
2068
+ }
2069
+ if (relativePath === "rp2350-initial-firmware") {
2070
+ return resolve(legacyRp2350Root, "initial_firmware");
2071
+ }
2072
+ if (relativePath === "rp2350-examples") {
2073
+ return resolve(legacyRp2350Root, "validation");
2074
+ }
2075
+ return direct;
2076
+ }
2077
+ async function findLegacyRp2350Root(sourceRoot) {
2078
+ const direct = resolve(sourceRoot, "RP2350");
2079
+ if (await pathExists(resolve(direct, "pico-sdk", "pico_sdk_init.cmake"))) {
2080
+ return direct;
2081
+ }
2082
+ let entries;
2083
+ try {
2084
+ entries = await readdir(sourceRoot, { withFileTypes: true });
2085
+ }
2086
+ catch {
2087
+ return undefined;
2088
+ }
2089
+ for (const entry of entries) {
2090
+ if (!entry.isDirectory()) {
2091
+ continue;
2092
+ }
2093
+ const candidate = resolve(sourceRoot, entry.name, "RP2350");
2094
+ if (await pathExists(resolve(candidate, "pico-sdk", "pico_sdk_init.cmake"))) {
2095
+ return candidate;
2096
+ }
2097
+ }
2098
+ return undefined;
2099
+ }
589
2100
  async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
590
2101
  const downloadsDir = join(installRoot, "cache", "downloads");
591
2102
  await mkdir(downloadsDir, { recursive: true });
@@ -707,6 +2218,112 @@ function compilerForSource(releaseRoot, sourcePath) {
707
2218
  }
708
2219
  throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
709
2220
  }
2221
+ function wslCompilerForSource(releaseRoot, sourcePath) {
2222
+ const extension = extname(sourcePath).toLowerCase();
2223
+ const binDir = `${trimTrailingSlash(releaseRoot)}/toolchain/llvm-cross/bin`;
2224
+ if (extension === ".c") {
2225
+ return `${binDir}/aarch64-linux-gnu-gcc`;
2226
+ }
2227
+ if ([".cc", ".cpp", ".cxx"].includes(extension)) {
2228
+ return `${binDir}/aarch64-linux-gnu-g++`;
2229
+ }
2230
+ throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
2231
+ }
2232
+ async function resolveWindowsWslTaishanPiReleaseRoot(mode = "compile") {
2233
+ const explicit = process.env.EMBEDLABS_WINDOWS_WSL_TAISHANPI_RELEASE_ROOT?.trim()
2234
+ || process.env.EMBEDLABS_WSL_TAISHANPI_RELEASE_ROOT?.trim();
2235
+ if (explicit) {
2236
+ return normalizeWindowsWslReleaseRoot(explicit);
2237
+ }
2238
+ const script = `
2239
+ set -euo pipefail
2240
+ REGISTRY="\${EMBEDLABS_WSL_INSTALL_ROOT:-$HOME/.embedlabs}/registry/local-toolchains.json"
2241
+ if [ ! -f "$REGISTRY" ]; then
2242
+ echo "Missing WSL TaishanPi registry at $REGISTRY. Run inside WSL first: npx -y embedlabs@latest local toolchain install --board taishanpi-1m-rk3566 --channel windows-test --mode ${mode}" >&2
2243
+ exit 2
2244
+ fi
2245
+ node - "$REGISTRY" <<'NODE'
2246
+ const fs = require("fs");
2247
+ const registry = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
2248
+ const env = registry.environments && registry.environments["taishanpi-1m-rk3566"] ? registry.environments["taishanpi-1m-rk3566"] : registry;
2249
+ if (!env.release_root) {
2250
+ console.error("WSL TaishanPi registry does not contain release_root.");
2251
+ process.exit(2);
2252
+ }
2253
+ console.log(env.release_root);
2254
+ NODE
2255
+ `;
2256
+ const result = await runWindowsWslBash(script);
2257
+ if (result.exit_code !== 0 || result.stdout_tail.length < 1) {
2258
+ throw new Error(`Unable to resolve TaishanPi WSL release root: ${result.stderr_tail.join("\n") || result.stdout_tail.join("\n")}`);
2259
+ }
2260
+ return normalizeWindowsWslReleaseRoot(result.stdout_tail[result.stdout_tail.length - 1]);
2261
+ }
2262
+ function normalizeWindowsWslReleaseRoot(value) {
2263
+ const trimmed = value.trim();
2264
+ if (!trimmed) {
2265
+ throw new Error("WSL release root cannot be empty.");
2266
+ }
2267
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
2268
+ return windowsPathToWslPath(resolve(trimmed), "release-root");
2269
+ }
2270
+ if (trimmed.startsWith("/")) {
2271
+ return trimTrailingSlash(trimmed);
2272
+ }
2273
+ throw new Error(`WSL release root must be a Linux absolute path or Windows drive path, got ${value}.`);
2274
+ }
2275
+ function normalizeWindowsWslInstallRoot(value) {
2276
+ const explicit = value?.trim()
2277
+ || process.env.EMBEDLABS_WINDOWS_WSL_INSTALL_ROOT?.trim()
2278
+ || process.env.EMBEDLABS_WSL_INSTALL_ROOT?.trim();
2279
+ return explicit ? normalizeWindowsWslMaybePath(explicit, "install-root") : "$HOME/.embedlabs";
2280
+ }
2281
+ function normalizeWindowsWslMaybePath(value, label) {
2282
+ const trimmed = value.trim();
2283
+ if (!trimmed) {
2284
+ throw new Error(`WSL ${label} cannot be empty.`);
2285
+ }
2286
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
2287
+ return windowsPathToWslPath(resolve(trimmed), label);
2288
+ }
2289
+ if (trimmed.startsWith("/") || trimmed.startsWith("$HOME/") || trimmed === "$HOME") {
2290
+ return trimTrailingSlash(trimmed);
2291
+ }
2292
+ throw new Error(`WSL ${label} must be a Linux absolute path, $HOME-relative path, or Windows drive path, got ${value}.`);
2293
+ }
2294
+ function windowsPathToWslPath(value, label) {
2295
+ const normalized = value.replaceAll("\\", "/");
2296
+ const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
2297
+ if (!match) {
2298
+ throw new Error(`Windows WSL ${label} path must be an absolute drive path, got ${value}.`);
2299
+ }
2300
+ return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
2301
+ }
2302
+ async function runWindowsWslBash(script) {
2303
+ return await runCommand(windowsWslBashCommand(), homedir(), script);
2304
+ }
2305
+ function windowsWslBashCommand() {
2306
+ const command = ["wsl.exe"];
2307
+ const distro = process.env.EMBEDLABS_WINDOWS_WSL_DISTRO?.trim();
2308
+ if (distro) {
2309
+ command.push("-d", distro);
2310
+ }
2311
+ command.push("--", "bash", "-s");
2312
+ return command;
2313
+ }
2314
+ function posixDirname(value) {
2315
+ const index = value.lastIndexOf("/");
2316
+ return index > 0 ? value.slice(0, index) : "/";
2317
+ }
2318
+ function sh(value) {
2319
+ return `'${value.replaceAll("'", "'\\''")}'`;
2320
+ }
2321
+ function shWslPath(value) {
2322
+ if (value === "$HOME" || value.startsWith("$HOME/")) {
2323
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("`", "\\`")}"`;
2324
+ }
2325
+ return sh(value);
2326
+ }
710
2327
  async function localCompileResult(input) {
711
2328
  await access(input.artifactPath, constants.R_OK);
712
2329
  const artifactInfo = await stat(input.artifactPath);
@@ -737,13 +2354,14 @@ function assertAuthenticated(auth) {
737
2354
  throw new Error("Embed Labs auth is required for local toolchain builds. Run: embedlabs auth login --token <user-api-key>");
738
2355
  }
739
2356
  }
740
- async function runCommand(command, cwd) {
2357
+ async function runCommand(command, cwd, stdin) {
741
2358
  return await new Promise((resolve) => {
742
2359
  const child = spawn(command[0], command.slice(1), {
743
2360
  cwd,
744
2361
  env: process.env,
745
- stdio: ["ignore", "pipe", "pipe"]
2362
+ stdio: ["pipe", "pipe", "pipe"]
746
2363
  });
2364
+ child.stdin.end(stdin ?? "");
747
2365
  let stdout = "";
748
2366
  let stderr = "";
749
2367
  child.stdout.setEncoding("utf8");
@@ -775,6 +2393,68 @@ async function runCommand(command, cwd) {
775
2393
  });
776
2394
  });
777
2395
  }
2396
+ async function runCommandWithTimeout(command, cwd, timeoutMs) {
2397
+ return await new Promise((resolve) => {
2398
+ const child = spawn(command[0], command.slice(1), {
2399
+ cwd,
2400
+ env: process.env,
2401
+ stdio: ["ignore", "pipe", "pipe"]
2402
+ });
2403
+ let stdout = "";
2404
+ let stderr = "";
2405
+ let settled = false;
2406
+ const timer = setTimeout(() => {
2407
+ if (settled) {
2408
+ return;
2409
+ }
2410
+ stderr += `Command timed out after ${timeoutMs} ms.\n`;
2411
+ child.kill("SIGTERM");
2412
+ setTimeout(() => {
2413
+ if (!settled) {
2414
+ child.kill("SIGKILL");
2415
+ }
2416
+ }, 2_000).unref();
2417
+ }, timeoutMs);
2418
+ child.stdout.setEncoding("utf8");
2419
+ child.stderr.setEncoding("utf8");
2420
+ child.stdout.on("data", (chunk) => {
2421
+ stdout += chunk;
2422
+ });
2423
+ child.stderr.on("data", (chunk) => {
2424
+ stderr += chunk;
2425
+ });
2426
+ child.on("error", (error) => {
2427
+ if (settled) {
2428
+ return;
2429
+ }
2430
+ settled = true;
2431
+ clearTimeout(timer);
2432
+ stderr += `${error.message}\n`;
2433
+ resolve({
2434
+ command,
2435
+ cwd,
2436
+ exit_code: 127,
2437
+ stdout_tail: tailLines(stdout),
2438
+ stderr_tail: tailLines(stderr)
2439
+ });
2440
+ });
2441
+ child.on("close", (code) => {
2442
+ if (settled) {
2443
+ return;
2444
+ }
2445
+ settled = true;
2446
+ clearTimeout(timer);
2447
+ const timedOut = stderr.includes(`Command timed out after ${timeoutMs} ms.`);
2448
+ resolve({
2449
+ command,
2450
+ cwd,
2451
+ exit_code: timedOut ? 124 : code ?? 1,
2452
+ stdout_tail: tailLines(stdout),
2453
+ stderr_tail: tailLines(stderr)
2454
+ });
2455
+ });
2456
+ });
2457
+ }
778
2458
  async function fileInfoFor(filePath) {
779
2459
  const result = await runCommand(["file", filePath], dirname(filePath));
780
2460
  return result.exit_code === 0 ? result.stdout_tail.join("\n") : undefined;