@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.29 → 0.1.2-dev.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/src/build.mjs +113 -15
- package/cli/src/cpp.mjs +22 -9
- package/cli/src/python.mjs +35 -19
- package/package.json +2 -2
package/cli/src/build.mjs
CHANGED
|
@@ -85,10 +85,11 @@ export async function exportRos2Build(root) {
|
|
|
85
85
|
log.ok("ros2_ws/install ready (will be included in the ZIP)");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release)
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
88
|
+
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
|
|
89
|
+
// and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
|
|
90
|
+
// robot-compatible executables. rb-runtime prefers dist/{arch}/ and falls back
|
|
91
|
+
// to dist/{entry} (legacy). Skipped (with a warning) if Docker is unavailable.
|
|
92
|
+
export async function exportCppBuild(root, distDir, archs = ["x86_64", "aarch64"]) {
|
|
92
93
|
if (manifestRuntime(root) !== "cpp") return;
|
|
93
94
|
const { assertDockerReady, ensureImage, cmakeBuild } = await import("./cpp.mjs");
|
|
94
95
|
try {
|
|
@@ -98,17 +99,27 @@ export async function exportCppBuild(root, distDir) {
|
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
101
|
ensureImage();
|
|
101
|
-
cmakeBuild(root, { buildType: "Release" });
|
|
102
102
|
|
|
103
103
|
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
if (!entry) {
|
|
105
|
+
log.warn("manifest.json runtime.entry not set — cpp build skipped");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const arch of archs) {
|
|
110
|
+
cmakeBuild(root, { buildType: "Release", arch });
|
|
111
|
+
const buildDir = `cpp/build-${arch}`;
|
|
112
|
+
const built = resolve(root, buildDir, entry);
|
|
113
|
+
if (existsSync(built)) {
|
|
114
|
+
const archDir = resolve(distDir, arch);
|
|
115
|
+
mkdirSync(archDir, { recursive: true });
|
|
116
|
+
const dest = resolve(archDir, entry);
|
|
117
|
+
copyFileSync(built, dest);
|
|
118
|
+
chmodSync(dest, 0o755);
|
|
119
|
+
log.ok(`cpp binary → dist/${arch}/${entry}`);
|
|
120
|
+
} else {
|
|
121
|
+
log.warn(`cpp binary not found at ${buildDir}/${entry} — skipped for ${arch}`);
|
|
122
|
+
}
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
|
|
@@ -258,6 +269,92 @@ function dirSize(dir) {
|
|
|
258
269
|
return total;
|
|
259
270
|
}
|
|
260
271
|
|
|
272
|
+
// Recursively sum directory size, skipping subdirs in `skip` list (by name).
|
|
273
|
+
function dirSizeExcluding(dir, skip = []) {
|
|
274
|
+
if (!existsSync(dir)) return 0;
|
|
275
|
+
let total = 0;
|
|
276
|
+
for (const entry of readdirSync(dir)) {
|
|
277
|
+
if (skip.includes(entry)) continue;
|
|
278
|
+
const full = resolve(dir, entry);
|
|
279
|
+
const st = statSync(full);
|
|
280
|
+
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
281
|
+
}
|
|
282
|
+
return total;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Read the uncompressed byte total of a .whl (zip) file by parsing its central
|
|
286
|
+
// directory — no external library needed, no actual extraction.
|
|
287
|
+
function unpackedWheelSize(whlPath) {
|
|
288
|
+
const buf = readFileSync(whlPath);
|
|
289
|
+
// Find EOCD signature (0x06054b50) scanning from end
|
|
290
|
+
let eocd = -1;
|
|
291
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65558); i--) {
|
|
292
|
+
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
|
293
|
+
}
|
|
294
|
+
if (eocd === -1) return 0;
|
|
295
|
+
const cdOffset = buf.readUInt32LE(eocd + 16);
|
|
296
|
+
const cdSize = buf.readUInt32LE(eocd + 12);
|
|
297
|
+
let pos = cdOffset;
|
|
298
|
+
let total = 0;
|
|
299
|
+
while (pos < cdOffset + cdSize) {
|
|
300
|
+
if (buf.readUInt32LE(pos) !== 0x02014b50) break; // central dir signature
|
|
301
|
+
total += buf.readUInt32LE(pos + 24); // uncompressed size
|
|
302
|
+
const fnLen = buf.readUInt16LE(pos + 28);
|
|
303
|
+
const extraLen = buf.readUInt16LE(pos + 30);
|
|
304
|
+
const commentLen = buf.readUInt16LE(pos + 32);
|
|
305
|
+
pos += 46 + fnLen + extraLen + commentLen;
|
|
306
|
+
}
|
|
307
|
+
return total;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Calculate max(arch installedSize) across requested archs and stamp it into
|
|
311
|
+
// both manifest.json (root) and dist/manifest.json.
|
|
312
|
+
// installedSize(arch) = dist/(excl. wheels/) + ros2_ws/install/ + uncompressed arch+none-any wheels
|
|
313
|
+
function calcInstalledSize(pluginRoot, archs) {
|
|
314
|
+
const distDir = resolve(pluginRoot, "dist");
|
|
315
|
+
const wheelsDir = resolve(distDir, "wheels");
|
|
316
|
+
const ros2Install = resolve(pluginRoot, "ros2_ws/install");
|
|
317
|
+
|
|
318
|
+
const baseSize = dirSizeExcluding(distDir, ["wheels"]);
|
|
319
|
+
const ros2Size = existsSync(ros2Install) ? dirSize(ros2Install) : 0;
|
|
320
|
+
|
|
321
|
+
// Collect all wheel files once
|
|
322
|
+
const wheels = existsSync(wheelsDir)
|
|
323
|
+
? readdirSync(wheelsDir).filter((f) => f.endsWith(".whl")).map((f) => resolve(wheelsDir, f))
|
|
324
|
+
: [];
|
|
325
|
+
|
|
326
|
+
const perArchSize = archs.map((arch) => {
|
|
327
|
+
const archWheels = wheels.filter((w) => {
|
|
328
|
+
const name = w.split("/").pop();
|
|
329
|
+
// none-any wheels count for every arch
|
|
330
|
+
if (name.endsWith("none-any.whl")) return true;
|
|
331
|
+
return name.includes(arch);
|
|
332
|
+
});
|
|
333
|
+
const wheelSize = archWheels.reduce((sum, w) => sum + unpackedWheelSize(w), 0);
|
|
334
|
+
return baseSize + ros2Size + wheelSize;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const installedSize = Math.max(0, ...perArchSize);
|
|
338
|
+
|
|
339
|
+
// Write to root manifest.json
|
|
340
|
+
const rootManifestPath = resolve(pluginRoot, "manifest.json");
|
|
341
|
+
if (existsSync(rootManifestPath)) {
|
|
342
|
+
const m = JSON.parse(readFileSync(rootManifestPath, "utf8"));
|
|
343
|
+
m.installedSize = installedSize;
|
|
344
|
+
writeFileSync(rootManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Re-stage dist/manifest.json so the ZIP carries the updated value
|
|
348
|
+
const distManifestPath = resolve(distDir, "manifest.json");
|
|
349
|
+
if (existsSync(distManifestPath)) {
|
|
350
|
+
const m = JSON.parse(readFileSync(distManifestPath, "utf8"));
|
|
351
|
+
m.installedSize = installedSize;
|
|
352
|
+
writeFileSync(distManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
log.ok(`installedSize → ${humanSize(installedSize)} (${archs.join(", ")} max, uncompressed)`);
|
|
356
|
+
}
|
|
357
|
+
|
|
261
358
|
// Zip dist/ (+ ros2_ws/install) into a distributable ZIP with a size report.
|
|
262
359
|
function packZip(pluginRoot) {
|
|
263
360
|
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
@@ -354,7 +451,7 @@ export async function runBuild(args) {
|
|
|
354
451
|
if (needsLock) {
|
|
355
452
|
const { lockAndVendor } = await import("./python.mjs");
|
|
356
453
|
try {
|
|
357
|
-
lockAndVendor(pluginRoot);
|
|
454
|
+
lockAndVendor(pluginRoot, archs);
|
|
358
455
|
} catch (e) {
|
|
359
456
|
log.warn(`python lock/vendor skipped: ${e.message}`);
|
|
360
457
|
}
|
|
@@ -362,6 +459,7 @@ export async function runBuild(args) {
|
|
|
362
459
|
}
|
|
363
460
|
|
|
364
461
|
await exportRos2Build(root);
|
|
365
|
-
await exportCppBuild(root, distDir);
|
|
462
|
+
await exportCppBuild(root, distDir, archs);
|
|
463
|
+
calcInstalledSize(pluginRoot, archs);
|
|
366
464
|
if (!noZip) packZip(pluginRoot);
|
|
367
465
|
}
|
package/cli/src/cpp.mjs
CHANGED
|
@@ -32,21 +32,34 @@ export function ensureImage(image = CPP_DEV_IMAGE) {
|
|
|
32
32
|
pruneMuskatImages();
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Docker platform token for each target arch
|
|
36
|
+
const ARCH_DOCKER_PLATFORM = {
|
|
37
|
+
x86_64: "linux/amd64",
|
|
38
|
+
aarch64: "linux/arm64",
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
|
-
* Configure + build cpp/ inside the container, producing cpp/build/<entry>
|
|
37
|
-
* the host (the workspace is mounted).
|
|
38
|
-
*
|
|
42
|
+
* Configure + build cpp/ inside the container, producing cpp/build-{arch}/<entry>
|
|
43
|
+
* on the host (the workspace is mounted).
|
|
44
|
+
* - arch null → native (dev mode, uses cpp/build/)
|
|
45
|
+
* - arch "x86_64" / "aarch64" → cross via --platform (Release builds for ZIP)
|
|
46
|
+
* buildType "Debug" for dev, "Release" for export builds that ship in the ZIP.
|
|
39
47
|
*/
|
|
40
|
-
export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug" } = {}) {
|
|
48
|
+
export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug", arch = null } = {}) {
|
|
49
|
+
const buildDir = arch ? `cpp/build-${arch}` : "cpp/build";
|
|
50
|
+
const platformFlags = arch && ARCH_DOCKER_PLATFORM[arch]
|
|
51
|
+
? ["--platform", ARCH_DOCKER_PLATFORM[arch]]
|
|
52
|
+
: [];
|
|
41
53
|
const script = [
|
|
42
|
-
`cmake -S cpp -B
|
|
43
|
-
|
|
54
|
+
`cmake -S cpp -B ${buildDir} -DCMAKE_BUILD_TYPE=${buildType}`,
|
|
55
|
+
`cmake --build ${buildDir} -j`,
|
|
44
56
|
].join(" && ");
|
|
45
|
-
|
|
57
|
+
const label = arch ? `cmake build (${buildType}, ${arch}, in container)…` : `cmake build (${buildType}, in container)…`;
|
|
58
|
+
log.step(label);
|
|
46
59
|
const r = spawnSync(
|
|
47
60
|
"docker",
|
|
48
|
-
["run", "--rm", "-v", `${root}:/plugin`, "-w", "/plugin", image, "bash", "-lc", script],
|
|
61
|
+
["run", "--rm", ...platformFlags, "-v", `${root}:/plugin`, "-w", "/plugin", image, "bash", "-lc", script],
|
|
49
62
|
{ cwd: root, stdio: "inherit" },
|
|
50
63
|
);
|
|
51
|
-
if (r.status !== 0) throw new Error(
|
|
64
|
+
if (r.status !== 0) throw new Error(`cmake build failed${arch ? ` (${arch})` : ""}.`);
|
|
52
65
|
}
|
package/cli/src/python.mjs
CHANGED
|
@@ -31,16 +31,21 @@ export function ensureImage(image = PYTHON_DEV_IMAGE) {
|
|
|
31
31
|
pruneMuskatImages();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// manylinux tag that matches each target arch for pip --platform
|
|
35
|
+
const ARCH_PLATFORM = {
|
|
36
|
+
x86_64: "manylinux2014_x86_64",
|
|
37
|
+
aarch64: "manylinux2014_aarch64",
|
|
38
|
+
};
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Ubuntu 24.04 + py3.12 컨테이너 안에서:
|
|
36
|
-
* 1. uv pip compile requirements.txt → dist/requirements.lock (
|
|
37
|
-
* 2. pip download
|
|
42
|
+
* 1. uv pip compile requirements.txt → dist/requirements.lock (arch 무관, 1회)
|
|
43
|
+
* 2. archs별 pip download --platform <manylinux_tag> → dist/wheels/ (네이티브 휠 벤더링)
|
|
38
44
|
*
|
|
39
|
-
*
|
|
40
|
-
* 순수 Python 패키지(py3-none-any)는 arch 무관.
|
|
45
|
+
* none-any 휠은 pip이 두 번째 arch에서 자동 skip → 중복 없음.
|
|
41
46
|
* requirements.txt 없으면 조용히 건너뜀.
|
|
42
47
|
*/
|
|
43
|
-
export function lockAndVendor(pluginRoot) {
|
|
48
|
+
export function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
|
|
44
49
|
const reqTxt = resolve(pluginRoot, "requirements.txt");
|
|
45
50
|
if (!existsSync(reqTxt)) {
|
|
46
51
|
log.warn("requirements.txt not found — lock/vendor skipped");
|
|
@@ -64,18 +69,29 @@ export function lockAndVendor(pluginRoot) {
|
|
|
64
69
|
if (lockResult.status !== 0) throw new Error("lock generation failed (uv pip compile)");
|
|
65
70
|
log.ok("dist/requirements.lock ready");
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
// arch별 휠 벤더링 — QEMU 불필요 (download only, --platform 고정)
|
|
73
|
+
for (const arch of archs) {
|
|
74
|
+
const platform = ARCH_PLATFORM[arch];
|
|
75
|
+
if (!platform) {
|
|
76
|
+
log.warn(`unknown arch '${arch}' — skipped`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
log.step(`vendoring wheels for ${arch} (${platform})…`);
|
|
80
|
+
const r = spawnSync(
|
|
81
|
+
"docker",
|
|
82
|
+
[
|
|
83
|
+
"run", "--rm",
|
|
84
|
+
"-v", `${pluginRoot}:/plugin`,
|
|
85
|
+
PYTHON_DEV_IMAGE,
|
|
86
|
+
"bash", "-c",
|
|
87
|
+
`set -e && cd /plugin && mkdir -p dist/wheels && ` +
|
|
88
|
+
`pip3 download -r dist/requirements.lock ` +
|
|
89
|
+
`--only-binary=:all: --platform ${platform} --python-version 312 --implementation cp ` +
|
|
90
|
+
`-d dist/wheels`,
|
|
91
|
+
],
|
|
92
|
+
{ stdio: "inherit" },
|
|
93
|
+
);
|
|
94
|
+
if (r.status !== 0) throw new Error(`wheel vendoring failed for ${arch}`);
|
|
95
|
+
}
|
|
96
|
+
log.ok(`dist/wheels/ ready (${archs.join(", ")})`);
|
|
81
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/muscat-plugin-kit",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/react-dom": "^19.0.4"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@rainbow-robotics/preview-host": "0.1.2-dev.
|
|
34
|
+
"@rainbow-robotics/preview-host": "0.1.2-dev.30",
|
|
35
35
|
"socket.io": "^4.8.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|