• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 27743094917

18 Jun 2026 07:13AM UTC coverage: 91.185% (-0.04%) from 91.22%
27743094917

Pull #1546

github

web-flow
Merge 0a7a8cb0f into bc78bfa19
Pull Request #1546: [Demo][Electron] Load prebuilt binaries at runtime

2046 of 2407 branches covered (85.0%)

Branch coverage included in aggregate %.

35 of 48 new or added lines in 1 file covered. (72.92%)

25 existing lines in 1 file now uncovered.

16718 of 18171 relevant lines covered (92.0%)

228.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.64
/lib/native_loader.js
1
// Copyright (c) 2025, The Robot Web Tools Contributors
29✔
2
//
29✔
3
// Licensed under the Apache License, Version 2.0 (the "License");
29✔
4
// you may not use this file except in compliance with the License.
29✔
5
// You may obtain a copy of the License at
29✔
6
//
29✔
7
//     http://www.apache.org/licenses/LICENSE-2.0
29✔
8
//
29✔
9
// Unless required by applicable law or agreed to in writing, software
29✔
10
// distributed under the License is distributed on an "AS IS" BASIS,
29✔
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29✔
12
// See the License for the specific language governing permissions and
29✔
13
// limitations under the License.
29✔
14

29✔
15
import fs from 'fs';
29✔
16
import path from 'path';
29✔
17
import childProcess from 'child_process';
29✔
18
import { fileURLToPath } from 'url';
29✔
19
import { createRequire } from 'module';
29✔
20
import { NativeError } from './errors.js';
29✔
21
import createDebug from 'debug';
29✔
22
import {
29✔
23
  detectPrebuildRuntime,
29✔
24
  getTaggedPrebuildFilename,
29✔
25
} from './prebuilds.js';
29✔
26
import { detectUbuntuCodename } from './utils.js';
29✔
27

29✔
28
const require = createRequire(import.meta.url);
29✔
29
// 'bindings' is a CommonJS helper that relies on CJS caller context to locate
29✔
30
// the compiled .node addon; load it via require() to preserve that context.
29✔
31
const bindings = require('bindings');
29✔
32
const __dirname = path.dirname(fileURLToPath(import.meta.url));
29✔
33
const debug = createDebug('rclnodejs');
29✔
34

29✔
35
let nativeModule = null;
29✔
36

29✔
37
// Copy a matched prebuilt binary into build/Release so that standard binary
29✔
38
// resolution (and packaged Electron apps where ROS_DISTRO may be unset at
29✔
39
// runtime) can still locate it. Non-destructive: never overwrites an existing
29✔
40
// build output, so local source builds are preserved.
29✔
41
function mirrorPrebuiltToBuildRelease(srcPath) {
2✔
42
  try {
2✔
43
    const destDir = path.join(__dirname, '..', 'build', 'Release');
2✔
44
    const destPath = path.join(destDir, 'rclnodejs.node');
2✔
45
    if (fs.existsSync(destPath)) {
2✔
46
      return;
2✔
47
    }
2✔
NEW
48
    fs.mkdirSync(destDir, { recursive: true });
×
NEW
49
    fs.copyFileSync(srcPath, destPath);
×
NEW
50
    debug(`Mirrored prebuilt binary to ${destPath}`);
×
NEW
51
  } catch (e) {
×
NEW
52
    debug('Could not mirror prebuilt binary to build/Release:', e.message);
×
NEW
53
  }
×
54
}
2✔
55

