@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.33 → 0.1.2-dev.35

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.
@@ -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)
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
9
9
  python3 \
10
10
  python3-pip \
11
11
  && rm -rf /var/lib/apt/lists/* \
12
- && pip3 install uv --break-system-packages
12
+ && pip3 install uv==0.8.18 --break-system-packages
13
13
 
14
14
  # TODO(cpp-sdk): the Python SDK is a pybind wrapper over the cpp SDK, so the
15
15
  # cpp SDK shared lib must be present for the python module to load. Install the
package/cli/src/build.mjs CHANGED
@@ -68,37 +68,53 @@ function manifestRuntime(root) {
68
68
  }
69
69
  }
70
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) {
71
+ // For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
72
+ // rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
73
+ // rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
74
+ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
75
75
  if (manifestRuntime(root) !== "ros2") return;
76
- const { assertDockerReady, ensureImage, colconBuild } = await import("./ros2.mjs");
76
+ const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
77
77
  try {
78
78
  assertDockerReady();
79
79
  } catch (e) {
80
80
  log.warn(`ros2 build skipped: ${e.message}`);
81
81
  return;
82
82
  }
83
- ensureImage();
84
- colconBuild(root, { symlink: false });
85
- log.ok("ros2_ws/install ready (will be included in the ZIP)");
83
+
84
+ const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
85
+ const rosClient = manifest.runtime?.rosClient;
86
+
87
+ if (rosClient === "rclcpp") {
88
+ // C++ ROS2: compile per arch so each robot picks the right install-{arch}/
89
+ for (const arch of archs) {
90
+ const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
91
+ if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
92
+ const archImage = `${ROS2_DEV_IMAGE}-${arch}`;
93
+ ensureImage(archImage, { platform: dockerPlatform });
94
+ colconBuild(root, { symlink: false, arch });
95
+ log.ok(`dist/ros2_ws/install-${arch}/ ready`);
96
+ }
97
+ } else {
98
+ // rclpy / pure Python: arch-agnostic, build once
99
+ ensureImage();
100
+ colconBuild(root, { symlink: false });
101
+ log.ok("dist/ros2_ws/install/ ready");
102
+ }
86
103
  }
87
104
 
88
105
  // For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
89
106
  // and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
90
107
  // robot-compatible executables. rb-runtime prefers dist/{arch}/ and falls back
91
108
  // to dist/{entry} (legacy). Skipped (with a warning) if Docker is unavailable.
92
- export async function exportCppBuild(root, distDir, archs = ["x86_64", "aarch64"]) {
109
+ export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
93
110
  if (manifestRuntime(root) !== "cpp") return;
94
- const { assertDockerReady, ensureImage, cmakeBuild } = await import("./cpp.mjs");
111
+ const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
95
112
  try {
96
113
  assertDockerReady();
97
114
  } catch (e) {
98
115
  log.warn(`cpp build skipped: ${e.message}`);
99
116
  return;
100
117
  }
101
- ensureImage();
102
118
 
103
119
  const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
104
120
  if (!entry) {
@@ -107,6 +123,11 @@ export async function exportCppBuild(root, distDir, archs = ["x86_64", "aarch64"
107
123
  }
108
124
 
109
125
  for (const arch of archs) {
126
+ // python과 동일하게 arch별 이미지를 빌드해야 --platform 불일치를 피할 수 있다.
127
+ const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
128
+ if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
129
+ const archImage = `${CPP_DEV_IMAGE}-${arch}`;
130
+ ensureImage(archImage, { platform: dockerPlatform });
110
131
  cmakeBuild(root, { buildType: "Release", arch });
111
132
  const buildDir = `cpp/build-${arch}`;
112
133
  const built = resolve(root, buildDir, entry);
@@ -284,57 +305,15 @@ function dirSizeExcluding(dir, skip = []) {
284
305
 
285
306
  // Read the uncompressed byte total of a .whl (zip) file by parsing its central
286
307
  // 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
308
 
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
- });
309
+ // installedSize = "이 플러그인 하나를 지우면 되찾는 용량"
310
+ // = dist/(wheels/ 제외)
311
+ // ros2_ws/install* 은 이제 dist/ 안에 있으므로 별도 합산 불필요.
312
+ // 휠은 로봇 공유 스토어(store/py/)에 언팩되므로 포함하지 않는다.
313
+ function calcInstalledSize(pluginRoot) {
314
+ const distDir = resolve(pluginRoot, "dist");
336
315
 
337
- const installedSize = Math.max(0, ...perArchSize);
316
+ const installedSize = dirSizeExcluding(distDir, ["wheels"]);
338
317
 
339
318
  // Write to root manifest.json
340
319
  const rootManifestPath = resolve(pluginRoot, "manifest.json");
@@ -352,10 +331,11 @@ function calcInstalledSize(pluginRoot, archs) {
352
331
  writeFileSync(distManifestPath, JSON.stringify(m, null, 2) + "\n");
353
332
  }
354
333
 
355
- log.ok(`installedSize → ${humanSize(installedSize)} (${archs.join(", ")} max, uncompressed)`);
334
+ log.ok(`installedSize → ${humanSize(installedSize)}`);
356
335
  }
357
336
 
358
- // Zip dist/ (+ ros2_ws/install) into a distributable ZIP with a size report.
337
+ // Zip dist/ into a distributable ZIP with a size report.
338
+ // ros2_ws/install* is now inside dist/, so only "dist" is needed.
359
339
  function packZip(pluginRoot) {
360
340
  const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
361
341
  const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
@@ -363,7 +343,6 @@ function packZip(pluginRoot) {
363
343
  rmSync(zipPath, { force: true });
364
344
 
365
345
  const entries = ["dist"];
366
- if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) entries.push("ros2_ws/install");
367
346
 
368
347
  log.step(`packing ${entries.join(", ")} → ${zipName}`);
369
348
  try {
@@ -387,27 +366,36 @@ function packZip(pluginRoot) {
387
366
  /**
388
367
  * Ask which target arch(es) to build for.
389
368
  * Non-TTY (CI): auto-selects current machine arch.
390
- * @returns {Promise<string[]>} e.g. ['x86_64'] | ['aarch64'] | ['x86_64','aarch64']
369
+ * @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
391
370
  */
