Skip to content

[Demo][Electron] Load prebuilt binaries at runtime#1546

Merged
minggangw merged 5 commits into
RobotWebTools:developfrom
minggangw:fix-1545
Jun 22, 2026
Merged

[Demo][Electron] Load prebuilt binaries at runtime#1546
minggangw merged 5 commits into
RobotWebTools:developfrom
minggangw:fix-1545

Conversation

@minggangw

@minggangw minggangw commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

Make the Electron demos load the prebuilt rclnodejs native binary at runtime instead of recompiling it during electron-forge packaging, fix the turtle_tf2 demo's stuck loading screen on GPU-less environments (WSL2/headless), shorten the demo READMEs, and harden the native-loader unit tests.

Native loader (lib/native_loader.js)

  • Add mirrorPrebuiltToBuildRelease(): when an exact-match prebuilt binary is found, copy it into build/Release/rclnodejs.node so standard bindings resolution (and packaged Electron apps where ROS_DISTRO may be unset at runtime) can locate it. Non-destructive — never overwrites an existing build output, preserving local source builds.
  • Extract buildFromSource() and make it Electron-aware: when running under Electron, target Electron's headers/ABI via npm_config_target, npm_config_dist_url, and npm_config_arch so the compiled addon matches the runtime (the addon links libuv directly, so host runtime matters).
  • Replace the two duplicated inline npm run rebuild execSync calls (forced build + fallback build paths) with the shared buildFromSource() helper.

Electron demos — load prebuilt at packaging time

  • demo/electron/{car,manipulator,topics,turtle_tf2}/package.json: Remove the "rebuild": "electron-rebuild" script; add forge.rebuildConfig.ignoreModules: ["rclnodejs"] so electron-forge does not recompile rclnodejs, using the shipped prebuilt binary instead.

turtle_tf2 demo — WebGL / loading-screen fixes

  • main.js: append ignore-gpu-blocklist and enable-unsafe-swiftshader command-line switches so WebGL works on systems without a usable GPU (WSL2, headless, VMs) where Chromium blocklists hardware WebGL.
  • renderer.js: register ROS2 status listeners before 3D scene setup so the loading screen always reflects initialization progress, and wrap initializeScene()/setupEventListeners()/setupKeyboardControls() in a try/catch that surfaces a clear user-facing message ("Failed to initialize 3D view (WebGL unavailable): …") if scene init fails.

Documentation

  • Rewrite/shorten the four demo READMEs (car, manipulator, topics, turtle_tf2) — net ~1100 lines removed, kept accurate against each package.json (scripts, dependency versions, output directory names).

Tests (test/test-native-loader.js)

  • Update the two "exact match" prebuild tests to locate the prebuild candidate via existsSync.getCalls() (filtering for the prebuilds + rclnodejs.node path) instead of relying on existsSync.lastCall, which now points at the new build/Release mirror probe. Adds a guard assertion for a clearer failure message and drops the now-redundant rclnodejs.node check.

Fix: #1545

Copilot AI review requested due to automatic review settings June 18, 2026 07:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Electron demos’ ability to run using rclnodejs prebuilt binaries at runtime (instead of requiring electron-rebuild), and adds a couple of Electron-demo runtime UX/compatibility tweaks.

Changes:

  • Extend the native addon loader to (a) mirror a matched prebuilt binary into build/Release and (b) attempt Electron-targeted compilation settings when building from source under Electron.
  • Update Electron demo docs and Forge configs to stop using electron-rebuild and to exclude rclnodejs from Forge’s rebuild step.
  • Improve demo robustness by catching 3D scene init failures in the renderer and by enabling GPU/WebGL-related Electron flags in the TF2 demo.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
