@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 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
- await ensureImage(archImage, { platform: dockerPlatform });
97
- colconBuild(root, { symlink: false, arch });
98
- log.ok(`dist/ros2_ws/install-${arch}/ ready`);
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
- await ensureImage(archImage, { platform: dockerPlatform });
134
- cmakeBuild(root, { buildType: "Release", arch });
135
- const buildDir = `cpp/build-${arch}`;
136
- const built = resolve(root, buildDir, entry);
137
- if (existsSync(built)) {
138
- const archDir = resolve(distDir, arch);
139
- mkdirSync(archDir, { recursive: true });
140
- const dest = resolve(archDir, entry);
141
- copyFileSync(built, dest);
142
- chmodSync(dest, 0o755);
143
- log.ok(`cpp binary → dist/${arch}/${entry}`);
144
- } else {
145
- log.warn(`cpp binary not found at ${buildDir}/${entry} — skipped for ${arch}`);
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 portIdx = args.indexOf("--port");
109
- const port = portIdx !== -1 ? args[portIdx + 1] : "4180";
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…");
@@ -27,22 +27,30 @@ export async function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null }
27
27
  pruneMuskatImages();
28
28
  }
29
29
 
30
- // Docker --platform 플래그: 컨테이너를 target arch로 띄우면
31
- // pip이 glibc + arch를 자동 감지해 올바른 휠을 받는다.
32
- const ARCH_DOCKER_PLATFORM = {
33
- amd64: "linux/amd64",
34
- arm64: "linux/arm64",
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
- * Ubuntu 24.04 + py3.12 컨테이너 안에서:
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 dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
74
- if (!dockerPlatform) {
75
- log.warn(`unknown arch '${arch}' — skipped`);
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
- "--platform", dockerPlatform,
86
+ "-e", "PIP_ROOT_USER_ACTION=ignore",
100
87
  "-v", `${pluginRoot}:/plugin`,
101
- archImage,
102
- "bash", "-c", script,
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} — some packages may not have manylinux wheels`);
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
- if (failed.length > 0) {
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.41",
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.41",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.42",
35
35
  "socket.io": "^4.8.1"
36
36
  },
37
37
  "optionalDependencies": {