@kvell007/embed-labs-cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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.
- package/README.md +47 -13
- package/dist/image-compose.d.ts +38 -0
- package/dist/image-compose.js +334 -0
- package/dist/image-compose.js.map +1 -0
- package/dist/index.js +1871 -98
- package/dist/index.js.map +1 -1
- package/dist/local-toolchain.d.ts +208 -0
- package/dist/local-toolchain.js +1019 -0
- package/dist/local-toolchain.js.map +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,1019 @@
|
|
|
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
|
+
"toolchain/qt6-rk3566-llvm-toolchain.cmake",
|
|
107
|
+
"qt-target/qt6-rk3566-llvm-6.8.3",
|
|
108
|
+
"qt-host/qt6-host-macos-6.8.3",
|
|
109
|
+
"tools/mac",
|
|
110
|
+
"toolkit-runtime/qtquick-live-preview",
|
|
111
|
+
"toolkit-runtime/rp2350-monitor",
|
|
112
|
+
"toolkit-runtime/RP2350-Monitor",
|
|
113
|
+
"images/current",
|
|
114
|
+
"userdata/rootfs",
|
|
115
|
+
"boot-workspace",
|
|
116
|
+
"README.md",
|
|
117
|
+
"meta",
|
|
118
|
+
"scripts",
|
|
119
|
+
"support",
|
|
120
|
+
"third_party"
|
|
121
|
+
];
|
|
122
|
+
const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "compile", "qt", "full", "images"];
|
|
123
|
+
export function defaultLocalReleaseRoot() {
|
|
124
|
+
return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
|
|
125
|
+
|| process.env.EMBEDLABS_RELEASE_ROOT?.trim()
|
|
126
|
+
|| DEFAULT_RELEASE_ROOT;
|
|
127
|
+
}
|
|
128
|
+
export async function latestLocalToolchain(options = {}) {
|
|
129
|
+
const boardId = options.boardId ?? DEFAULT_BOARD_ID;
|
|
130
|
+
const channelName = options.channel ?? DEFAULT_CHANNEL;
|
|
131
|
+
const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
|
|
132
|
+
const boardPackageId = boardPackageIdFor(boardId);
|
|
133
|
+
const board = manifests.get(boardPackageId);
|
|
134
|
+
if (!board) {
|
|
135
|
+
throw new Error(`No local toolchain board package found for ${boardId}.`);
|
|
136
|
+
}
|
|
137
|
+
const packages = resolvePackageRefs(boardPackageId, channel, manifests);
|
|
138
|
+
let download;
|
|
139
|
+
let downloadError;
|
|
140
|
+
try {
|
|
141
|
+
download = await resolveLocalToolchainDownloadPlan({
|
|
142
|
+
boardId,
|
|
143
|
+
channel: channelName,
|
|
144
|
+
host: hostId(),
|
|
145
|
+
toolchain: "llvm"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
downloadError = error instanceof Error ? error.message : String(error);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
board_id: boardId,
|
|
153
|
+
channel: channel.channel,
|
|
154
|
+
host: hostId(),
|
|
155
|
+
version: download?.version ?? board.version,
|
|
156
|
+
metadata_root: metadataRoot,
|
|
157
|
+
packages,
|
|
158
|
+
download,
|
|
159
|
+
download_error: downloadError
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD_ID) {
|
|
163
|
+
const root = resolveInstallRoot(installRoot);
|
|
164
|
+
const registryPath = localToolchainRegistryPath(root);
|
|
165
|
+
try {
|
|
166
|
+
const registry = JSON.parse(await readFile(registryPath, "utf8"));
|
|
167
|
+
const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
|
|
168
|
+
return {
|
|
169
|
+
installed: !!releaseRoot,
|
|
170
|
+
board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
|
|
171
|
+
version: typeof registry.version === "string" ? registry.version : undefined,
|
|
172
|
+
mode: typeof registry.mode === "string" ? registry.mode : undefined,
|
|
173
|
+
release_root: releaseRoot,
|
|
174
|
+
registry_path: registryPath,
|
|
175
|
+
install_root: root,
|
|
176
|
+
channel: typeof registry.channel === "string" ? registry.channel : undefined,
|
|
177
|
+
packages: Array.isArray(registry.packages) ? registry.packages : undefined
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return {
|
|
182
|
+
installed: false,
|
|
183
|
+
board_id: boardId,
|
|
184
|
+
registry_path: registryPath,
|
|
185
|
+
install_root: root
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export async function installLocalToolchain(options = {}) {
|
|
190
|
+
const latest = await latestLocalToolchain(options);
|
|
191
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
192
|
+
const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
|
|
193
|
+
const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
|
|
194
|
+
if (await pathExists(releaseRoot) && !options.force) {
|
|
195
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
|
|
196
|
+
if (!validation.ok) {
|
|
197
|
+
if (latest.download?.components?.length) {
|
|
198
|
+
// Component installs can upgrade an existing lower-mode install by overlaying
|
|
199
|
+
// only the newly selected components instead of deleting the whole tree.
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
|
|
207
|
+
return {
|
|
208
|
+
board_id: latest.board_id,
|
|
209
|
+
version: latest.version,
|
|
210
|
+
channel: latest.channel,
|
|
211
|
+
host: latest.host,
|
|
212
|
+
mode: installMode,
|
|
213
|
+
install_root: installRoot,
|
|
214
|
+
release_root: releaseRoot,
|
|
215
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
216
|
+
source: { kind: "directory", value: releaseRoot },
|
|
217
|
+
installed_paths: [],
|
|
218
|
+
packages: latest.packages,
|
|
219
|
+
validation
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
|
|
224
|
+
try {
|
|
225
|
+
const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
|
|
226
|
+
if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
|
|
227
|
+
await rm(releaseRoot, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
await mkdir(releaseRoot, { recursive: true });
|
|
230
|
+
const installedPaths = [];
|
|
231
|
+
for (const relativePath of INSTALL_COPY_PATHS) {
|
|
232
|
+
const sourcePath = resolve(sourceRoot.path, relativePath);
|
|
233
|
+
if (!await pathExists(sourcePath)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const targetPath = resolve(releaseRoot, relativePath);
|
|
237
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
238
|
+
await cp(sourcePath, targetPath, {
|
|
239
|
+
recursive: true,
|
|
240
|
+
force: true,
|
|
241
|
+
preserveTimestamps: true,
|
|
242
|
+
verbatimSymlinks: true
|
|
243
|
+
});
|
|
244
|
+
installedPaths.push(relativePath);
|
|
245
|
+
}
|
|
246
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
|
|
247
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
|
|
248
|
+
if (!validation.ok) {
|
|
249
|
+
throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
board_id: latest.board_id,
|
|
253
|
+
version: latest.version,
|
|
254
|
+
channel: latest.channel,
|
|
255
|
+
host: latest.host,
|
|
256
|
+
mode: installMode,
|
|
257
|
+
install_root: installRoot,
|
|
258
|
+
release_root: releaseRoot,
|
|
259
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
260
|
+
source: sourceRoot.source,
|
|
261
|
+
installed_paths: installedPaths,
|
|
262
|
+
packages: latest.packages,
|
|
263
|
+
validation
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export async function validateLocalToolchain(input) {
|
|
271
|
+
const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
|
|
272
|
+
const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
|
|
273
|
+
const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
|
|
274
|
+
const required = requiredLocalToolchainChecks(mode);
|
|
275
|
+
const checked_paths = [];
|
|
276
|
+
for (const [label, relativePath] of required) {
|
|
277
|
+
const absolutePath = resolve(resolvedRoot, relativePath);
|
|
278
|
+
checked_paths.push({
|
|
279
|
+
label,
|
|
280
|
+
path: absolutePath,
|
|
281
|
+
exists: await pathExists(absolutePath)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
|
|
285
|
+
return {
|
|
286
|
+
ok: missing_paths.length === 0,
|
|
287
|
+
mode,
|
|
288
|
+
host: {
|
|
289
|
+
platform: platform(),
|
|
290
|
+
arch: arch()
|
|
291
|
+
},
|
|
292
|
+
board_id: DEFAULT_BOARD_ID,
|
|
293
|
+
release_root: resolvedRoot,
|
|
294
|
+
checked_paths,
|
|
295
|
+
missing_paths,
|
|
296
|
+
notes: [
|
|
297
|
+
"Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
|
|
298
|
+
`This validator checks the TaishanPi LLVM local support layout for install mode ${mode}.`
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function normalizeLocalToolchainInstallMode(mode) {
|
|
303
|
+
const normalized = mode?.trim();
|
|
304
|
+
if (!normalized) {
|
|
305
|
+
return "qt";
|
|
306
|
+
}
|
|
307
|
+
if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
|
|
308
|
+
return normalized;
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
|
|
311
|
+
}
|
|
312
|
+
function requiredLocalToolchainChecks(mode) {
|
|
313
|
+
const base = [
|
|
314
|
+
["release root", "."],
|
|
315
|
+
["Rockchip mkimage", "tools/mac/mkimage"],
|
|
316
|
+
["Rockchip dumpimage", "tools/mac/dumpimage"],
|
|
317
|
+
["Rockchip resource_tool", "tools/mac/resource_tool"],
|
|
318
|
+
["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
|
|
319
|
+
["boot resource image", "boot-workspace/out/resource.img"],
|
|
320
|
+
["boot image", "boot-workspace/out/boot.img"],
|
|
321
|
+
["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
|
|
322
|
+
["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
|
|
323
|
+
["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
|
|
324
|
+
["package metadata", "meta"]
|
|
325
|
+
];
|
|
326
|
+
const compile = [
|
|
327
|
+
["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
|
|
328
|
+
["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
|
|
329
|
+
["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
|
|
330
|
+
["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
|
|
331
|
+
["host GCC libraries", "toolchain/host/lib/gcc"],
|
|
332
|
+
["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
|
|
333
|
+
["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
|
|
334
|
+
];
|
|
335
|
+
const qt = [
|
|
336
|
+
["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
|
|
337
|
+
["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
|
|
338
|
+
["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
|
|
339
|
+
["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
|
|
340
|
+
];
|
|
341
|
+
const images = [
|
|
342
|
+
["base boot image", "images/current/boot.img"],
|
|
343
|
+
["base rootfs image", "images/current/rootfs.img"],
|
|
344
|
+
["base image parameter", "images/current/parameter.txt"]
|
|
345
|
+
];
|
|
346
|
+
const full = [
|
|
347
|
+
["rootfs overlay", "userdata/rootfs"]
|
|
348
|
+
];
|
|
349
|
+
if (mode === "minimal") {
|
|
350
|
+
return base;
|
|
351
|
+
}
|
|
352
|
+
if (mode === "compile") {
|
|
353
|
+
return [...base, ...compile];
|
|
354
|
+
}
|
|
355
|
+
if (mode === "qt") {
|
|
356
|
+
return [...base, ...compile, ...qt];
|
|
357
|
+
}
|
|
358
|
+
if (mode === "images") {
|
|
359
|
+
return [...base, ...images];
|
|
360
|
+
}
|
|
361
|
+
return [...base, ...compile, ...qt, ...images, ...full];
|
|
362
|
+
}
|
|
363
|
+
export async function compileTaishanPiSingleFile(options) {
|
|
364
|
+
assertAuthenticated(options.auth);
|
|
365
|
+
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
366
|
+
const sourcePath = resolve(options.sourcePath);
|
|
367
|
+
const outputPath = resolve(options.outputPath);
|
|
368
|
+
await access(sourcePath, constants.R_OK);
|
|
369
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
370
|
+
const compiler = compilerForSource(releaseRoot, sourcePath);
|
|
371
|
+
await access(compiler, constants.X_OK);
|
|
372
|
+
const sysroot = join(releaseRoot, "toolchain", "host", "aarch64-buildroot-linux-gnu", "sysroot");
|
|
373
|
+
await access(sysroot, constants.R_OK);
|
|
374
|
+
const command = [compiler, `--sysroot=${sysroot}`, "-O2", sourcePath, "-o", outputPath];
|
|
375
|
+
const buildResult = await runCommand(command, dirname(sourcePath));
|
|
376
|
+
if (buildResult.exit_code !== 0) {
|
|
377
|
+
throw new Error(`Local compile failed with exit code ${buildResult.exit_code}: ${buildResult.stderr_tail.join("\n")}`);
|
|
378
|
+
}
|
|
379
|
+
return await localCompileResult({
|
|
380
|
+
boardId: options.boardId,
|
|
381
|
+
operation: "local.compile.single_file",
|
|
382
|
+
releaseRoot,
|
|
383
|
+
accountId: options.accountId,
|
|
384
|
+
auth: options.auth,
|
|
385
|
+
sourcePath,
|
|
386
|
+
artifactPath: outputPath,
|
|
387
|
+
commands: [buildResult]
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
export async function buildTaishanPiQtSmoke(options) {
|
|
391
|
+
assertAuthenticated(options.auth);
|
|
392
|
+
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
393
|
+
const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
|
|
394
|
+
const buildDir = resolve(options.buildDir);
|
|
395
|
+
const targetName = options.targetName ?? "qt_llvm_smoke";
|
|
396
|
+
const qtCmake = join(releaseRoot, "qt-target", "qt6-rk3566-llvm-6.8.3", "bin", "qt-cmake");
|
|
397
|
+
await access(join(sourceDir, "CMakeLists.txt"), constants.R_OK);
|
|
398
|
+
await access(qtCmake, constants.X_OK);
|
|
399
|
+
await mkdir(buildDir, { recursive: true });
|
|
400
|
+
const configure = await runCommand([
|
|
401
|
+
qtCmake,
|
|
402
|
+
"-S",
|
|
403
|
+
sourceDir,
|
|
404
|
+
"-B",
|
|
405
|
+
buildDir,
|
|
406
|
+
"-G",
|
|
407
|
+
"Ninja",
|
|
408
|
+
"-DCMAKE_BUILD_TYPE=Release"
|
|
409
|
+
], sourceDir);
|
|
410
|
+
if (configure.exit_code !== 0) {
|
|
411
|
+
throw new Error(`Qt smoke configure failed with exit code ${configure.exit_code}: ${configure.stderr_tail.join("\n")}`);
|
|
412
|
+
}
|
|
413
|
+
const build = await runCommand(["cmake", "--build", buildDir, "--parallel"], sourceDir);
|
|
414
|
+
if (build.exit_code !== 0) {
|
|
415
|
+
throw new Error(`Qt smoke build failed with exit code ${build.exit_code}: ${build.stderr_tail.join("\n")}`);
|
|
416
|
+
}
|
|
417
|
+
const artifactPath = join(buildDir, targetName);
|
|
418
|
+
return await localCompileResult({
|
|
419
|
+
boardId: DEFAULT_BOARD_ID,
|
|
420
|
+
operation: "local.build.qt_smoke",
|
|
421
|
+
releaseRoot,
|
|
422
|
+
accountId: options.accountId,
|
|
423
|
+
auth: options.auth,
|
|
424
|
+
sourcePath: sourceDir,
|
|
425
|
+
buildDir,
|
|
426
|
+
artifactPath,
|
|
427
|
+
commands: [configure, build]
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
431
|
+
const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
|
|
432
|
+
const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
|
|
433
|
+
if (!candidateRoot) {
|
|
434
|
+
return {
|
|
435
|
+
channel: BUILT_IN_CHANNEL,
|
|
436
|
+
manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
|
|
437
|
+
metadataRoot: undefined
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const root = resolve(candidateRoot);
|
|
441
|
+
const channelPath = join(root, "channels", channelName, "index.json");
|
|
442
|
+
const channel = JSON.parse(await readFile(channelPath, "utf8"));
|
|
443
|
+
if (channel.schema !== "embedlabs.channel.v1") {
|
|
444
|
+
throw new Error(`Unexpected local toolchain channel schema ${channel.schema}.`);
|
|
445
|
+
}
|
|
446
|
+
const manifests = new Map();
|
|
447
|
+
for (const entry of channel.packages) {
|
|
448
|
+
const manifestPath = entry.manifest
|
|
449
|
+
? resolve(dirname(channelPath), entry.manifest)
|
|
450
|
+
: join(root, "manifests", entry.id, entry.version, "manifest.json");
|
|
451
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
452
|
+
if (manifest.schema !== "embedlabs.package.v1") {
|
|
453
|
+
throw new Error(`Unexpected local toolchain manifest schema for ${entry.id}: ${manifest.schema}.`);
|
|
454
|
+
}
|
|
455
|
+
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
456
|
+
throw new Error(`Local toolchain manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
457
|
+
}
|
|
458
|
+
manifests.set(manifest.id, manifest);
|
|
459
|
+
}
|
|
460
|
+
return { channel, manifests, metadataRoot: root };
|
|
461
|
+
}
|
|
462
|
+
async function resolveLocalToolchainDownloadPlan(input) {
|
|
463
|
+
const channelUrl = downloadChannelUrl(input.channel);
|
|
464
|
+
const channel = await fetchJson(channelUrl);
|
|
465
|
+
if (channel.schema !== "embedlabs.download-channel.v1") {
|
|
466
|
+
throw new Error(`Unexpected download channel schema ${channel.schema}.`);
|
|
467
|
+
}
|
|
468
|
+
const entry = (channel.packages ?? []).find((item) => {
|
|
469
|
+
return item.board_id === input.boardId
|
|
470
|
+
&& item.host === input.host
|
|
471
|
+
&& item.toolchain === input.toolchain
|
|
472
|
+
&& (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
|
|
473
|
+
});
|
|
474
|
+
if (!entry?.manifest) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
const manifestUrl = new URL(entry.manifest, channelUrl).toString();
|
|
478
|
+
const manifest = await fetchJson(manifestUrl);
|
|
479
|
+
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
480
|
+
throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
481
|
+
}
|
|
482
|
+
if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
|
|
483
|
+
throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
|
|
484
|
+
}
|
|
485
|
+
if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
|
|
486
|
+
&& (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
|
|
487
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
|
|
488
|
+
}
|
|
489
|
+
const mirrors = manifest.archive
|
|
490
|
+
? orderDownloadMirrors((manifest.mirrors ?? [])
|
|
491
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
492
|
+
.map((mirror) => ({
|
|
493
|
+
kind: mirror.kind || "unknown",
|
|
494
|
+
enabled: mirror.enabled !== false,
|
|
495
|
+
url: mirror.url,
|
|
496
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
|
|
497
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
|
|
498
|
+
})), manifest.download_policy?.preferred_order)
|
|
499
|
+
: [];
|
|
500
|
+
const components = (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl));
|
|
501
|
+
const first = mirrors[0];
|
|
502
|
+
if (!first && components.length === 0) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
channel_url: channelUrl,
|
|
507
|
+
manifest_url: manifestUrl,
|
|
508
|
+
package_id: manifest.id,
|
|
509
|
+
version: manifest.version,
|
|
510
|
+
board_id: input.boardId,
|
|
511
|
+
host: input.host,
|
|
512
|
+
toolchain: input.toolchain,
|
|
513
|
+
source_url: first?.url,
|
|
514
|
+
mirror_kind: first?.kind,
|
|
515
|
+
archive: manifest.archive ? {
|
|
516
|
+
file: manifest.archive.file,
|
|
517
|
+
size_bytes: manifest.archive.size_bytes,
|
|
518
|
+
sha256: manifest.archive.sha256,
|
|
519
|
+
content_type: manifest.archive.content_type
|
|
520
|
+
} : undefined,
|
|
521
|
+
mirrors,
|
|
522
|
+
components: components.length > 0 ? components : undefined,
|
|
523
|
+
default_mode: manifest.download_policy?.default_mode
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function normalizeDownloadComponent(component, manifest, manifestUrl) {
|
|
527
|
+
if (!component.id || !component.version) {
|
|
528
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
|
|
529
|
+
}
|
|
530
|
+
if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
|
|
531
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
|
|
532
|
+
}
|
|
533
|
+
const mirrors = orderDownloadMirrors((component.mirrors ?? [])
|
|
534
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
535
|
+
.map((mirror) => ({
|
|
536
|
+
kind: mirror.kind || "unknown",
|
|
537
|
+
enabled: mirror.enabled !== false,
|
|
538
|
+
url: new URL(mirror.url, manifestUrl).toString(),
|
|
539
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
|
|
540
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
|
|
541
|
+
})), manifest.download_policy?.preferred_order);
|
|
542
|
+
const first = mirrors[0];
|
|
543
|
+
if (!first) {
|
|
544
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
id: component.id,
|
|
548
|
+
version: component.version,
|
|
549
|
+
role: component.role,
|
|
550
|
+
install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
|
|
551
|
+
archive: {
|
|
552
|
+
file: component.archive.file,
|
|
553
|
+
size_bytes: component.archive.size_bytes,
|
|
554
|
+
sha256: component.archive.sha256,
|
|
555
|
+
content_type: component.archive.content_type
|
|
556
|
+
},
|
|
557
|
+
source_url: first.url,
|
|
558
|
+
mirror_kind: first.kind,
|
|
559
|
+
mirrors
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function orderDownloadMirrors(mirrors, preferredOrder) {
|
|
563
|
+
const preference = preferredOrder?.length
|
|
564
|
+
? preferredOrder
|
|
565
|
+
: ["github_release", "embedlabs_cdn", "cloudfront", "embedlabs_server"];
|
|
566
|
+
return [...mirrors].sort((left, right) => mirrorRank(left.kind, preference) - mirrorRank(right.kind, preference));
|
|
567
|
+
}
|
|
568
|
+
function mirrorRank(kind, preferredOrder) {
|
|
569
|
+
const index = preferredOrder.indexOf(kind);
|
|
570
|
+
return index >= 0 ? index : preferredOrder.length + 1;
|
|
571
|
+
}
|
|
572
|
+
async function fetchJson(url) {
|
|
573
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
|
|
574
|
+
if (!response.ok) {
|
|
575
|
+
throw new Error(`HTTP ${response.status} while loading ${url}`);
|
|
576
|
+
}
|
|
577
|
+
return await response.json();
|
|
578
|
+
}
|
|
579
|
+
function downloadChannelUrl(channelName) {
|
|
580
|
+
const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
|
|
581
|
+
|| process.env.EMBEDLABS_DOWNLOAD_CHANNEL_URL?.trim();
|
|
582
|
+
if (explicit) {
|
|
583
|
+
return explicit;
|
|
584
|
+
}
|
|
585
|
+
return `${downloadBaseUrl()}/downloads/metadata/channels/${encodeURIComponent(channelName)}/index.json`;
|
|
586
|
+
}
|
|
587
|
+
function downloadBaseUrl() {
|
|
588
|
+
return trimTrailingSlash(process.env.EMBED_DOWNLOAD_BASE_URL?.trim()
|
|
589
|
+
|| process.env.EMBEDLABS_DOWNLOAD_BASE_URL?.trim()
|
|
590
|
+
|| DEFAULT_DOWNLOAD_BASE_URL);
|
|
591
|
+
}
|
|
592
|
+
function trimTrailingSlash(value) {
|
|
593
|
+
return value.replace(/\/+$/, "");
|
|
594
|
+
}
|
|
595
|
+
function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
|
|
596
|
+
if (seen.has(packageId)) {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
seen.add(packageId);
|
|
600
|
+
const manifest = manifests.get(packageId);
|
|
601
|
+
if (!manifest) {
|
|
602
|
+
throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
|
|
603
|
+
}
|
|
604
|
+
const refs = [];
|
|
605
|
+
for (const requirement of manifest.requires ?? []) {
|
|
606
|
+
refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
|
|
607
|
+
}
|
|
608
|
+
const channelEntry = channel.packages.find((item) => item.id === packageId);
|
|
609
|
+
refs.push({
|
|
610
|
+
id: packageId,
|
|
611
|
+
version: manifest.version,
|
|
612
|
+
manifest: channelEntry?.manifest
|
|
613
|
+
});
|
|
614
|
+
return refs;
|
|
615
|
+
}
|
|
616
|
+
function boardPackageIdFor(boardId) {
|
|
617
|
+
if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
|
|
618
|
+
return "embedlabs.board.taishanpi.1m-rk3566";
|
|
619
|
+
}
|
|
620
|
+
if (boardId.startsWith("embedlabs.board.")) {
|
|
621
|
+
return boardId;
|
|
622
|
+
}
|
|
623
|
+
throw new Error(`Unsupported local toolchain board ${boardId}.`);
|
|
624
|
+
}
|
|
625
|
+
function hostId() {
|
|
626
|
+
if (platform() === "darwin" && arch() === "arm64") {
|
|
627
|
+
return "darwin-arm64";
|
|
628
|
+
}
|
|
629
|
+
if (platform() === "linux" && arch() === "x64") {
|
|
630
|
+
return "linux-x86_64";
|
|
631
|
+
}
|
|
632
|
+
return `${platform()}-${arch()}`;
|
|
633
|
+
}
|
|
634
|
+
function resolveInstallRoot(installRoot) {
|
|
635
|
+
return resolve(installRoot
|
|
636
|
+
|| process.env.EMBEDLABS_HOME?.trim()
|
|
637
|
+
|| join(homedir(), ".embedlabs"));
|
|
638
|
+
}
|
|
639
|
+
function localToolchainRegistryPath(installRoot) {
|
|
640
|
+
return join(installRoot, "registry", "local-toolchains.json");
|
|
641
|
+
}
|
|
642
|
+
async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
|
|
643
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
644
|
+
await mkdir(dirname(registryPath), { recursive: true });
|
|
645
|
+
await writeFile(registryPath, `${JSON.stringify({
|
|
646
|
+
installed: true,
|
|
647
|
+
board_id: latest.board_id,
|
|
648
|
+
version: latest.version,
|
|
649
|
+
channel: latest.channel,
|
|
650
|
+
host: latest.host,
|
|
651
|
+
mode,
|
|
652
|
+
release_root: releaseRoot,
|
|
653
|
+
packages: latest.packages,
|
|
654
|
+
source,
|
|
655
|
+
installed_components: source?.components,
|
|
656
|
+
updated_at: new Date().toISOString()
|
|
657
|
+
}, null, 2)}\n`, "utf8");
|
|
658
|
+
}
|
|
659
|
+
async function resolveLocalReleaseRoot(releaseRoot) {
|
|
660
|
+
if (releaseRoot?.trim()) {
|
|
661
|
+
return resolve(releaseRoot);
|
|
662
|
+
}
|
|
663
|
+
const envRoot = process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim() || process.env.EMBEDLABS_RELEASE_ROOT?.trim();
|
|
664
|
+
if (envRoot) {
|
|
665
|
+
return resolve(envRoot);
|
|
666
|
+
}
|
|
667
|
+
const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
|
|
668
|
+
if (current.release_root) {
|
|
669
|
+
return resolve(current.release_root);
|
|
670
|
+
}
|
|
671
|
+
return resolve(DEFAULT_RELEASE_ROOT);
|
|
672
|
+
}
|
|
673
|
+
async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir) {
|
|
674
|
+
if (options.sourceReleaseRoot) {
|
|
675
|
+
return {
|
|
676
|
+
path: resolve(options.sourceReleaseRoot),
|
|
677
|
+
source: { kind: "directory", value: resolve(options.sourceReleaseRoot) }
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (options.sourceUrl) {
|
|
681
|
+
const downloadedPath = await downloadToolchainArchive(options.sourceUrl, installRoot);
|
|
682
|
+
const extractRoot = join(tempDir, "extract");
|
|
683
|
+
await mkdir(extractRoot, { recursive: true });
|
|
684
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
685
|
+
if (extracted.exit_code !== 0) {
|
|
686
|
+
throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
path: await findReleaseRoot(extractRoot),
|
|
690
|
+
source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (latest.download?.components?.length) {
|
|
694
|
+
return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
|
|
695
|
+
}
|
|
696
|
+
if (latest.download) {
|
|
697
|
+
const failures = [];
|
|
698
|
+
for (const mirror of latest.download.mirrors) {
|
|
699
|
+
if (!mirror.enabled) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (!latest.download.archive) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
|
|
706
|
+
try {
|
|
707
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
708
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
709
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
710
|
+
});
|
|
711
|
+
await rm(extractRoot, { recursive: true, force: true });
|
|
712
|
+
await mkdir(extractRoot, { recursive: true });
|
|
713
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
714
|
+
if (extracted.exit_code !== 0) {
|
|
715
|
+
throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
path: await findReleaseRoot(extractRoot),
|
|
719
|
+
source: {
|
|
720
|
+
kind: "url",
|
|
721
|
+
value: mirror.url,
|
|
722
|
+
downloaded_path: downloadedPath,
|
|
723
|
+
mirror_kind: mirror.kind,
|
|
724
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
725
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
failures.push(`${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (failures.length > 0 && !await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
734
|
+
throw new Error(`Could not install local toolchain from download mirrors: ${failures.join("; ")}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
738
|
+
return {
|
|
739
|
+
path: resolve(DEFAULT_RELEASE_ROOT),
|
|
740
|
+
source: { kind: "directory", value: resolve(DEFAULT_RELEASE_ROOT) }
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
throw new Error("Local toolchain install could not resolve a source. Pass --source-url <tar.gz>, --source-release-root <path>, or configure EMBED_DOWNLOAD_BASE_URL.");
|
|
744
|
+
}
|
|
745
|
+
async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
|
|
746
|
+
const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
|
|
747
|
+
const components = selectedDownloadComponents(download.components ?? [], mode);
|
|
748
|
+
if (components.length === 0) {
|
|
749
|
+
throw new Error(`No local toolchain components selected for mode ${mode}.`);
|
|
750
|
+
}
|
|
751
|
+
const extractRoot = join(tempDir, "extract-components");
|
|
752
|
+
await mkdir(extractRoot, { recursive: true });
|
|
753
|
+
const installedComponents = [];
|
|
754
|
+
const failures = [];
|
|
755
|
+
for (const component of components) {
|
|
756
|
+
let installed = false;
|
|
757
|
+
for (const mirror of component.mirrors) {
|
|
758
|
+
if (!mirror.enabled) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
763
|
+
sha256: mirror.sha256 ?? component.archive.sha256,
|
|
764
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes
|
|
765
|
+
});
|
|
766
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
767
|
+
if (extracted.exit_code !== 0) {
|
|
768
|
+
throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
|
|
769
|
+
}
|
|
770
|
+
installedComponents.push({
|
|
771
|
+
id: component.id,
|
|
772
|
+
version: component.version,
|
|
773
|
+
role: component.role,
|
|
774
|
+
archive_file: component.archive.file,
|
|
775
|
+
mirror_kind: mirror.kind,
|
|
776
|
+
downloaded_path: downloadedPath,
|
|
777
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
|
|
778
|
+
sha256: mirror.sha256 ?? component.archive.sha256
|
|
779
|
+
});
|
|
780
|
+
installed = true;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (!installed) {
|
|
788
|
+
throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
path: extractRoot,
|
|
793
|
+
source: {
|
|
794
|
+
kind: "components",
|
|
795
|
+
value: download.manifest_url,
|
|
796
|
+
mirror_kind: "components",
|
|
797
|
+
size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
|
|
798
|
+
components: installedComponents
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function selectedDownloadComponents(components, mode) {
|
|
803
|
+
return components.filter((component) => {
|
|
804
|
+
if (!component.install_modes?.length) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
return component.install_modes.includes(mode);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
|
|
811
|
+
const downloadsDir = join(installRoot, "cache", "downloads");
|
|
812
|
+
await mkdir(downloadsDir, { recursive: true });
|
|
813
|
+
const parsed = new URL(sourceUrl);
|
|
814
|
+
const filename = basename(parsed.pathname) || `local-toolchain-${Date.now()}.tar.gz`;
|
|
815
|
+
const outputPath = join(downloadsDir, filename);
|
|
816
|
+
if (parsed.protocol === "file:") {
|
|
817
|
+
await cp(fileURLToPath(parsed), outputPath, { force: true });
|
|
818
|
+
return outputPath;
|
|
819
|
+
}
|
|
820
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
821
|
+
throw new Error(`Unsupported local toolchain download URL protocol: ${parsed.protocol}`);
|
|
822
|
+
}
|
|
823
|
+
const partialPath = `${outputPath}.part`;
|
|
824
|
+
const expectedSize = expected?.size_bytes;
|
|
825
|
+
const existingComplete = await fileSize(outputPath);
|
|
826
|
+
const remoteSize = await remoteContentLength(sourceUrl);
|
|
827
|
+
const targetSize = expectedSize ?? remoteSize;
|
|
828
|
+
if (existingComplete > 0 && targetSize !== undefined && existingComplete === targetSize) {
|
|
829
|
+
if (expected?.sha256) {
|
|
830
|
+
const actual = await sha256(outputPath);
|
|
831
|
+
if (actual === expected.sha256) {
|
|
832
|
+
return outputPath;
|
|
833
|
+
}
|
|
834
|
+
await rm(outputPath, { force: true });
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
return outputPath;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
let resumeFrom = await fileSize(partialPath);
|
|
841
|
+
const headers = new Headers();
|
|
842
|
+
if (resumeFrom > 0) {
|
|
843
|
+
headers.set("range", `bytes=${resumeFrom}-`);
|
|
844
|
+
}
|
|
845
|
+
let response = await fetch(sourceUrl, { headers });
|
|
846
|
+
if (resumeFrom > 0 && response.status !== 206) {
|
|
847
|
+
await rm(partialPath, { force: true });
|
|
848
|
+
resumeFrom = 0;
|
|
849
|
+
response = await fetch(sourceUrl);
|
|
850
|
+
}
|
|
851
|
+
if (!response.ok) {
|
|
852
|
+
throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
|
|
853
|
+
}
|
|
854
|
+
if (!response.body) {
|
|
855
|
+
throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
|
|
856
|
+
}
|
|
857
|
+
const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
|
|
858
|
+
await pipeline(Readable.fromWeb(response.body), writeStream);
|
|
859
|
+
const downloadedSize = await fileSize(partialPath);
|
|
860
|
+
if (targetSize !== undefined && downloadedSize !== targetSize) {
|
|
861
|
+
throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
|
|
862
|
+
}
|
|
863
|
+
await rename(partialPath, outputPath);
|
|
864
|
+
if (expected?.sha256) {
|
|
865
|
+
const actual = await sha256(outputPath);
|
|
866
|
+
if (actual !== expected.sha256) {
|
|
867
|
+
await rm(outputPath, { force: true });
|
|
868
|
+
throw new Error(`Local toolchain download SHA256 mismatch: expected ${expected.sha256}, got ${actual}.`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return outputPath;
|
|
872
|
+
}
|
|
873
|
+
async function remoteContentLength(sourceUrl) {
|
|
874
|
+
try {
|
|
875
|
+
const response = await fetch(sourceUrl, { method: "HEAD" });
|
|
876
|
+
if (!response.ok) {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
const length = response.headers.get("content-length");
|
|
880
|
+
if (!length) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
const parsed = Number.parseInt(length, 10);
|
|
884
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async function fileSize(filePath) {
|
|
891
|
+
try {
|
|
892
|
+
return (await stat(filePath)).size;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return 0;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function safeFileToken(value) {
|
|
899
|
+
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_") || "mirror";
|
|
900
|
+
}
|
|
901
|
+
async function findReleaseRoot(extractRoot) {
|
|
902
|
+
if (await pathExists(join(extractRoot, "toolchain"))) {
|
|
903
|
+
return extractRoot;
|
|
904
|
+
}
|
|
905
|
+
const entries = await readdir(extractRoot);
|
|
906
|
+
for (const entry of entries) {
|
|
907
|
+
const candidate = join(extractRoot, entry);
|
|
908
|
+
try {
|
|
909
|
+
const info = await stat(candidate);
|
|
910
|
+
if (info.isDirectory() && await pathExists(join(candidate, "toolchain"))) {
|
|
911
|
+
return candidate;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// Ignore entries that disappear during extraction cleanup.
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
throw new Error("Downloaded local toolchain archive did not contain a release root with toolchain/.");
|
|
919
|
+
}
|
|
920
|
+
function compilerForSource(releaseRoot, sourcePath) {
|
|
921
|
+
const extension = extname(sourcePath).toLowerCase();
|
|
922
|
+
const binDir = join(releaseRoot, "toolchain", "llvm-cross", "bin");
|
|
923
|
+
if (extension === ".c") {
|
|
924
|
+
return join(binDir, "aarch64-linux-gnu-gcc");
|
|
925
|
+
}
|
|
926
|
+
if ([".cc", ".cpp", ".cxx"].includes(extension)) {
|
|
927
|
+
return join(binDir, "aarch64-linux-gnu-g++");
|
|
928
|
+
}
|
|
929
|
+
throw new Error(`Unsupported source extension ${extension || "<none>"}; use .c, .cc, .cpp, or .cxx.`);
|
|
930
|
+
}
|
|
931
|
+
async function localCompileResult(input) {
|
|
932
|
+
await access(input.artifactPath, constants.R_OK);
|
|
933
|
+
const artifactInfo = await stat(input.artifactPath);
|
|
934
|
+
const artifactSha256 = await sha256(input.artifactPath);
|
|
935
|
+
const fileInfo = await fileInfoFor(input.artifactPath);
|
|
936
|
+
const manifestPath = `${input.artifactPath}.embedlabs-local-build.json`;
|
|
937
|
+
const result = {
|
|
938
|
+
board_id: input.boardId,
|
|
939
|
+
operation: input.operation,
|
|
940
|
+
release_root: input.releaseRoot,
|
|
941
|
+
account_id: input.accountId,
|
|
942
|
+
auth: input.auth,
|
|
943
|
+
source_path: input.sourcePath,
|
|
944
|
+
build_dir: input.buildDir,
|
|
945
|
+
artifact_path: input.artifactPath,
|
|
946
|
+
artifact_name: basename(input.artifactPath),
|
|
947
|
+
artifact_size_bytes: artifactInfo.size,
|
|
948
|
+
artifact_sha256: artifactSha256,
|
|
949
|
+
file_info: fileInfo,
|
|
950
|
+
commands: input.commands,
|
|
951
|
+
manifest_path: manifestPath
|
|
952
|
+
};
|
|
953
|
+
await writeFile(manifestPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
function assertAuthenticated(auth) {
|
|
957
|
+
if (!auth.authenticated) {
|
|
958
|
+
throw new Error("Embed Labs auth is required for local toolchain builds. Run: embedlabs auth login --token <user-api-key>");
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function runCommand(command, cwd) {
|
|
962
|
+
return await new Promise((resolve) => {
|
|
963
|
+
const child = spawn(command[0], command.slice(1), {
|
|
964
|
+
cwd,
|
|
965
|
+
env: process.env,
|
|
966
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
967
|
+
});
|
|
968
|
+
let stdout = "";
|
|
969
|
+
let stderr = "";
|
|
970
|
+
child.stdout.setEncoding("utf8");
|
|
971
|
+
child.stderr.setEncoding("utf8");
|
|
972
|
+
child.stdout.on("data", (chunk) => {
|
|
973
|
+
stdout += chunk;
|
|
974
|
+
});
|
|
975
|
+
child.stderr.on("data", (chunk) => {
|
|
976
|
+
stderr += chunk;
|
|
977
|
+
});
|
|
978
|
+
child.on("error", (error) => {
|
|
979
|
+
stderr += `${error.message}\n`;
|
|
980
|
+
resolve({
|
|
981
|
+
command,
|
|
982
|
+
cwd,
|
|
983
|
+
exit_code: 127,
|
|
984
|
+
stdout_tail: [],
|
|
985
|
+
stderr_tail: tailLines(stderr)
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
child.on("close", (code) => {
|
|
989
|
+
resolve({
|
|
990
|
+
command,
|
|
991
|
+
cwd,
|
|
992
|
+
exit_code: code ?? 1,
|
|
993
|
+
stdout_tail: tailLines(stdout),
|
|
994
|
+
stderr_tail: tailLines(stderr)
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
async function fileInfoFor(filePath) {
|
|
1000
|
+
const result = await runCommand(["file", filePath], dirname(filePath));
|
|
1001
|
+
return result.exit_code === 0 ? result.stdout_tail.join("\n") : undefined;
|
|
1002
|
+
}
|
|
1003
|
+
async function sha256(filePath) {
|
|
1004
|
+
const content = await readFile(filePath);
|
|
1005
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1006
|
+
}
|
|
1007
|
+
async function pathExists(filePath) {
|
|
1008
|
+
try {
|
|
1009
|
+
await access(filePath);
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function tailLines(text, maxLines = 80) {
|
|
1017
|
+
return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
|
|
1018
|
+
}
|
|
1019
|
+
//# sourceMappingURL=local-toolchain.js.map
|