@kvell007/embed-labs-cli 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +44 -3
- package/dist/index.js +2022 -81
- package/dist/index.js.map +1 -1
- package/dist/local-toolchain.d.ts +198 -1
- package/dist/local-toolchain.js +980 -22
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +3 -3
package/dist/local-toolchain.js
CHANGED
|
@@ -1,27 +1,369 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
-
import { arch, platform } from "node:os";
|
|
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
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";
|
|
7
10
|
const DEFAULT_RELEASE_ROOT = "/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal";
|
|
8
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"];
|
|
9
123
|
export function defaultLocalReleaseRoot() {
|
|
10
|
-
return
|
|
124
|
+
return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
|
|
125
|
+
|| process.env.EMBEDLABS_RELEASE_ROOT?.trim()
|
|
126
|
+
|| DEFAULT_RELEASE_ROOT;
|
|
11
127
|
}
|
|
12
|
-
export async function
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 listLocalToolchainEnvironments(options = {}) {
|
|
190
|
+
const channelName = options.channel ?? DEFAULT_CHANNEL;
|
|
191
|
+
const host = hostId();
|
|
192
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
193
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
194
|
+
const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
|
|
195
|
+
const current = await currentLocalToolchain(installRoot);
|
|
196
|
+
const boardManifests = [...manifests.values()]
|
|
197
|
+
.filter((manifest) => manifest.kind === "board")
|
|
198
|
+
.filter((manifest) => {
|
|
199
|
+
if (!options.boardId) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
const normalizedFilter = normalizeBoardId(options.boardId);
|
|
203
|
+
return boardIdForPackageManifest(manifest) === normalizedFilter
|
|
204
|
+
|| manifest.id === options.boardId
|
|
205
|
+
|| manifest.id === packageIdForBoardFilter(options.boardId);
|
|
206
|
+
})
|
|
207
|
+
.sort((left, right) => boardIdForPackageManifest(left).localeCompare(boardIdForPackageManifest(right)));
|
|
208
|
+
const environments = [];
|
|
209
|
+
for (const board of boardManifests) {
|
|
210
|
+
const boardId = boardIdForPackageManifest(board);
|
|
211
|
+
const packages = resolvePackageRefs(board.id, channel, manifests);
|
|
212
|
+
const hostSupport = packageHostSupport(packages, manifests, host);
|
|
213
|
+
let download;
|
|
214
|
+
let downloadError;
|
|
215
|
+
try {
|
|
216
|
+
download = await resolveLocalToolchainDownloadPlan({
|
|
217
|
+
boardId,
|
|
218
|
+
channel: channelName,
|
|
219
|
+
host,
|
|
220
|
+
toolchain: "llvm"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
downloadError = error instanceof Error ? error.message : String(error);
|
|
225
|
+
}
|
|
226
|
+
const latestVersion = download?.version ?? board.version;
|
|
227
|
+
const installed = current.installed && normalizeBoardId(current.board_id) === normalizeBoardId(boardId)
|
|
228
|
+
? {
|
|
229
|
+
version: current.version,
|
|
230
|
+
channel: current.channel,
|
|
231
|
+
mode: current.mode,
|
|
232
|
+
release_root: current.release_root
|
|
233
|
+
}
|
|
234
|
+
: undefined;
|
|
235
|
+
const updateAvailable = !!installed?.version && installed.version !== latestVersion;
|
|
236
|
+
const status = !hostSupport.supported
|
|
237
|
+
? "unsupported_host"
|
|
238
|
+
: updateAvailable
|
|
239
|
+
? "update_available"
|
|
240
|
+
: installed
|
|
241
|
+
? "installed"
|
|
242
|
+
: "available";
|
|
243
|
+
const mode = download?.default_mode ?? "qt";
|
|
244
|
+
environments.push({
|
|
245
|
+
board_id: boardId,
|
|
246
|
+
package_id: board.id,
|
|
247
|
+
display_name: board.display_name || [board.board, board.variant].filter(Boolean).join(" ") || boardId,
|
|
248
|
+
family: board.family,
|
|
249
|
+
variant: board.variant,
|
|
250
|
+
channel: channel.channel,
|
|
251
|
+
host,
|
|
252
|
+
status,
|
|
253
|
+
supported_host: hostSupport.supported,
|
|
254
|
+
unsupported_packages: hostSupport.unsupportedPackages,
|
|
255
|
+
install_modes: [...LOCAL_TOOLCHAIN_INSTALL_MODES],
|
|
256
|
+
installed,
|
|
257
|
+
latest: {
|
|
258
|
+
version: latestVersion,
|
|
259
|
+
default_mode: download?.default_mode,
|
|
260
|
+
source_url: download?.source_url,
|
|
261
|
+
manifest_url: download?.manifest_url,
|
|
262
|
+
component_count: download?.components?.length,
|
|
263
|
+
download_error: downloadError
|
|
264
|
+
},
|
|
265
|
+
packages,
|
|
266
|
+
components: download?.components?.map(localToolchainEnvironmentComponent),
|
|
267
|
+
install_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode}`,
|
|
268
|
+
update_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode} --force`,
|
|
269
|
+
notes: environmentNotes({ status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
host,
|
|
274
|
+
channel: channel.channel,
|
|
275
|
+
metadata_root: metadataRoot,
|
|
276
|
+
install_root: installRoot,
|
|
277
|
+
registry_path: registryPath,
|
|
278
|
+
environments
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export async function installLocalToolchain(options = {}) {
|
|
282
|
+
const latest = await latestLocalToolchain(options);
|
|
283
|
+
const installRoot = resolveInstallRoot(options.installRoot);
|
|
284
|
+
const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
|
|
285
|
+
const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
|
|
286
|
+
if (await pathExists(releaseRoot) && !options.force) {
|
|
287
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
|
|
288
|
+
if (!validation.ok) {
|
|
289
|
+
if (latest.download?.components?.length) {
|
|
290
|
+
// Component installs can upgrade an existing lower-mode install by overlaying
|
|
291
|
+
// only the newly selected components instead of deleting the whole tree.
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
throw new Error(`Existing local toolchain is incomplete at ${releaseRoot}; rerun with --force.`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode);
|
|
299
|
+
return {
|
|
300
|
+
board_id: latest.board_id,
|
|
301
|
+
version: latest.version,
|
|
302
|
+
channel: latest.channel,
|
|
303
|
+
host: latest.host,
|
|
304
|
+
mode: installMode,
|
|
305
|
+
install_root: installRoot,
|
|
306
|
+
release_root: releaseRoot,
|
|
307
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
308
|
+
source: { kind: "directory", value: releaseRoot },
|
|
309
|
+
installed_paths: [],
|
|
310
|
+
packages: latest.packages,
|
|
311
|
+
validation
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const tempDir = await mkdtemp(join(tmpdir(), "embedlabs-local-toolchain-install-"));
|
|
316
|
+
try {
|
|
317
|
+
const sourceRoot = await sourceReleaseRootForInstall(options, latest, installRoot, tempDir);
|
|
318
|
+
if (options.force || !await pathExists(releaseRoot) || sourceRoot.source.kind !== "components") {
|
|
319
|
+
await rm(releaseRoot, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
await mkdir(releaseRoot, { recursive: true });
|
|
322
|
+
const installedPaths = [];
|
|
323
|
+
for (const relativePath of INSTALL_COPY_PATHS) {
|
|
324
|
+
const sourcePath = resolve(sourceRoot.path, relativePath);
|
|
325
|
+
if (!await pathExists(sourcePath)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const targetPath = resolve(releaseRoot, relativePath);
|
|
329
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
330
|
+
await cp(sourcePath, targetPath, {
|
|
331
|
+
recursive: true,
|
|
332
|
+
force: true,
|
|
333
|
+
preserveTimestamps: true,
|
|
334
|
+
verbatimSymlinks: true
|
|
335
|
+
});
|
|
336
|
+
installedPaths.push(relativePath);
|
|
337
|
+
}
|
|
338
|
+
await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
|
|
339
|
+
const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
|
|
340
|
+
if (!validation.ok) {
|
|
341
|
+
throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
board_id: latest.board_id,
|
|
345
|
+
version: latest.version,
|
|
346
|
+
channel: latest.channel,
|
|
347
|
+
host: latest.host,
|
|
348
|
+
mode: installMode,
|
|
349
|
+
install_root: installRoot,
|
|
350
|
+
release_root: releaseRoot,
|
|
351
|
+
registry_path: localToolchainRegistryPath(installRoot),
|
|
352
|
+
source: sourceRoot.source,
|
|
353
|
+
installed_paths: installedPaths,
|
|
354
|
+
packages: latest.packages,
|
|
355
|
+
validation
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export async function validateLocalToolchain(input) {
|
|
363
|
+
const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
|
|
364
|
+
const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
|
|
365
|
+
const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
|
|
366
|
+
const required = requiredLocalToolchainChecks(mode);
|
|
25
367
|
const checked_paths = [];
|
|
26
368
|
for (const [label, relativePath] of required) {
|
|
27
369
|
const absolutePath = resolve(resolvedRoot, relativePath);
|
|
@@ -34,23 +376,85 @@ export async function validateLocalToolchain(releaseRoot = DEFAULT_RELEASE_ROOT)
|
|
|
34
376
|
const missing_paths = checked_paths.filter((item) => !item.exists).map((item) => item.path);
|
|
35
377
|
return {
|
|
36
378
|
ok: missing_paths.length === 0,
|
|
379
|
+
mode,
|
|
37
380
|
host: {
|
|
38
381
|
platform: platform(),
|
|
39
382
|
arch: arch()
|
|
40
383
|
},
|
|
41
|
-
board_id:
|
|
384
|
+
board_id: DEFAULT_BOARD_ID,
|
|
42
385
|
release_root: resolvedRoot,
|
|
43
386
|
checked_paths,
|
|
44
387
|
missing_paths,
|
|
45
388
|
notes: [
|
|
46
389
|
"Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
|
|
47
|
-
|
|
390
|
+
`This validator checks the TaishanPi LLVM local support layout for install mode ${mode}.`
|
|
48
391
|
]
|
|
49
392
|
};
|
|
50
393
|
}
|
|
394
|
+
function normalizeLocalToolchainInstallMode(mode) {
|
|
395
|
+
const normalized = mode?.trim();
|
|
396
|
+
if (!normalized) {
|
|
397
|
+
return "qt";
|
|
398
|
+
}
|
|
399
|
+
if (LOCAL_TOOLCHAIN_INSTALL_MODES.includes(normalized)) {
|
|
400
|
+
return normalized;
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
|
|
403
|
+
}
|
|
404
|
+
function requiredLocalToolchainChecks(mode) {
|
|
405
|
+
const base = [
|
|
406
|
+
["release root", "."],
|
|
407
|
+
["Rockchip mkimage", "tools/mac/mkimage"],
|
|
408
|
+
["Rockchip dumpimage", "tools/mac/dumpimage"],
|
|
409
|
+
["Rockchip resource_tool", "tools/mac/resource_tool"],
|
|
410
|
+
["Rockchip rkdeveloptool", "tools/mac/rkdeveloptool"],
|
|
411
|
+
["boot resource image", "boot-workspace/out/resource.img"],
|
|
412
|
+
["boot image", "boot-workspace/out/boot.img"],
|
|
413
|
+
["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
|
|
414
|
+
["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
|
|
415
|
+
["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
|
|
416
|
+
["package metadata", "meta"]
|
|
417
|
+
];
|
|
418
|
+
const compile = [
|
|
419
|
+
["C compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-gcc"],
|
|
420
|
+
["C++ compiler", "toolchain/llvm-cross/bin/aarch64-linux-gnu-g++"],
|
|
421
|
+
["readelf", "toolchain/llvm-cross/bin/aarch64-linux-gnu-readelf"],
|
|
422
|
+
["host clang wrapper", "toolchain/host-tools/bin/clang-aarch64-linux-gnu"],
|
|
423
|
+
["host GCC libraries", "toolchain/host/lib/gcc"],
|
|
424
|
+
["target sysroot", "toolchain/host/aarch64-buildroot-linux-gnu/sysroot"],
|
|
425
|
+
["target include directory", "toolchain/host/aarch64-buildroot-linux-gnu/include"]
|
|
426
|
+
];
|
|
427
|
+
const qt = [
|
|
428
|
+
["Qt CMake", "qt-target/qt6-rk3566-llvm-6.8.3/bin/qt-cmake"],
|
|
429
|
+
["Qt target libraries", "qt-target/qt6-rk3566-llvm-6.8.3/lib"],
|
|
430
|
+
["Qt host tools", "qt-host/qt6-host-macos-6.8.3"],
|
|
431
|
+
["QtQuick live preview", "toolkit-runtime/qtquick-live-preview/bin/embed-qml-live-preview"]
|
|
432
|
+
];
|
|
433
|
+
const images = [
|
|
434
|
+
["base boot image", "images/current/boot.img"],
|
|
435
|
+
["base rootfs image", "images/current/rootfs.img"],
|
|
436
|
+
["base image parameter", "images/current/parameter.txt"]
|
|
437
|
+
];
|
|
438
|
+
const full = [
|
|
439
|
+
["rootfs overlay", "userdata/rootfs"]
|
|
440
|
+
];
|
|
441
|
+
if (mode === "minimal") {
|
|
442
|
+
return base;
|
|
443
|
+
}
|
|
444
|
+
if (mode === "compile") {
|
|
445
|
+
return [...base, ...compile];
|
|
446
|
+
}
|
|
447
|
+
if (mode === "qt") {
|
|
448
|
+
return [...base, ...compile, ...qt];
|
|
449
|
+
}
|
|
450
|
+
if (mode === "images") {
|
|
451
|
+
return [...base, ...images];
|
|
452
|
+
}
|
|
453
|
+
return [...base, ...compile, ...qt, ...images, ...full];
|
|
454
|
+
}
|
|
51
455
|
export async function compileTaishanPiSingleFile(options) {
|
|
52
456
|
assertAuthenticated(options.auth);
|
|
53
|
-
const releaseRoot =
|
|
457
|
+
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
54
458
|
const sourcePath = resolve(options.sourcePath);
|
|
55
459
|
const outputPath = resolve(options.outputPath);
|
|
56
460
|
await access(sourcePath, constants.R_OK);
|
|
@@ -77,7 +481,7 @@ export async function compileTaishanPiSingleFile(options) {
|
|
|
77
481
|
}
|
|
78
482
|
export async function buildTaishanPiQtSmoke(options) {
|
|
79
483
|
assertAuthenticated(options.auth);
|
|
80
|
-
const releaseRoot =
|
|
484
|
+
const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
|
|
81
485
|
const sourceDir = resolve(options.sourceDir ?? DEFAULT_QT_SMOKE_SOURCE);
|
|
82
486
|
const buildDir = resolve(options.buildDir);
|
|
83
487
|
const targetName = options.targetName ?? "qt_llvm_smoke";
|
|
@@ -104,7 +508,7 @@ export async function buildTaishanPiQtSmoke(options) {
|
|
|
104
508
|
}
|
|
105
509
|
const artifactPath = join(buildDir, targetName);
|
|
106
510
|
return await localCompileResult({
|
|
107
|
-
boardId:
|
|
511
|
+
boardId: DEFAULT_BOARD_ID,
|
|
108
512
|
operation: "local.build.qt_smoke",
|
|
109
513
|
releaseRoot,
|
|
110
514
|
accountId: options.accountId,
|
|
@@ -115,6 +519,560 @@ export async function buildTaishanPiQtSmoke(options) {
|
|
|
115
519
|
commands: [configure, build]
|
|
116
520
|
});
|
|
117
521
|
}
|
|
522
|
+
async function loadLocalToolchainMetadata(metadataRoot, channelName) {
|
|
523
|
+
const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
|
|
524
|
+
const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
|
|
525
|
+
if (!candidateRoot) {
|
|
526
|
+
return {
|
|
527
|
+
channel: BUILT_IN_CHANNEL,
|
|
528
|
+
manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
|
|
529
|
+
metadataRoot: undefined
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const root = resolve(candidateRoot);
|
|
533
|
+
const channelPath = join(root, "channels", channelName, "index.json");
|
|
534
|
+
const channel = JSON.parse(await readFile(channelPath, "utf8"));
|
|
535
|
+
if (channel.schema !== "embedlabs.channel.v1") {
|
|
536
|
+
throw new Error(`Unexpected local toolchain channel schema ${channel.schema}.`);
|
|
537
|
+
}
|
|
538
|
+
const manifests = new Map();
|
|
539
|
+
for (const entry of channel.packages) {
|
|
540
|
+
const manifestPath = entry.manifest
|
|
541
|
+
? resolve(dirname(channelPath), entry.manifest)
|
|
542
|
+
: join(root, "manifests", entry.id, entry.version, "manifest.json");
|
|
543
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
544
|
+
if (manifest.schema !== "embedlabs.package.v1") {
|
|
545
|
+
throw new Error(`Unexpected local toolchain manifest schema for ${entry.id}: ${manifest.schema}.`);
|
|
546
|
+
}
|
|
547
|
+
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
548
|
+
throw new Error(`Local toolchain manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
549
|
+
}
|
|
550
|
+
manifests.set(manifest.id, manifest);
|
|
551
|
+
}
|
|
552
|
+
return { channel, manifests, metadataRoot: root };
|
|
553
|
+
}
|
|
554
|
+
async function resolveLocalToolchainDownloadPlan(input) {
|
|
555
|
+
const channelUrl = downloadChannelUrl(input.channel);
|
|
556
|
+
const channel = await fetchJson(channelUrl);
|
|
557
|
+
if (channel.schema !== "embedlabs.download-channel.v1") {
|
|
558
|
+
throw new Error(`Unexpected download channel schema ${channel.schema}.`);
|
|
559
|
+
}
|
|
560
|
+
const entry = (channel.packages ?? []).find((item) => {
|
|
561
|
+
return item.board_id === input.boardId
|
|
562
|
+
&& item.host === input.host
|
|
563
|
+
&& item.toolchain === input.toolchain
|
|
564
|
+
&& (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
|
|
565
|
+
});
|
|
566
|
+
if (!entry?.manifest) {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const manifestUrl = new URL(entry.manifest, channelUrl).toString();
|
|
570
|
+
const manifest = await fetchJson(manifestUrl);
|
|
571
|
+
if (manifest.id !== entry.id || manifest.version !== entry.version) {
|
|
572
|
+
throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
|
|
573
|
+
}
|
|
574
|
+
if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
|
|
575
|
+
throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
|
|
576
|
+
}
|
|
577
|
+
if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
|
|
578
|
+
&& (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
|
|
579
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} is missing archive/components metadata.`);
|
|
580
|
+
}
|
|
581
|
+
const mirrors = manifest.archive
|
|
582
|
+
? orderDownloadMirrors((manifest.mirrors ?? [])
|
|
583
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
584
|
+
.map((mirror) => ({
|
|
585
|
+
kind: mirror.kind || "unknown",
|
|
586
|
+
enabled: mirror.enabled !== false,
|
|
587
|
+
url: mirror.url,
|
|
588
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : manifest.archive?.sha256,
|
|
589
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
|
|
590
|
+
})), manifest.download_policy?.preferred_order)
|
|
591
|
+
: [];
|
|
592
|
+
const components = (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl));
|
|
593
|
+
const first = mirrors[0];
|
|
594
|
+
if (!first && components.length === 0) {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
channel_url: channelUrl,
|
|
599
|
+
manifest_url: manifestUrl,
|
|
600
|
+
package_id: manifest.id,
|
|
601
|
+
version: manifest.version,
|
|
602
|
+
board_id: input.boardId,
|
|
603
|
+
host: input.host,
|
|
604
|
+
toolchain: input.toolchain,
|
|
605
|
+
source_url: first?.url,
|
|
606
|
+
mirror_kind: first?.kind,
|
|
607
|
+
archive: manifest.archive ? {
|
|
608
|
+
file: manifest.archive.file,
|
|
609
|
+
size_bytes: manifest.archive.size_bytes,
|
|
610
|
+
sha256: manifest.archive.sha256,
|
|
611
|
+
content_type: manifest.archive.content_type
|
|
612
|
+
} : undefined,
|
|
613
|
+
mirrors,
|
|
614
|
+
components: components.length > 0 ? components : undefined,
|
|
615
|
+
default_mode: manifest.download_policy?.default_mode
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function normalizeDownloadComponent(component, manifest, manifestUrl) {
|
|
619
|
+
if (!component.id || !component.version) {
|
|
620
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} contains a component without id/version.`);
|
|
621
|
+
}
|
|
622
|
+
if (!component.archive?.file || !component.archive.sha256 || !Number.isFinite(component.archive.size_bytes)) {
|
|
623
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} is missing archive metadata.`);
|
|
624
|
+
}
|
|
625
|
+
const mirrors = orderDownloadMirrors((component.mirrors ?? [])
|
|
626
|
+
.filter((mirror) => mirror.enabled !== false && typeof mirror.url === "string" && mirror.url.length > 0)
|
|
627
|
+
.map((mirror) => ({
|
|
628
|
+
kind: mirror.kind || "unknown",
|
|
629
|
+
enabled: mirror.enabled !== false,
|
|
630
|
+
url: new URL(mirror.url, manifestUrl).toString(),
|
|
631
|
+
sha256: typeof mirror.sha256 === "string" ? mirror.sha256 : component.archive?.sha256,
|
|
632
|
+
size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : component.archive?.size_bytes
|
|
633
|
+
})), manifest.download_policy?.preferred_order);
|
|
634
|
+
const first = mirrors[0];
|
|
635
|
+
if (!first) {
|
|
636
|
+
throw new Error(`Download manifest ${manifest.id}@${manifest.version} component ${component.id} has no enabled mirrors.`);
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
id: component.id,
|
|
640
|
+
version: component.version,
|
|
641
|
+
role: component.role,
|
|
642
|
+
install_modes: Array.isArray(component.install_modes) ? component.install_modes : undefined,
|
|
643
|
+
archive: {
|
|
644
|
+
file: component.archive.file,
|
|
645
|
+
size_bytes: component.archive.size_bytes,
|
|
646
|
+
sha256: component.archive.sha256,
|
|
647
|
+
content_type: component.archive.content_type
|
|
648
|
+
},
|
|
649
|
+
source_url: first.url,
|
|
650
|
+
mirror_kind: first.kind,
|
|
651
|
+
mirrors
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function orderDownloadMirrors(mirrors, preferredOrder) {
|
|
655
|
+
const preference = preferredOrder?.length
|
|
656
|
+
? preferredOrder
|
|
657
|
+
: ["github_release", "embedlabs_cdn", "cloudfront", "embedlabs_server"];
|
|
658
|
+
return [...mirrors].sort((left, right) => mirrorRank(left.kind, preference) - mirrorRank(right.kind, preference));
|
|
659
|
+
}
|
|
660
|
+
function mirrorRank(kind, preferredOrder) {
|
|
661
|
+
const index = preferredOrder.indexOf(kind);
|
|
662
|
+
return index >= 0 ? index : preferredOrder.length + 1;
|
|
663
|
+
}
|
|
664
|
+
async function fetchJson(url) {
|
|
665
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_REQUEST_TIMEOUT_MS) });
|
|
666
|
+
if (!response.ok) {
|
|
667
|
+
throw new Error(`HTTP ${response.status} while loading ${url}`);
|
|
668
|
+
}
|
|
669
|
+
return await response.json();
|
|
670
|
+
}
|
|
671
|
+
function downloadChannelUrl(channelName) {
|
|
672
|
+
const explicit = process.env.EMBED_DOWNLOAD_CHANNEL_URL?.trim()
|
|
673
|
+
|| process.env.EMBEDLABS_DOWNLOAD_CHANNEL_URL?.trim();
|
|
674
|
+
if (explicit) {
|
|
675
|
+
return explicit;
|
|
676
|
+
}
|
|
677
|
+
return `${downloadBaseUrl()}/downloads/metadata/channels/${encodeURIComponent(channelName)}/index.json`;
|
|
678
|
+
}
|
|
679
|
+
function downloadBaseUrl() {
|
|
680
|
+
return trimTrailingSlash(process.env.EMBED_DOWNLOAD_BASE_URL?.trim()
|
|
681
|
+
|| process.env.EMBEDLABS_DOWNLOAD_BASE_URL?.trim()
|
|
682
|
+
|| DEFAULT_DOWNLOAD_BASE_URL);
|
|
683
|
+
}
|
|
684
|
+
function trimTrailingSlash(value) {
|
|
685
|
+
return value.replace(/\/+$/, "");
|
|
686
|
+
}
|
|
687
|
+
function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
|
|
688
|
+
if (seen.has(packageId)) {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
seen.add(packageId);
|
|
692
|
+
const manifest = manifests.get(packageId);
|
|
693
|
+
if (!manifest) {
|
|
694
|
+
throw new Error(`Local toolchain package ${packageId} is not present in the channel.`);
|
|
695
|
+
}
|
|
696
|
+
const refs = [];
|
|
697
|
+
for (const requirement of manifest.requires ?? []) {
|
|
698
|
+
refs.push(...resolvePackageRefs(requirement.id, channel, manifests, seen));
|
|
699
|
+
}
|
|
700
|
+
const channelEntry = channel.packages.find((item) => item.id === packageId);
|
|
701
|
+
refs.push({
|
|
702
|
+
id: packageId,
|
|
703
|
+
version: manifest.version,
|
|
704
|
+
manifest: channelEntry?.manifest
|
|
705
|
+
});
|
|
706
|
+
return refs;
|
|
707
|
+
}
|
|
708
|
+
function boardPackageIdFor(boardId) {
|
|
709
|
+
if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
|
|
710
|
+
return "embedlabs.board.taishanpi.1m-rk3566";
|
|
711
|
+
}
|
|
712
|
+
if (boardId.startsWith("embedlabs.board.")) {
|
|
713
|
+
return boardId;
|
|
714
|
+
}
|
|
715
|
+
throw new Error(`Unsupported local toolchain board ${boardId}.`);
|
|
716
|
+
}
|
|
717
|
+
function packageIdForBoardFilter(boardId) {
|
|
718
|
+
try {
|
|
719
|
+
return boardPackageIdFor(boardId);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function boardIdForPackageManifest(manifest) {
|
|
726
|
+
const explicit = manifest.board_id;
|
|
727
|
+
if (explicit?.trim()) {
|
|
728
|
+
return normalizeBoardId(explicit);
|
|
729
|
+
}
|
|
730
|
+
if (manifest.id === "embedlabs.board.taishanpi.1m-rk3566") {
|
|
731
|
+
return DEFAULT_BOARD_ID;
|
|
732
|
+
}
|
|
733
|
+
if (manifest.id.startsWith("embedlabs.board.")) {
|
|
734
|
+
return normalizeBoardId(manifest.id.slice("embedlabs.board.".length).replaceAll(".", "-"));
|
|
735
|
+
}
|
|
736
|
+
return normalizeBoardId([manifest.board, manifest.variant].filter(Boolean).join("-") || manifest.id);
|
|
737
|
+
}
|
|
738
|
+
function normalizeBoardId(boardId) {
|
|
739
|
+
return boardId.trim().toLowerCase().replaceAll("_", "-");
|
|
740
|
+
}
|
|
741
|
+
function packageHostSupport(packages, manifests, host) {
|
|
742
|
+
const unsupportedPackages = [];
|
|
743
|
+
for (const item of packages) {
|
|
744
|
+
const manifest = manifests.get(item.id);
|
|
745
|
+
if (manifest?.hosts?.length && !manifest.hosts.includes(host)) {
|
|
746
|
+
unsupportedPackages.push(`${item.id}@${manifest.version}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
supported: unsupportedPackages.length === 0,
|
|
751
|
+
unsupportedPackages
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function environmentNotes(input) {
|
|
755
|
+
const notes = [];
|
|
756
|
+
if (input.status === "available") {
|
|
757
|
+
notes.push("Environment is available but not installed on this computer.");
|
|
758
|
+
}
|
|
759
|
+
if (input.status === "update_available") {
|
|
760
|
+
notes.push("A newer package is available; run the update command to refresh only the selected environment.");
|
|
761
|
+
}
|
|
762
|
+
if (input.status === "unsupported_host") {
|
|
763
|
+
notes.push(`This host is missing platform support for: ${input.unsupportedPackages.join(", ")}`);
|
|
764
|
+
}
|
|
765
|
+
if (input.downloadError) {
|
|
766
|
+
notes.push(`Download manifest could not be resolved yet: ${input.downloadError}`);
|
|
767
|
+
}
|
|
768
|
+
return notes;
|
|
769
|
+
}
|
|
770
|
+
function localToolchainEnvironmentComponent(component) {
|
|
771
|
+
return {
|
|
772
|
+
id: component.id,
|
|
773
|
+
version: component.version,
|
|
774
|
+
role: component.role,
|
|
775
|
+
install_modes: component.install_modes,
|
|
776
|
+
file: component.archive.file,
|
|
777
|
+
size_bytes: component.archive.size_bytes,
|
|
778
|
+
source_url: component.source_url
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function hostId() {
|
|
782
|
+
if (platform() === "darwin" && arch() === "arm64") {
|
|
783
|
+
return "darwin-arm64";
|
|
784
|
+
}
|
|
785
|
+
if (platform() === "linux" && arch() === "x64") {
|
|
786
|
+
return "linux-x86_64";
|
|
787
|
+
}
|
|
788
|
+
return `${platform()}-${arch()}`;
|
|
789
|
+
}
|
|
790
|
+
function resolveInstallRoot(installRoot) {
|
|
791
|
+
return resolve(installRoot
|
|
792
|
+
|| process.env.EMBEDLABS_HOME?.trim()
|
|
793
|
+
|| join(homedir(), ".embedlabs"));
|
|
794
|
+
}
|
|
795
|
+
function localToolchainRegistryPath(installRoot) {
|
|
796
|
+
return join(installRoot, "registry", "local-toolchains.json");
|
|
797
|
+
}
|
|
798
|
+
async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
|
|
799
|
+
const registryPath = localToolchainRegistryPath(installRoot);
|
|
800
|
+
await mkdir(dirname(registryPath), { recursive: true });
|
|
801
|
+
await writeFile(registryPath, `${JSON.stringify({
|
|
802
|
+
installed: true,
|
|
803
|
+
board_id: latest.board_id,
|
|
804
|
+
version: latest.version,
|
|
805
|
+
channel: latest.channel,
|
|
806
|
+
host: latest.host,
|
|
807
|
+
mode,
|
|
808
|
+
release_root: releaseRoot,
|
|
809
|
+
packages: latest.packages,
|
|
810
|
+
source,
|
|
811
|
+
installed_components: source?.components,
|
|
812
|
+
updated_at: new Date().toISOString()
|
|
813
|
+
}, null, 2)}\n`, "utf8");
|
|
814
|
+
}
|
|
815
|
+
async function resolveLocalReleaseRoot(releaseRoot) {
|
|
816
|
+
if (releaseRoot?.trim()) {
|
|
817
|
+
return resolve(releaseRoot);
|
|
818
|
+
}
|
|
819
|
+
const envRoot = process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim() || process.env.EMBEDLABS_RELEASE_ROOT?.trim();
|
|
820
|
+
if (envRoot) {
|
|
821
|
+
return resolve(envRoot);
|
|
822
|
+
}
|
|
823
|
+
const current = await currentLocalToolchain(undefined, DEFAULT_BOARD_ID);
|
|
824
|
+
if (current.release_root) {
|
|
825
|
+
return resolve(current.release_root);
|
|
826
|
+
}
|
|
827
|
+
return resolve(DEFAULT_RELEASE_ROOT);
|
|
828
|
+
}
|
|
829
|
+
async function sourceReleaseRootForInstall(options, latest, installRoot, tempDir) {
|
|
830
|
+
if (options.sourceReleaseRoot) {
|
|
831
|
+
return {
|
|
832
|
+
path: resolve(options.sourceReleaseRoot),
|
|
833
|
+
source: { kind: "directory", value: resolve(options.sourceReleaseRoot) }
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
if (options.sourceUrl) {
|
|
837
|
+
const downloadedPath = await downloadToolchainArchive(options.sourceUrl, installRoot);
|
|
838
|
+
const extractRoot = join(tempDir, "extract");
|
|
839
|
+
await mkdir(extractRoot, { recursive: true });
|
|
840
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
841
|
+
if (extracted.exit_code !== 0) {
|
|
842
|
+
throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
path: await findReleaseRoot(extractRoot),
|
|
846
|
+
source: { kind: "url", value: options.sourceUrl, downloaded_path: downloadedPath }
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (latest.download?.components?.length) {
|
|
850
|
+
return await componentSourceRootForInstall(options, latest.download, installRoot, tempDir);
|
|
851
|
+
}
|
|
852
|
+
if (latest.download) {
|
|
853
|
+
const failures = [];
|
|
854
|
+
for (const mirror of latest.download.mirrors) {
|
|
855
|
+
if (!mirror.enabled) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (!latest.download.archive) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const extractRoot = join(tempDir, `extract-${safeFileToken(mirror.kind)}`);
|
|
862
|
+
try {
|
|
863
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
864
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
865
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
866
|
+
});
|
|
867
|
+
await rm(extractRoot, { recursive: true, force: true });
|
|
868
|
+
await mkdir(extractRoot, { recursive: true });
|
|
869
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
870
|
+
if (extracted.exit_code !== 0) {
|
|
871
|
+
throw new Error(`Could not extract local toolchain archive: ${extracted.stderr_tail.join("\n")}`);
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
path: await findReleaseRoot(extractRoot),
|
|
875
|
+
source: {
|
|
876
|
+
kind: "url",
|
|
877
|
+
value: mirror.url,
|
|
878
|
+
downloaded_path: downloadedPath,
|
|
879
|
+
mirror_kind: mirror.kind,
|
|
880
|
+
sha256: mirror.sha256 ?? latest.download.archive.sha256,
|
|
881
|
+
size_bytes: mirror.size_bytes ?? latest.download.archive.size_bytes
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
failures.push(`${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (failures.length > 0 && !await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
890
|
+
throw new Error(`Could not install local toolchain from download mirrors: ${failures.join("; ")}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (await pathExists(DEFAULT_RELEASE_ROOT)) {
|
|
894
|
+
return {
|
|
895
|
+
path: resolve(DEFAULT_RELEASE_ROOT),
|
|
896
|
+
source: { kind: "directory", value: resolve(DEFAULT_RELEASE_ROOT) }
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
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.");
|
|
900
|
+
}
|
|
901
|
+
async function componentSourceRootForInstall(options, download, installRoot, tempDir) {
|
|
902
|
+
const mode = normalizeLocalToolchainInstallMode(options.mode ?? download.default_mode);
|
|
903
|
+
const components = selectedDownloadComponents(download.components ?? [], mode);
|
|
904
|
+
if (components.length === 0) {
|
|
905
|
+
throw new Error(`No local toolchain components selected for mode ${mode}.`);
|
|
906
|
+
}
|
|
907
|
+
const extractRoot = join(tempDir, "extract-components");
|
|
908
|
+
await mkdir(extractRoot, { recursive: true });
|
|
909
|
+
const installedComponents = [];
|
|
910
|
+
const failures = [];
|
|
911
|
+
for (const component of components) {
|
|
912
|
+
let installed = false;
|
|
913
|
+
for (const mirror of component.mirrors) {
|
|
914
|
+
if (!mirror.enabled) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const downloadedPath = await downloadToolchainArchive(mirror.url, installRoot, {
|
|
919
|
+
sha256: mirror.sha256 ?? component.archive.sha256,
|
|
920
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes
|
|
921
|
+
});
|
|
922
|
+
const extracted = await runCommand(["tar", "-xzf", downloadedPath, "-C", extractRoot], tempDir);
|
|
923
|
+
if (extracted.exit_code !== 0) {
|
|
924
|
+
throw new Error(`Could not extract component ${component.id}: ${extracted.stderr_tail.join("\n")}`);
|
|
925
|
+
}
|
|
926
|
+
installedComponents.push({
|
|
927
|
+
id: component.id,
|
|
928
|
+
version: component.version,
|
|
929
|
+
role: component.role,
|
|
930
|
+
archive_file: component.archive.file,
|
|
931
|
+
mirror_kind: mirror.kind,
|
|
932
|
+
downloaded_path: downloadedPath,
|
|
933
|
+
size_bytes: mirror.size_bytes ?? component.archive.size_bytes,
|
|
934
|
+
sha256: mirror.sha256 ?? component.archive.sha256
|
|
935
|
+
});
|
|
936
|
+
installed = true;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
failures.push(`${component.id}@${component.version}/${mirror.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!installed) {
|
|
944
|
+
throw new Error(`Could not install component ${component.id}@${component.version}: ${failures.join("; ")}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
path: extractRoot,
|
|
949
|
+
source: {
|
|
950
|
+
kind: "components",
|
|
951
|
+
value: download.manifest_url,
|
|
952
|
+
mirror_kind: "components",
|
|
953
|
+
size_bytes: installedComponents.reduce((total, component) => total + component.size_bytes, 0),
|
|
954
|
+
components: installedComponents
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function selectedDownloadComponents(components, mode) {
|
|
959
|
+
return components.filter((component) => {
|
|
960
|
+
if (!component.install_modes?.length) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
return component.install_modes.includes(mode);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
|
|
967
|
+
const downloadsDir = join(installRoot, "cache", "downloads");
|
|
968
|
+
await mkdir(downloadsDir, { recursive: true });
|
|
969
|
+
const parsed = new URL(sourceUrl);
|
|
970
|
+
const filename = basename(parsed.pathname) || `local-toolchain-${Date.now()}.tar.gz`;
|
|
971
|
+
const outputPath = join(downloadsDir, filename);
|
|
972
|
+
if (parsed.protocol === "file:") {
|
|
973
|
+
await cp(fileURLToPath(parsed), outputPath, { force: true });
|
|
974
|
+
return outputPath;
|
|
975
|
+
}
|
|
976
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
977
|
+
throw new Error(`Unsupported local toolchain download URL protocol: ${parsed.protocol}`);
|
|
978
|
+
}
|
|
979
|
+
const partialPath = `${outputPath}.part`;
|
|
980
|
+
const expectedSize = expected?.size_bytes;
|
|
981
|
+
const existingComplete = await fileSize(outputPath);
|
|
982
|
+
const remoteSize = await remoteContentLength(sourceUrl);
|
|
983
|
+
const targetSize = expectedSize ?? remoteSize;
|
|
984
|
+
if (existingComplete > 0 && targetSize !== undefined && existingComplete === targetSize) {
|
|
985
|
+
if (expected?.sha256) {
|
|
986
|
+
const actual = await sha256(outputPath);
|
|
987
|
+
if (actual === expected.sha256) {
|
|
988
|
+
return outputPath;
|
|
989
|
+
}
|
|
990
|
+
await rm(outputPath, { force: true });
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
return outputPath;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
let resumeFrom = await fileSize(partialPath);
|
|
997
|
+
const headers = new Headers();
|
|
998
|
+
if (resumeFrom > 0) {
|
|
999
|
+
headers.set("range", `bytes=${resumeFrom}-`);
|
|
1000
|
+
}
|
|
1001
|
+
let response = await fetch(sourceUrl, { headers });
|
|
1002
|
+
if (resumeFrom > 0 && response.status !== 206) {
|
|
1003
|
+
await rm(partialPath, { force: true });
|
|
1004
|
+
resumeFrom = 0;
|
|
1005
|
+
response = await fetch(sourceUrl);
|
|
1006
|
+
}
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
throw new Error(`Local toolchain download failed with HTTP ${response.status}: ${sourceUrl}`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!response.body) {
|
|
1011
|
+
throw new Error(`Local toolchain download returned an empty response body: ${sourceUrl}`);
|
|
1012
|
+
}
|
|
1013
|
+
const writeStream = createWriteStream(partialPath, { flags: resumeFrom > 0 ? "a" : "w" });
|
|
1014
|
+
await pipeline(Readable.fromWeb(response.body), writeStream);
|
|
1015
|
+
const downloadedSize = await fileSize(partialPath);
|
|
1016
|
+
if (targetSize !== undefined && downloadedSize !== targetSize) {
|
|
1017
|
+
throw new Error(`Local toolchain download incomplete: expected ${targetSize} bytes, got ${downloadedSize} bytes.`);
|
|
1018
|
+
}
|
|
1019
|
+
await rename(partialPath, outputPath);
|
|
1020
|
+
if (expected?.sha256) {
|
|
1021
|
+
const actual = await sha256(outputPath);
|
|
1022
|
+
if (actual !== expected.sha256) {
|
|
1023
|
+
await rm(outputPath, { force: true });
|
|
1024
|
+
throw new Error(`Local toolchain download SHA256 mismatch: expected ${expected.sha256}, got ${actual}.`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return outputPath;
|
|
1028
|
+
}
|
|
1029
|
+
async function remoteContentLength(sourceUrl) {
|
|
1030
|
+
try {
|
|
1031
|
+
const response = await fetch(sourceUrl, { method: "HEAD" });
|
|
1032
|
+
if (!response.ok) {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
const length = response.headers.get("content-length");
|
|
1036
|
+
if (!length) {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
const parsed = Number.parseInt(length, 10);
|
|
1040
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function fileSize(filePath) {
|
|
1047
|
+
try {
|
|
1048
|
+
return (await stat(filePath)).size;
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
return 0;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function safeFileToken(value) {
|
|
1055
|
+
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_") || "mirror";
|
|
1056
|
+
}
|
|
1057
|
+
async function findReleaseRoot(extractRoot) {
|
|
1058
|
+
if (await pathExists(join(extractRoot, "toolchain"))) {
|
|
1059
|
+
return extractRoot;
|
|
1060
|
+
}
|
|
1061
|
+
const entries = await readdir(extractRoot);
|
|
1062
|
+
for (const entry of entries) {
|
|
1063
|
+
const candidate = join(extractRoot, entry);
|
|
1064
|
+
try {
|
|
1065
|
+
const info = await stat(candidate);
|
|
1066
|
+
if (info.isDirectory() && await pathExists(join(candidate, "toolchain"))) {
|
|
1067
|
+
return candidate;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
// Ignore entries that disappear during extraction cleanup.
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error("Downloaded local toolchain archive did not contain a release root with toolchain/.");
|
|
1075
|
+
}
|
|
118
1076
|
function compilerForSource(releaseRoot, sourcePath) {
|
|
119
1077
|
const extension = extname(sourcePath).toLowerCase();
|
|
120
1078
|
const binDir = join(releaseRoot, "toolchain", "llvm-cross", "bin");
|