392
- async function promptArch() {
371
+ async function promptArch(args = []) {
372
+ // --arch x86_64 | --arch aarch64 | --arch both
373
+ const archFlag = args.find((a) => a.startsWith("--arch="))?.slice(7)
374
+ ?? (args.includes("--arch") ? args[args.indexOf("--arch") + 1] : null);
375
+ if (archFlag) {
376
+ if (archFlag === "both") return ["amd64", "arm64"];
377
+ if (archFlag === "amd64" || archFlag === "x86_64") return ["amd64"];
378
+ if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
379
+ throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
380
+ }
393
381
  if (!process.stdin.isTTY) {
394
382
  const { arch } = await import("node:os");
395
- const machine = arch() === "arm64" ? "aarch64" : "x86_64";
383
+ const machine = arch() === "arm64" ? "arm64" : "amd64";
396
384
  log.info(`arch auto-selected (non-TTY): ${machine}`);
397
385
  return [machine];
398
386
  }
399
387
  const rl = createInterface({ input: process.stdin, output: process.stdout });
400
388
  process.stdout.write(
401
389
  "\n Target arch:\n" +
402
- " 1) amd64 (x86_64)\n" +
403
- " 2) arm64 (aarch64)\n" +
390
+ " 1) amd64\n" +
391
+ " 2) arm64\n" +
404
392
  " 3) Both (fat ZIP)\n",
405
393
  );
406
394
  const answer = (await rl.question(" Select [1/2/3, default: 3]: ")).trim() || "3";
407
395
  rl.close();
408
- if (answer === "1") return ["x86_64"];
409
- if (answer === "2") return ["aarch64"];
410
- return ["x86_64", "aarch64"];
396
+ if (answer === "1") return ["amd64"];
397
+ if (answer === "2") return ["arm64"];
398
+ return ["amd64", "arm64"];
411
399
  }
412
400
 
413
401
  async function promptVersion(root) {
@@ -437,7 +425,7 @@ export async function runBuild(args) {
437
425
  if (rt !== "none") ensureDocker();
438
426
 
439
427
  await promptVersion(root);
440
- const archs = await promptArch();
428
+ const archs = await promptArch(args);
441
429
 
442
430
  const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
443
431
 
@@ -457,8 +445,8 @@ export async function runBuild(args) {
457
445
  }
458
446
  }
459
447
 
460
- await exportRos2Build(root);
448
+ await exportRos2Build(root, archs);
461
449
  await exportCppBuild(root, distDir, archs);
462
- calcInstalledSize(pluginRoot, archs);
450
+ calcInstalledSize(pluginRoot);
463
451
  if (!noZip) packZip(pluginRoot);
464
452
  }
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
- x86_64: "linux/amd64",
38
- aarch64: "linux/arm64",
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 platformFlags = arch && ARCH_DOCKER_PLATFORM[arch]
51
- ? ["--platform", ARCH_DOCKER_PLATFORM[arch]]
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", image, "bash", "-lc", script],
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})` : ""}.`);
@@ -20,21 +20,23 @@ export function assertDockerReady() {
20
20
  }
