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

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.
@@ -0,0 +1,834 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { constants, createWriteStream } from "node:fs";
4
+ import { access, cp, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
+ import { arch, homedir, platform, tmpdir } from "node:os";
6
+ import { basename, dirname, extname, join, resolve } from "node:path";
7
+ import { Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { fileURLToPath } from "node:url";
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
+ const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
14
+ const DEFAULT_CHANNEL = "stable";
15
+ const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
16
+ const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
17
+ const BUILT_IN_CHANNEL = {
18
+ schema: "embedlabs.channel.v1",
19
+ channel: DEFAULT_CHANNEL,
20
+ packages: [
21
+ { id: "embedlabs.tools.vendor.rockchip", version: "1.0.0", manifest: "" },
22
+ { id: "embedlabs.tools.common.llvm", version: "22.1.3", manifest: "" },
23
+ { id: "embedlabs.tools.common.e2fsprogs", version: "1.0.0", manifest: "" },
24
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.31", manifest: "" },
25
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.31", manifest: "" },
26
+ { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
27
+ { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" }
28
+ ]
29
+ };
30
+ const BUILT_IN_MANIFESTS = {
31
+ "embedlabs.tools.vendor.rockchip": {
32
+ schema: "embedlabs.package.v1",
33
+ id: "embedlabs.tools.vendor.rockchip",
34
+ version: "1.0.0",
35
+ kind: "tools",
36
+ hosts: ["darwin-arm64", "linux-x86_64"],
37
+ provides: ["rockchip.mkimage", "rockchip.dumpimage", "rockchip.resource_tool", "rockchip.rkdeveloptool"]
38
+ },
39
+ "embedlabs.tools.common.llvm": {
40
+ schema: "embedlabs.package.v1",
41
+ id: "embedlabs.tools.common.llvm",
42
+ version: "22.1.3",
43
+ kind: "tools",
44
+ hosts: ["darwin-arm64", "linux-x86_64"],
45
+ provides: ["llvm.clang", "llvm.clangxx", "llvm.ld_lld", "llvm.ar", "llvm.objcopy", "llvm.readelf"]
46
+ },
47
+ "embedlabs.tools.common.e2fsprogs": {
48
+ schema: "embedlabs.package.v1",
49
+ id: "embedlabs.tools.common.e2fsprogs",
50
+ version: "1.0.0",
51
+ kind: "tools",
52
+ hosts: ["darwin-arm64", "linux-x86_64"],
53
+ provides: ["ext4.mke2fs", "ext4.resize2fs", "fakeroot"]
54
+ },
55
+ "embedlabs.tools.runtime.qtquick-live-preview": {
56
+ schema: "embedlabs.package.v1",
57
+ id: "embedlabs.tools.runtime.qtquick-live-preview",
58
+ version: "1.0.31",
59
+ kind: "tools",
60
+ hosts: ["darwin-arm64", "linux-x86_64"],
61
+ provides: ["qtquick.live_preview", "qtquick.live_preview.inspector", "qtquick.live_preview.feedback"]
62
+ },
63
+ "embedlabs.tools.runtime.rp2350-monitor": {
64
+ schema: "embedlabs.package.v1",
65
+ id: "embedlabs.tools.runtime.rp2350-monitor",
66
+ version: "1.0.31",
67
+ kind: "tools",
68
+ hosts: ["darwin-arm64", "linux-x86_64"],
69
+ provides: ["rp2350.monitor.cli", "rp2350.monitor.logic_analyzer", "rp2350.monitor.logic_decode"]
70
+ },
71
+ "embedlabs.family.rk356x": {
72
+ schema: "embedlabs.package.v1",
73
+ id: "embedlabs.family.rk356x",
74
+ version: "1.0.0",
75
+ kind: "family",
76
+ family: "rk356x",
77
+ requires: [
78
+ { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0" },
79
+ { id: "embedlabs.tools.common.llvm", version: "22.x" },
80
+ { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
81
+ ]
82
+ },
83
+ "embedlabs.board.taishanpi.1m-rk3566": {
84
+ schema: "embedlabs.package.v1",
85
+ id: "embedlabs.board.taishanpi.1m-rk3566",
86
+ version: "1.0.31",
87
+ kind: "board",
88
+ family: "rk356x",
89
+ board: "TaishanPi",
90
+ variant: "1M-RK3566",
91
+ requires: [
92
+ { id: "embedlabs.family.rk356x", version: "^1.0.0" },
93
+ { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
94
+ { id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
95
+ { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
96
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.31", roles: ["qtquick-preview"] },
97
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["rp2350-monitor"] }
98
+ ],
99
+ build_modes: ["local-llvm"]
100
+ }
101
+ };
102
+ const INSTALL_COPY_PATHS = [
103
+ "toolchain/llvm-cross",
104
+ "toolchain/host",
105
+ "toolchain/host-tools",
106
+ "qt-target/qt6-rk3566-llvm-6.8.3",
107
+ "tools/mac",
108
+ "toolkit-runtime/qtquick-live-preview",
109
+ "toolkit-runtime/rp2350-monitor",
110
+ "toolkit-runtime/RP2350-Monitor",
111
+ "images/current",
112
+ "userdata/rootfs",
113
+ "boot-workspace/kernel-tree"
114
+ ];
115
+ export function defaultLocalReleaseRoot() {
116
+ return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
117
+ || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
118
+ || DEFAULT_RELEASE_ROOT;
119
+ }
120
+ export async function latestLocalToolchain(options = {}) {
121
+ const boardId = options.boardId ?? DEFAULT_BOARD_ID;
122
+ const channelName = options.channel ?? DEFAULT_CHANNEL;
123
+ const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
124
+ const boardPackageId = boardPackageIdFor(boardId);
125
+ const board = manifests.get(boardPackageId);
126
+ if (!board) {
127
+ throw new Error(`No local toolchain board package found for ${boardId}.`);
128
+ }
129
+ const packages = resolvePackageRefs(boardPackageId, channel, manifests);
130
+ let download;
131
+ let downloadError;
132
+ try {
133
+ download = await resolveLocalToolchainDownloadPlan({
134
+ boardId,
135
+ channel: channelName,
136
+ host: hostId(),
137
+ toolchain: "llvm"
138
+ });
139
+ }
140
+ catch (error) {
141
+ downloadError = error instanceof Error ? error.message : String(error);
142
+ }
143
+ return {
144
+ board_id: boardId,
145
+ channel: channel.channel,
146
+ host: hostId(),
147
+ version: board.version,
148
+ metadata_root: metadataRoot,
149
+ packages,
150
+ download,
151
+ download_error: downloadError
152
+ };
153
+ }
154
+ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD_ID) {
155
+ const root = resolveInstallRoot(installRoot);
156
+ const registryPath = localToolchainRegistryPath(root);
157
+ try {
158
+ const registry = JSON.parse(await readFile(registryPath, "utf8"));
159
+ const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
160
+ return {
161
+ installed: !!releaseRoot,
162
+ board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
163
+ version: typeof registry.version === "string" ? registry.version : undefined,
164
+ release_root: releaseRoot,
165
+ registry_path: registryPath,
166
+ install_root: root,
167
+ channel: typeof registry.channel === "string" ? registry.channel : undefined,
168
+ packages: Array.isArray(registry.packages) ? registry.packages : undefined
169
+ };
170
+ }
171
+ catch {
172
+ return {
173
+ installed: false,
174
+ board_id: boardId,
175
+ registry_path: registryPath,
176
+ install_root: root
177
+ };
178
+ }
179
+ }
180
+ export async function installLocalToolchain(options = {}) {
181
+ const latest = await latestLocalToolchain(options);
182
+ const installRoot = resolveInstallRoot(options.installRoot);
183
+ const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
184
+ if (await pathExists(releaseRoot) && !options.force) {
185
+ const validation = await validateLocalToolchain(releaseRoot);
186
+ if (!validation.ok) {
187
+ throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
188
+ }
189
+ await writeCurrentRegistry(installRoot, latest, releaseRoot);
190
+ return {
191
+ board_id: latest.board_id,
192
+ version: latest.version,
193
+ channel: latest.channel,
194
+ host: latest.host,
195
+ install_root: installRoot,
196
+ release_root: releaseRoot,
197
+ registry_path: localToolchainRegistryPath(installRoot),
198
+ source: { kind: "directory", value: releaseRoot },
199
+ installed_paths: [],
200
+ packages: latest.packages,
201
+ validation
202
+ };
203
+ }
204
+ const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
205
+ try {
206
+ const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
207
+ await rm(releaseRoot, { recursive: true, force: true });
208
+ await mkdir(releaseRoot, { recursive: true });
209
+ const installedPaths = [];
210
+ for (const relativePath of INSTALL_COPY_PATHS) {
211
+ const sourcePath = resolve(sourceRoot.path, relativePath);
212
+ if (!await pathExists(sourcePath)) {
213
+ continue;
214
+ }
215
+ const targetPath = resolve(releaseRoot, relativePath);
216
+ await mkdir(dirname(targetPath), { recursive: true });
217
+ await cp(sourcePath, targetPath, {
218
+ recursive: true,
219
+ force: true,
220
+ preserveTimestamps: true,
221
+ verbatimSymlinks: true
222
+ });
223
+ installedPaths.push(relativePath);
224
+ }
225
+ await writeCurrentRegistry(installRoot, latest, releaseRoot);
226
+ const validation = await validateLocalToolchain(releaseRoot);
227
+ if (!validation.ok) {
228
+ throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
229
+ }
230
+ return {
231
+ board_id: latest.board_id,
232
+ version: latest.version,
233
+ channel: latest.channel,
234
+ host: latest.host,
235
+ install_root: installRoot,
236
+ release_root: releaseRoot,
237
+ registry_path: localToolchainRegistryPath(installRoot),
238
+ source: sourceRoot.source,
239
+ installed_paths: installedPaths,
240
+ packages: latest.packages,
241
+ validation
242
+ };
243
+ }
244
+ finally {
245
+ await rm(tempDir, { recursive: true, force: true });
246
+ }
247
+ }
248
+ export async function validateLocalToolchain(releaseRoot) {
249
+ const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
250
+ const required = [
251
+ ["release root", "."],
252
+ ["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
253
+ ["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
254
+ ["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
255
+ ["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
256
+ ["host GCC libraries", "toolchain/host/lib/gcc"],
257
+ ["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
258
+ ["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
259
+ ["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
260
+ ["Rockchip mkimage", "tools/mac/mkimage"],
261
+ ["Rockchip dumpimage", "tools/mac/dumpimage"],
262
+ ["Rockchip resource_tool", "tools/mac/resource_tool"],
263
+ ["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
264
+ ["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"],
265
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
266
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"]
267
+ ];
268
+ const checked_paths = [];
269
+ for (const [label, relativePath] of required) {
270
+ const absolutePath = resolve(resolvedRoot, relativePath);
271
+ checked_paths.push({
272
+ label,
273
+ path: absolutePath,
274
+ exists: await pathExists(absolutePath)
275
+ });
276
+ }
277
+ const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
278
+ return {
279
+ ok: missing_paths.length === 0,
280
+ host: {
281
+ platform: platform(),
282
+ arch: arch()
283
+ },
284
+ board_id: DEFAULT_BOARD_ID,
285
+ release_root: resolvedRoot,
286
+ checked_paths,
287
+ missing_paths,
288
+ notes: [
289
+ "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
290
+ "This validator checks the Mac-first TaishanPi LLVM release layout; package install/update registry work is tracked separately."
291
+ ]
292
+ };
293
+ }
294
+ export async function compileTaishanPiSingleFile(options) {
295
+ assertAuthenticated(options.auth);
296
+ const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
297
+ const sourcePath = resolve(options.sourcePath);
298
+ const outputPath = resolve(options.outputPath);
299
+ await access(sourcePath, constants.R_OK);
300
+ await mkdir(dirname(outputPath), { recursive: true });
301
+ const compiler = compilerForSource(releaseRoot, sourcePath);
302
+ await access(compiler, constants.X_OK);
303
+ const sysroot = join(releaseRoot, "toolchain", "host", "aarch64-buildroot-linux-gnu", "sysroot");
304
+ await access(sysroot, constants.R_OK);
305
+ const command = [compiler, `--sysroot=${sysroot}`, "-O2", sourcePath, "-o", outputPath];
306
+ const buildResult = await runCommand(command, dirname(sourcePath));
307
+ if (buildResult.exit_code !== 0) {
308
+ throw new Error(`Local compile failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
309
+ }
310
+ return await localCompileResult({
311
+ boardId: options.boardId,
312
+ operation: "local.compile.single_file",
313
+ releaseRoot,
314
+ accountId: options.accountId,
315
+ auth: options.auth,
316
+ sourcePath,
317
+ artifactPath: outputPath,
318
+ commands: [buildResult]
319
+ });
320
+ }
321
+ export async function buildTaishanPiQtSmoke(options) {
322
+ assertAuthenticated(options.auth);
323
+ const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
324
+ const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
325
+ const buildDir = resolve(options.buildDir);
326
+ const targetName = options.targetName ?? "qt_llvm_smoke";
327
+ const qtCmake = join(releaseRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", "bin", "qt-cmake");
328
+ await access(join(sourceDir, "CMakeLists.txt"), constants.R_OK);
329
+ await access(qtCmake, constants.X_OK);
330
+ await mkdir(buildDir, { recursive: true });
331
+ const configure = await runCommand([
332
+ qtCmake,
333
+ "-S",
334
+ sourceDir,
335
+ "-B",
336
+ buildDir,
337
+ "-G",
338
+ "Ninja",
339
+ "-DCMAKE_BUILD_TYPE=Release"
340
+ ], sourceDir);
341
+ if (configure.exit_code !== 0) {
342
+ throw new Error(`Qt smoke configure failed with exit code ${configure.exit_code}: ${configure.stderr_tail.join("\n")}`);
343
+ }
344
+ const build = await runCommand(["cmake", "--build", buildDir, "--parallel"], sourceDir);
345
+ if (build.exit_code !== 0) {
346
+ throw new Error(`Qt smoke build failed with exit code ${build.exit_code}: ${build.stderr_tail.join("\n")}`);
347
+ }
348
+ const artifactPath = join(buildDir, targetName);
349
+ return await localCompileResult({
350
+ boardId: DEFAULT_BOARD_ID,
351
+ operation: "local.build.qt_smoke",
352
+ releaseRoot,
353
+ accountId: options.accountId,
354
+ auth: options.auth,
355
+ sourcePath: sourceDir,
356
+ buildDir,
357
+ artifactPath,
358
+ commands: [configure, build]
359
+ });
360
+ }
361
+ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
362
+ const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
363
+ const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
364
+ if (!candidateRoot) {
365
+ return {
366
+ channel: BUILT_IN_CHANNEL,
367
+ manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
368
+ metadataRoot: undefined
369
+ };
370
+ }
371
+ const root = resolve(candidateRoot);
372
+ const channelPath = join(root, "channels", channelName, "index.json");
373
+ const channel = JSON.parse(await readFile(channelPath, "utf8"));
374
+ if (channel.schema !== "embedlabs.channel.v1") {
375
+ throw new Error(`Unexpected local toolchain channel schema ${channel.schema}.`);
376
+ }
377
+ const manifests = new Map();
378
+ for (const entry of channel.packages) {
379
+ const manifestPath = entry.manifest
380
+ ? resolve(dirname(channelPath), entry.manifest)
381
+ : join(root, "manifests", entry.id, entry.version, "manifest.json");
382
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
383
+ if (manifest.schema !== "embedlabs.package.v1") {
384
+ throw new Error(`Unexpected local toolchain manifest schema for ${entry.id}: ${manifest.schema}.`);
385
+ }
386
+ if (manifest.id !== entry.id || manifest.version !== entry.version) {
387
+ throw new Error(`Local toolchain manifest mismatch for ${entry.id}@${entry.version}.`);
388
+ }
389
+ manifests.set(manifest.id, manifest);
390
+ }
391
+ return { channel, manifests, metadataRoot: root };
392
+ }
393
+ async function resolveLocalToolchainDownloadPlan(input) {
394
+ const channelUrl = downloadChannelUrl(input.channel);
395
+ const channel = await fetchJson(channelUrl);
396
+ if (channel.schema !== "embedlabs.download-channel.v1") {
397
+ throw new Error(`Unexpected download channel schema ${channel.schema}.`);
398
+ }
399
+ const entry = (channel.packages ?? []).find((item) => {
400
+ return item.board_id === input.boardId
401
+ && item.host === input.host
402
+ && item.toolchain === input.toolchain
403
+ && (item.kind === undefined || item.kind === "toolchain-archive");
404
+ });
405
+ if (!entry?.manifest) {
406
+ return undefined;
407
+ }
408
+ const manifestUrl = new URL(entry.manifest, channelUrl).toString();
409
+ const manifest = await fetchJson(manifestUrl);
410
+ if (manifest.id !== entry.id || manifest.version !== entry.version) {
411
+ throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
412
+ }
413
+ if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
414
+ throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
415
+ }
416
+ if (!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes)) {
417
+ throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive file, size, or SHA256.`);
418
+ }
419
+ const mirrors = orderDownloadMirrors((manifest.mirrors ?? [])
420
+ .filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
421
+ .map((mirror) => ({
422
+ kind: mirror.kind || "unknown",
423
+ enabled: mirror.enabled !== false,
424
+ url: mirror.url,
425
+ sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
426
+ size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
427
+ })), manifest.download_policy?.preferred_order);
428
+ const first = mirrors[0];
429
+ if (!first) {
430
+ return undefined;
431
+ }
432
+ return {
433
+ channel_url: channelUrl,
434
+ manifest_url: manifestUrl,
435
+ package_id: manifest.id,
436
+ version: manifest.version,
437
+ board_id: input.boardId,
438
+ host: input.host,
439
+ toolchain: input.toolchain,
440
+ source_url: first.url,
441
+ mirror_kind: first.kind,
442
+ archive: {
443
+ file: manifest.archive.file,
444
+ size_bytes: manifest.archive.size_bytes,
445
+ sha256: manifest.archive.sha256,
446
+ content_type: manifest.archive.content_type
447
+ },
448
+ mirrors
449
+ };
450
+ }
451
+ function orderDownloadMirrors(mirrors, preferredOrder) {
452
+ const preference = preferredOrder?.length
453
+ ? preferredOrder
454
+ : ["github_release", "embedlabs_cdn", "cloudfront", "embedlabs_server"];
455
+ return [...mirrors].sort((left, right) => mirrorRank(left.kind, preference) - mirrorRank(right.kind, preference));
456
+ }
457
+ function mirrorRank(kind, preferredOrder) {
458
+ const index = preferredOrder.indexOf(kind);
459
+ return index >= 0 ? index : preferredOrder.length + 1;
460
+ }
461
+ async function fetchJson(url) {
462
+ const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
463
+ if (!response.ok) {
464
+ throw new Error(`HTTP ${response.status} while loading ${url}`);
465
+ }
466
+ return await response.json();
467
+ }
468
+ function downloadChannelUrl(channelName) {
469
+ const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
470
+ || process.env.EMBEDLABS_DOWNLOAD_CHANNEL_URL?.trim();
471
+ if (explicit) {
472
+ return explicit;
473
+ }
474
+ return `${downloadBaseUrl()}/downloads/metadata/channels/${encodeURIComponent(channelName)}/index.json`;
475
+ }
476
+ function downloadBaseUrl() {
477
+ return trimTrailingSlash(process.env.EMBED_DOWNLOAD_BASE_URL?.trim()
478
+ || process.env.EMBEDLABS_DOWNLOAD_BASE_URL?.trim()
479
+ || DEFAULT_DOWNLOAD_BASE_URL);
480
+ }
481
+ function trimTrailingSlash(value) {
482
+ return value.replace(/\/+$/, "");
483
+ }
484
+ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
485
+ if (seen.has(packageId)) {
486
+ return [];
487
+ }
488
+ seen.add(packageId);
489
+ const manifest = manifests.get(packageId);
490
+ if (!manifest) {
491
+ throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
492
+ }
493
+ const refs = [];
494
+ for (const requirement of manifest.requires ?? []) {
495
+ refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
496
+ }
497
+ const channelEntry = channel.packages.find((item) => item.id === packageId);
498
+ refs.push({
499
+ id: packageId,
500
+ version: manifest.version,
501
+ manifest: channelEntry?.manifest
502
+ });
503
+ return refs;
504
+ }
505
+ function boardPackageIdFor(boardId) {
506
+ if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
507
+ return "embedlabs.board.taishanpi.1m-rk3566";
508
+ }
509
+ if (boardId.startsWith("embedlabs.board.")) {
510
+ return boardId;
511
+ }
512
+ throw new Error(`Unsupported local toolchain board ${boardId}.`);
513
+ }
514
+ function hostId() {
515
+ if (platform() === "darwin" && arch() === "arm64") {
516
+ return "darwin-arm64";
517
+ }
518
+ if (platform() === "linux" && arch() === "x64") {
519
+ return "linux-x86_64";
520
+ }
521
+ return `${platform()}-${arch()}`;
522
+ }
523
+ function resolveInstallRoot(installRoot) {
524
+ return resolve(installRoot
525
+ || process.env.EMBEDLABS_HOME?.trim()
526
+ || join(homedir(), ".embedlabs"));
527
+ }
528
+ function localToolchainRegistryPath(installRoot) {
529
+ return join(installRoot, "registry", "local-toolchains.json");
530
+ }
531
+ async function writeCurrentRegistry(installRoot, latest, releaseRoot) {
532
+ const registryPath = localToolchainRegistryPath(installRoot);
533
+ await mkdir(dirname(registryPath), { recursive: true });
534
+ await writeFile(registryPath, `${JSON.stringify({
535
+ installed: true,
536
+ board_id: latest.board_id,
537
+ version: latest.version,
538
+ channel: latest.channel,
539
+ host: latest.host,
540
+ release_root: releaseRoot,
541
+ packages: latest.packages,
542
+ updated_at: new Date().toISOString()
543
+ }, null, 2)}\n`, "utf8");
544
+ }
545
+ async function resolveLocalReleaseRoot(releaseRoot) {
546
+ if (releaseRoot?.trim()) {
547
+ return resolve(releaseRoot);
548
+ }
549
+ const envRoot = process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim() || process.env.EMBEDLABS_RELEASE_ROOT?.trim();
550
+ if (envRoot) {
551
+ return resolve(envRoot);
552
+ }
553
+ const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
554
+ if (current.release_root) {
555
+ return resolve(current.release_root);
556
+ }
557
+ return resolve(DEFAULT_RELEASE_ROOT);
558
+ }
559
+ async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir) {
560
+ if (options.sourceReleaseRoot) {
561
+ return {
562
+ path: resolve(options.sourceReleaseRoot),
563
+ source: { kind: "directory", value: resolve(options.sourceReleaseRoot) }
564
+ };
565
+ }
566
+ if (options.sourceUrl) {
567
+ const downloadedPath = await downloadToolchainArchive(options.sourceUrl, installRoot);
568
+ const extractRoot = join(tempDir, "extract");
569
+ await mkdir(extractRoot, { recursive: true });
570
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
571
+ if (extracted.exit_code !== 0) {
572
+ throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
573
+ }
574
+ return {
575
+ path: await findReleaseRoot(extractRoot),
576
+ source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
577
+ };
578
+ }
579
+ if (latest.download) {
580
+ const failures = [];
581
+ for (const mirror of latest.download.mirrors) {
582
+ if (!mirror.enabled) {
583
+ continue;
584
+ }
585
+ const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
586
+ try {
587
+ const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
588
+ sha256: mirror.sha256 ?? latest.download.archive.sha256,
589
+ size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
590
+ });
591
+ await rm(extractRoot, { recursive: true, force: true });
592
+ await mkdir(extractRoot, { recursive: true });
593
+ const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
594
+ if (extracted.exit_code !== 0) {
595
+ throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
596
+ }
597
+ return {
598
+ path: await findReleaseRoot(extractRoot),
599
+ source: {
600
+ kind: "url",
601
+ value: mirror.url,
602
+ downloaded_path: downloadedPath,
603
+ mirror_kind: mirror.kind,
604
+ sha256: mirror.sha256 ?? latest.download.archive.sha256,
605
+ size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
606
+ }
607
+ };
608
+ }
609
+ catch (error) {
610
+ failures.push(`${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
611
+ }
612
+ }
613
+ if (failures.length > 0 && !await pathExists(DEFAULT_RELEASE_ROOT)) {
614
+ throw new Error(`Could not install local toolchain from download mirrors: ${failures.join("; ")}`);
615
+ }
616
+ }
617
+ if (await pathExists(DEFAULT_RELEASE_ROOT)) {
618
+ return {
619
+ path: resolve(DEFAULT_RELEASE_ROOT),
620
+ source: { kind: "directory", value: resolve(DEFAULT_RELEASE_ROOT) }
621
+ };
622
+ }
623
+ throw new Error("Local toolchain install could not resolve a source. Pass --source-url <tar.gz>, --source-release-root <path>, or configure EMBED_DOWNLOAD_BASE_URL.");
624
+ }
625
+ async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
626
+ const downloadsDir = join(installRoot, "cache", "downloads");
627
+ await mkdir(downloadsDir, { recursive: true });
628
+ const parsed = new URL(sourceUrl);
629
+ const filename = basename(parsed.pathname) || `local-toolchain-${Date.now()}.tar.gz`;
630
+ const outputPath = join(downloadsDir, filename);
631
+ if (parsed.protocol === "file:") {
632
+ await cp(fileURLToPath(parsed), outputPath, { force: true });
633
+ return outputPath;
634
+ }
635
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
636
+ throw new Error(`Unsupported local toolchain download URL protocol: ${parsed.protocol}`);
637
+ }
638
+ const partialPath = `${outputPath}.part`;
639
+ const expectedSize = expected?.size_bytes;
640
+ const existingComplete = await fileSize(outputPath);
641
+ const remoteSize = await remoteContentLength(sourceUrl);
642
+ const targetSize = expectedSize ?? remoteSize;
643
+ if (existingComplete > 0 && targetSize !== undefined && existingComplete === targetSize) {
644
+ if (expected?.sha256) {
645
+ const actual = await sha256(outputPath);
646
+ if (actual === expected.sha256) {
647
+ return outputPath;
648
+ }
649
+ await rm(outputPath, { force: true });
650
+ }
651
+ else {
652
+ return outputPath;
653
+ }
654
+ }
655
+ let resumeFrom = await fileSize(partialPath);
656
+ const headers = new Headers();
657
+ if (resumeFrom > 0) {
658
+ headers.set("range", `bytes=${resumeFrom}-`);
659
+ }
660
+ let response = await fetch(sourceUrl, { headers });
661
+ if (resumeFrom > 0 && response.status !== 206) {
662
+ await rm(partialPath, { force: true });
663
+ resumeFrom = 0;
664
+ response = await fetch(sourceUrl);
665
+ }
666
+ if (!response.ok) {
667
+ throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
668
+ }
669
+ if (!response.body) {
670
+ throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
671
+ }
672
+ const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
673
+ await pipeline(Readable.fromWeb(response.body), writeStream);
674
+ const downloadedSize = await fileSize(partialPath);
675
+ if (targetSize !== undefined && downloadedSize !== targetSize) {
676
+ throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
677
+ }
678
+ await rename(partialPath, outputPath);
679
+ if (expected?.sha256) {
680
+ const actual = await sha256(outputPath);
681
+ if (actual !== expected.sha256) {
682
+ await rm(outputPath, { force: true });
683
+ throw new Error(`Local toolchain download SHA256 mismatch: expected ${expected.sha256}, got ${actual}.`);
684
+ }
685
+ }
686
+ return outputPath;
687
+ }
688
+ async function remoteContentLength(sourceUrl) {
689
+ try {
690
+ const response = await fetch(sourceUrl, { method: "HEAD" });
691
+ if (!response.ok) {
692
+ return undefined;
693
+ }
694
+ const length = response.headers.get("content-length");
695
+ if (!length) {
696
+ return undefined;
697
+ }
698
+ const parsed = Number.parseInt(length, 10);
699
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
700
+ }
701
+ catch {
702
+ return undefined;
703
+ }
704
+ }
705
+ async function fileSize(filePath) {
706
+ try {
707
+ return (await stat(filePath)).size;
708
+ }
709
+ catch {
710
+ return 0;
711
+ }
712
+ }
713
+ function safeFileToken(value) {
714
+ return value.replace(/[^a-zA-Z0-9_.-]+/g, "_") || "mirror";
715
+ }
716
+ async function findReleaseRoot(extractRoot) {
717
+ if (await pathExists(join(extractRoot, "toolchain"))) {
718
+ return extractRoot;
719
+ }
720
+ const entries = await readdir(extractRoot);
721
+ for (const entry of entries) {
722
+ const candidate = join(extractRoot, entry);
723
+ try {
724
+ const info = await stat(candidate);
725
+ if (info.isDirectory() && await pathExists(join(candidate, "toolchain"))) {
726
+ return candidate;
727
+ }
728
+ }
729
+ catch {
730
+ // Ignore entries that disappear during extraction cleanup.
731
+ }
732
+ }
733
+ throw new Error("Downloaded local toolchain archive did not contain a release root with toolchain/.");
734
+ }
735
+ function compilerForSource(releaseRoot, sourcePath) {
736
+ const extension = extname(sourcePath).toLowerCase();
737
+ const binDir = join(releaseRoot, "toolchain", "llvm-cross", "bin");
738
+ if (extension === ".c") {
739
+ return join(binDir, "aarch64-linux-gnu-gcc");
740
+ }
741
+ if ([".cc", ".cpp", ".cxx"].includes(extension)) {
742
+ return join(binDir, "aarch64-linux-gnu-g++");
743
+ }
744
+ throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
745
+ }
746
+ async function localCompileResult(input) {
747
+ await access(input.artifactPath, constants.R_OK);
748
+ const artifactInfo = await stat(input.artifactPath);
749
+ const artifactSha256 = await sha256(input.artifactPath);
750
+ const fileInfo = await fileInfoFor(input.artifactPath);
751
+ const manifestPath = `${input.artifactPath}.embedlabs-local-build.json`;
752
+ const result = {
753
+ board_id: input.boardId,
754
+ operation: input.operation,
755
+ release_root: input.releaseRoot,
756
+ account_id: input.accountId,
757
+ auth: input.auth,
758
+ source_path: input.sourcePath,
759
+ build_dir: input.buildDir,
760
+ artifact_path: input.artifactPath,
761
+ artifact_name: basename(input.artifactPath),
762
+ artifact_size_bytes: artifactInfo.size,
763
+ artifact_sha256: artifactSha256,
764
+ file_info: fileInfo,
765
+ commands: input.commands,
766
+ manifest_path: manifestPath
767
+ };
768
+ await writeFile(manifestPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
769
+ return result;
770
+ }
771
+ function assertAuthenticated(auth) {
772
+ if (!auth.authenticated) {
773
+ throw new Error("Embed Labs auth is required for local toolchain builds. Run: embedlabs auth login --token <user-api-key>");
774
+ }
775
+ }
776
+ async function runCommand(command, cwd) {
777
+ return await new Promise((resolve) => {
778
+ const child = spawn(command[0], command.slice(1), {
779
+ cwd,
780
+ env: process.env,
781
+ stdio: ["ignore", "pipe", "pipe"]
782
+ });
783
+ let stdout = "";
784
+ let stderr = "";
785
+ child.stdout.setEncoding("utf8");
786
+ child.stderr.setEncoding("utf8");
787
+ child.stdout.on("data", (chunk) => {
788
+ stdout += chunk;
789
+ });
790
+ child.stderr.on("data", (chunk) => {
791
+ stderr += chunk;
792
+ });
793
+ child.on("error", (error) => {
794
+ stderr += `${error.message}\n`;
795
+ resolve({
796
+ command,
797
+ cwd,
798
+ exit_code: 127,
799
+ stdout_tail: [],
800
+ stderr_tail: tailLines(stderr)
801
+ });
802
+ });
803
+ child.on("close", (code) => {
804
+ resolve({
805
+ command,
806
+ cwd,
807
+ exit_code: code ?? 1,
808
+ stdout_tail: tailLines(stdout),
809
+ stderr_tail: tailLines(stderr)
810
+ });
811
+ });
812
+ });
813
+ }
814
+ async function fileInfoFor(filePath) {
815
+ const result = await runCommand(["file", filePath], dirname(filePath));
816
+ return result.exit_code === 0 ? result.stdout_tail.join("\n") : undefined;
817
+ }
818
+ async function sha256(filePath) {
819
+ const content = await readFile(filePath);
820
+ return createHash("sha256").update(content).digest("hex");
821
+ }
822
+ async function pathExists(filePath) {
823
+ try {
824
+ await access(filePath);
825
+ return true;
826
+ }
827
+ catch {
828
+ return false;
829
+ }
830
+ }
831
+ function tailLines(text, maxLines = 80) {
832
+ return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
833
+ }
834
+ //# sourceMappingURL=local-toolchain.js.map