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