@kvell007/embed-labs-cli 0.1.0-alpha.9 → 0.1.0-alpha.91

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,21 +115,69 @@ 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
158
  "toolchain/host",
85
159
  "toolchain/host-tools",
160
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
86
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",
87
164
  "tools/mac",
165
+ "toolkit-runtime/qtquick-live-preview",
166
+ "toolkit-runtime/rp2350-monitor",
167
+ "rp2350-sdk",
168
+ "rp2350-initial-firmware",
169
+ "rp2350-examples",
88
170
  "images/current",
89
171
  "userdata/rootfs",
90
- "boot-workspace/kernel-tree"
172
+ "boot-workspace",
173
+ "examples/qt-smoke",
174
+ "README.md",
175
+ "meta",
176
+ "scripts",
177
+ "support",
178
+ "third_party"
91
179
  ];
180
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
92
181
  export function defaultLocalReleaseRoot() {
93
182
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
94
183
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -97,31 +186,39 @@ export function defaultLocalReleaseRoot() {
97
186
  export async function latestLocalToolchain(options = {}) {
98
187
  const boardId = options.boardId ?? DEFAULT_BOARD_ID;
99
188
  const channelName = options.channel ?? DEFAULT_CHANNEL;
189
+ const host = localToolchainHostId();
100
190
  const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
101
191
  const boardPackageId = boardPackageIdFor(boardId);
102
192
  const board = manifests.get(boardPackageId);
103
193
  if (!board) {
104
194
  throw new Error(`No local toolchain board package found for ${boardId}.`);
105
195
  }
196
+ const canonicalBoardId = boardIdForPackageManifest(board);
106
197
  const packages = resolvePackageRefs(boardPackageId, channel, manifests);
198
+ const downloadHost = isNativeWindowsTaishanPiHost(canonicalBoardId, host)
199
+ ? wslHostForWindowsHost(host) ?? host
200
+ : host;
107
201
  let download;
108
202
  let downloadError;
109
203
  try {
110
204
  download = await resolveLocalToolchainDownloadPlan({
111
- boardId,
205
+ boardId: canonicalBoardId,
112
206
  channel: channelName,
113
- host: hostId(),
207
+ host: downloadHost,
114
208
  toolchain: "llvm"
115
209
  });
116
210
  }
117
211
  catch (error) {
118
212
  downloadError = error instanceof Error ? error.message : String(error);
119
213
  }
214
+ if (!download && isNativeWindowsTaishanPiHost(canonicalBoardId, host)) {
215
+ downloadError = taishanPiWindowsRequirementMessage(host);
216
+ }
120
217
  return {
121
- board_id: boardId,
122
- channel: channel.channel,
123
- host: hostId(),
124
- version: board.version,
218
+ board_id: canonicalBoardId,
219
+ channel: channelName,
220
+ host,
221
+ version: download?.version ?? board.version,
125
222
  metadata_root: metadataRoot,
126
223
  packages,
127
224
  download,
@@ -133,11 +230,36 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
133
230
  const registryPath = localToolchainRegistryPath(root);
134
231
  try {
135
232
  const registry = JSON.parse(await readFile(registryPath, "utf8"));
233
+ const environments = registry.environments;
234
+ const boardInstall = environments?.[normalizeBoardId(boardId)];
235
+ if (boardInstall?.release_root) {
236
+ return {
237
+ installed: true,
238
+ board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
239
+ version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
240
+ mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
241
+ release_root: boardInstall.release_root,
242
+ registry_path: registryPath,
243
+ install_root: root,
244
+ channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
245
+ packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
246
+ };
247
+ }
136
248
  const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
249
+ const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
250
+ if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
251
+ return {
252
+ installed: false,
253
+ board_id: boardId,
254
+ registry_path: registryPath,
255
+ install_root: root
256
+ };
257
+ }
137
258
  return {
138
259
  installed: !!releaseRoot,
139
- board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
260
+ board_id: registryBoardId,
140
261
  version: typeof registry.version === "string" ? registry.version : undefined,
262
+ mode: typeof registry.mode === "string" ? registry.mode : undefined,
141
263
  release_root: releaseRoot,
142
264
  registry_path: registryPath,
143
265
  install_root: root,
@@ -154,61 +276,269 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
154
276
  };
155
277
  }
156
278
  }
279
+ async function discoverInstalledLocalToolchains(installRoot, current) {
280
+ const installed = new Map();
281
+ if (current.installed && current.release_root) {
282
+ installed.set(normalizeBoardId(current.board_id), {
283
+ board_id: current.board_id,
284
+ version: current.version,
285
+ channel: current.channel,
286
+ mode: current.mode,
287
+ release_root: current.release_root
288
+ });
289
+ }
290
+ const toolchainsRoot = join(installRoot, "toolchains");
291
+ let boardEntries;
292
+ try {
293
+ boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
294
+ }
295
+ catch {
296
+ return installed;
297
+ }
298
+ for (const boardEntry of boardEntries) {
299
+ if (!boardEntry.isDirectory()) {
300
+ continue;
301
+ }
302
+ const boardId = normalizeBoardId(boardEntry.name);
303
+ if (installed.has(boardId)) {
304
+ continue;
305
+ }
306
+ const boardRoot = join(toolchainsRoot, boardEntry.name);
307
+ let versionEntries;
308
+ try {
309
+ versionEntries = await readdir(boardRoot, { withFileTypes: true });
310
+ }
311
+ catch {
312
+ continue;
313
+ }
314
+ const versions = versionEntries
315
+ .filter((entry) => entry.isDirectory())
316
+ .map((entry) => entry.name)
317
+ .sort(compareVersionLike)
318
+ .reverse();
319
+ const version = versions[0];
320
+ if (!version) {
321
+ continue;
322
+ }
323
+ installed.set(boardId, {
324
+ board_id: boardId,
325
+ version,
326
+ release_root: join(boardRoot, version)
327
+ });
328
+ }
329
+ return installed;
330
+ }
331
+ export async function listLocalToolchainEnvironments(options = {}) {
332
+ const channelName = options.channel ?? DEFAULT_CHANNEL;
333
+ const host = localToolchainHostId();
334
+ const installRoot = resolveInstallRoot(options.installRoot);
335
+ const registryPath = localToolchainRegistryPath(installRoot);
336
+ const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
337
+ const current = await currentLocalToolchain(installRoot);
338
+ const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
339
+ const boardManifests = [...manifests.values()]
340
+ .filter((manifest) => manifest.kind === "board")
341
+ .filter((manifest) => {
342
+ if (!options.boardId) {
343
+ return true;
344
+ }
345
+ const normalizedFilter = normalizeBoardId(options.boardId);
346
+ return boardIdForPackageManifest(manifest) === normalizedFilter
347
+ || manifest.id === options.boardId
348
+ || manifest.id === packageIdForBoardFilter(options.boardId);
349
+ })
350
+ .sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
351
+ const environments = [];
352
+ const wslStatus = host.startsWith("win32-")
353
+ ? await windowsWslStatus()
354
+ : undefined;
355
+ for (const board of boardManifests) {
356
+ const boardId = boardIdForPackageManifest(board);
357
+ const packages = resolvePackageRefs(board.id, channel, manifests);
358
+ const hostSupport = packageHostSupport(packages, manifests, host);
359
+ let download;
360
+ let downloadError;
361
+ const downloadHost = isNativeWindowsTaishanPiHost(boardId, host)
362
+ ? wslHostForWindowsHost(host) ?? host
363
+ : host;
364
+ try {
365
+ download = await resolveLocalToolchainDownloadPlan({
366
+ boardId,
367
+ channel: channelName,
368
+ host: downloadHost,
369
+ toolchain: "llvm"
370
+ });
371
+ }
372
+ catch (error) {
373
+ downloadError = error instanceof Error ? error.message : String(error);
374
+ }
375
+ if (!download && isNativeWindowsTaishanPiHost(boardId, host)) {
376
+ downloadError = taishanPiWindowsRequirementMessage(host);
377
+ }
378
+ const latestVersion = download?.version ?? board.version;
379
+ const currentForBoard = await currentLocalToolchain(installRoot, boardId);
380
+ const installedCandidate = currentForBoard.installed
381
+ ? currentForBoard
382
+ : installedByBoard.get(normalizeBoardId(boardId));
383
+ const installed = installedCandidate
384
+ ? {
385
+ version: installedCandidate.version,
386
+ channel: installedCandidate.channel,
387
+ mode: installedCandidate.mode,
388
+ release_root: installedCandidate.release_root
389
+ }
390
+ : undefined;
391
+ const updateAvailable = !!installed?.version && installed.version !== latestVersion;
392
+ const nativeWindowsTaishanPi = isNativeWindowsTaishanPiHost(boardId, host);
393
+ const execution = nativeWindowsTaishanPi
394
+ ? taishanPiWindowsExecutionRoute(host, wslStatus, !!download)
395
+ : {
396
+ kind: "native",
397
+ supported: !!download || !!installed || hostSupport.supported,
398
+ actual_host: host
399
+ };
400
+ const effectiveHostSupport = nativeWindowsTaishanPi
401
+ ? { supported: !!download && execution.supported, unsupportedPackages: download ? [] : ["taishanpi-1m-rk3566 native Windows toolchain"] }
402
+ : download || installed
403
+ ? { supported: true, unsupportedPackages: [] }
404
+ : hostSupport;
405
+ const status = !effectiveHostSupport.supported
406
+ ? "unsupported_host"
407
+ : updateAvailable
408
+ ? "update_available"
409
+ : installed
410
+ ? "installed"
411
+ : "available";
412
+ const mode = download?.default_mode ?? "qt";
413
+ const installModes = download
414
+ ? localToolchainInstallModesForDownload(download)
415
+ : installed?.mode
416
+ ? [installed.mode]
417
+ : localToolchainInstallModesForDownload(download);
418
+ environments.push({
419
+ board_id: boardId,
420
+ package_id: board.id,
421
+ display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
422
+ family: board.family,
423
+ variant: board.variant,
424
+ channel: channelName,
425
+ host,
426
+ status,
427
+ supported_host: effectiveHostSupport.supported,
428
+ unsupported_packages: effectiveHostSupport.unsupportedPackages,
429
+ install_modes: installModes,
430
+ installed,
431
+ latest: {
432
+ version: latestVersion,
433
+ default_mode: download?.default_mode,
434
+ source_url: download?.source_url,
435
+ manifest_url: download?.manifest_url,
436
+ component_count: download?.components?.length,
437
+ download_error: downloadError
438
+ },
439
+ packages,
440
+ components: download?.components?.map(localToolchainEnvironmentComponent),
441
+ execution,
442
+ install_command: nativeWindowsTaishanPi ? `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode}` : `embedlabs local toolchain install --board ${boardId}${localToolchainChannelFlag(channelName)} --mode ${mode}`,
443
+ 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`,
444
+ notes: environmentNotes({ boardId, host, status, downloadError, unsupportedPackages: effectiveHostSupport.unsupportedPackages })
445
+ });
446
+ }
447
+ const filteredEnvironments = options.installedOnly
448
+ ? environments.filter((environment) => !!environment.installed)
449
+ : environments;
450
+ return {
451
+ host,
452
+ channel: channelName,
453
+ metadata_source: metadataRoot ? "local_override" : "built_in",
454
+ metadata_root: metadataRoot,
455
+ install_root: installRoot,
456
+ registry_path: registryPath,
457
+ environments: filteredEnvironments
458
+ };
459
+ }
157
460
  export async function installLocalToolchain(options = {}) {
158
461
  const latest = await latestLocalToolchain(options);
462
+ if (isNativeWindowsTaishanPiHost(latest.board_id, latest.host)) {
463
+ return await installLocalToolchainWindowsWsl(options, latest);
464
+ }
159
465
  const installRoot = resolveInstallRoot(options.installRoot);
160
466
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
467
+ const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
161
468
  if (await pathExists(releaseRoot) && !options.force) {
162
- const validation = await validateLocalToolchain(releaseRoot);
469
+ await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
470
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
163
471
  if (!validation.ok) {
164
- throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
472
+ if (latest.download?.components?.length) {
473
+ // Component installs can upgrade an existing lower-mode install by overlaying
474
+ // only the newly selected components instead of deleting the whole tree.
475
+ }
476
+ else {
477
+ throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
478
+ }
479
+ }
480
+ else {
481
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
482
+ const removedOldVersions = await pruneOldLocalToolchainVersions(installRoot, latest.board_id, releaseRoot);
483
+ return {
484
+ board_id: latest.board_id,
485
+ version: latest.version,
486
+ channel: latest.channel,
487
+ host: latest.host,
488
+ mode: installMode,
489
+ install_root: installRoot,
490
+ release_root: releaseRoot,
491
+ registry_path: localToolchainRegistryPath(installRoot),
492
+ source: { kind: "directory", value: releaseRoot },
493
+ installed_paths: [],
494
+ removed_old_versions: removedOldVersions,
495
+ packages: latest.packages,
496
+ validation
497
+ };
165
498
  }
166
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
167
- return {
168
- board_id: latest.board_id,
169
- version: latest.version,
170
- channel: latest.channel,
171
- host: latest.host,
172
- install_root: installRoot,
173
- release_root: releaseRoot,
174
- registry_path: localToolchainRegistryPath(installRoot),
175
- source: { kind: "directory", value: releaseRoot },
176
- installed_paths: [],
177
- packages: latest.packages,
178
- validation
179
- };
180
499
  }
181
500
  const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
182
501
  try {
183
502
  const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
184
- await rm(releaseRoot, { recursive: true, force: true });
503
+ if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
504
+ await rm(releaseRoot, { recursive: true, force: true });
505
+ }
185
506
  await mkdir(releaseRoot, { recursive: true });
186
507
  const installedPaths = [];
187
- for (const relativePath of INSTALL_COPY_PATHS) {
188
- const sourcePath = resolve(sourceRoot.path, relativePath);
508
+ for (const relativePath of installCopyPathsForBoard(latest.board_id)) {
509
+ const sourcePath = await resolveInstallSourcePathForBoard(sourceRoot.path, relativePath, latest.board_id);
189
510
  if (!await pathExists(sourcePath)) {
190
511
  continue;
191
512
  }
192
513
  const targetPath = resolve(releaseRoot, relativePath);
193
514
  await mkdir(dirname(targetPath), { recursive: true });
194
- await cp(sourcePath, targetPath, { recursive: true, force: true, preserveTimestamps: true });
515
+ await cp(sourcePath, targetPath, {
516
+ recursive: true,
517
+ force: true,
518
+ preserveTimestamps: true,
519
+ verbatimSymlinks: true
520
+ });
195
521
  installedPaths.push(relativePath);
196
522
  }
197
- await writeCurrentRegistry(installRoot, latest, releaseRoot);
198
- const validation = await validateLocalToolchain(releaseRoot);
523
+ await rewriteLocalToolchainPortablePaths(releaseRoot, latest.board_id);
524
+ await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
525
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
199
526
  if (!validation.ok) {
200
527
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
201
528
  }
529
+ const removedOldVersions = await pruneOldLocalToolchainVersions(installRoot, latest.board_id, releaseRoot);
202
530
  return {
203
531
  board_id: latest.board_id,
204
532
  version: latest.version,
205
533
  channel: latest.channel,
206
534
  host: latest.host,
535
+ mode: installMode,
207
536
  install_root: installRoot,
208
537
  release_root: releaseRoot,
209
538
  registry_path: localToolchainRegistryPath(installRoot),
210
539
  source: sourceRoot.source,
211
540
  installed_paths: installedPaths,
541
+ removed_old_versions: removedOldVersions,
212
542
  packages: latest.packages,
213
543
  validation
214
544
  };
@@ -217,23 +547,230 @@ export async function installLocalToolchain(options = {}) {
217
547
  await rm(tempDir, { recursive: true, force: true });
218
548
  }
219
549
  }
220
- export async function validateLocalToolchain(releaseRoot) {
221
- const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
222
- const required = [
223
- ["release root", "."],
224
- ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
225
- ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
226
- ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
227
- ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
228
- ["host GCC libraries", "toolchain/host/lib/gcc"],
229
- ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
230
- ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
231
- ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
232
- ["Rockchip mkimage", "tools/mac/mkimage"],
233
- ["Rockchip dumpimage", "tools/mac/dumpimage"],
234
- ["Rockchip resource_tool", "tools/mac/resource_tool"],
235
- ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"]
236
- ];
550
+ async function installLocalToolchainWindowsWsl(options, latest) {
551
+ const route = taishanPiWindowsExecutionRoute(latest.host, await windowsWslStatus());
552
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
553
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
554
+ }
555
+ const installRoot = normalizeWindowsWslInstallRoot(options.installRoot);
556
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
557
+ const cli = process.env.EMBEDLABS_WINDOWS_WSL_CLI?.trim() || process.env.EMBEDLABS_WSL_CLI?.trim() || "npx -y embedlabs@latest";
558
+ const command = [
559
+ cli,
560
+ "local toolchain install",
561
+ `--board ${sh(latest.board_id)}`,
562
+ `--channel ${sh(latest.channel)}`,
563
+ `--mode ${sh(mode)}`,
564
+ `--install-root ${shWslPath(installRoot)}`,
565
+ options.force ? "--force" : "",
566
+ options.sourceUrl ? `--source-url ${sh(options.sourceUrl)}` : "",
567
+ options.sourceReleaseRoot ? `--source-release-root ${shWslPath(normalizeWindowsWslMaybePath(options.sourceReleaseRoot, "source-release-root"))}` : "",
568
+ "--json"
569
+ ].filter(Boolean).join(" ");
570
+ const script = [
571
+ "set -euo pipefail",
572
+ `RESULT_FILE="$(mktemp)"`,
573
+ `EMBED_DOWNLOAD_BASE_URL=${sh(downloadBaseUrl())} ${command} > "$RESULT_FILE"`,
574
+ "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\""
575
+ ].join("\n");
576
+ const result = await runWindowsWslBash(script);
577
+ if (result.exit_code !== 0 || result.stdout_tail.length < 1) {
578
+ throw new Error(`Windows WSL TaishanPi toolchain install failed: ${result.stderr_tail.join("\n") || result.stdout_tail.join("\n")}`);
579
+ }
580
+ const payload = JSON.parse(result.stdout_tail[result.stdout_tail.length - 1]);
581
+ return {
582
+ ...payload,
583
+ host: latest.host,
584
+ source: {
585
+ ...payload.source,
586
+ value: `wsl:${payload.source.value}`
587
+ },
588
+ validation: {
589
+ ...payload.validation,
590
+ notes: [
591
+ ...payload.validation.notes,
592
+ "Installed through Windows CLI by delegating TaishanPi linux-x86_64 tools to WSL2."
593
+ ]
594
+ }
595
+ };
596
+ }
597
+ export async function windowsWslStatus() {
598
+ const result = {
599
+ host: hostId(),
600
+ platform: platform(),
601
+ arch: arch(),
602
+ checked_at: new Date().toISOString(),
603
+ applicable: platform() === "win32",
604
+ wsl_available: false,
605
+ usable: false,
606
+ distributions: [],
607
+ online_distributions: [],
608
+ taishanpi_execution: {
609
+ supported: false,
610
+ required_host: "linux-x86_64",
611
+ windows_host: hostId(),
612
+ route: "wsl2",
613
+ status: platform() === "win32" ? "wsl_missing" : "wsl_not_applicable",
614
+ install_command: "embedlabs local wsl status",
615
+ reason: platform() === "win32"
616
+ ? "WSL status has not been checked yet."
617
+ : "WSL2 is only applicable on Windows hosts."
618
+ },
619
+ commands: {
620
+ status: "wsl.exe --status",
621
+ list: "wsl.exe -l -v",
622
+ list_online: "wsl.exe --list --online",
623
+ install_ubuntu: "wsl.exe --install -d Ubuntu --web-download"
624
+ },
625
+ notes: []
626
+ };
627
+ if (!result.applicable) {
628
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
629
+ result.notes.push("WSL2 is only required for TaishanPi local development on Windows hosts.");
630
+ return result;
631
+ }
632
+ const whereWsl = await runCommand(["cmd", "/c", "where wsl.exe"], homedir());
633
+ result.wsl_available = whereWsl.exit_code === 0;
634
+ if (!result.wsl_available) {
635
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
636
+ result.notes.push("wsl.exe was not found. Enable Windows Subsystem for Linux before installing TaishanPi local tools.");
637
+ return result;
638
+ }
639
+ const list = await runCommand(["wsl.exe", "-l", "-v"], homedir());
640
+ const normalized = normalizeWindowsCommandText([...list.stdout_tail, ...list.stderr_tail].join("\n"));
641
+ result.distributions = list.exit_code === 0 ? parseWslDistributionList(normalized) : [];
642
+ const online = await runCommand(["wsl.exe", "--list", "--online"], homedir());
643
+ const normalizedOnline = normalizeWindowsCommandText([...online.stdout_tail, ...online.stderr_tail].join("\n"));
644
+ result.online_distributions = online.exit_code === 0 ? parseWslOnlineDistributionList(normalizedOnline) : [];
645
+ result.usable = list.exit_code === 0 && result.distributions.length > 0;
646
+ result.taishanpi_execution = taishanPiWindowsExecutionRoute(result.host, result);
647
+ if (!result.usable) {
648
+ result.notes.push("No usable WSL2 distribution is configured yet. Install Ubuntu, restart Windows if requested, then run Embed Labs TaishanPi tools inside WSL2.");
649
+ }
650
+ else {
651
+ 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.");
652
+ }
653
+ if (result.host === "win32-arm64") {
654
+ 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.");
655
+ }
656
+ return result;
657
+ }
658
+ export async function windowsWslInstall(options = {}) {
659
+ const distro = options.distribution?.trim() || "Ubuntu";
660
+ const command = ["wsl.exe", "--install", "-d", distro];
661
+ if (options.noLaunch !== false) {
662
+ command.push("--no-launch");
663
+ }
664
+ if (options.webDownload !== false) {
665
+ command.push("--web-download");
666
+ }
667
+ const notes = [];
668
+ if (platform() !== "win32") {
669
+ notes.push("WSL installation can only run on Windows hosts.");
670
+ return {
671
+ host: hostId(),
672
+ platform: platform(),
673
+ arch: arch(),
674
+ command,
675
+ exit_code: 1,
676
+ stdout_tail: [],
677
+ stderr_tail: [],
678
+ status_after: await windowsWslStatus(),
679
+ notes
680
+ };
681
+ }
682
+ const result = await runCommandWithTimeout(command, homedir(), options.timeoutMs ?? 600_000);
683
+ const statusAfter = await windowsWslStatus();
684
+ if (result.exit_code === 0) {
685
+ notes.push("WSL install command completed. If Windows requests a restart, restart before running TaishanPi local tools.");
686
+ }
687
+ else {
688
+ 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.");
689
+ }
690
+ if (!statusAfter.usable) {
691
+ notes.push("WSL2 is still not usable after this command. Complete first-launch setup for the distribution, then rerun embedlabs local wsl status.");
692
+ }
693
+ return {
694
+ host: hostId(),
695
+ platform: platform(),
696
+ arch: arch(),
697
+ command,
698
+ exit_code: result.exit_code,
699
+ stdout_tail: result.stdout_tail,
700
+ stderr_tail: result.stderr_tail,
701
+ status_after: statusAfter,
702
+ notes
703
+ };
704
+ }
705
+ export async function uninstallLocalToolchain(options = {}) {
706
+ if (!options.boardId?.trim()) {
707
+ throw new Error("board_id is required for local toolchain uninstall.");
708
+ }
709
+ const boardId = normalizeBoardId(options.boardId);
710
+ const installRoot = resolveInstallRoot(options.installRoot);
711
+ const registryPath = localToolchainRegistryPath(installRoot);
712
+ const boardRoot = resolve(installRoot, "toolchains", boardId);
713
+ const removedPaths = [];
714
+ if (await pathExists(boardRoot)) {
715
+ await rm(boardRoot, { recursive: true, force: true });
716
+ removedPaths.push(boardRoot);
717
+ }
718
+ let removedRegistryEntry = false;
719
+ let existing = {};
720
+ try {
721
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
722
+ }
723
+ catch {
724
+ existing = {};
725
+ }
726
+ const environments = isRecord(existing.environments)
727
+ ? { ...existing.environments }
728
+ : {};
729
+ if (Object.prototype.hasOwnProperty.call(environments, boardId)) {
730
+ delete environments[boardId];
731
+ removedRegistryEntry = true;
732
+ }
733
+ const topLevelBoard = normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID));
734
+ const topLevelPointsToBoard = topLevelBoard === boardId && typeof existing.release_root === "string";
735
+ const cleaned = { ...existing, environments, updated_at: new Date().toISOString() };
736
+ if (topLevelPointsToBoard) {
737
+ for (const key of [
738
+ "installed",
739
+ "board_id",
740
+ "version",
741
+ "channel",
742
+ "host",
743
+ "mode",
744
+ "release_root",
745
+ "packages",
746
+ "source",
747
+ "installed_components"
748
+ ]) {
749
+ delete cleaned[key];
750
+ }
751
+ removedRegistryEntry = true;
752
+ }
753
+ if (removedRegistryEntry || Object.keys(environments).length > 0 || Object.keys(existing).length > 0) {
754
+ await mkdir(dirname(registryPath), { recursive: true });
755
+ await writeFile(registryPath, `${JSON.stringify(cleaned, null, 2)}\n`, "utf8");
756
+ }
757
+ return {
758
+ board_id: boardId,
759
+ install_root: installRoot,
760
+ registry_path: registryPath,
761
+ removed: removedPaths.length > 0 || removedRegistryEntry,
762
+ removed_paths: removedPaths,
763
+ removed_registry_entry: removedRegistryEntry,
764
+ remaining_installed_boards: Object.keys(environments).sort(),
765
+ observed_at: new Date().toISOString()
766
+ };
767
+ }
768
+ export async function validateLocalToolchain(input) {
769
+ const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
770
+ const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
771
+ const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
772
+ const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot, boardId);
773
+ const required = requiredLocalToolchainChecks(mode, boardId);
237
774
  const checked_paths = [];
238
775
  for (const [label, relativePath] of required) {
239
776
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -244,24 +781,441 @@ export async function validateLocalToolchain(releaseRoot) {
244
781
  });
245
782
  }
246
783
  const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
784
+ const path_leaks = await localToolchainPathLeaks(resolvedRoot, boardId);
785
+ const missing_groups = localToolchainMissingGroups(checked_paths, path_leaks);
786
+ const repair_command = missing_paths.length > 0 || path_leaks.length > 0
787
+ ? `embedlabs local toolchain install --board ${boardId} --mode ${mode}`
788
+ : undefined;
247
789
  return {
248
- ok: missing_paths.length === 0,
790
+ ok: missing_paths.length === 0 && path_leaks.length === 0,
791
+ mode,
249
792
  host: {
250
793
  platform: platform(),
251
794
  arch: arch()
252
795
  },
253
- board_id: DEFAULT_BOARD_ID,
796
+ board_id: boardId,
254
797
  release_root: resolvedRoot,
255
798
  checked_paths,
256
799
  missing_paths,
800
+ path_leaks,
801
+ missing_groups,
802
+ repair_command,
803
+ summary_for_user: localToolchainValidationSummary({
804
+ boardId,
805
+ mode,
806
+ ok: missing_paths.length === 0,
807
+ missingGroups: missing_groups,
808
+ repairCommand: repair_command
809
+ }),
257
810
  notes: [
258
811
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
259
- "This validator checks the Mac-first TaishanPi LLVM release layout; package install/update registry work is tracked separately."
812
+ `This validator checks the ${boardId} local support layout for install mode ${mode}.`
260
813
  ]
261
814
  };
262
815
  }
816
+ function localToolchainMissingGroups(checkedPaths, pathLeaks = []) {
817
+ const groups = new Set();
818
+ if (pathLeaks.length > 0) {
819
+ groups.add("portable-paths/安装包可移植路径");
820
+ }
821
+ for (const check of checkedPaths) {
822
+ if (check.exists) {
823
+ continue;
824
+ }
825
+ const label = check.label.toLowerCase();
826
+ const text = `${check.label} ${check.path}`.toLowerCase();
827
+ if (label.includes("qt ") || text.includes("qt host") || text.includes("qtquick") || text.includes("qt cmake") || text.includes("qt target")) {
828
+ groups.add("qt/Qt host、target 或实时预览组件");
829
+ continue;
830
+ }
831
+ if (text.includes("compiler") || text.includes("readelf") || text.includes("sysroot") || text.includes("include directory") || text.includes("clang wrapper") || text.includes("gcc libraries")) {
832
+ groups.add("compile/交叉编译器与 sysroot");
833
+ continue;
834
+ }
835
+ if (text.includes("boot") || text.includes("dtb") || text.includes("resource image") || text.includes("metadata") || text.includes("/meta")) {
836
+ groups.add("board-resources/启动资源、DTB 与元数据");
837
+ continue;
838
+ }
839
+ if (text.includes("image") || text.includes("rootfs") || text.includes("parameter")) {
840
+ groups.add("images/基础镜像资源");
841
+ continue;
842
+ }
843
+ if (text.includes("rp2350") || text.includes("pico")) {
844
+ groups.add("rp2350/RP2350 SDK、固件或监控运行时");
845
+ continue;
846
+ }
847
+ groups.add("runtime/运行时文件");
848
+ }
849
+ return [...groups];
850
+ }
851
+ async function rewriteLocalToolchainPortablePaths(releaseRoot, boardId) {
852
+ if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID) {
853
+ return;
854
+ }
855
+ const replacements = taishanPiPortablePathReplacements(releaseRoot);
856
+ const files = await taishanPiPortableTextFiles(releaseRoot);
857
+ for (const file of files) {
858
+ let content;
859
+ try {
860
+ content = await readFile(file, "utf8");
861
+ }
862
+ catch {
863
+ continue;
864
+ }
865
+ const rewritten = rewritePortablePathContent(content, replacements);
866
+ if (rewritten !== content) {
867
+ await writeFile(file, rewritten, "utf8");
868
+ }
869
+ }
870
+ }
871
+ async function localToolchainPathLeaks(releaseRoot, boardId) {
872
+ if (normalizeBoardId(boardId) !== DEFAULT_BOARD_ID || !await pathExists(releaseRoot)) {
873
+ return [];
874
+ }
875
+ const forbidden = taishanPiForbiddenPortablePathFragments();
876
+ const files = await taishanPiPortableTextFiles(releaseRoot);
877
+ const leaks = [];
878
+ for (const file of files) {
879
+ let content;
880
+ try {
881
+ content = await readFile(file, "utf8");
882
+ }
883
+ catch {
884
+ continue;
885
+ }
886
+ for (const fragment of forbidden) {
887
+ if (content.includes(fragment)) {
888
+ leaks.push({
889
+ label: "portable path check",
890
+ path: file,
891
+ forbidden: fragment
892
+ });
893
+ break;
894
+ }
895
+ }
896
+ }
897
+ return leaks;
898
+ }
899
+ async function taishanPiPortableTextFiles(releaseRoot) {
900
+ const qtHostDirs = [
901
+ "qt-host/qt6-host-macos-6.8.3",
902
+ "qt-host/qt6-host-linux-x86_64-6.8.3"
903
+ ];
904
+ const qtModulePriNames = [
905
+ "widgets",
906
+ "qml",
907
+ "quick",
908
+ "quickcontrols2",
909
+ "charts",
910
+ "multimedia",
911
+ "bluetooth",
912
+ "positioning"
913
+ ];
914
+ const qtCmakeModules = [
915
+ "Qt6Widgets",
916
+ "Qt6Qml",
917
+ "Qt6Quick",
918
+ "Qt6QuickControls2",
919
+ "Qt6Charts",
920
+ "Qt6Multimedia",
921
+ "Qt6Bluetooth",
922
+ "Qt6Positioning"
923
+ ];
924
+ const candidates = [
925
+ "toolchain/qt6-rk3566-llvm-toolchain.cmake",
926
+ "qt-target/qt6-rk3566-llvm-6.8.3/bin/target_qt.conf",
927
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/Qt6Dependencies.cmake",
928
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6/qt.toolchain.cmake",
929
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake",
930
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qdevice.pri",
931
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/qmodule.pri",
932
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_core_private.pri",
933
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_gui_private.pri",
934
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport.pri",
935
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialport_private.pri",
936
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus.pri",
937
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_serialbus_private.pri",
938
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialPort.pc",
939
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6SerialBus.pc",
940
+ "qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_ext_openxr_loader.pri",
941
+ ...qtModulePriNames.flatMap((module) => [
942
+ `qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_${module}.pri`,
943
+ `qt-target/qt6-rk3566-llvm-6.8.3/mkspecs/modules/qt_lib_${module}_private.pri`,
944
+ `qt-target/qt6-rk3566-llvm-6.8.3/lib/pkgconfig/Qt6${moduleToCmakeSuffix(module)}.pc`
945
+ ]),
946
+ ...qtHostDirs.flatMap((qtHostDir) => [
947
+ `${qtHostDir}/lib/cmake/Qt6BuildInternals/QtBuildInternalsExtra.cmake`,
948
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialport.pri`,
949
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialport_private.pri`,
950
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialbus.pri`,
951
+ `${qtHostDir}/mkspecs/modules/qt_lib_serialbus_private.pri`,
952
+ `${qtHostDir}/mkspecs/modules/qt_ext_openxr_loader.pri`,
953
+ ...qtModulePriNames.flatMap((module) => [
954
+ `${qtHostDir}/mkspecs/modules/qt_lib_${module}.pri`,
955
+ `${qtHostDir}/mkspecs/modules/qt_lib_${module}_private.pri`
956
+ ])
957
+ ])
958
+ ];
959
+ const files = [];
960
+ for (const candidate of candidates) {
961
+ const file = join(releaseRoot, candidate);
962
+ if (await pathExists(file)) {
963
+ files.push(file);
964
+ }
965
+ }
966
+ for (const relativeDir of [
967
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort",
968
+ "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus",
969
+ ...qtCmakeModules.map((module) => `qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/${module}`),
970
+ ...qtHostDirs.flatMap((qtHostDir) => [
971
+ `${qtHostDir}/lib/cmake/Qt6SerialPort`,
972
+ `${qtHostDir}/lib/cmake/Qt6SerialBus`,
973
+ ...qtCmakeModules.map((module) => `${qtHostDir}/lib/cmake/${module}`)
974
+ ])
975
+ ]) {
976
+ const dir = join(releaseRoot, relativeDir);
977
+ if (!await pathExists(dir)) {
978
+ continue;
979
+ }
980
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
981
+ if (!entry.isFile()) {
982
+ continue;
983
+ }
984
+ const file = join(dir, entry.name);
985
+ if ([".cmake", ".pri", ".pc", ".json"].includes(extname(file))) {
986
+ files.push(file);
987
+ }
988
+ }
989
+ }
990
+ return files;
991
+ }
992
+ function moduleToCmakeSuffix(module) {
993
+ const map = {
994
+ widgets: "Widgets",
995
+ qml: "Qml",
996
+ quick: "Quick",
997
+ quickcontrols2: "QuickControls2",
998
+ charts: "Charts",
999
+ multimedia: "Multimedia",
1000
+ bluetooth: "Bluetooth",
1001
+ positioning: "Positioning"
1002
+ };
1003
+ return map[module] ?? module;
1004
+ }
1005
+ function taishanPiPortablePathReplacements(releaseRoot) {
1006
+ const normalizedRoot = resolve(releaseRoot);
1007
+ // Legacy development paths are recognized only so older staged packages can
1008
+ // be rewritten into a portable installed layout during validation/repair.
1009
+ return [
1010
+ ["@EMBEDLABS_RELEASE_ROOT@", normalizedRoot],
1011
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/scripts/qt6-rk3566-llvm-toolchain.cmake", join(normalizedRoot, "toolchain", "qt6-rk3566-llvm-toolchain.cmake")],
1012
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/.llvm-cross", join(normalizedRoot, "toolchain", "llvm-cross")],
1013
+ ["/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal", normalizedRoot],
1014
+ ["/Volumes/LLVM-TSPI/sdk-tools/buildroot/output/rockchip_rk3566/host", join(normalizedRoot, "toolchain", "host")],
1015
+ ["/Volumes/LLVM-TSPI/qt6-host-macos-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-macos-6.8.3")],
1016
+ ["/home/ubuntu/embedlabs-qt/qt6-host-linux-x86_64-6.8.3", join(normalizedRoot, "qt-host", "qt6-host-linux-x86_64-6.8.3")],
1017
+ ["/Volumes/LLVM-TSPI/qt6-rk3566-llvm-6.8.3", join(normalizedRoot, "qt-target", "qt6-rk3566-llvm-6.8.3")],
1018
+ ["/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")],
1019
+ ["/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")],
1020
+ ["/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")],
1021
+ ["/Volumes/LLVM-TSPI/qt-everywhere-src-6.8.3", join(normalizedRoot, "meta", "source", "qt-everywhere-src-6.8.3")],
1022
+ ["/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi", join(normalizedRoot, "meta", "source", "llvm-build-tspi")]
1023
+ ];
1024
+ }
1025
+ function taishanPiForbiddenPortablePathFragments() {
1026
+ return [
1027
+ "/Volumes/LLVM-TSPI",
1028
+ "/Users/kvell/kk-project/DBT-Agent-Project"
1029
+ ];
1030
+ }
1031
+ function rewritePortablePathContent(content, replacements) {
1032
+ let rewritten = content;
1033
+ for (const [from, to] of replacements) {
1034
+ rewritten = rewritten.split(from).join(to);
1035
+ }
1036
+ return rewritten;
1037
+ }
1038
+ function localToolchainValidationSummary(input) {
1039
+ const boardName = input.boardId === DEFAULT_BOARD_ID
1040
+ ? "泰山派 1M-RK3566"
1041
+ : input.boardId;
1042
+ if (input.ok) {
1043
+ if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
1044
+ return `${boardName} Qt 本地开发环境完整,可以继续 QtQuick 原型、一键交叉编译和真机部署。`;
1045
+ }
1046
+ return `${boardName} 本地开发环境完整,可以继续使用 ${input.mode} 模式。`;
1047
+ }
1048
+ const groups = input.missingGroups.length > 0 ? input.missingGroups.join("、") : "部分文件";
1049
+ if (input.boardId === DEFAULT_BOARD_ID && input.mode === "qt") {
1050
+ return `${boardName} Qt 本地开发环境不完整,缺少 ${groups}。这会影响一键交叉编译、实时预览或真机部署;请先执行 ${input.repairCommand} 补装缺失组件。`;
1051
+ }
1052
+ return `${boardName} 本地开发环境不完整,缺少 ${groups};请先执行 ${input.repairCommand} 补装缺失组件。`;
1053
+ }
1054
+ function normalizeLocalToolchainInstallMode(mode) {
1055
+ const normalized = mode?.trim();
1056
+ if (!normalized) {
1057
+ return "qt";
1058
+ }
1059
+ if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
1060
+ return normalized;
1061
+ }
1062
+ throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
1063
+ }
1064
+ function requiredLocalToolchainChecks(mode, boardId) {
1065
+ if (isRp2350MonitorBoardId(boardId)) {
1066
+ return requiredRp2350MonitorChecks(mode);
1067
+ }
1068
+ const qtHostDir = taishanPiQtHostDirName();
1069
+ const base = [
1070
+ ["release root", "."],
1071
+ ["boot resource image", "boot-workspace/out/resource.img"],
1072
+ ["boot image", "boot-workspace/out/boot.img"],
1073
+ ["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
1074
+ ["package metadata", "meta"]
1075
+ ];
1076
+ const rockchipTools = [
1077
+ ["Rockchip mkimage", "tools/mac/mkimage"],
1078
+ ["Rockchip dumpimage", "tools/mac/dumpimage"],
1079
+ ["Rockchip resource_tool", "tools/mac/resource_tool"],
1080
+ ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"]
1081
+ ];
1082
+ const compile = [
1083
+ ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
1084
+ ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
1085
+ ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
1086
+ ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
1087
+ ["Qt toolchain file", "toolchain/qt6-rk3566-llvm-toolchain.cmake"],
1088
+ ["host GCC libraries", "toolchain/host/lib/gcc"],
1089
+ ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
1090
+ ["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
1091
+ ];
1092
+ const qt = [
1093
+ ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
1094
+ ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
1095
+ ["Qt host tools", `qt-host/${qtHostDir}`],
1096
+ ["Qt host qtpaths", `qt-host/${qtHostDir}/bin/qtpaths`],
1097
+ ["Qt host moc", `qt-host/${qtHostDir}/libexec/moc`],
1098
+ ["Qt target SerialPort library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialPort.so.6"],
1099
+ ["Qt target SerialPort CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialPort/Qt6SerialPortConfig.cmake"],
1100
+ ["Qt target SerialBus library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6SerialBus.so.6"],
1101
+ ["Qt target SerialBus CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6SerialBus/Qt6SerialBusConfig.cmake"],
1102
+ ["Qt target Widgets library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Widgets.so.6"],
1103
+ ["Qt target Widgets CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Widgets/Qt6WidgetsConfig.cmake"],
1104
+ ["Qt target QML library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Qml.so.6"],
1105
+ ["Qt target QML CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Qml/Qt6QmlConfig.cmake"],
1106
+ ["Qt target Quick library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Quick.so.6"],
1107
+ ["Qt target Quick CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Quick/Qt6QuickConfig.cmake"],
1108
+ ["Qt target QuickControls2 library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6QuickControls2.so.6"],
1109
+ ["Qt target QuickControls2 CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6QuickControls2/Qt6QuickControls2Config.cmake"],
1110
+ ["Qt target Charts library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Charts.so.6"],
1111
+ ["Qt target Charts CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Charts/Qt6ChartsConfig.cmake"],
1112
+ ["Qt target Multimedia library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Multimedia.so.6"],
1113
+ ["Qt target Multimedia CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Multimedia/Qt6MultimediaConfig.cmake"],
1114
+ ["Qt target Bluetooth library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Bluetooth.so.6"],
1115
+ ["Qt target Bluetooth CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Bluetooth/Qt6BluetoothConfig.cmake"],
1116
+ ["Qt target Positioning library", "qt-target/qt6-rk3566-llvm-6.8.3/lib/libQt6Positioning.so.6"],
1117
+ ["Qt target Positioning CMake package", "qt-target/qt6-rk3566-llvm-6.8.3/lib/cmake/Qt6Positioning/Qt6PositioningConfig.cmake"],
1118
+ ["Qt board runtime SerialPort library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialPort.so.6"],
1119
+ ["Qt board runtime SerialBus library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6SerialBus.so.6"],
1120
+ ["Qt board runtime Widgets library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Widgets.so.6"],
1121
+ ["Qt board runtime QML library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Qml.so.6"],
1122
+ ["Qt board runtime Quick library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Quick.so.6"],
1123
+ ["Qt board runtime QuickControls2 library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6QuickControls2.so.6"],
1124
+ ["Qt board runtime Charts library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Charts.so.6"],
1125
+ ["Qt board runtime Multimedia library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Multimedia.so.6"],
1126
+ ["Qt board runtime Bluetooth library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Bluetooth.so.6"],
1127
+ ["Qt board runtime Positioning library", "userdata/rootfs/qt6-rk3566-llvm/lib/libQt6Positioning.so.6"],
1128
+ ["Qt board runtime QML imports", "userdata/rootfs/qt6-rk3566-llvm/qml"],
1129
+ ["Qt smoke example", "examples/qt-smoke/CMakeLists.txt"],
1130
+ ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
1131
+ ];
1132
+ const images = [
1133
+ ["base boot image", "images/current/boot.img"],
1134
+ ["base rootfs image", "images/current/rootfs.img"],
1135
+ ["base image parameter", "images/current/parameter.txt"]
1136
+ ];
1137
+ const full = [
1138
+ ["rootfs overlay", "userdata/rootfs"]
1139
+ ];
1140
+ if (mode === "minimal") {
1141
+ return [...base, ...rockchipTools];
1142
+ }
1143
+ if (mode === "compile") {
1144
+ return [...base, ...compile];
1145
+ }
1146
+ if (mode === "qt") {
1147
+ return [...base, ...compile, ...qt];
1148
+ }
1149
+ if (mode === "images") {
1150
+ return [...base, ...rockchipTools, ...images];
1151
+ }
1152
+ return [...base, ...rockchipTools, ...compile, ...qt, ...images, ...full];
1153
+ }
1154
+ function requiredRp2350MonitorChecks(mode) {
1155
+ const armToolchainDir = rp2350ArmToolchainDirName();
1156
+ const executableSuffix = platform() === "win32" ? ".exe" : "";
1157
+ const monitorLauncher = platform() === "win32"
1158
+ ? "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer.cmd"
1159
+ : "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer";
1160
+ const base = [
1161
+ ["release root", "."],
1162
+ ];
1163
+ const runtime = [
1164
+ ["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
1165
+ ["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
1166
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
1167
+ ["RP2350 picotool", `toolkit-runtime/rp2350-monitor/tools/picotool${executableSuffix}`],
1168
+ ["RP2350 Monitor logic analyzer", monitorLauncher],
1169
+ ["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
1170
+ ["package metadata", "meta"]
1171
+ ];
1172
+ if (platform() === "win32") {
1173
+ runtime.splice(4, 0, ["RP2350 Monitor bundled Python", "toolkit-runtime/rp2350-monitor/tools/python/python.exe"]);
1174
+ runtime.splice(5, 0, ["RP2350 Monitor pyserial", "toolkit-runtime/rp2350-monitor/tools/python/Lib/site-packages/serial/__init__.py"]);
1175
+ }
1176
+ const firmware = [
1177
+ ["RP2350 Monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
1178
+ ["RP2350 Monitor firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
1179
+ ];
1180
+ const initialFirmware = [
1181
+ ["Pico 2 W initialization UF2", "rp2350-initial-firmware/pico2w/initial.uf2"],
1182
+ ["ColorEasyPICO2 initialization UF2", "rp2350-initial-firmware/coloreasypico2/initial.uf2"]
1183
+ ];
1184
+ const compile = [
1185
+ ["Pico SDK", "rp2350-sdk/pico-sdk/pico_sdk_init.cmake"],
1186
+ ["Pico SDK CMakeLists", "rp2350-sdk/pico-sdk/CMakeLists.txt"],
1187
+ ["ARM bare-metal C compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-gcc${executableSuffix}`],
1188
+ ["ARM bare-metal C++ compiler", `rp2350-sdk/toolchains/${armToolchainDir}/bin/arm-none-eabi-g++${executableSuffix}`],
1189
+ ["picotool", `rp2350-sdk/picotool/install/picotool/picotool${executableSuffix}`]
1190
+ ];
1191
+ if (mode === "compile") {
1192
+ return [...base, ...compile, ...initialFirmware];
1193
+ }
1194
+ if (mode === "firmware") {
1195
+ return [...base, ...runtime, ...firmware, ...initialFirmware];
1196
+ }
1197
+ if (mode === "full") {
1198
+ return [...base, ...runtime, ...firmware, ...initialFirmware, ...compile];
1199
+ }
1200
+ return [...base, ...runtime];
1201
+ }
1202
+ function rp2350ArmToolchainDirName() {
1203
+ if (platform() === "win32") {
1204
+ return "arm-gnu-toolchain-15.2.rel1-mingw-w64-x86_64-arm-none-eabi";
1205
+ }
1206
+ if (platform() === "linux" && arch() === "x64") {
1207
+ return "arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi";
1208
+ }
1209
+ if (platform() === "linux" && arch() === "arm64") {
1210
+ return "arm-gnu-toolchain-15.2.rel1-aarch64-arm-none-eabi";
1211
+ }
1212
+ return "arm-gnu-toolchain-15.2.rel1-darwin-arm64-arm-none-eabi";
1213
+ }
263
1214
  export async function compileTaishanPiSingleFile(options) {
264
1215
  assertAuthenticated(options.auth);
1216
+ if (platform() === "win32" && normalizeBoardId(options.boardId) === DEFAULT_BOARD_ID) {
1217
+ return await compileTaishanPiSingleFileWindowsWsl(options);
1218
+ }
265
1219
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
266
1220
  const sourcePath = resolve(options.sourcePath);
267
1221
  const outputPath = resolve(options.outputPath);
@@ -287,15 +1241,58 @@ export async function compileTaishanPiSingleFile(options) {
287
1241
  commands: [buildResult]
288
1242
  });
289
1243
  }
1244
+ async function compileTaishanPiSingleFileWindowsWsl(options) {
1245
+ const route = taishanPiWindowsExecutionRoute(hostId(), await windowsWslStatus());
1246
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
1247
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
1248
+ }
1249
+ const sourcePath = resolve(options.sourcePath);
1250
+ const outputPath = resolve(options.outputPath);
1251
+ await access(sourcePath, constants.R_OK);
1252
+ await mkdir(dirname(outputPath), { recursive: true });
1253
+ const releaseRoot = options.releaseRoot?.trim()
1254
+ ? normalizeWindowsWslReleaseRoot(options.releaseRoot)
1255
+ : await resolveWindowsWslTaishanPiReleaseRoot();
1256
+ const sourceWslPath = windowsPathToWslPath(sourcePath, "source");
1257
+ const outputWslPath = windowsPathToWslPath(outputPath, "output");
1258
+ const compiler = wslCompilerForSource(releaseRoot, sourcePath);
1259
+ const sysroot = `${trimTrailingSlash(releaseRoot)}/toolchain/host/aarch64-buildroot-linux-gnu/sysroot`;
1260
+ const commandScript = [
1261
+ "set -euo pipefail",
1262
+ `test -x ${sh(compiler)}`,
1263
+ `test -r ${sh(sysroot)}`,
1264
+ `mkdir -p ${sh(posixDirname(outputWslPath))}`,
1265
+ `${sh(compiler)} --sysroot=${sh(sysroot)} -O2 ${sh(sourceWslPath)} -o ${sh(outputWslPath)}`
1266
+ ].join("\n");
1267
+ const buildResult = await runWindowsWslBash(commandScript);
1268
+ if (buildResult.exit_code !== 0) {
1269
+ throw new Error(`Windows WSL TaishanPi compile failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
1270
+ }
1271
+ return await localCompileResult({
1272
+ boardId: options.boardId,
1273
+ operation: "local.compile.single_file",
1274
+ releaseRoot,
1275
+ accountId: options.accountId,
1276
+ auth: options.auth,
1277
+ sourcePath,
1278
+ artifactPath: outputPath,
1279
+ commands: [buildResult]
1280
+ });
1281
+ }
290
1282
  export async function buildTaishanPiQtSmoke(options) {
291
1283
  assertAuthenticated(options.auth);
1284
+ if (platform() === "win32") {
1285
+ return await buildTaishanPiQtSmokeWindowsWsl(options);
1286
+ }
292
1287
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
293
- const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
1288
+ const sourceDir = resolve(options.sourceDir ?? join(releaseRoot, "examples", "qt-smoke"));
294
1289
  const buildDir = resolve(options.buildDir);
295
- const targetName = options.targetName ?? "qt_llvm_smoke";
1290
+ const targetName = options.targetName ?? "qt_smoke";
296
1291
  const qtCmake = join(releaseRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", "bin", "qt-cmake");
1292
+ const qtHostPath = join(releaseRoot, "qt-host", taishanPiQtHostDirName());
297
1293
  await access(join(sourceDir, "CMakeLists.txt"), constants.R_OK);
298
1294
  await access(qtCmake, constants.X_OK);
1295
+ await access(qtHostPath, constants.R_OK);
299
1296
  await mkdir(buildDir, { recursive: true });
300
1297
  const configure = await runCommand([
301
1298
  qtCmake,
@@ -305,7 +1302,8 @@ export async function buildTaishanPiQtSmoke(options) {
305
1302
  buildDir,
306
1303
  "-G",
307
1304
  "Ninja",
308
- "-DCMAKE_BUILD_TYPE=Release"
1305
+ "-DCMAKE_BUILD_TYPE=Release",
1306
+ `-DQT_HOST_PATH=${qtHostPath}`
309
1307
  ], sourceDir);
310
1308
  if (configure.exit_code !== 0) {
311
1309
  throw new Error(`Qt smoke configure failed with exit code ${configure.exit_code}: ${configure.stderr_tail.join("\n")}`);
@@ -327,17 +1325,67 @@ export async function buildTaishanPiQtSmoke(options) {
327
1325
  commands: [configure, build]
328
1326
  });
329
1327
  }
1328
+ async function buildTaishanPiQtSmokeWindowsWsl(options) {
1329
+ const route = taishanPiWindowsExecutionRoute(hostId(), await windowsWslStatus());
1330
+ if (route.actual_host !== "linux-x86_64" || route.status === "wsl_missing" || route.status === "wsl_not_configured" || route.status === "architecture_mismatch") {
1331
+ throw new Error(`${route.reason} Run: embedlabs local wsl status`);
1332
+ }
1333
+ const releaseRoot = options.releaseRoot?.trim()
1334
+ ? normalizeWindowsWslReleaseRoot(options.releaseRoot)
1335
+ : await resolveWindowsWslTaishanPiReleaseRoot("qt");
1336
+ const targetName = options.targetName ?? "qt_smoke";
1337
+ const buildDir = resolve(options.buildDir);
1338
+ const buildWslDir = windowsPathToWslPath(buildDir, "build-dir");
1339
+ const sourceDir = options.sourceDir?.trim()
1340
+ ? resolve(options.sourceDir)
1341
+ : "";
1342
+ if (sourceDir) {
1343
+ await access(join(sourceDir, "CMakeLists.txt"), constants.R_OK);
1344
+ }
1345
+ await mkdir(buildDir, { recursive: true });
1346
+ const sourceWslDir = sourceDir
1347
+ ? windowsPathToWslPath(sourceDir, "source")
1348
+ : `${trimTrailingSlash(releaseRoot)}/examples/qt-smoke`;
1349
+ const qtCmake = `${trimTrailingSlash(releaseRoot)}/qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake`;
1350
+ const qtHostPath = `${trimTrailingSlash(releaseRoot)}/qt-host/qt6-host-linux-x86_64-6.8.3`;
1351
+ const artifactPath = join(buildDir, targetName);
1352
+ const commandScript = [
1353
+ "set -euo pipefail",
1354
+ `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; }`,
1355
+ `test -d ${sh(qtHostPath)} || { echo "Missing Qt host path at ${qtHostPath}. Install the TaishanPi Qt package with: embedlabs local toolchain install --board taishanpi-1m-rk3566 --channel windows-test --mode qt" >&2; exit 2; }`,
1356
+ `test -r ${sh(`${sourceWslDir}/CMakeLists.txt`)} || { echo "Missing CMakeLists.txt in ${sourceWslDir}" >&2; exit 2; }`,
1357
+ "command -v cmake >/dev/null 2>&1 || { echo \"Missing WSL tool: cmake\" >&2; exit 2; }",
1358
+ "command -v ninja >/dev/null 2>&1 || { echo \"Missing WSL tool: ninja\" >&2; exit 2; }",
1359
+ `mkdir -p ${sh(buildWslDir)}`,
1360
+ `${sh(qtCmake)} -S ${sh(sourceWslDir)} -B ${sh(buildWslDir)} -G Ninja -DCMAKE_BUILD_TYPE=Release -DQT_HOST_PATH=${sh(qtHostPath)}`,
1361
+ `cmake --build ${sh(buildWslDir)} --parallel`
1362
+ ].join("\n");
1363
+ const buildResult = await runWindowsWslBash(commandScript);
1364
+ if (buildResult.exit_code !== 0) {
1365
+ throw new Error(`Windows WSL TaishanPi Qt smoke build failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
1366
+ }
1367
+ return await localCompileResult({
1368
+ boardId: DEFAULT_BOARD_ID,
1369
+ operation: "local.build.qt_smoke",
1370
+ releaseRoot,
1371
+ accountId: options.accountId,
1372
+ auth: options.auth,
1373
+ sourcePath: sourceDir || sourceWslDir,
1374
+ buildDir,
1375
+ artifactPath,
1376
+ commands: [buildResult]
1377
+ });
1378
+ }
330
1379
  async function loadLocalToolchainMetadata(metadataRoot, channelName) {
331
1380
  const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
332
- const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
333
- if (!candidateRoot) {
1381
+ if (!explicitRoot) {
334
1382
  return {
335
1383
  channel: BUILT_IN_CHANNEL,
336
1384
  manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
337
1385
  metadataRoot: undefined
338
1386
  };
339
1387
  }
340
- const root = resolve(candidateRoot);
1388
+ const root = resolve(explicitRoot);
341
1389
  const channelPath = join(root, "channels", channelName, "index.json");
342
1390
  const channel = JSON.parse(await readFile(channelPath, "utf8"));
343
1391
  if (channel.schema !== "embedlabs.channel.v1") {
@@ -360,17 +1408,26 @@ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
360
1408
  return { channel, manifests, metadataRoot: root };
361
1409
  }
362
1410
  async function resolveLocalToolchainDownloadPlan(input) {
1411
+ const requestedBoardId = normalizeBoardId(input.boardId);
1412
+ const compatibleBoardIds = compatibleDownloadBoardIds(requestedBoardId);
363
1413
  const channelUrl = downloadChannelUrl(input.channel);
364
1414
  const channel = await fetchJson(channelUrl);
365
1415
  if (channel.schema !== "embedlabs.download-channel.v1") {
366
1416
  throw new Error(`Unexpected download channel schema ${channel.schema}.`);
367
1417
  }
368
- const entry = (channel.packages ?? []).find((item) => {
369
- return item.board_id === input.boardId
370
- && item.host === input.host
371
- && item.toolchain === input.toolchain
372
- && (item.kind === undefined || item.kind === "toolchain-archive");
373
- });
1418
+ const entries = channel.packages ?? [];
1419
+ let entry;
1420
+ for (const boardId of compatibleBoardIds) {
1421
+ entry = entries.find((item) => {
1422
+ return normalizeBoardId(item.board_id ?? "") === boardId
1423
+ && item.host === input.host
1424
+ && item.toolchain === input.toolchain
1425
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
1426
+ });
1427
+ if (entry) {
1428
+ break;
1429
+ }
1430
+ }
374
1431
  if (!entry?.manifest) {
375
1432
  return undefined;
376
1433
  }
@@ -379,23 +1436,28 @@ async function resolveLocalToolchainDownloadPlan(input) {
379
1436
  if (manifest.id !== entry.id || manifest.version !== entry.version) {
380
1437
  throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
381
1438
  }
382
- if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
383
- throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
1439
+ const manifestBoardId = normalizeBoardId(manifest.board_id ?? "");
1440
+ if (!compatibleBoardIds.includes(manifestBoardId) || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
1441
+ throw new Error(`Download manifest does not match requested ${requestedBoardId}/${input.host}/${input.toolchain}.`);
384
1442
  }
385
- if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes)) {
386
- throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive file, size, or SHA256.`);
1443
+ if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
1444
+ && (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
1445
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
387
1446
  }
388
- const mirrors = orderDownloadMirrors((manifest.mirrors ?? [])
389
- .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
390
- .map((mirror) => ({
391
- kind: mirror.kind || "unknown",
392
- enabled: mirror.enabled !== false,
393
- url: mirror.url,
394
- sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
395
- size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
396
- })), manifest.download_policy?.preferred_order);
1447
+ const mirrors = manifest.archive
1448
+ ? orderDownloadMirrors((manifest.mirrors ?? [])
1449
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
1450
+ .map((mirror) => ({
1451
+ kind: mirror.kind || "unknown",
1452
+ enabled: mirror.enabled !== false,
1453
+ url: mirror.url,
1454
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
1455
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
1456
+ })), manifest.download_policy?.preferred_order)
1457
+ : [];
1458
+ const components = downloadComponentsForBoard(requestedBoardId, (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl)));
397
1459
  const first = mirrors[0];
398
- if (!first) {
1460
+ if (!first && components.length === 0) {
399
1461
  return undefined;
400
1462
  }
401
1463
  return {
@@ -403,17 +1465,55 @@ async function resolveLocalToolchainDownloadPlan(input) {
403
1465
  manifest_url: manifestUrl,
404
1466
  package_id: manifest.id,
405
1467
  version: manifest.version,
406
- board_id: input.boardId,
1468
+ board_id: requestedBoardId,
407
1469
  host: input.host,
408
1470
  toolchain: input.toolchain,
409
- source_url: first.url,
410
- mirror_kind: first.kind,
411
- archive: {
1471
+ source_url: first?.url,
1472
+ mirror_kind: first?.kind,
1473
+ archive: manifest.archive ? {
412
1474
  file: manifest.archive.file,
413
1475
  size_bytes: manifest.archive.size_bytes,
414
1476
  sha256: manifest.archive.sha256,
415
1477
  content_type: manifest.archive.content_type
1478
+ } : undefined,
1479
+ mirrors,
1480
+ components: components.length > 0 ? components : undefined,
1481
+ default_mode: manifest.download_policy?.default_mode
1482
+ };
1483
+ }
1484
+ function normalizeDownloadComponent(component, manifest, manifestUrl) {
1485
+ if (!component.id || !component.version) {
1486
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
1487
+ }
1488
+ if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
1489
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
1490
+ }
1491
+ const mirrors = orderDownloadMirrors((component.mirrors ?? [])
1492
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
1493
+ .map((mirror) => ({
1494
+ kind: mirror.kind || "unknown",
1495
+ enabled: mirror.enabled !== false,
1496
+ url: new URL(mirror.url, manifestUrl).toString(),
1497
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
1498
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
1499
+ })), manifest.download_policy?.preferred_order);
1500
+ const first = mirrors[0];
1501
+ if (!first) {
1502
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
1503
+ }
1504
+ return {
1505
+ id: component.id,
1506
+ version: component.version,
1507
+ role: component.role,
1508
+ install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
1509
+ archive: {
1510
+ file: component.archive.file,
1511
+ size_bytes: component.archive.size_bytes,
1512
+ sha256: component.archive.sha256,
1513
+ content_type: component.archive.content_type
416
1514
  },
1515
+ source_url: first.url,
1516
+ mirror_kind: first.kind,
417
1517
  mirrors
418
1518
  };
419
1519
  }
@@ -428,11 +1528,53 @@ function mirrorRank(kind, preferredOrder) {
428
1528
  return index >= 0 ? index : preferredOrder.length + 1;
429
1529
  }
430
1530
  async function fetchJson(url) {
431
- const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
432
- if (!response.ok) {
433
- throw new Error(`HTTP ${response.status} while loading ${url}`);
1531
+ try {
1532
+ const response = await fetch(url, {
1533
+ headers: {
1534
+ "accept-encoding": "identity"
1535
+ },
1536
+ signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS)
1537
+ });
1538
+ if (!response.ok) {
1539
+ throw new Error(`HTTP ${response.status} while loading ${url}`);
1540
+ }
1541
+ return await response.json();
1542
+ }
1543
+ catch (fetchError) {
1544
+ try {
1545
+ return JSON.parse(await fetchTextWithCurl(url));
1546
+ }
1547
+ catch (curlError) {
1548
+ throw new Error(`Could not load JSON ${url}: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}; curl fallback: ${curlError instanceof Error ? curlError.message : String(curlError)}`);
1549
+ }
434
1550
  }
435
- return await response.json();
1551
+ }
1552
+ async function fetchTextWithCurl(url) {
1553
+ return await new Promise((resolvePromise, reject) => {
1554
+ const child = spawn("curl", [
1555
+ "--location",
1556
+ "--fail",
1557
+ "--silent",
1558
+ "--show-error",
1559
+ "--max-time", "30",
1560
+ url
1561
+ ], {
1562
+ stdio: ["ignore", "pipe", "pipe"]
1563
+ });
1564
+ const stdout = [];
1565
+ const stderr = [];
1566
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
1567
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
1568
+ child.on("error", reject);
1569
+ child.on("exit", (code) => {
1570
+ if (code === 0) {
1571
+ resolvePromise(Buffer.concat(stdout).toString("utf8"));
1572
+ }
1573
+ else {
1574
+ reject(new Error(`curl exited with code ${code ?? "unknown"}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
1575
+ }
1576
+ });
1577
+ });
436
1578
  }
437
1579
  function downloadChannelUrl(channelName) {
438
1580
  const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
@@ -450,35 +1592,183 @@ function downloadBaseUrl() {
450
1592
  function trimTrailingSlash(value) {
451
1593
  return value.replace(/\/+$/, "");
452
1594
  }
453
- function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
454
- if (seen.has(packageId)) {
455
- return [];
1595
+ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
1596
+ if (seen.has(packageId)) {
1597
+ return [];
1598
+ }
1599
+ seen.add(packageId);
1600
+ const manifest = manifests.get(packageId);
1601
+ if (!manifest) {
1602
+ throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
1603
+ }
1604
+ const refs = [];
1605
+ for (const requirement of manifest.requires ?? []) {
1606
+ refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
1607
+ }
1608
+ const channelEntry = channel.packages.find((item) => item.id === packageId);
1609
+ refs.push({
1610
+ id: packageId,
1611
+ version: manifest.version,
1612
+ manifest: channelEntry?.manifest
1613
+ });
1614
+ return refs;
1615
+ }
1616
+ function boardPackageIdFor(boardId) {
1617
+ const normalized = normalizeBoardId(boardId);
1618
+ if (normalized === DEFAULT_BOARD_ID || normalized === "taishanpi" || normalized === "taishanpi-1m-rk3566") {
1619
+ return "embedlabs.board.taishanpi.1m-rk3566";
1620
+ }
1621
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID
1622
+ || normalized === "coloreasypico2"
1623
+ || normalized === "color-easy-pico2"
1624
+ || normalized === "color-easy-pico-2"
1625
+ || normalized === "color-easy-pico-2-rp2350-monitor"
1626
+ || normalized === "ce-pico2") {
1627
+ return "embedlabs.board.coloreasypico2.rp2350-monitor";
1628
+ }
1629
+ if (normalized === PICO2W_RP2350_BOARD_ID
1630
+ || normalized === "pico2w"
1631
+ || normalized === "pico-2-w"
1632
+ || normalized === "pico2"
1633
+ || normalized === "rp2350"
1634
+ || normalized === "rp2350-monitor") {
1635
+ return "embedlabs.board.pico2w.rp2350-monitor";
1636
+ }
1637
+ if (normalized.startsWith("embedlabs.board.")) {
1638
+ return boardId;
1639
+ }
1640
+ throw new Error(`Unsupported local toolchain board ${boardId}.`);
1641
+ }
1642
+ function packageIdForBoardFilter(boardId) {
1643
+ try {
1644
+ return boardPackageIdFor(boardId);
1645
+ }
1646
+ catch {
1647
+ return undefined;
1648
+ }
1649
+ }
1650
+ function boardIdForPackageManifest(manifest) {
1651
+ const explicit = manifest.board_id;
1652
+ if (explicit?.trim()) {
1653
+ return normalizeBoardId(explicit);
1654
+ }
1655
+ if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
1656
+ return DEFAULT_BOARD_ID;
1657
+ }
1658
+ if (manifest.id.startsWith("embedlabs.board.")) {
1659
+ return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
1660
+ }
1661
+ return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
1662
+ }
1663
+ function normalizeBoardId(boardId) {
1664
+ return boardId.trim().toLowerCase().replaceAll("_", "-");
1665
+ }
1666
+ function isRp2350MonitorBoardId(boardId) {
1667
+ const normalized = normalizeBoardId(boardId);
1668
+ return normalized === PICO2W_RP2350_BOARD_ID
1669
+ || normalized === COLOREASYPICO2_RP2350_BOARD_ID
1670
+ || normalized === "pico2w"
1671
+ || normalized === "pico-2-w"
1672
+ || normalized === "pico2"
1673
+ || normalized === "rp2350"
1674
+ || normalized === "rp2350-monitor"
1675
+ || normalized === "coloreasypico2"
1676
+ || normalized === "color-easy-pico2"
1677
+ || normalized === "color-easy-pico-2"
1678
+ || normalized === "ce-pico2";
1679
+ }
1680
+ function compatibleDownloadBoardIds(boardId) {
1681
+ const normalized = normalizeBoardId(boardId);
1682
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID) {
1683
+ return [COLOREASYPICO2_RP2350_BOARD_ID, PICO2W_RP2350_BOARD_ID];
1684
+ }
1685
+ return [normalized];
1686
+ }
1687
+ function downloadComponentsForBoard(boardId, components) {
1688
+ const normalized = normalizeBoardId(boardId);
1689
+ if (normalized === DEFAULT_BOARD_ID) {
1690
+ return components.filter((component) => !isRp2350MonitorComponent(component));
1691
+ }
1692
+ return components;
1693
+ }
1694
+ function isRp2350MonitorComponent(component) {
1695
+ const text = `${component.id} ${component.role ?? ""} ${component.archive.file}`.toLowerCase();
1696
+ return text.includes("rp2350-monitor") || text.includes("pico2w-rp2350-monitor");
1697
+ }
1698
+ function compareVersionLike(left, right) {
1699
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
1700
+ }
1701
+ function isRecord(value) {
1702
+ return !!value && typeof value === "object" && !Array.isArray(value);
1703
+ }
1704
+ function packageHostSupport(packages, manifests, host) {
1705
+ const unsupportedPackages = [];
1706
+ for (const item of packages) {
1707
+ const manifest = manifests.get(item.id);
1708
+ if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
1709
+ unsupportedPackages.push(`${item.id}@${manifest.version}`);
1710
+ }
1711
+ }
1712
+ return {
1713
+ supported: unsupportedPackages.length === 0,
1714
+ unsupportedPackages
1715
+ };
1716
+ }
1717
+ function environmentNotes(input) {
1718
+ const notes = [];
1719
+ if (normalizeBoardId(input.boardId) === COLOREASYPICO2_RP2350_BOARD_ID) {
1720
+ 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.");
456
1721
  }
457
- seen.add(packageId);
458
- const manifest = manifests.get(packageId);
459
- if (!manifest) {
460
- throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
1722
+ if (isRp2350MonitorBoardId(input.boardId)) {
1723
+ 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.");
461
1724
  }
462
- const refs = [];
463
- for (const requirement of manifest.requires ?? []) {
464
- refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
1725
+ if (isNativeWindowsTaishanPiHost(input.boardId, input.host)) {
1726
+ notes.push(taishanPiWindowsRequirementMessage(input.host));
1727
+ notes.push("Check this computer with: embedlabs local wsl status");
465
1728
  }
466
- const channelEntry = channel.packages.find((item) => item.id === packageId);
467
- refs.push({
468
- id: packageId,
469
- version: manifest.version,
470
- manifest: channelEntry?.manifest
471
- });
472
- return refs;
1729
+ if (input.status === "available") {
1730
+ notes.push("Environment is available but not installed on this computer.");
1731
+ }
1732
+ if (input.status === "update_available") {
1733
+ notes.push("A newer package is available; run the update command to refresh only the selected environment.");
1734
+ }
1735
+ if (input.status === "unsupported_host") {
1736
+ if (input.unsupportedPackages.length > 0) {
1737
+ notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
1738
+ }
1739
+ }
1740
+ if (input.downloadError) {
1741
+ notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
1742
+ }
1743
+ return notes;
473
1744
  }
474
- function boardPackageIdFor(boardId) {
475
- if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
476
- return "embedlabs.board.taishanpi.1m-rk3566";
1745
+ function localToolchainInstallModesForDownload(download) {
1746
+ if (!download?.components?.length) {
1747
+ return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
477
1748
  }
478
- if (boardId.startsWith("embedlabs.board.")) {
479
- return boardId;
1749
+ const modes = new Set();
1750
+ for (const component of download.components) {
1751
+ for (const mode of component.install_modes ?? []) {
1752
+ modes.add(mode);
1753
+ }
480
1754
  }
481
- throw new Error(`Unsupported local toolchain board ${boardId}.`);
1755
+ return modes.size > 0
1756
+ ? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
1757
+ : [...LOCAL_TOOLCHAIN_INSTALL_MODES];
1758
+ }
1759
+ function localToolchainChannelFlag(channelName) {
1760
+ return channelName === DEFAULT_CHANNEL ? "" : ` --channel ${channelName}`;
1761
+ }
1762
+ function localToolchainEnvironmentComponent(component) {
1763
+ return {
1764
+ id: component.id,
1765
+ version: component.version,
1766
+ role: component.role,
1767
+ install_modes: component.install_modes,
1768
+ file: component.archive.file,
1769
+ size_bytes: component.archive.size_bytes,
1770
+ source_url: component.source_url
1771
+ };
482
1772
  }
483
1773
  function hostId() {
484
1774
  if (platform() === "darwin" && arch() === "arm64") {
@@ -489,6 +1779,191 @@ function hostId() {
489
1779
  }
490
1780
  return `${platform()}-${arch()}`;
491
1781
  }
1782
+ function taishanPiQtHostDirName(host = localToolchainHostId()) {
1783
+ if (host === "linux-x86_64") {
1784
+ return "qt6-host-linux-x86_64-6.8.3";
1785
+ }
1786
+ return "qt6-host-macos-6.8.3";
1787
+ }
1788
+ const LOCAL_TOOLCHAIN_HOST_IDS = new Set([
1789
+ "darwin-arm64",
1790
+ "linux-x86_64",
1791
+ "linux-arm64",
1792
+ "win32-x64",
1793
+ "win32-arm64"
1794
+ ]);
1795
+ function localToolchainHostId() {
1796
+ const override = process.env.EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST?.trim();
1797
+ if (override) {
1798
+ if (!LOCAL_TOOLCHAIN_HOST_IDS.has(override)) {
1799
+ throw new Error(`Unsupported EMBEDLABS_TEST_LOCAL_TOOLCHAIN_HOST value: ${override}`);
1800
+ }
1801
+ return override;
1802
+ }
1803
+ return hostId();
1804
+ }
1805
+ function isNativeWindowsTaishanPiHost(boardId, host) {
1806
+ return normalizeBoardId(boardId) === DEFAULT_BOARD_ID && host.startsWith("win32-");
1807
+ }
1808
+ function taishanPiWindowsExecutionRoute(host, status, packageAvailable = false) {
1809
+ const actualHost = wslHostForWindowsHost(host);
1810
+ if (!host.startsWith("win32-")) {
1811
+ return {
1812
+ kind: "native",
1813
+ supported: false,
1814
+ required_host: "linux-x86_64",
1815
+ actual_host: host,
1816
+ windows_host: host,
1817
+ route: "wsl2",
1818
+ status: "wsl_not_applicable",
1819
+ status_command: "embedlabs local wsl status",
1820
+ install_command: "embedlabs local wsl status",
1821
+ reason: "WSL2 routing is only used from Windows hosts."
1822
+ };
1823
+ }
1824
+ if (!status?.wsl_available) {
1825
+ return {
1826
+ kind: "wsl2",
1827
+ supported: false,
1828
+ required_host: "linux-x86_64",
1829
+ actual_host: actualHost,
1830
+ windows_host: host,
1831
+ route: "wsl2",
1832
+ status: "wsl_missing",
1833
+ status_command: "embedlabs local wsl status",
1834
+ install_command: "embedlabs local wsl install --distribution Ubuntu",
1835
+ reason: "wsl.exe is not available on this Windows host."
1836
+ };
1837
+ }
1838
+ if (actualHost !== "linux-x86_64") {
1839
+ return {
1840
+ kind: "wsl2",
1841
+ supported: false,
1842
+ required_host: "linux-x86_64",
1843
+ actual_host: actualHost,
1844
+ windows_host: host,
1845
+ route: "wsl2",
1846
+ status: "architecture_mismatch",
1847
+ status_command: "embedlabs local wsl status",
1848
+ install_command: "embedlabs local wsl status",
1849
+ reason: `This Windows host maps WSL2 to ${actualHost}, but the current TaishanPi package requires linux-x86_64.`
1850
+ };
1851
+ }
1852
+ if (!status.usable) {
1853
+ return {
1854
+ kind: "wsl2",
1855
+ supported: false,
1856
+ required_host: "linux-x86_64",
1857
+ actual_host: actualHost,
1858
+ windows_host: host,
1859
+ route: "wsl2",
1860
+ status: "wsl_not_configured",
1861
+ status_command: "embedlabs local wsl status",
1862
+ install_command: "embedlabs local wsl install --distribution Ubuntu",
1863
+ reason: "No usable WSL2 distribution is configured."
1864
+ };
1865
+ }
1866
+ if (packageAvailable) {
1867
+ return {
1868
+ kind: "wsl2",
1869
+ supported: true,
1870
+ required_host: "linux-x86_64",
1871
+ actual_host: actualHost,
1872
+ windows_host: host,
1873
+ route: "wsl2",
1874
+ status: "ready",
1875
+ status_command: "embedlabs local wsl status",
1876
+ install_command: "embedlabs local toolchain install --board taishanpi-1m-rk3566 --channel windows-test --mode compile",
1877
+ reason: "Windows x64 can run the TaishanPi linux-x86_64 package through WSL2."
1878
+ };
1879
+ }
1880
+ return {
1881
+ kind: "wsl2",
1882
+ supported: false,
1883
+ required_host: "linux-x86_64",
1884
+ actual_host: actualHost,
1885
+ windows_host: host,
1886
+ route: "wsl2",
1887
+ status: "package_missing",
1888
+ status_command: "embedlabs local wsl status",
1889
+ install_command: "embedlabs local wsl status",
1890
+ reason: "The Windows x64 WSL2 route is structurally compatible, but no TaishanPi linux-x86_64 package is published in the current stable channel."
1891
+ };
1892
+ }
1893
+ function wslHostForWindowsHost(host) {
1894
+ if (host === "win32-x64") {
1895
+ return "linux-x86_64";
1896
+ }
1897
+ if (host === "win32-arm64") {
1898
+ return "linux-arm64";
1899
+ }
1900
+ return undefined;
1901
+ }
1902
+ function taishanPiWindowsRequirementMessage(host) {
1903
+ if (host === "win32-arm64") {
1904
+ 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.";
1905
+ }
1906
+ 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.";
1907
+ }
1908
+ function normalizeWindowsCommandText(value) {
1909
+ return value.replace(/\0/g, "").replace(/\r/g, "\n");
1910
+ }
1911
+ function parseWslDistributionList(value) {
1912
+ const distributions = [];
1913
+ const lines = value.split(/\n+/)
1914
+ .map((line) => line.trim())
1915
+ .filter(Boolean);
1916
+ for (const rawLine of lines) {
1917
+ if (/^(NAME|Windows Subsystem|Usage|Copyright|\-|用法|选项|命令)/i.test(rawLine)) {
1918
+ continue;
1919
+ }
1920
+ const isDefault = rawLine.startsWith("*");
1921
+ const line = rawLine.replace(/^\*\s*/, "").trim();
1922
+ const parts = line.split(/\s+/).filter(Boolean);
1923
+ if (parts.length === 0 || parts[0].includes(":")) {
1924
+ continue;
1925
+ }
1926
+ const maybeVersion = parts.at(-1);
1927
+ const version = maybeVersion && /^[12]$/.test(maybeVersion) ? maybeVersion : undefined;
1928
+ const state = version && parts.length >= 3 ? parts.at(-2) : parts.length >= 2 ? parts.at(-1) : undefined;
1929
+ distributions.push({
1930
+ name: parts[0],
1931
+ state,
1932
+ version,
1933
+ default: isDefault
1934
+ });
1935
+ }
1936
+ return distributions;
1937
+ }
1938
+ function parseWslOnlineDistributionList(value) {
1939
+ const distributions = [];
1940
+ const lines = value.split(/\n+/)
1941
+ .map((line) => line.trim())
1942
+ .filter(Boolean);
1943
+ let inTable = false;
1944
+ for (const rawLine of lines) {
1945
+ if (/^NAME\s+FRIENDLY NAME/i.test(rawLine)) {
1946
+ inTable = true;
1947
+ continue;
1948
+ }
1949
+ if (!inTable) {
1950
+ continue;
1951
+ }
1952
+ const isDefault = rawLine.startsWith("*");
1953
+ const line = rawLine.replace(/^\*\s*/, "").trim();
1954
+ const parts = line.split(/\s{2,}/).filter(Boolean);
1955
+ const name = parts[0]?.trim();
1956
+ if (!name || name.includes(":")) {
1957
+ continue;
1958
+ }
1959
+ distributions.push({
1960
+ name,
1961
+ friendly_name: parts.slice(1).join(" ").trim() || undefined,
1962
+ default: isDefault
1963
+ });
1964
+ }
1965
+ return distributions;
1966
+ }
492
1967
  function resolveInstallRoot(installRoot) {
493
1968
  return resolve(installRoot
494
1969
  || process.env.EMBEDLABS_HOME?.trim()
@@ -497,21 +1972,68 @@ function resolveInstallRoot(installRoot) {
497
1972
  function localToolchainRegistryPath(installRoot) {
498
1973
  return join(installRoot, "registry", "local-toolchains.json");
499
1974
  }
500
- async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
1975
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
501
1976
  const registryPath = localToolchainRegistryPath(installRoot);
502
1977
  await mkdir(dirname(registryPath), { recursive: true });
503
- await writeFile(registryPath, `${JSON.stringify({
1978
+ let existing = {};
1979
+ try {
1980
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
1981
+ }
1982
+ catch {
1983
+ existing = {};
1984
+ }
1985
+ const entry = {
504
1986
  installed: true,
505
1987
  board_id: latest.board_id,
506
1988
  version: latest.version,
507
1989
  channel: latest.channel,
508
1990
  host: latest.host,
1991
+ mode,
509
1992
  release_root: releaseRoot,
510
1993
  packages: latest.packages,
1994
+ source,
1995
+ installed_components: source?.components,
1996
+ updated_at: new Date().toISOString()
1997
+ };
1998
+ const environments = isRecord(existing.environments)
1999
+ ? { ...existing.environments }
2000
+ : {};
2001
+ environments[normalizeBoardId(latest.board_id)] = entry;
2002
+ const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
2003
+ && typeof existing.release_root === "string"
2004
+ && normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
2005
+ const topLevel = preserveTopLevel ? existing : entry;
2006
+ await writeFile(registryPath, `${JSON.stringify({
2007
+ ...topLevel,
2008
+ environments,
511
2009
  updated_at: new Date().toISOString()
512
2010
  }, null, 2)}\n`, "utf8");
513
2011
  }
514
- async function resolveLocalReleaseRoot(releaseRoot) {
2012
+ async function pruneOldLocalToolchainVersions(installRoot, boardId, keepReleaseRoot) {
2013
+ const boardRoot = resolve(installRoot, "toolchains", normalizeBoardId(boardId));
2014
+ const keepRoot = resolve(keepReleaseRoot);
2015
+ let entries;
2016
+ try {
2017
+ entries = await readdir(boardRoot, { withFileTypes: true });
2018
+ }
2019
+ catch {
2020
+ return [];
2021
+ }
2022
+ const removed = [];
2023
+ for (const entry of entries) {
2024
+ if (!entry.isDirectory()) {
2025
+ continue;
2026
+ }
2027
+ const candidate = resolve(boardRoot, entry.name);
2028
+ if (candidate === keepRoot) {
2029
+ continue;
2030
+ }
2031
+ await rm(candidate, { recursive: true, force: true });
2032
+ removed.push(candidate);
2033
+ }
2034
+ return removed.sort((left, right) => compareVersionLike(basename(left), basename(right)));
2035
+ }
2036
+ async function resolveLocalReleaseRoot(releaseRoot, boardId = DEFAULT_BOARD_ID) {
515
2037
  if (releaseRoot?.trim()) {
516
2038
  return resolve(releaseRoot);
517
2039
  }
@@ -519,7 +2041,7 @@ async function resolveLocalReleaseRoot(releaseRoot) {
519
2041
  if (envRoot) {
520
2042
  return resolve(envRoot);
521
2043
  }
522
- const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
2044
+ const current = await currentLocalToolchain(undefined, boardId);
523
2045
  if (current.release_root) {
524
2046
  return resolve(current.release_root);
525
2047
  }
@@ -545,12 +2067,18 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
545
2067
  source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
546
2068
  };
547
2069
  }
2070
+ if (latest.download?.components?.length) {
2071
+ return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
2072
+ }
548
2073
  if (latest.download) {
549
2074
  const failures = [];
550
2075
  for (const mirror of latest.download.mirrors) {
551
2076
  if (!mirror.enabled) {
552
2077
  continue;
553
2078
  }
2079
+ if (!latest.download.archive) {
2080
+ continue;
2081
+ }
554
2082
  const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
555
2083
  try {
556
2084
  const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
@@ -591,6 +2119,127 @@ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir
591
2119
  }
592
2120
  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.");
593
2121
  }
2122
+ async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
2123
+ const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
2124
+ const components = selectedDownloadComponents(download.components ?? [], mode);
2125
+ if (components.length === 0) {
2126
+ throw new Error(`No local toolchain components selected for mode ${mode}.`);
2127
+ }
2128
+ const extractRoot = join(tempDir, "extract-components");
2129
+ await mkdir(extractRoot, { recursive: true });
2130
+ const installedComponents = [];
2131
+ const failures = [];
2132
+ for (const component of components) {
2133
+ let installed = false;
2134
+ for (const mirror of component.mirrors) {
2135
+ if (!mirror.enabled) {
2136
+ continue;
2137
+ }
2138
+ try {
2139
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
2140
+ sha256: mirror.sha256 ?? component.archive.sha256,
2141
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes
2142
+ });
2143
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
2144
+ if (extracted.exit_code !== 0) {
2145
+ throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
2146
+ }
2147
+ installedComponents.push({
2148
+ id: component.id,
2149
+ version: component.version,
2150
+ role: component.role,
2151
+ archive_file: component.archive.file,
2152
+ mirror_kind: mirror.kind,
2153
+ downloaded_path: downloadedPath,
2154
+ size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
2155
+ sha256: mirror.sha256 ?? component.archive.sha256
2156
+ });
2157
+ installed = true;
2158
+ break;
2159
+ }
2160
+ catch (error) {
2161
+ failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
2162
+ }
2163
+ }
2164
+ if (!installed) {
2165
+ throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
2166
+ }
2167
+ }
2168
+ return {
2169
+ path: extractRoot,
2170
+ source: {
2171
+ kind: "components",
2172
+ value: download.manifest_url,
2173
+ mirror_kind: "components",
2174
+ size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
2175
+ components: installedComponents
2176
+ }
2177
+ };
2178
+ }
2179
+ function selectedDownloadComponents(components, mode) {
2180
+ return components.filter((component) => {
2181
+ if (!component.install_modes?.length) {
2182
+ return true;
2183
+ }
2184
+ return component.install_modes.includes(mode);
2185
+ });
2186
+ }
2187
+ function installCopyPathsForBoard(boardId) {
2188
+ if (normalizeBoardId(boardId) === DEFAULT_BOARD_ID) {
2189
+ return INSTALL_COPY_PATHS.filter((relativePath) => {
2190
+ const normalized = relativePath.toLowerCase();
2191
+ return normalized !== "toolkit-runtime/rp2350-monitor"
2192
+ && normalized !== "toolkit-runtime/rp2350-monitor/";
2193
+ });
2194
+ }
2195
+ return INSTALL_COPY_PATHS;
2196
+ }
2197
+ async function resolveInstallSourcePathForBoard(sourceRoot, relativePath, boardId) {
2198
+ const direct = resolve(sourceRoot, relativePath);
2199
+ if (await pathExists(direct)) {
2200
+ return direct;
2201
+ }
2202
+ if (!isRp2350MonitorBoardId(boardId)) {
2203
+ return direct;
2204
+ }
2205
+ const legacyRp2350Root = await findLegacyRp2350Root(sourceRoot);
2206
+ if (!legacyRp2350Root) {
2207
+ return direct;
2208
+ }
2209
+ if (relativePath === "rp2350-sdk") {
2210
+ return legacyRp2350Root;
2211
+ }
2212
+ if (relativePath === "rp2350-initial-firmware") {
2213
+ return resolve(legacyRp2350Root, "initial_firmware");
2214
+ }
2215
+ if (relativePath === "rp2350-examples") {
2216
+ return resolve(legacyRp2350Root, "validation");
2217
+ }
2218
+ return direct;
2219
+ }
2220
+ async function findLegacyRp2350Root(sourceRoot) {
2221
+ const direct = resolve(sourceRoot, "RP2350");
2222
+ if (await pathExists(resolve(direct, "pico-sdk", "pico_sdk_init.cmake"))) {
2223
+ return direct;
2224
+ }
2225
+ let entries;
2226
+ try {
2227
+ entries = await readdir(sourceRoot, { withFileTypes: true });
2228
+ }
2229
+ catch {
2230
+ return undefined;
2231
+ }
2232
+ for (const entry of entries) {
2233
+ if (!entry.isDirectory()) {
2234
+ continue;
2235
+ }
2236
+ const candidate = resolve(sourceRoot, entry.name, "RP2350");
2237
+ if (await pathExists(resolve(candidate, "pico-sdk", "pico_sdk_init.cmake"))) {
2238
+ return candidate;
2239
+ }
2240
+ }
2241
+ return undefined;
2242
+ }
594
2243
  async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
595
2244
  const downloadsDir = join(installRoot, "cache", "downloads");
596
2245
  await mkdir(downloadsDir, { recursive: true });
@@ -621,25 +2270,17 @@ async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
621
2270
  return outputPath;
622
2271
  }
623
2272
  }
624
- let resumeFrom = await fileSize(partialPath);
625
- const headers = new Headers();
626
- if (resumeFrom > 0) {
627
- headers.set("range", `bytes=${resumeFrom}-`);
628
- }
629
- let response = await fetch(sourceUrl, { headers });
630
- if (resumeFrom > 0 && response.status !== 206) {
631
- await rm(partialPath, { force: true });
632
- resumeFrom = 0;
633
- response = await fetch(sourceUrl);
634
- }
635
- if (!response.ok) {
636
- throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
2273
+ try {
2274
+ await downloadWithCurl(sourceUrl, partialPath);
637
2275
  }
638
- if (!response.body) {
639
- throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
2276
+ catch (curlError) {
2277
+ try {
2278
+ await downloadWithFetch(sourceUrl, partialPath);
2279
+ }
2280
+ catch (fetchError) {
2281
+ throw new Error(`Local toolchain download failed: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}; curl fallback: ${curlError instanceof Error ? curlError.message : String(curlError)}`);
2282
+ }
640
2283
  }
641
- const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
642
- await pipeline(Readable.fromWeb(response.body), writeStream);
643
2284
  const downloadedSize = await fileSize(partialPath);
644
2285
  if (targetSize !== undefined && downloadedSize !== targetSize) {
645
2286
  throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
@@ -654,6 +2295,61 @@ async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
654
2295
  }
655
2296
  return outputPath;
656
2297
  }
2298
+ async function downloadWithCurl(sourceUrl, partialPath) {
2299
+ await new Promise((resolvePromise, reject) => {
2300
+ const child = spawn("curl", [
2301
+ "--location",
2302
+ "--fail",
2303
+ "--retry", "3",
2304
+ "--retry-delay", "1",
2305
+ "--connect-timeout", "20",
2306
+ "--speed-time", "60",
2307
+ "--speed-limit", "1024",
2308
+ "--continue-at", "-",
2309
+ "--progress-bar",
2310
+ "--output", partialPath,
2311
+ sourceUrl
2312
+ ], {
2313
+ stdio: ["ignore", "ignore", "inherit"]
2314
+ });
2315
+ child.on("error", reject);
2316
+ child.on("exit", (code) => {
2317
+ if (code === 0) {
2318
+ resolvePromise();
2319
+ }
2320
+ else {
2321
+ reject(new Error(`curl exited with code ${code ?? "unknown"}`));
2322
+ }
2323
+ });
2324
+ });
2325
+ }
2326
+ async function downloadWithFetch(sourceUrl, partialPath) {
2327
+ let resumeFrom = await fileSize(partialPath);
2328
+ const headers = new Headers({
2329
+ "accept-encoding": "identity"
2330
+ });
2331
+ if (resumeFrom > 0) {
2332
+ headers.set("range", `bytes=${resumeFrom}-`);
2333
+ }
2334
+ let response = await fetch(sourceUrl, { headers });
2335
+ if (resumeFrom > 0 && response.status !== 206) {
2336
+ await rm(partialPath, { force: true });
2337
+ resumeFrom = 0;
2338
+ response = await fetch(sourceUrl, {
2339
+ headers: {
2340
+ "accept-encoding": "identity"
2341
+ }
2342
+ });
2343
+ }
2344
+ if (!response.ok) {
2345
+ throw new Error(`HTTP ${response.status}: ${sourceUrl}`);
2346
+ }
2347
+ if (!response.body) {
2348
+ throw new Error(`empty response body: ${sourceUrl}`);
2349
+ }
2350
+ const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
2351
+ await pipeline(Readable.fromWeb(response.body), writeStream);
2352
+ }
657
2353
  async function remoteContentLength(sourceUrl) {
658
2354
  try {
659
2355
  const response = await fetch(sourceUrl, { method: "HEAD" });
@@ -712,6 +2408,112 @@ function compilerForSource(releaseRoot, sourcePath) {
712
2408
  }
713
2409
  throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
714
2410
  }
2411
+ function wslCompilerForSource(releaseRoot, sourcePath) {
2412
+ const extension = extname(sourcePath).toLowerCase();
2413
+ const binDir = `${trimTrailingSlash(releaseRoot)}/toolchain/llvm-cross/bin`;
2414
+ if (extension === ".c") {
2415
+ return `${binDir}/aarch64-linux-gnu-gcc`;
2416
+ }
2417
+ if ([".cc", ".cpp", ".cxx"].includes(extension)) {
2418
+ return `${binDir}/aarch64-linux-gnu-g++`;
2419
+ }
2420
+ throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
2421
+ }
2422
+ async function resolveWindowsWslTaishanPiReleaseRoot(mode = "compile") {
2423
+ const explicit = process.env.EMBEDLABS_WINDOWS_WSL_TAISHANPI_RELEASE_ROOT?.trim()
2424
+ || process.env.EMBEDLABS_WSL_TAISHANPI_RELEASE_ROOT?.trim();
2425
+ if (explicit) {
2426
+ return normalizeWindowsWslReleaseRoot(explicit);
2427
+ }
2428
+ const script = `
2429
+ set -euo pipefail
2430
+ REGISTRY="\${EMBEDLABS_WSL_INSTALL_ROOT:-$HOME/.embedlabs}/registry/local-toolchains.json"
2431
+ if [ ! -f "$REGISTRY" ]; then
2432
+ 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
2433
+ exit 2
2434
+ fi
2435
+ node - "$REGISTRY" <<'NODE'
2436
+ const fs = require("fs");
2437
+ const registry = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
2438
+ const env = registry.environments && registry.environments["taishanpi-1m-rk3566"] ? registry.environments["taishanpi-1m-rk3566"] : registry;
2439
+ if (!env.release_root) {
2440
+ console.error("WSL TaishanPi registry does not contain release_root.");
2441
+ process.exit(2);
2442
+ }
2443
+ console.log(env.release_root);
2444
+ NODE
2445
+ `;
2446
+ const result = await runWindowsWslBash(script);
2447
+ if (result.exit_code !== 0 || result.stdout_tail.length < 1) {
2448
+ throw new Error(`Unable to resolve TaishanPi WSL release root: ${result.stderr_tail.join("\n") || result.stdout_tail.join("\n")}`);
2449
+ }
2450
+ return normalizeWindowsWslReleaseRoot(result.stdout_tail[result.stdout_tail.length - 1]);
2451
+ }
2452
+ function normalizeWindowsWslReleaseRoot(value) {
2453
+ const trimmed = value.trim();
2454
+ if (!trimmed) {
2455
+ throw new Error("WSL release root cannot be empty.");
2456
+ }
2457
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
2458
+ return windowsPathToWslPath(resolve(trimmed), "release-root");
2459
+ }
2460
+ if (trimmed.startsWith("/")) {
2461
+ return trimTrailingSlash(trimmed);
2462
+ }
2463
+ throw new Error(`WSL release root must be a Linux absolute path or Windows drive path, got ${value}.`);
2464
+ }
2465
+ function normalizeWindowsWslInstallRoot(value) {
2466
+ const explicit = value?.trim()
2467
+ || process.env.EMBEDLABS_WINDOWS_WSL_INSTALL_ROOT?.trim()
2468
+ || process.env.EMBEDLABS_WSL_INSTALL_ROOT?.trim();
2469
+ return explicit ? normalizeWindowsWslMaybePath(explicit, "install-root") : "$HOME/.embedlabs";
2470
+ }
2471
+ function normalizeWindowsWslMaybePath(value, label) {
2472
+ const trimmed = value.trim();
2473
+ if (!trimmed) {
2474
+ throw new Error(`WSL ${label} cannot be empty.`);
2475
+ }
2476
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
2477
+ return windowsPathToWslPath(resolve(trimmed), label);
2478
+ }
2479
+ if (trimmed.startsWith("/") || trimmed.startsWith("$HOME/") || trimmed === "$HOME") {
2480
+ return trimTrailingSlash(trimmed);
2481
+ }
2482
+ throw new Error(`WSL ${label} must be a Linux absolute path, $HOME-relative path, or Windows drive path, got ${value}.`);
2483
+ }
2484
+ function windowsPathToWslPath(value, label) {
2485
+ const normalized = value.replaceAll("\\", "/");
2486
+ const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
2487
+ if (!match) {
2488
+ throw new Error(`Windows WSL ${label} path must be an absolute drive path, got ${value}.`);
2489
+ }
2490
+ return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
2491
+ }
2492
+ async function runWindowsWslBash(script) {
2493
+ return await runCommand(windowsWslBashCommand(), homedir(), script);
2494
+ }
2495
+ function windowsWslBashCommand() {
2496
+ const command = ["wsl.exe"];
2497
+ const distro = process.env.EMBEDLABS_WINDOWS_WSL_DISTRO?.trim();
2498
+ if (distro) {
2499
+ command.push("-d", distro);
2500
+ }
2501
+ command.push("--", "bash", "-s");
2502
+ return command;
2503
+ }
2504
+ function posixDirname(value) {
2505
+ const index = value.lastIndexOf("/");
2506
+ return index > 0 ? value.slice(0, index) : "/";
2507
+ }
2508
+ function sh(value) {
2509
+ return `'${value.replaceAll("'", "'\\''")}'`;
2510
+ }
2511
+ function shWslPath(value) {
2512
+ if (value === "$HOME" || value.startsWith("$HOME/")) {
2513
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("`", "\\`")}"`;
2514
+ }
2515
+ return sh(value);
2516
+ }
715
2517
  async function localCompileResult(input) {
716
2518
  await access(input.artifactPath, constants.R_OK);
717
2519
  const artifactInfo = await stat(input.artifactPath);
@@ -742,13 +2544,14 @@ function assertAuthenticated(auth) {
742
2544
  throw new Error("Embed Labs auth is required for local toolchain builds. Run: embedlabs auth login --token <user-api-key>");
743
2545
  }
744
2546
  }
745
- async function runCommand(command, cwd) {
2547
+ async function runCommand(command, cwd, stdin) {
746
2548
  return await new Promise((resolve) => {
747
2549
  const child = spawn(command[0], command.slice(1), {
748
2550
  cwd,
749
2551
  env: process.env,
750
- stdio: ["ignore", "pipe", "pipe"]
2552
+ stdio: ["pipe", "pipe", "pipe"]
751
2553
  });
2554
+ child.stdin.end(stdin ?? "");
752
2555
  let stdout = "";
753
2556
  let stderr = "";
754
2557
  child.stdout.setEncoding("utf8");
@@ -780,6 +2583,68 @@ async function runCommand(command, cwd) {
780
2583
  });
781
2584
  });
782
2585
  }
2586
+ async function runCommandWithTimeout(command, cwd, timeoutMs) {
2587
+ return await new Promise((resolve) => {
2588
+ const child = spawn(command[0], command.slice(1), {
2589
+ cwd,
2590
+ env: process.env,
2591
+ stdio: ["ignore", "pipe", "pipe"]
2592
+ });
2593
+ let stdout = "";
2594
+ let stderr = "";
2595
+ let settled = false;
2596
+ const timer = setTimeout(() => {
2597
+ if (settled) {
2598
+ return;
2599
+ }
2600
+ stderr += `Command timed out after ${timeoutMs} ms.\n`;
2601
+ child.kill("SIGTERM");
2602
+ setTimeout(() => {
2603
+ if (!settled) {
2604
+ child.kill("SIGKILL");
2605
+ }
2606
+ }, 2_000).unref();
2607
+ }, timeoutMs);
2608
+ child.stdout.setEncoding("utf8");
2609
+ child.stderr.setEncoding("utf8");
2610
+ child.stdout.on("data", (chunk) => {
2611
+ stdout += chunk;
2612
+ });
2613
+ child.stderr.on("data", (chunk) => {
2614
+ stderr += chunk;
2615
+ });
2616
+ child.on("error", (error) => {
2617
+ if (settled) {
2618
+ return;
2619
+ }
2620
+ settled = true;
2621
+ clearTimeout(timer);
2622
+ stderr += `${error.message}\n`;
2623
+ resolve({
2624
+ command,
2625
+ cwd,
2626
+ exit_code: 127,
2627
+ stdout_tail: tailLines(stdout),
2628
+ stderr_tail: tailLines(stderr)
2629
+ });
2630
+ });
2631
+ child.on("close", (code) => {
2632
+ if (settled) {
2633
+ return;
2634
+ }
2635
+ settled = true;
2636
+ clearTimeout(timer);
2637
+ const timedOut = stderr.includes(`Command timed out after ${timeoutMs} ms.`);
2638
+ resolve({
2639
+ command,
2640
+ cwd,
2641
+ exit_code: timedOut ? 124 : code ?? 1,
2642
+ stdout_tail: tailLines(stdout),
2643
+ stderr_tail: tailLines(stderr)
2644
+ });
2645
+ });
2646
+ });
2647
+ }
783
2648
  async function fileInfoFor(filePath) {
784
2649
  const result = await runCommand(["file", filePath], dirname(filePath));
785
2650
  return result.exit_code === 0 ? result.stdout_tail.join("\n") : undefined;