29✔
56
// Build the native addon from source. When running under Electron, target
29✔
57
// Electron's headers/ABI so the resulting binary matches the runtime instead
29✔
58
// of producing a Node-targeted binary (the addon links libuv directly, so the
29✔
59
// host runtime matters).
29✔
60
function buildFromSource() {
1✔
61
  const env = { ...process.env };
1✔
62

1✔
63
  if (detectPrebuildRuntime() === 'electron' && process.versions.electron) {
1!
NEW
64
    // node-gyp targets Electron via --target (Electron version) + --dist-url
×
NEW
65
    // (Electron headers); it reads these from npm_config_* env vars.
×
NEW
66
    env.npm_config_target = process.versions.electron;
×
NEW
67
    env.npm_config_dist_url = 'https://electronjs.org/headers';
×
NEW
68
    env.npm_config_arch = process.arch;
×
NEW
69
    debug(`Building from source for Electron ${process.versions.electron}`);
×
70
  } else {
1✔
71
    debug('Building from source for Node.js');
1✔
72
  }
1✔
73

1✔
74
  childProcess.execSync('npm run rebuild', {
1✔
75
    stdio: 'inherit',
1✔
76
    cwd: path.join(__dirname, '..'),
1✔
77
    timeout: 300000, // 5 minute timeout
1✔
78
    env,
1✔
79
  });
1✔
80
}
1✔
81

29✔
82
// Simplified loader: only use prebuilt binaries with exact Ubuntu/ROS2/arch match
29✔
83
// Note: Prebuilt binaries are only supported on Linux (Ubuntu) platform
29✔
84
function customFallbackLoader() {
33✔
85
  // Prebuilt binaries are only for Linux platform
33✔
86
  if (process.platform !== 'linux') {
33✔
87
    debug('Prebuilt binaries are only supported on Linux platform');
1✔
88
    return null;
1✔
89
  }
1✔
90

32✔
91
  const rosDistro = process.env.ROS_DISTRO;
32✔
92
  const arch = process.arch;
32✔
93
  const runtime = detectPrebuildRuntime();
32✔
94
  const ubuntuCodename = detectUbuntuCodename();
32✔
95

32✔
96
  // Require all three components for exact match
32✔
97
  if (!rosDistro || !ubuntuCodename || !arch) {
33✔
98
    debug(
1✔
99
      `Missing environment info - ROS: ${rosDistro}, Ubuntu: ${ubuntuCodename}, Arch: ${arch}`
1✔
100
    );
1✔
101
    return null;
1✔
102
  }
1✔
103

31✔
104
  const prebuildDir = path.join(
31✔
105
    __dirname,
31✔
106
    '..',
31✔
107
    'prebuilds',
31✔
108
    `${process.platform}-${arch}`
31✔
109
  );
31✔
110

31✔
111
  if (!fs.existsSync(prebuildDir)) {
33✔
112
    debug('No prebuilds directory found');
29✔
113
    return null;
29✔
114
  }
29✔
115

2✔
116
  try {
2✔
117
    const candidate = getTaggedPrebuildFilename({
2✔
118
      rosDistro,
2✔
119
      ubuntuCodename,
2✔
120
      arch,
2✔
121
      runtime,
2✔
122
    });
2✔
123
    const candidatePath = path.join(prebuildDir, candidate);
2✔
124

2✔
125
    if (fs.existsSync(candidatePath)) {
2✔
126
      debug(`Found ${runtime} prebuilt binary: ${candidate}`);
2✔
127
      mirrorPrebuiltToBuildRelease(candidatePath);
2✔
128
      return require(candidatePath);
2✔
129
    }
2✔
130

×
131
    debug(
×
132
      `No matching ${runtime} prebuilt binary found for ${rosDistro}-${ubuntuCodename}-${arch}`
×
133
    );
×
134
    return null;
×
135
  } catch (e) {
6✔
136
    debug('Error in simplified prebuilt loader:', e.message);
2✔
137
  }
2✔
138

2✔
139
  return null;
2✔
140
}
33✔
141

