@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.41 → 0.1.2-dev.42
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 +54 -26
- package/cli/src/devUi.mjs +9 -5
- package/cli/src/python.mjs +34 -47
- package/package.json +2 -2
package/cli/src/build.mjs
CHANGED
|
@@ -75,27 +75,33 @@ function isMainBranch() {
|
|
|
75
75
|
// rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
|
|
76
76
|
// rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
|
|
77
77
|
export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
78
|
-
if (manifestRuntime(root) !== "ros2") return;
|
|
78
|
+
if (manifestRuntime(root) !== "ros2") return [];
|
|
79
79
|
const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
|
|
80
80
|
try {
|
|
81
81
|
assertDockerReady();
|
|
82
82
|
} catch (e) {
|
|
83
83
|
log.warn(`ros2 build skipped: ${e.message}`);
|
|
84
|
-
return;
|
|
84
|
+
return [];
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
88
88
|
const rosClient = manifest.runtime?.rosClient;
|
|
89
89
|
|
|
90
|
+
const skipped = [];
|
|
90
91
|
if (rosClient === "rclcpp") {
|
|
91
92
|
// C++ ROS2: compile per arch so each robot picks the right install-{arch}/
|
|
92
93
|
for (const arch of archs) {
|
|
93
94
|
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
94
|
-
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
95
|
+
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); skipped.push(arch); continue; }
|
|
95
96
|
const archImage = `${ROS2_DEV_IMAGE}-${arch}`;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
try {
|
|
98
|
+
await ensureImage(archImage, { platform: dockerPlatform });
|
|
99
|
+
colconBuild(root, { symlink: false, arch });
|
|
100
|
+
log.ok(`dist/ros2_ws/install-${arch}/ ready`);
|
|
101
|
+
} catch {
|
|
102
|
+
log.warn(`${arch} ros2 build skipped — QEMU not available`);
|
|
103
|
+
skipped.push(arch);
|
|
104
|
+
}
|
|
99
105
|
}
|
|
100
106
|
} else {
|
|
101
107
|
// rclpy / pure Python: arch-agnostic, build once
|
|
@@ -103,6 +109,7 @@ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
|
103
109
|
colconBuild(root, { symlink: false });
|
|
104
110
|
log.ok("dist/ros2_ws/install/ ready");
|
|
105
111
|
}
|
|
112
|
+
return skipped;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
|
|
@@ -110,13 +117,13 @@ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
|
110
117
|
// robot-compatible executables. rb-runtime prefers dist/{arch}/ and falls back
|
|
111
118
|
// to dist/{entry} (legacy). Skipped (with a warning) if Docker is unavailable.
|
|
112
119
|
export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
|
|
113
|
-
if (manifestRuntime(root) !== "cpp") return;
|
|
120
|
+
if (manifestRuntime(root) !== "cpp") return [];
|
|
114
121
|
const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
|
|
115
122
|
try {
|
|
116
123
|
assertDockerReady();
|
|
117
124
|
} catch (e) {
|
|
118
125
|
log.warn(`cpp build skipped: ${e.message}`);
|
|
119
|
-
return;
|
|
126
|
+
return [];
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
@@ -125,26 +132,31 @@ export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"])
|
|
|
125
132
|
return;
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
const skipped = [];
|
|
128
136
|
for (const arch of archs) {
|
|
129
|
-
// python과 동일하게 arch별 이미지를 빌드해야 --platform 불일치를 피할 수 있다.
|
|
130
137
|
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
131
|
-
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
138
|
+
if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); skipped.push(arch); continue; }
|
|
132
139
|
const archImage = `${CPP_DEV_IMAGE}-${arch}`;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
try {
|
|
141
|
+
await ensureImage(archImage, { platform: dockerPlatform });
|
|
142
|
+
cmakeBuild(root, { buildType: "Release", arch });
|
|
143
|
+
const built = resolve(root, `cpp/build-${arch}`, entry);
|
|
144
|
+
if (existsSync(built)) {
|
|
145
|
+
const archDir = resolve(distDir, arch);
|
|
146
|
+
mkdirSync(archDir, { recursive: true });
|
|
147
|
+
const dest = resolve(archDir, entry);
|
|
148
|
+
copyFileSync(built, dest);
|
|
149
|
+
chmodSync(dest, 0o755);
|
|
150
|
+
log.ok(`cpp binary → dist/${arch}/${entry}`);
|
|
151
|
+
} else {
|
|
152
|
+
log.warn(`cpp binary not found at cpp/build-${arch}/${entry}`);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
log.warn(`${arch} cpp build skipped — QEMU not available`);
|
|
156
|
+
skipped.push(arch);
|
|
146
157
|
}
|
|
147
158
|
}
|
|
159
|
+
return skipped;
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
const PRERELEASE_RE = /\.(dev|a|b|rc|alpha|beta)\d*/i;
|
|
@@ -434,6 +446,8 @@ export async function runBuild(args) {
|
|
|
434
446
|
|
|
435
447
|
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
436
448
|
|
|
449
|
+
const skippedArches = { wheels: [], ros2: [], cpp: [] };
|
|
450
|
+
|
|
437
451
|
// python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
|
|
438
452
|
if (!noLock) {
|
|
439
453
|
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
|
|
@@ -443,15 +457,29 @@ export async function runBuild(args) {
|
|
|
443
457
|
if (needsLock) {
|
|
444
458
|
const { lockAndVendor } = await import("./python.mjs");
|
|
445
459
|
try {
|
|
446
|
-
await lockAndVendor(pluginRoot, archs);
|
|
460
|
+
skippedArches.wheels = await lockAndVendor(pluginRoot, archs);
|
|
447
461
|
} catch (e) {
|
|
448
462
|
log.warn(`python lock/vendor skipped: ${e.message}`);
|
|
463
|
+
skippedArches.wheels = archs;
|
|
449
464
|
}
|
|
450
465
|
}
|
|
451
466
|
}
|
|
452
467
|
|
|
453
|
-
await exportRos2Build(root, archs);
|
|
454
|
-
await exportCppBuild(root, distDir, archs);
|
|
468
|
+
skippedArches.ros2 = await exportRos2Build(root, archs) ?? [];
|
|
469
|
+
skippedArches.cpp = await exportCppBuild(root, distDir, archs) ?? [];
|
|
455
470
|
calcInstalledSize(pluginRoot);
|
|
456
471
|
if (!noZip) packZip(pluginRoot);
|
|
472
|
+
|
|
473
|
+
// Build summary
|
|
474
|
+
const allSkipped = [...new Set([...skippedArches.wheels, ...skippedArches.ros2, ...skippedArches.cpp])];
|
|
475
|
+
if (allSkipped.length > 0) {
|
|
476
|
+
const line = "─".repeat(58);
|
|
477
|
+
console.log(`\n${line}`);
|
|
478
|
+
log.warn(`Cross-arch builds skipped (QEMU not available): ${allSkipped.join(", ")}`);
|
|
479
|
+
if (skippedArches.wheels.length) log.warn(` wheels: ${skippedArches.wheels.join(", ")}`);
|
|
480
|
+
if (skippedArches.ros2.length) log.warn(` ros2: ${skippedArches.ros2.join(", ")}`);
|
|
481
|
+
if (skippedArches.cpp.length) log.warn(` cpp: ${skippedArches.cpp.join(", ")}`);
|
|
482
|
+
log.warn(` Enable: docker run --privileged --rm tonistiigi/binfmt --install all`);
|
|
483
|
+
console.log(line);
|
|
484
|
+
}
|
|
457
485
|
}
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
9
|
import { SANDBOX_HTML } from "./build.mjs";
|
|
10
|
-
import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log } from "./util.mjs";
|
|
10
|
+
import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log, parseArgs, prompt } from "./util.mjs";
|
|
11
11
|
|
|
12
12
|
const PREVIEW_PKG = "@rainbow-robotics/preview-host";
|
|
13
13
|
const CACHE_DIR = resolve(homedir(), ".muscat-pk");
|
|
@@ -79,7 +79,7 @@ function findViteBin(pluginRoot) {
|
|
|
79
79
|
|
|
80
80
|
// Spawn vite build --watch as a background process.
|
|
81
81
|
// Returns the child process, or null if vite/node not available.
|
|
82
|
-
function spawnViteWatch(pluginRoot, node) {
|
|
82
|
+
function spawnViteWatch(pluginRoot, node, { robotIp = "" } = {}) {
|
|
83
83
|
const viteBin = findViteBin(pluginRoot);
|
|
84
84
|
if (!node || !viteBin || !existsSync(viteBin)) return null;
|
|
85
85
|
|
|
@@ -87,6 +87,7 @@ function spawnViteWatch(pluginRoot, node) {
|
|
|
87
87
|
return spawn(node, [viteBin, "build", "--watch", "--mode", "development", "--outDir", DEV_OUT], {
|
|
88
88
|
cwd: pluginRoot,
|
|
89
89
|
stdio: "inherit",
|
|
90
|
+
env: { ...process.env, ...(robotIp ? { VITE_RRS_PY_HOST: robotIp } : {}) },
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -105,15 +106,18 @@ function waitForInitialBuild(distDir, timeoutMs = 30_000) {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export async function runDevUi(args = []) {
|
|
108
|
-
const
|
|
109
|
-
const port =
|
|
109
|
+
const parsed = parseArgs(args);
|
|
110
|
+
const port = parsed.port ?? "4180";
|
|
111
|
+
const robotIp = parsed["robot-ip"] ?? await prompt(" Robot IP (Enter to skip): ");
|
|
110
112
|
|
|
111
113
|
await checkKitUpdate();
|
|
112
114
|
const pluginRoot = findPluginRoot();
|
|
113
115
|
const outDir = stageDevAssets(pluginRoot);
|
|
114
116
|
|
|
117
|
+
if (robotIp) log.info(`MUSCAT_API_ORIGIN → http://${robotIp}:*`);
|
|
118
|
+
|
|
115
119
|
const node = findNodeBin();
|
|
116
|
-
const viteProc = spawnViteWatch(pluginRoot, node);
|
|
120
|
+
const viteProc = spawnViteWatch(pluginRoot, node, { robotIp });
|
|
117
121
|
|
|
118
122
|
if (viteProc) {
|
|
119
123
|
log.step("waiting for initial vite build…");
|
package/cli/src/python.mjs
CHANGED
|
@@ -27,22 +27,30 @@ export async function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null }
|
|
|
27
27
|
pruneMuskatImages();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
amd64:
|
|
34
|
-
|
|
30
|
+
// pip --platform does NOT auto-expand manylinux tags (unlike sys_tags()) — exact match only.
|
|
31
|
+
// Specifying all common glibc milestones ensures packages built for any Ubuntu era are found.
|
|
32
|
+
const PIP_PLATFORM = {
|
|
33
|
+
amd64: [
|
|
34
|
+
"manylinux_2_5_x86_64",
|
|
35
|
+
"manylinux_2_17_x86_64",
|
|
36
|
+
"manylinux_2_27_x86_64",
|
|
37
|
+
"manylinux_2_28_x86_64",
|
|
38
|
+
"manylinux_2_34_x86_64",
|
|
39
|
+
"manylinux_2_39_x86_64",
|
|
40
|
+
"linux_x86_64",
|
|
41
|
+
],
|
|
42
|
+
arm64: [
|
|
43
|
+
"manylinux_2_17_aarch64",
|
|
44
|
+
"manylinux_2_27_aarch64",
|
|
45
|
+
"manylinux_2_28_aarch64",
|
|
46
|
+
"manylinux_2_34_aarch64",
|
|
47
|
+
"manylinux_2_39_aarch64",
|
|
48
|
+
"linux_aarch64",
|
|
49
|
+
],
|
|
35
50
|
};
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* 1. uv pip compile requirements.txt → dist/requirements.lock (arch 무관, 1회)
|
|
40
|
-
* 2. archs별 pip download --platform <manylinux_tag> → dist/wheels/ (네이티브 휠 벤더링)
|
|
41
|
-
*
|
|
42
|
-
* none-any 휠은 pip이 두 번째 arch에서 자동 skip → 중복 없음.
|
|
43
|
-
* requirements.txt 없으면 조용히 건너뜀.
|
|
44
|
-
*/
|
|
45
|
-
export async function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
|
|
52
|
+
export async function lockAndVendor(pluginRoot, archs = ["amd64", "arm64"]) {
|
|
53
|
+
// ponytail: pip --platform used intentionally (not QEMU) — downloads wheels without running target arch code.
|
|
46
54
|
const reqTxt = resolve(pluginRoot, "requirements.txt");
|
|
47
55
|
if (!existsSync(reqTxt)) {
|
|
48
56
|
log.warn("requirements.txt not found — lock/vendor skipped");
|
|
@@ -66,45 +74,28 @@ export async function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
|
|
|
66
74
|
if (lockResult.status !== 0) throw new Error("lock generation failed (uv pip compile)");
|
|
67
75
|
log.ok("dist/requirements.lock ready");
|
|
68
76
|
|
|
69
|
-
// arch별 휠 벤더링 — QEMU 불필요 (download only, --platform 고정)
|
|
70
|
-
// 한 arch가 실패해도 나머지 arch는 계속 시도한다.
|
|
71
77
|
const failed = [];
|
|
72
78
|
for (const arch of archs) {
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
// arch별 이미지 빌드 (--platform 지정) → pip이 glibc+arch 자동 감지 → 올바른 휠 선택.
|
|
79
|
-
const archImage = `${PYTHON_DEV_IMAGE}-${arch}`;
|
|
80
|
-
await ensureImage(archImage, { platform: dockerPlatform });
|
|
81
|
-
|
|
82
|
-
log.step(`vendoring wheels for ${arch} (${dockerPlatform})…`);
|
|
83
|
-
// 패키지별 개별 download로 private/미공개 패키지 실패 시에도 나머지 계속 진행.
|
|
84
|
-
const script = [
|
|
85
|
-
`set -e`,
|
|
86
|
-
`cd /plugin`,
|
|
87
|
-
`mkdir -p dist/wheels`,
|
|
88
|
-
`pkgs=$(grep -v '^#' dist/requirements.lock | grep -v '^[[:space:]]' | grep -v '^$')`,
|
|
89
|
-
`SKIP=""`,
|
|
90
|
-
`for pkg in $pkgs; do`,
|
|
91
|
-
` pip3 download "$pkg" --no-deps --only-binary=:all: -d dist/wheels -q 2>&1 || SKIP="$SKIP $pkg"`,
|
|
92
|
-
`done`,
|
|
93
|
-
`[ -z "$SKIP" ] || echo "WARN: no wheel found (package not on PyPI or missing binary wheel):$SKIP"`,
|
|
94
|
-
].join("\n");
|
|
79
|
+
const pipPlatforms = PIP_PLATFORM[arch];
|
|
80
|
+
if (!pipPlatforms) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
81
|
+
log.step(`vendoring wheels for ${arch}…`);
|
|
95
82
|
const r = spawnSync(
|
|
96
83
|
"docker",
|
|
97
84
|
[
|
|
98
85
|
"run", "--rm",
|
|
99
|
-
"
|
|
86
|
+
"-e", "PIP_ROOT_USER_ACTION=ignore",
|
|
100
87
|
"-v", `${pluginRoot}:/plugin`,
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
"-w", "/plugin",
|
|
89
|
+
PYTHON_DEV_IMAGE,
|
|
90
|
+
"bash", "-c",
|
|
91
|
+
"mkdir -p dist/wheels && pip3 download -r dist/requirements.lock --no-deps --only-binary=:all: -d dist/wheels -q " +
|
|
92
|
+
"--python-version 312 --implementation cp --abi cp312 " +
|
|
93
|
+
pipPlatforms.map((p) => `--platform ${p}`).join(" "),
|
|
103
94
|
],
|
|
104
95
|
{ stdio: "inherit" },
|
|
105
96
|
);
|
|
106
97
|
if (r.status !== 0) {
|
|
107
|
-
log.warn(`wheel vendoring failed for ${arch}
|
|
98
|
+
log.warn(`wheel vendoring failed for ${arch}`);
|
|
108
99
|
failed.push(arch);
|
|
109
100
|
} else {
|
|
110
101
|
log.ok(`dist/wheels/ ready (${arch})`);
|
|
@@ -114,9 +105,5 @@ export async function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
|
|
|
114
105
|
if (failed.length === archs.length) {
|
|
115
106
|
throw new Error(`wheel vendoring failed for all archs: ${failed.join(", ")}`);
|
|
116
107
|
}
|
|
117
|
-
|
|
118
|
-
log.warn(`wheel vendoring partial — failed: ${failed.join(", ")}, ok: ${archs.filter((a) => !failed.includes(a)).join(", ")}`);
|
|
119
|
-
} else {
|
|
120
|
-
log.ok(`dist/wheels/ ready (${archs.join(", ")})`);
|
|
121
|
-
}
|
|
108
|
+
return failed;
|
|
122
109
|
}
|
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.42",
|
|
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.42",
|
|
35
35
|
"socket.io": "^4.8.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|