@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.34 → 0.1.2-dev.36
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/bin/muscat-pk.mjs +1 -1
- package/cli/src/build.mjs +63 -96
- package/cli/src/check-package.mjs +8 -24
- package/cli/src/cpp.mjs +12 -10
- package/cli/src/devBack.mjs +3 -15
- package/cli/src/manifest.mjs +4 -0
- package/cli/src/new.mjs +1 -10
- package/cli/src/python.mjs +2 -2
- package/cli/src/ros2.mjs +38 -10
- package/cli/src/type.mjs +1 -10
- package/cli/src/update.mjs +1 -9
- package/cli/src/util.mjs +10 -0
- package/package.json +2 -2
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -21,7 +21,7 @@ Usage: muscat-pk <command> [options]
|
|
|
21
21
|
Commands:
|
|
22
22
|
new [dir] Scaffold a new plugin project (prompts: author, email, description, type, front-package-manager)
|
|
23
23
|
type change Switch backend type (python | cpp | ros2-python | ros2-cpp)
|
|
24
|
-
build Build + lock deps + vendor wheels + zip (--no-zip: skip zip, --no-lock: skip python lock/vendor)
|
|
24
|
+
build Build + lock deps + vendor wheels + zip (--no-zip: skip zip, --no-lock: skip python lock/vendor, --arch x86_64|aarch64|both)
|
|
25
25
|
dev Run Socket.IO bridge, dev:ui, and dev:back together
|
|
26
26
|
dev:ui Preview the plugin in a mini-Muscat (preview-host)
|
|
27
27
|
dev:back Run the plugin back end (python: watchfiles reload / ros2: Docker)
|
package/cli/src/build.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import { createRequire } from "node:module";
|
|
|
14
14
|
import { basename, dirname, resolve } from "node:path";
|
|
15
15
|
import { createInterface } from "node:readline/promises";
|
|
16
16
|
import { execSync, spawnSync } from "node:child_process";
|
|
17
|
-
import { ensureDocker, findPluginRoot, hasBin, humanSize, log } from "./util.mjs";
|
|
18
|
-
import { assertManifest } from "./manifest.mjs";
|
|
17
|
+
import { ensureDocker, findPluginRoot, hasBin, humanSize, log, parseArgs } from "./util.mjs";
|
|
18
|
+
import { assertManifest, manifestRuntime } from "./manifest.mjs";
|
|
19
19
|
|
|
20
20
|
// Find a usable node binary (for running vite as subprocess).
|
|
21
21
|
function findNodeBin() {
|
|
@@ -60,45 +60,53 @@ function isMainBranch() {
|
|
|
60
60
|
return branch === "main" || branch === "master";
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return "python";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12
|
|
72
|
-
// container so the shipped ZIP carries ros2_ws/install. Real (non-symlink) build
|
|
73
|
-
// → relocatable on the robot. Skipped (with a warning) if Docker is unavailable.
|
|
74
|
-
export async function exportRos2Build(root) {
|
|
63
|
+
// For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
|
|
64
|
+
// rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
|
|
65
|
+
// rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
|
|
66
|
+
export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
75
67
|
if (manifestRuntime(root) !== "ros2") return;
|
|
76
|
-
const { assertDockerReady, ensureImage, colconBuild } = await import("./ros2.mjs");
|
|
68
|
+
const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
|
|
77
69
|
try {
|
|
78
70
|
assertDockerReady();
|
|
79
71
|
} catch (e) {
|
|
80
72
|
log.warn(`ros2 build skipped: ${e.message}`);
|
|
81
73
|
return;
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
|
|
76
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
77
|
+
const rosClient = manifest.runtime?.rosClient;
|
|
78
|
+
|
|
79
|
+
if (rosClient === "rclcpp") {
|
|
80
|
+
// C++ ROS2: compile per arch so each robot picks the right install-{arch}/
|
|
81
|
+
for (const arch of archs) {
|
|
82
|
+
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
83
|
+
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
84
|
+
const archImage = `${ROS2_DEV_IMAGE}-${arch}`;
|
|
85
|
+
ensureImage(archImage, { platform: dockerPlatform });
|
|
86
|
+
colconBuild(root, { symlink: false, arch });
|
|
87
|
+
log.ok(`dist/ros2_ws/install-${arch}/ ready`);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// rclpy / pure Python: arch-agnostic, build once
|
|
91
|
+
ensureImage();
|
|
92
|
+
colconBuild(root, { symlink: false });
|
|
93
|
+
log.ok("dist/ros2_ws/install/ ready");
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
|
|
89
98
|
// and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
|
|
90
99
|
// robot-compatible executables. rb-runtime prefers dist/{arch}/ and falls back
|
|
91
100
|
// to dist/{entry} (legacy). Skipped (with a warning) if Docker is unavailable.
|
|
92
|
-
export async function exportCppBuild(root, distDir, archs = ["
|
|
101
|
+
export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
|
|
93
102
|
if (manifestRuntime(root) !== "cpp") return;
|
|
94
|
-
const { assertDockerReady, ensureImage, cmakeBuild } = await import("./cpp.mjs");
|
|
103
|
+
const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
|
|
95
104
|
try {
|
|
96
105
|
assertDockerReady();
|
|
97
106
|
} catch (e) {
|
|
98
107
|
log.warn(`cpp build skipped: ${e.message}`);
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
|
-
ensureImage();
|
|
102
110
|
|
|
103
111
|
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
104
112
|
if (!entry) {
|
|
@@ -107,6 +115,11 @@ export async function exportCppBuild(root, distDir, archs = ["x86_64", "aarch64"
|
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
for (const arch of archs) {
|
|
118
|
+
// python과 동일하게 arch별 이미지를 빌드해야 --platform 불일치를 피할 수 있다.
|
|
119
|
+
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
120
|
+
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
121
|
+
const archImage = `${CPP_DEV_IMAGE}-${arch}`;
|
|
122
|
+
ensureImage(archImage, { platform: dockerPlatform });
|
|
110
123
|
cmakeBuild(root, { buildType: "Release", arch });
|
|
111
124
|
const buildDir = `cpp/build-${arch}`;
|
|
112
125
|
const built = resolve(root, buildDir, entry);
|
|
@@ -259,18 +272,7 @@ export async function buildPlugin({ root, minify = false } = {}) {
|
|
|
259
272
|
return { pluginRoot, distDir };
|
|
260
273
|
}
|
|
261
274
|
|
|
262
|
-
function dirSize(dir) {
|
|
263
|
-
let total = 0;
|
|
264
|
-
for (const entry of readdirSync(dir)) {
|
|
265
|
-
const full = resolve(dir, entry);
|
|
266
|
-
const st = statSync(full);
|
|
267
|
-
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
268
|
-
}
|
|
269
|
-
return total;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Recursively sum directory size, skipping subdirs in `skip` list (by name).
|
|
273
|
-
function dirSizeExcluding(dir, skip = []) {
|
|
275
|
+
function dirSize(dir, skip = []) {
|
|
274
276
|
if (!existsSync(dir)) return 0;
|
|
275
277
|
let total = 0;
|
|
276
278
|
for (const entry of readdirSync(dir)) {
|
|
@@ -284,57 +286,15 @@ function dirSizeExcluding(dir, skip = []) {
|
|
|
284
286
|
|
|
285
287
|
// Read the uncompressed byte total of a .whl (zip) file by parsing its central
|
|
286
288
|
// 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
289
|
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
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
|
-
});
|
|
290
|
+
// installedSize = "이 플러그인 하나를 지우면 되찾는 용량"
|
|
291
|
+
// = dist/(wheels/ 제외)
|
|
292
|
+
// ros2_ws/install* 은 이제 dist/ 안에 있으므로 별도 합산 불필요.
|
|
293
|
+
// 휠은 로봇 공유 스토어(store/py/)에 언팩되므로 포함하지 않는다.
|
|
294
|
+
function calcInstalledSize(pluginRoot) {
|
|
295
|
+
const distDir = resolve(pluginRoot, "dist");
|
|
336
296
|
|
|
337
|
-
const installedSize =
|
|
297
|
+
const installedSize = dirSize(distDir, ["wheels"]);
|
|
338
298
|
|
|
339
299
|
// Write to root manifest.json
|
|
340
300
|
const rootManifestPath = resolve(pluginRoot, "manifest.json");
|
|
@@ -352,10 +312,11 @@ function calcInstalledSize(pluginRoot, archs) {
|
|
|
352
312
|
writeFileSync(distManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
353
313
|
}
|
|
354
314
|
|
|
355
|
-
log.ok(`installedSize → ${humanSize(installedSize)}
|
|
315
|
+
log.ok(`installedSize → ${humanSize(installedSize)}`);
|
|
356
316
|
}
|
|
357
317
|
|
|
358
|
-
// Zip dist/
|
|
318
|
+
// Zip dist/ into a distributable ZIP with a size report.
|
|
319
|
+
// ros2_ws/install* is now inside dist/, so only "dist" is needed.
|
|
359
320
|
function packZip(pluginRoot) {
|
|
360
321
|
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
361
322
|
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
@@ -363,7 +324,6 @@ function packZip(pluginRoot) {
|
|
|
363
324
|
rmSync(zipPath, { force: true });
|
|
364
325
|
|
|
365
326
|
const entries = ["dist"];
|
|
366
|
-
if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) entries.push("ros2_ws/install");
|
|
367
327
|
|
|
368
328
|
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
369
329
|
try {
|
|
@@ -387,27 +347,34 @@ function packZip(pluginRoot) {
|
|
|
387
347
|
/**
|
|
388
348
|
* Ask which target arch(es) to build for.
|
|
389
349
|
* Non-TTY (CI): auto-selects current machine arch.
|
|
390
|
-
* @returns {Promise<string[]>} e.g. ['
|
|
350
|
+
* @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
|
|
391
351
|
*/
|
|
392
|
-
async function promptArch() {
|
|
352
|
+
async function promptArch(args = []) {
|
|
353
|
+
const archFlag = parseArgs(args).arch ?? null;
|
|
354
|
+
if (archFlag) {
|
|
355
|
+
if (archFlag === "both") return ["amd64", "arm64"];
|
|
356
|
+
if (archFlag === "amd64" || archFlag === "x86_64") return ["amd64"];
|
|
357
|
+
if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
|
|
358
|
+
throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
|
|
359
|
+
}
|
|
393
360
|
if (!process.stdin.isTTY) {
|
|
394
361
|
const { arch } = await import("node:os");
|
|
395
|
-
const machine = arch() === "arm64" ? "
|
|
362
|
+
const machine = arch() === "arm64" ? "arm64" : "amd64";
|
|
396
363
|
log.info(`arch auto-selected (non-TTY): ${machine}`);
|
|
397
364
|
return [machine];
|
|
398
365
|
}
|
|
399
366
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
400
367
|
process.stdout.write(
|
|
401
368
|
"\n Target arch:\n" +
|
|
402
|
-
" 1) amd64
|
|
403
|
-
" 2) arm64
|
|
369
|
+
" 1) amd64\n" +
|
|
370
|
+
" 2) arm64\n" +
|
|
404
371
|
" 3) Both (fat ZIP)\n",
|
|
405
372
|
);
|
|
406
373
|
const answer = (await rl.question(" Select [1/2/3, default: 3]: ")).trim() || "3";
|
|
407
374
|
rl.close();
|
|
408
|
-
if (answer === "1") return ["
|
|
409
|
-
if (answer === "2") return ["
|
|
410
|
-
return ["
|
|
375
|
+
if (answer === "1") return ["amd64"];
|
|
376
|
+
if (answer === "2") return ["arm64"];
|
|
377
|
+
return ["amd64", "arm64"];
|
|
411
378
|
}
|
|
412
379
|
|
|
413
380
|
async function promptVersion(root) {
|
|
@@ -437,7 +404,7 @@ export async function runBuild(args) {
|
|
|
437
404
|
if (rt !== "none") ensureDocker();
|
|
438
405
|
|
|
439
406
|
await promptVersion(root);
|
|
440
|
-
const archs = await promptArch();
|
|
407
|
+
const archs = await promptArch(args);
|
|
441
408
|
|
|
442
409
|
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
443
410
|
|
|
@@ -457,8 +424,8 @@ export async function runBuild(args) {
|
|
|
457
424
|
}
|
|
458
425
|
}
|
|
459
426
|
|
|
460
|
-
await exportRos2Build(root);
|
|
427
|
+
await exportRos2Build(root, archs);
|
|
461
428
|
await exportCppBuild(root, distDir, archs);
|
|
462
|
-
calcInstalledSize(pluginRoot
|
|
429
|
+
calcInstalledSize(pluginRoot);
|
|
463
430
|
if (!noZip) packZip(pluginRoot);
|
|
464
431
|
}
|
|
@@ -1,29 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (stat.isDirectory()) {
|
|
14
|
-
if (entry === "dist" || entry === "runtime" || entry === "templates") {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
yield* mjsFiles(full);
|
|
18
|
-
} else if (entry.endsWith(".mjs")) {
|
|
19
|
-
yield full;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
for (const dir of roots) {
|
|
25
|
-
if (!existsSync(dir)) continue;
|
|
26
|
-
for (const file of mjsFiles(dir)) {
|
|
27
|
-
execFileSync(process.execPath, ["--check", file], { stdio: "inherit" });
|
|
28
|
-
}
|
|
7
|
+
for (const dir of ["cli", "preview-host"]) {
|
|
8
|
+
if (!existsSync(resolve(root, dir))) continue;
|
|
9
|
+
execSync(
|
|
10
|
+
`find ${dir} -name '*.mjs' ! -path '*/dist/*' ! -path '*/runtime/*' ! -path '*/templates/*' -exec node --check {} +`,
|
|
11
|
+
{ cwd: root, stdio: "inherit" },
|
|
12
|
+
);
|
|
29
13
|
}
|
package/cli/src/cpp.mjs
CHANGED
|
@@ -21,11 +21,12 @@ export function assertDockerReady() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
24
|
-
export function ensureImage(image = CPP_DEV_IMAGE) {
|
|
25
|
-
log.step(`docker build -t ${image} (first run is slow, then cached)…`);
|
|
24
|
+
export function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
|
|
25
|
+
log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
|
|
26
|
+
const platformFlags = platform ? ["--platform", platform] : [];
|
|
26
27
|
const r = spawnSync(
|
|
27
28
|
"docker",
|
|
28
|
-
["build", "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
29
|
+
["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
29
30
|
{ stdio: "inherit" },
|
|
30
31
|
);
|
|
31
32
|
if (r.status !== 0) throw new Error("docker build failed.");
|
|
@@ -33,9 +34,9 @@ export function ensureImage(image = CPP_DEV_IMAGE) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Docker platform token for each target arch
|
|
36
|
-
const ARCH_DOCKER_PLATFORM = {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
export const ARCH_DOCKER_PLATFORM = {
|
|
38
|
+
amd64: "linux/amd64",
|
|
39
|
+
arm64: "linux/arm64",
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -47,9 +48,10 @@ const ARCH_DOCKER_PLATFORM = {
|
|
|
47
48
|
*/
|
|
48
49
|
export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug", arch = null } = {}) {
|
|
49
50
|
const buildDir = arch ? `cpp/build-${arch}` : "cpp/build";
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const dockerPlatform = arch ? ARCH_DOCKER_PLATFORM[arch] : null;
|
|
52
|
+
const platformFlags = dockerPlatform ? ["--platform", dockerPlatform] : [];
|
|
53
|
+
// arch 빌드 시 arch별 이미지 사용 (로컬 이미지와 platform 불일치 방지)
|
|
54
|
+
const actualImage = arch ? `${CPP_DEV_IMAGE}-${arch}` : image;
|
|
53
55
|
const script = [
|
|
54
56
|
`cmake -S cpp -B ${buildDir} -DCMAKE_BUILD_TYPE=${buildType}`,
|
|
55
57
|
`cmake --build ${buildDir} -j`,
|
|
@@ -58,7 +60,7 @@ export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug", a
|
|
|
58
60
|
log.step(label);
|
|
59
61
|
const r = spawnSync(
|
|
60
62
|
"docker",
|
|
61
|
-
["run", "--rm", ...platformFlags, "-v", `${root}:/plugin`, "-w", "/plugin",
|
|
63
|
+
["run", "--rm", ...platformFlags, "-v", `${root}:/plugin`, "-w", "/plugin", actualImage, "bash", "-lc", script],
|
|
62
64
|
{ cwd: root, stdio: "inherit" },
|
|
63
65
|
);
|
|
64
66
|
if (r.status !== 0) throw new Error(`cmake build failed${arch ? ` (${arch})` : ""}.`);
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
9
|
// on source changes (rclcpp rebuilds on change).
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, watch } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
import { ensureDocker, findPluginRoot, log } from "./util.mjs";
|
|
14
|
-
import { readManifest
|
|
14
|
+
import { manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
|
|
15
15
|
import {
|
|
16
16
|
PYTHON_DEV_IMAGE,
|
|
17
17
|
assertDockerReady as pyAssertDockerReady,
|
|
@@ -29,18 +29,6 @@ import {
|
|
|
29
29
|
ensureImage as cppEnsureImage,
|
|
30
30
|
} from "./cpp.mjs";
|
|
31
31
|
|
|
32
|
-
function readManifest(root) {
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
35
|
-
} catch {
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function manifestRuntime(root) {
|
|
41
|
-
return readManifest(root).runtime?.type ?? "python";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
32
|
// ── python ────────────────────────────────────────────────────────────────────
|
|
45
33
|
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
46
34
|
// Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
|
|
@@ -245,7 +233,7 @@ function runCpp(root) {
|
|
|
245
233
|
}
|
|
246
234
|
|
|
247
235
|
function warnManifest(root) {
|
|
248
|
-
const manifest =
|
|
236
|
+
const manifest = readManifest(root);
|
|
249
237
|
if (!manifest || Object.keys(manifest).length === 0) return;
|
|
250
238
|
const errors = validateManifest(manifest);
|
|
251
239
|
for (const e of errors) log.warn(`manifest.json: ${e}`);
|
package/cli/src/manifest.mjs
CHANGED
|
@@ -36,6 +36,10 @@ export function readManifest(root) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function manifestRuntime(root) {
|
|
40
|
+
return readManifest(root)?.runtime?.type ?? "python";
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
function validateIcons(icons, errors) {
|
|
40
44
|
if (icons === undefined || icons === null) {
|
|
41
45
|
errors.push("icons is required");
|
package/cli/src/new.mjs
CHANGED
|
@@ -10,10 +10,9 @@ import {
|
|
|
10
10
|
statSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { createInterface } from "node:readline/promises";
|
|
14
13
|
import { basename, resolve } from "node:path";
|
|
15
14
|
import { rmSync } from "node:fs";
|
|
16
|
-
import { hasBin, kitVersion, parseArgs, templatesDir, log } from "./util.mjs";
|
|
15
|
+
import { hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
|
|
17
16
|
|
|
18
17
|
// `--type` values map to a runtime descriptor. ros2 has a python (rclpy) and a
|
|
19
18
|
// C++ (rclcpp) flavor; we expose them as distinct type choices for clarity.
|
|
@@ -41,14 +40,6 @@ export function runtimeForChoice(choice, slug) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
async function prompt(question, fallback) {
|
|
45
|
-
if (!process.stdin.isTTY) return fallback;
|
|
46
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
-
const answer = (await rl.question(question)).trim();
|
|
48
|
-
rl.close();
|
|
49
|
-
return answer || fallback;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
export function applyTokens(dir, tokens) {
|
|
53
44
|
for (const entry of readdirSync(dir)) {
|
|
54
45
|
const full = resolve(dir, entry);
|
package/cli/src/python.mjs
CHANGED
|
@@ -35,8 +35,8 @@ export function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {})
|
|
|
35
35
|
// Docker --platform 플래그: 컨테이너를 target arch로 띄우면
|
|
36
36
|
// pip이 glibc + arch를 자동 감지해 올바른 휠을 받는다.
|
|
37
37
|
const ARCH_DOCKER_PLATFORM = {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
amd64: "linux/amd64",
|
|
39
|
+
arm64: "linux/arm64",
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/**
|
package/cli/src/ros2.mjs
CHANGED
|
@@ -30,12 +30,18 @@ export function assertDockerReady() {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export const ARCH_DOCKER_PLATFORM = {
|
|
34
|
+
amd64: "linux/amd64",
|
|
35
|
+
arm64: "linux/arm64",
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
34
|
-
export function ensureImage(image = ROS2_DEV_IMAGE) {
|
|
35
|
-
log.step(`docker build -t ${image} (first run is slow, then cached)…`);
|
|
39
|
+
export function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } = {}) {
|
|
40
|
+
log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
|
|
41
|
+
const platformFlags = platform ? ["--platform", platform] : [];
|
|
36
42
|
const r = spawnSync(
|
|
37
43
|
"docker",
|
|
38
|
-
["build", "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
44
|
+
["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
39
45
|
{ stdio: "inherit" },
|
|
40
46
|
);
|
|
41
47
|
if (r.status !== 0) throw new Error("docker build failed.");
|
|
@@ -47,20 +53,42 @@ export function ensureImage(image = ROS2_DEV_IMAGE) {
|
|
|
47
53
|
* (the workspace is mounted). symlink=true for dev (fast, edit-in-place);
|
|
48
54
|
* symlink=false for export builds that ship in the ZIP (real copies, relocatable).
|
|
49
55
|
*/
|
|
50
|
-
|
|
56
|
+
/**
|
|
57
|
+
* arch=null → dev mode, builds to ros2_ws/install/ (native)
|
|
58
|
+
* arch="amd64"|"arm64" → export mode, builds to ros2_ws/install-{arch}/ using arch-specific image
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* symlink=true (dev): ros2_ws/install/ — local dev workspace, not for ZIP
|
|
62
|
+
* symlink=false, arch=null (rclpy export): dist/ros2_ws/install/
|
|
63
|
+
* symlink=false, arch="amd64"|"arm64" (rclcpp export): dist/ros2_ws/install-{arch}/
|
|
64
|
+
*/
|
|
65
|
+
export function colconBuild(root, { image = ROS2_DEV_IMAGE, symlink = false, arch = null } = {}) {
|
|
66
|
+
// paths are relative to ros2_ws/ inside the container
|
|
67
|
+
let installBase;
|
|
68
|
+
if (symlink) {
|
|
69
|
+
installBase = "install";
|
|
70
|
+
} else if (arch) {
|
|
71
|
+
installBase = `../dist/ros2_ws/install-${arch}`;
|
|
72
|
+
} else {
|
|
73
|
+
installBase = "../dist/ros2_ws/install";
|
|
74
|
+
}
|
|
51
75
|
const flag = symlink ? "--symlink-install" : "";
|
|
52
76
|
const script = [
|
|
53
|
-
// --ignore-installed: don't try to uninstall debian-managed packages
|
|
54
|
-
// (e.g. psutil), which fails with "RECORD file not found".
|
|
55
77
|
"pip install -r requirements.txt --break-system-packages --ignore-installed -q || true",
|
|
56
78
|
"cd ros2_ws",
|
|
57
|
-
`colcon build ${flag}`.trim(),
|
|
79
|
+
`colcon build ${flag} --install-base ${installBase}`.trim(),
|
|
58
80
|
].join(" && ");
|
|
59
|
-
|
|
81
|
+
const label = arch
|
|
82
|
+
? `colcon build (${arch}) → dist/ros2_ws/install-${arch}/…`
|
|
83
|
+
: `colcon build${symlink ? " --symlink-install" : ""} (in container)…`;
|
|
84
|
+
log.step(label);
|
|
85
|
+
const actualImage = arch ? `${ROS2_DEV_IMAGE}-${arch}` : image;
|
|
86
|
+
const dockerPlatform = arch ? ARCH_DOCKER_PLATFORM[arch] : null;
|
|
87
|
+
const platformFlags = dockerPlatform ? ["--platform", dockerPlatform] : [];
|
|
60
88
|
const r = spawnSync(
|
|
61
89
|
"docker",
|
|
62
|
-
["run", "--rm", "-v", `${root}:/plugin`, "-w", "/plugin",
|
|
90
|
+
["run", "--rm", ...platformFlags, "-v", `${root}:/plugin`, "-w", "/plugin", actualImage, "bash", "-lc", script],
|
|
63
91
|
{ cwd: root, stdio: "inherit" },
|
|
64
92
|
);
|
|
65
|
-
if (r.status !== 0) throw new Error(
|
|
93
|
+
if (r.status !== 0) throw new Error(`colcon build failed${arch ? ` (${arch})` : ""}.`);
|
|
66
94
|
}
|
package/cli/src/type.mjs
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// Adds the matching backend overlay; never deletes existing user code
|
|
4
4
|
// (e.g. ros2_ws/ or cpp/ are left in place when switching away).
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
6
|
import { basename, resolve } from "node:path";
|
|
8
|
-
import { findPluginRoot, log } from "./util.mjs";
|
|
7
|
+
import { findPluginRoot, log, prompt } from "./util.mjs";
|
|
9
8
|
import {
|
|
10
9
|
TYPE_CHOICES,
|
|
11
10
|
applyTokens,
|
|
@@ -17,14 +16,6 @@ import {
|
|
|
17
16
|
|
|
18
17
|
const TYPES = Object.keys(TYPE_CHOICES);
|
|
19
18
|
|
|
20
|
-
async function prompt(question, fallback) {
|
|
21
|
-
if (!process.stdin.isTTY) return fallback;
|
|
22
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
-
const answer = (await rl.question(question)).trim();
|
|
24
|
-
rl.close();
|
|
25
|
-
return answer || fallback;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
19
|
function readManifest(root) {
|
|
29
20
|
const path = resolve(root, "manifest.json");
|
|
30
21
|
if (!existsSync(path)) throw new Error(`manifest.json not found in ${root}`);
|
package/cli/src/update.mjs
CHANGED
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
//
|
|
11
11
|
// After updating the CLI itself, also bumps the plugin project's local deps
|
|
12
12
|
// and migrates the project structure if it's outdated (src/ → ui/, root main.py → py/).
|
|
13
|
-
import { createInterface } from "node:readline";
|
|
14
13
|
import { execSync, spawn } from "node:child_process";
|
|
15
14
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
17
16
|
import { join, resolve } from "node:path";
|
|
18
|
-
import { findPluginRoot, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs } from "./util.mjs";
|
|
17
|
+
import { findPluginRoot, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
|
|
19
18
|
|
|
20
19
|
const PLUGIN_UI_PACKAGE = "@rainbow-robotics/plugin-ui";
|
|
21
20
|
const S3_BASE = "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit";
|
|
@@ -135,13 +134,6 @@ function detectOldStructure(root) {
|
|
|
135
134
|
return issues;
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
function prompt(question) {
|
|
139
|
-
return new Promise((resolve) => {
|
|
140
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
141
|
-
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
137
|
async function migrateStructure(root, issues) {
|
|
146
138
|
log.warn("This plugin project's structure is outdated:");
|
|
147
139
|
for (const issue of issues) log.info(" • " + issue.desc);
|
package/cli/src/util.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
|
|
7
8
|
/** True if an executable is on PATH (cross-platform). */
|
|
8
9
|
export function hasBin(bin) {
|
|
@@ -65,6 +66,15 @@ export function parseArgs(args, { booleans = [] } = {}) {
|
|
|
65
66
|
return out;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Prompt user for input. Returns trimmed answer or fallback (non-TTY or empty). */
|
|
70
|
+
export async function prompt(question, fallback = "") {
|
|
71
|
+
if (!process.stdin.isTTY) return fallback;
|
|
72
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
73
|
+
const answer = (await rl.question(question)).trim();
|
|
74
|
+
rl.close();
|
|
75
|
+
return answer || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
/** Walk up from cwd to find the plugin project root (has manifest.json). */
|
|
69
79
|
export function findPluginRoot(start = process.cwd()) {
|
|
70
80
|
let dir = resolve(start);
|
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.36",
|
|
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.36",
|
|
35
35
|
"socket.io": "^4.8.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|