21
21
 
22
22
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
23
- export function ensureImage(image = PYTHON_DEV_IMAGE) {
24
- log.step(`docker build -t ${image} (first run is slow, then cached)…`);
23
+ export function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {}) {
24
+ log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
25
+ const platformFlags = platform ? ["--platform", platform] : [];
25
26
  const r = spawnSync(
26
27
  "docker",
27
- ["build", "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
28
+ ["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
28
29
  { stdio: "inherit" },
29
30
  );
30
31
  if (r.status !== 0) throw new Error("docker build failed.");
31
32
  pruneMuskatImages();
32
33
  }
33
34
 
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",
35
+ // Docker --platform 플래그: 컨테이너를 target arch로 띄우면
36
+ // pip이 glibc + arch를 자동 감지해 올바른 휠을 받는다.
37
+ const ARCH_DOCKER_PLATFORM = {
38
+ amd64: "linux/amd64",
39
+ arm64: "linux/arm64",
38
40
  };
39
41
 
40
42
  /**
@@ -73,24 +75,36 @@ export function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
73
75
  // 한 arch가 실패해도 나머지 arch는 계속 시도한다.
74
76
  const failed = [];
75
77
  for (const arch of archs) {
76
- const platform = ARCH_PLATFORM[arch];
77
- if (!platform) {
78
+ const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
79
+ if (!dockerPlatform) {
78
80
  log.warn(`unknown arch '${arch}' — skipped`);
79
81
  continue;
80
82
  }
81
- log.step(`vendoring wheels for ${arch} (${platform})…`);
83
+ // arch별 이미지 빌드 (--platform 지정) → pip이 glibc+arch 자동 감지 → 올바른 휠 선택.
84
+ const archImage = `${PYTHON_DEV_IMAGE}-${arch}`;
85
+ ensureImage(archImage, { platform: dockerPlatform });
86
+
87
+ log.step(`vendoring wheels for ${arch} (${dockerPlatform})…`);
88
+ // 패키지별 개별 download로 private/미공개 패키지 실패 시에도 나머지 계속 진행.
89
+ const script = [
90
+ `set -e`,
91
+ `cd /plugin`,
92
+ `mkdir -p dist/wheels`,
93
+ `pkgs=$(grep -v '^#' dist/requirements.lock | grep -v '^[[:space:]]' | grep -v '^$')`,
94
+ `SKIP=""`,
95
+ `for pkg in $pkgs; do`,
96
+ ` pip3 download "$pkg" --no-deps --only-binary=:all: -d dist/wheels -q 2>&1 || SKIP="$SKIP $pkg"`,
97
+ `done`,
98
+ `[ -z "$SKIP" ] || echo "WARN: no wheel found (package not on PyPI or missing binary wheel):$SKIP"`,
99
+ ].join("\n");
82
100
  const r = spawnSync(
83
101
  "docker",
84
102
  [
85
103
  "run", "--rm",
104
+ "--platform", dockerPlatform,
86
105
  "-v", `${pluginRoot}:/plugin`,
87
- PYTHON_DEV_IMAGE,
88
- "bash", "-c",
89
- `set -e && cd /plugin && mkdir -p dist/wheels && ` +
90
- `python3 -m pip install -q --upgrade pip && ` +
91
- `pip3 download -r dist/requirements.lock ` +
92
- `--only-binary=:all: --platform ${platform} --python-version 312 --implementation cp ` +
93
- `-d dist/wheels`,
106
+ archImage,
107
+ "bash", "-c", script,
94
108
  ],
95
109
  { stdio: "inherit" },
96
110
  );
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
- export function colconBuild(root, { image = ROS2_DEV_IMAGE, symlink = false } = {}) {
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
- log.step(`colcon build${symlink ? " --symlink-install" : ""} (in container)…`);
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", image, "bash", "-lc", script],
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("colcon build failed.");
93
+ if (r.status !== 0) throw new Error(`colcon build failed${arch ? ` (${arch})` : ""}.`);
66
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.33",
3
+ "version": "0.1.2-dev.35",
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.33",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.35",
35
35
  "socket.io": "^4.8.1"
36
36
  },
37
37
  "optionalDependencies": {