lib/native_loader.js Adds mirroring of matched prebuilds to build/Release and introduces Electron-aware source build environment.
demo/electron/turtle_tf2/renderer.js Wraps 3D initialization in try/catch and updates the loading screen on failure.
demo/electron/turtle_tf2/README.md Updates setup/troubleshooting guidance to rely on runtime prebuilt selection instead of manual rebuild.
demo/electron/turtle_tf2/package.json Removes rebuild script and configures Forge rebuild to ignore rclnodejs.
demo/electron/turtle_tf2/main.js Adds Chromium command-line switches to improve WebGL availability on constrained systems.
demo/electron/topics/README.md Updates setup instructions to avoid electron-rebuild and rely on runtime prebuilt selection.
demo/electron/topics/package.json Removes rebuild script and configures Forge rebuild to ignore rclnodejs.
demo/electron/manipulator/README.md Updates setup guidance to source ROS 2 for prebuilt selection and removes rebuild instructions.
demo/electron/manipulator/package.json Removes rebuild script and configures Forge rebuild to ignore rclnodejs.
demo/electron/car/README.md Updates setup/troubleshooting to avoid manual rebuild and rely on prebuilt selection.
demo/electron/car/package.json Removes rebuild script and configures Forge rebuild to ignore rclnodejs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/native_loader.js
Comment on lines +63 to +71
if (detectPrebuildRuntime() === 'electron' && process.versions.electron) {
// node-gyp targets Electron via --target (Electron version) + --dist-url
// (Electron headers); it reads these from npm_config_* env vars.
env.npm_config_target = process.versions.electron;
env.npm_config_dist_url = 'https://electronjs.org/headers';
env.npm_config_arch = process.arch;
debug(`Building from source for Electron ${process.versions.electron}`);
} else {
debug('Building from source for Node.js');
Comment thread lib/native_loader.js
Comment on lines +37 to +44
// Copy a matched prebuilt binary into build/Release so that standard binary
// resolution (and packaged Electron apps where ROS_DISTRO may be unset at
// runtime) can still locate it. Non-destructive: never overwrites an existing
// build output, so local source builds are preserved.
function mirrorPrebuiltToBuildRelease(srcPath) {
try {
const destDir = path.join(__dirname, '..', 'build', 'Release');
const destPath = path.join(destDir, 'rclnodejs.node');
Comment on lines +70 to +76
const loadingText = loadingScreen.querySelector('div:last-child');
if (loadingText) {
loadingText.textContent =
'Failed to initialize 3D view (WebGL unavailable): ' + err.message;
loadingText.style.color = '#ff4444';
}
}
Comment on lines +16 to +21
// Allow WebGL to work on systems without a usable GPU (e.g. WSL2, headless,
// or VMs) where Chromium blocklists hardware WebGL. Without this, the 3D
// renderer fails to create a WebGL context and the visualization cannot start.
app.commandLine.appendSwitch('ignore-gpu-blocklist');
app.commandLine.appendSwitch('enable-unsafe-swiftshader');

@coveralls

coveralls commented Jun 18, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 91.143% (-0.08%) from 91.22% — minggangw:fix-1545 into RobotWebTools:develop

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread lib/native_loader.js
Comment on lines +41 to +50
function mirrorPrebuiltToBuildRelease(srcPath) {
try {
const destDir = path.join(__dirname, '..', 'build', 'Release');
const destPath = path.join(destDir, 'rclnodejs.node');
if (fs.existsSync(destPath)) {
return;
}
fs.mkdirSync(destDir, { recursive: true });
fs.copyFileSync(srcPath, destPath);
debug(`Mirrored prebuilt binary to ${destPath}`);
Comment thread lib/native_loader.js
Comment on lines 116 to 129
try {
const candidate = getTaggedPrebuildFilename({
rosDistro,
ubuntuCodename,
arch,
runtime,
});
const candidatePath = path.join(prebuildDir, candidate);

if (fs.existsSync(candidatePath)) {
debug(`Found ${runtime} prebuilt binary: ${candidate}`);
mirrorPrebuiltToBuildRelease(candidatePath);
return require(candidatePath);
}
Comment thread lib/native_loader.js
Comment on lines +60 to +80
function buildFromSource() {
const env = { ...process.env };

if (detectPrebuildRuntime() === 'electron' && process.versions.electron) {
// node-gyp targets Electron via --target (Electron version) + --dist-url
// (Electron headers); it reads these from npm_config_* env vars.
env.npm_config_target = process.versions.electron;
env.npm_config_dist_url = 'https://electronjs.org/headers';
env.npm_config_arch = process.arch;
debug(`Building from source for Electron ${process.versions.electron}`);
} else {
debug('Building from source for Node.js');
}

childProcess.execSync('npm run rebuild', {
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
timeout: 300000, // 5 minute timeout
env,
});
}

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment on lines +24 to +27
cd demo/electron/car
npm install
npm run build
```

## 🚀 Installation & Running

1. **Navigate to the demo directory:**

```bash
cd electron_car_demo
```

2. **Install dependencies:**

```bash
npm install
```

3. **Rebuild native modules for Electron:**

```bash
npm run rebuild
```

4. **Start the demo:**
```bash
# Make sure ROS2 is sourced first
source /opt/ros/<your-ros-distro>/setup.bash
npm start
```

## 📦 Packaging for Distribution

You can package the application into a standalone folder using **Electron Forge**.

### 1. Build the Package

Run the following command to create a distributable executable:

```bash
npm run package
```

The output will be located in the `out/` directory.

**Technical Note on ASAR:** We enable ASAR but configure it to **unpack** the `rclnodejs` module. `rclnodejs` (v1.8.1+) requires file system access to generated code and native bindings, so we use the `asar.unpack` configuration in `package.json` to keep `rclnodejs` files accessible on disk while packing the rest of the application.

```json
"config": {
"forge": {
"packagerConfig": {
"asar": {
"unpack": "**/node_modules/rclnodejs/**"
}
}
}
}
```

### 2. Create Installers (Optional)

To create a `.zip` file or other platform-specific installers (deb/rpm), run:

```bash
npm run make
```

**Note**: Creating DEB/RPM installers requires system tools like `dpkg` and `fakeroot`. For ZIP files, you need `zip`.

### 3. Running the Standalone Application

Even as a standalone application, **ROS 2 must be installed and sourced on the target machine** because `rclnodejs` links dynamically to the ROS 2 shared libraries.

```bash
# Source ROS2 environment
source /opt/ros/<your-ros-distro>/setup.bash

# Run the packaged executable
./out/rclnodejs-electron-car-demo-linux-x64/rclnodejs-electron-car-demo
```

![demo screenshot](./car-control-electron.gif)

## 🎮 How to Use

### Control Methods

#### Mouse Controls

- Click the directional buttons (↑↓←→) on the joystick
- Click the red **STOP** button to halt movement

#### Keyboard Controls

- **W** or **↑**: Move forward
- **S** or **↓**: Move backward
- **A** or **←**: Turn left
- **D** or **→**: Turn right
- **Space** or **Esc**: Stop

### Understanding the Interface

#### Left Panel - Joystick Control

- **Command**: Shows the current command being sent
- **Linear X**: Forward/backward velocity (m/s)
- **Angular Z**: Rotation velocity (rad/s)
- **Topic**: ROS2 topic name (`cmd_vel`)

#### Right Panel - Car Visualization

- **Received Commands**: Total count of commands received
- **Last Command**: Most recent command processed
- **Car Position**: Current position in the visualization area

## 🔍 Technical Details

### ROS2 Message Format

The demo uses `geometry_msgs/Twist` messages with the following structure:

```javascript
{
linear: {
x: 1.0, // Forward/backward velocity (m/s)
y: 0.0, // Left/right velocity (typically 0 for cars)
z: 0.0 // Up/down velocity (typically 0 for ground vehicles)
},
angular: {
x: 0.0, // Roll rate (typically 0 for cars)
y: 0.0, // Pitch rate (typically 0 for cars)
z: 1.0 // Yaw rate (turning left/right in rad/s)
}
}
source /opt/ros/<distro>/setup.bash # required before launch
npm start
Comment on lines +25 to 28
cd demo/electron/manipulator
npm install
source /opt/ros/$ROS_DISTRO/setup.bash # required before launch
npm start
Comment on lines +28 to 32
cd demo/electron/turtle_tf2
npm install
source /opt/ros/$ROS_DISTRO/setup.bash # required, in the same terminal as npm start
ros2 run turtlesim turtlesim_node # in another sourced terminal
npm start
@minggangw minggangw merged commit 24dabc4 into RobotWebTools:develop Jun 22, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Demo][Electron] Load prebuilt binaries at runtime

3 participants