29✔
142
// Simplified prebuilt binary loader: exact match or build from source
29✔
143
function loadNativeAddon() {
29✔
144
  if (nativeModule) {
29!
145
    return nativeModule;
×
146
  }
×
147

29✔
148
  // Environment variable to force building from source
29✔
149
  if (process.env.RCLNODEJS_FORCE_BUILD === '1') {
29✔
150
    debug('Forcing build from source (RCLNODEJS_FORCE_BUILD=1)');
1✔
151

1✔
152
    // Trigger actual compilation
1✔
153
    try {
1✔
154
      debug('Running forced node-gyp rebuild...');
1✔
155
      buildFromSource();
1✔
156

1✔
157
      // Load the newly built binary
1✔
158
      nativeModule = bindings('rclnodejs');
1✔
159
      debug('Successfully force compiled and loaded from source');
1✔
160
      return nativeModule;
1✔
161
    } catch (compileError) {
1!
UNCOV
162
      debug('Forced compilation failed:', compileError.message);
×
UNCOV
163
      throw new NativeError(
×
UNCOV
164
        `Failed to force build rclnodejs from source: ${compileError.message}`,
×
UNCOV
165
        'Forced compilation',
×
UNCOV
166
        { cause: compileError }
×
UNCOV
167
      );
×
168
    }
×
169
  }
1✔
170

28✔
171
  const rosDistro = process.env.ROS_DISTRO;
28✔
172
  const runtime = detectPrebuildRuntime();
28✔
173
  const ubuntuCodename = detectUbuntuCodename();
28✔
174

28✔
175
  debug(
28✔
176
    `Platform: ${process.platform}, Arch: ${process.arch}, Runtime: ${runtime}, Ubuntu: ${ubuntuCodename || 'unknown'}, ROS: ${rosDistro || 'unknown'}`
29!
177
  );
29✔
178

29✔
179
  // Prebuilt binaries are only supported on Linux (Ubuntu)
29✔
180
  if (process.platform === 'linux') {
29✔
181
    // Try exact match prebuilt binary first
28✔
182
    try {
28✔
183
      const prebuiltModule = customFallbackLoader();
28✔
184
      if (prebuiltModule) {
28!
UNCOV
185
        nativeModule = prebuiltModule;
×
UNCOV
186
        return nativeModule;
×
UNCOV
187
      }
×
188
    } catch (e) {
28!
UNCOV
189
      debug('Exact match prebuilt loading failed:', e.message);
×
UNCOV
190
    }
×
191

28✔
192
    debug(
28✔
193
      'No exact match prebuilt binary found, falling back to build from source'
28✔
194
    );
28✔
195
  } else {
28!
UNCOV
196
    debug(
×
UNCOV
197
      `Platform ${process.platform} does not support prebuilt binaries, will try existing build or compile from source`
×
198
    );
×
199
  }
×
200

28✔
201
  try {
28✔
202
    // Try to find existing built binary first (works on all platforms)
28✔
203
    // The 'bindings' module will search standard locations like:
28✔
204
    // - build/Release/rclnodejs.node
28✔
205
    // - build/Debug/rclnodejs.node
28✔
206
    // - compiled/{node_version}/{platform}/{arch}/rclnodejs.node
28✔
207
    // etc.
28✔
208
    nativeModule = bindings('rclnodejs');
28✔
209
    debug('Found and loaded existing built binary');
28✔
210
    return nativeModule;
28✔
211
  } catch {
28!
212
    debug('No existing built binary found, triggering compilation...');
×
UNCOV
213

×
UNCOV
214
    // Trigger actual compilation
×
UNCOV
215
    try {
×
UNCOV
216
      debug('Running node-gyp rebuild...');
×
NEW
UNCOV
217
      buildFromSource();
×
UNCOV
218

×
UNCOV
219
      // Try to load the newly built binary
×
UNCOV
220
      nativeModule = bindings('rclnodejs');
×
UNCOV
221
      debug('Successfully compiled and loaded from source');
×
UNCOV
222
      return nativeModule;
×
UNCOV
223
    } catch (compileError) {
×
UNCOV
224
      debug('Compilation failed:', compileError.message);
×
225
      throw new NativeError(
×
226
        `Failed to build rclnodejs from source: ${compileError.message}`,
×
227
        'Compilation',
×
228
        { cause: compileError }
×
229
      );
×
230
    }
×
231
  }
×
232
}
29✔
233

29✔
234
const addon = loadNativeAddon();
29✔
235

29✔
236
// Export internal functions for testing purposes
29✔
237
if (process.env.NODE_ENV === 'test') {
29✔
238
  addon.TestHelpers = {
28✔
239
    customFallbackLoader,
28✔
240
    loadNativeAddon,
28✔
241
  };
28✔
242
}
28✔
243

29✔
244
export default addon;
29✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc