diff --git a/.github/workflows/prebuild-linux-arm64.yml b/.github/workflows/prebuild-linux-arm64.yml new file mode 100644 index 00000000..5464a786 --- /dev/null +++ b/.github/workflows/prebuild-linux-arm64.yml @@ -0,0 +1,79 @@ +name: Prebuild Linux ARM64 + +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + prebuild: + runs-on: ubuntu-24.04-arm + container: + image: ${{ matrix.docker_image }} + strategy: + fail-fast: false + matrix: + node-version: [22.X] + architecture: [arm64] + ros_distribution: + - humble + - jazzy + - kilted + include: + # Humble Hawksbill (May 2022 - May 2027) + - docker_image: ubuntu:jammy + ros_distribution: humble + ubuntu_codename: jammy + # Jazzy Jalisco (May 2024 - May 2029) + - docker_image: ubuntu:noble + ros_distribution: jazzy + ubuntu_codename: noble + # Kilted Kaiju (May 2025 - Dec 2026) + - docker_image: ubuntu:noble + ros_distribution: kilted + ubuntu_codename: noble + + steps: + - name: Setup Node.js ${{ matrix.node-version }} on ${{ matrix.architecture }} + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + architecture: ${{ matrix.architecture }} + + - name: Setup ROS2 + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: ${{ matrix.ros_distribution }} + + - uses: actions/checkout@v5 + + - name: Install dependencies + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + npm i + + - name: Generate prebuilt binary + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + npm run prebuild + + - name: Upload prebuilt binary + uses: actions/upload-artifact@v4 + with: + name: prebuilt-linux-arm64-node${{ matrix.node-version }}-${{ matrix.ubuntu_codename }}-${{ matrix.ros_distribution }} + path: prebuilds/linux-arm64/*.node + if-no-files-found: error + + - name: Test loading prebuilt + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + node -e " + const rclnodejs = require('./index.js'); + console.log('Successfully loaded rclnodejs with prebuilt binary'); + console.log('Platform:', process.platform, 'Arch:', process.arch); + console.log('ROS_DISTRO:', process.env.ROS_DISTRO); + " diff --git a/.github/workflows/prebuild-linux-x64.yml b/.github/workflows/prebuild-linux-x64.yml new file mode 100644 index 00000000..89f8f6aa --- /dev/null +++ b/.github/workflows/prebuild-linux-x64.yml @@ -0,0 +1,79 @@ +name: Prebuild Linux x64 + +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + prebuild: + runs-on: ubuntu-latest + container: + image: ${{ matrix.docker_image }} + strategy: + fail-fast: false + matrix: + node-version: [22.X] + architecture: [x64] + ros_distribution: + - humble + - jazzy + - kilted + include: + # Humble Hawksbill (May 2022 - May 2027) + - docker_image: ubuntu:jammy + ros_distribution: humble + ubuntu_codename: jammy + # Jazzy Jalisco (May 2024 - May 2029) + - docker_image: ubuntu:noble + ros_distribution: jazzy + ubuntu_codename: noble + # Kilted Kaiju (May 2025 - Dec 2026) + - docker_image: ubuntu:noble + ros_distribution: kilted + ubuntu_codename: noble + + steps: + - name: Setup Node.js ${{ matrix.node-version }} on ${{ matrix.architecture }} + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + architecture: ${{ matrix.architecture }} + + - name: Setup ROS2 + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: ${{ matrix.ros_distribution }} + + - uses: actions/checkout@v5 + + - name: Install dependencies + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + npm i + + - name: Generate prebuilt binary + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + npm run prebuild + + - name: Upload prebuilt binary + uses: actions/upload-artifact@v4 + with: + name: prebuilt-linux-x64-node${{ matrix.node-version }}-${{ matrix.ubuntu_codename }}-${{ matrix.ros_distribution }} + path: prebuilds/linux-x64/*.node + if-no-files-found: error + + - name: Test loading prebuilt + shell: bash + run: | + source /opt/ros/${{ matrix.ros_distribution }}/setup.bash + node -e " + const rclnodejs = require('./index.js'); + console.log('Successfully loaded rclnodejs with prebuilt binary'); + console.log('Platform:', process.platform, 'Arch:', process.arch); + console.log('ROS_DISTRO:', process.env.ROS_DISTRO); + " diff --git a/README.md b/README.md index d453def9..915246fc 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,26 @@ npm i rclnodejs See the [features](./docs/FEATURES.md) and try the [examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) to get started. +### Prebuilt Binaries + +rclnodejs ships with prebuilt native binaries for common Linux configurations since `v1.5.2`, eliminating the need for compilation during installation. This significantly speeds up installation and reduces dependencies. + +**Supported Platforms:** + +- **Ubuntu 22.04 (Jammy)** - ROS 2 Humble +- **Ubuntu 24.04 (Noble)** - ROS 2 Jazzy, Kilted +- **Architectures:** x64, arm64 +- **Node.js:** >= 16.20.2 (N-API compatible) + +**Force Building from Source:** + +If you need to build from source even when a prebuilt binary is available, set the environment variable: + +```bash +export RCLNODEJS_FORCE_BUILD=1 +npm install rclnodejs +``` + ## rclnodejs-cli [rclnodejs-cli](https://github.com/RobotWebTools/rclnodejs-cli/) is a companion project we recently launched to provide a commandline interface to a set of developer tools for working with this `rclnodejs`. You may find `rclnodejs-cli` particularly useful if you plan to create ROS 2 node(s) and launch files for working with multiple node orchestrations. diff --git a/index.js b/index.js index 504981dd..89239be3 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,7 @@ const { } = require('./lib/parameter.js'); const path = require('path'); const QoS = require('./lib/qos.js'); -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./lib/native_loader.js'); const tsdGenerator = require('./rostsd_gen/index.js'); const validator = require('./lib/validator.js'); const Time = require('./lib/time.js'); diff --git a/lib/action/client.js b/lib/action/client.js index aae06f7e..df130e29 100644 --- a/lib/action/client.js +++ b/lib/action/client.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../native_loader.js'); const ActionInterfaces = require('./interfaces.js'); const ActionUuid = require('./uuid.js'); const ClientGoalHandle = require('./client_goal_handle.js'); diff --git a/lib/action/graph.js b/lib/action/graph.js index cdd491c2..c211a4ad 100644 --- a/lib/action/graph.js +++ b/lib/action/graph.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../native_loader.js'); /** * Get a list of action names and types for action clients associated with a node. diff --git a/lib/action/server.js b/lib/action/server.js index 32eb3d9a..6896427f 100644 --- a/lib/action/server.js +++ b/lib/action/server.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../native_loader.js'); const ActionInterfaces = require('./interfaces.js'); const ActionUuid = require('./uuid.js'); const DistroUtils = require('../distro.js'); diff --git a/lib/action/server_goal_handle.js b/lib/action/server_goal_handle.js index eecfab72..ffc74faa 100644 --- a/lib/action/server_goal_handle.js +++ b/lib/action/server_goal_handle.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../native_loader.js'); const ActionInterfaces = require('./interfaces.js'); const Deferred = require('./deferred.js'); const { GoalEvent } = require('./response.js'); diff --git a/lib/client.js b/lib/client.js index 50b7b2e6..fd7f0f27 100644 --- a/lib/client.js +++ b/lib/client.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:client'); diff --git a/lib/clock.js b/lib/clock.js index f99ce5d3..ff208cae 100644 --- a/lib/clock.js +++ b/lib/clock.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Time = require('./time.js'); const ClockType = require('./clock_type.js'); diff --git a/lib/context.js b/lib/context.js index f36acf2a..4033f91a 100644 --- a/lib/context.js +++ b/lib/context.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); let defaultContext = null; diff --git a/lib/duration.js b/lib/duration.js index e499586b..d5acd0d2 100644 --- a/lib/duration.js +++ b/lib/duration.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const S_TO_NS = 10n ** 9n; /** diff --git a/lib/event_handler.js b/lib/event_handler.js index 585bdf26..b2b506e0 100644 --- a/lib/event_handler.js +++ b/lib/event_handler.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); diff --git a/lib/guard_condition.js b/lib/guard_condition.js index 68a892a1..70fd3c0e 100644 --- a/lib/guard_condition.js +++ b/lib/guard_condition.js @@ -12,7 +12,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Entity = require('./entity.js'); const Context = require('./context.js'); diff --git a/lib/lifecycle.js b/lib/lifecycle.js index 9bdb0176..cee8e8bd 100644 --- a/lib/lifecycle.js +++ b/lib/lifecycle.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const LifecyclePublisher = require('./lifecycle_publisher.js'); const loader = require('./interface_loader.js'); const Context = require('./context.js'); diff --git a/lib/lifecycle_publisher.js b/lib/lifecycle_publisher.js index b004109c..f22aa280 100644 --- a/lib/lifecycle_publisher.js +++ b/lib/lifecycle_publisher.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Logging = require('./logging.js'); const Publisher = require('./publisher.js'); diff --git a/lib/logging.js b/lib/logging.js index 46e26f3a..500f190c 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -15,7 +15,7 @@ 'use strict'; const path = require('path'); -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); /** * Enum for LoggingSeverity diff --git a/lib/native_loader.js b/lib/native_loader.js new file mode 100644 index 00000000..866a8ca7 --- /dev/null +++ b/lib/native_loader.js @@ -0,0 +1,173 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const bindings = require('bindings'); +const debug = require('debug')('rclnodejs'); +const { detectUbuntuCodename } = require('./utils'); + +let nativeModule = null; + +// Simplified loader: only use prebuilt binaries with exact Ubuntu/ROS2/arch match +// Note: Prebuilt binaries are only supported on Linux (Ubuntu) platform +function customFallbackLoader() { + // Prebuilt binaries are only for Linux platform + if (process.platform !== 'linux') { + debug('Prebuilt binaries are only supported on Linux platform'); + return null; + } + + const rosDistro = process.env.ROS_DISTRO; + const arch = process.arch; + const ubuntuCodename = detectUbuntuCodename(); + + // Require all three components for exact match + if (!rosDistro || !ubuntuCodename || !arch) { + debug( + `Missing environment info - ROS: ${rosDistro}, Ubuntu: ${ubuntuCodename}, Arch: ${arch}` + ); + return null; + } + + const prebuildDir = path.join( + __dirname, + '..', + 'prebuilds', + `${process.platform}-${arch}` + ); + + if (!fs.existsSync(prebuildDir)) { + debug('No prebuilds directory found'); + return null; + } + + try { + // Look for exact match binary: {ros_distro}-{ubuntu_codename}-{arch}-rclnodejs.node + const exactMatchFilename = `${rosDistro}-${ubuntuCodename}-${arch}-rclnodejs.node`; + const exactMatchPath = path.join(prebuildDir, exactMatchFilename); + + if (fs.existsSync(exactMatchPath)) { + debug(`Found exact match binary: ${exactMatchFilename}`); + return require(exactMatchPath); + } + + debug(`No exact match found for: ${exactMatchFilename}`); + return null; + } catch (e) { + debug('Error in simplified prebuilt loader:', e.message); + } + + return null; +} + +// Simplified prebuilt binary loader: exact match or build from source +function loadNativeAddon() { + if (nativeModule) { + return nativeModule; + } + + // Environment variable to force building from source + if (process.env.RCLNODEJS_FORCE_BUILD === '1') { + debug('Forcing build from source (RCLNODEJS_FORCE_BUILD=1)'); + + // Trigger actual compilation + try { + debug('Running forced node-gyp rebuild...'); + execSync('npm run rebuild', { + stdio: 'inherit', + cwd: path.join(__dirname, '..'), + timeout: 300000, // 5 minute timeout + }); + + // Load the newly built binary + nativeModule = bindings('rclnodejs'); + debug('Successfully force compiled and loaded from source'); + return nativeModule; + } catch (compileError) { + debug('Forced compilation failed:', compileError.message); + throw new Error( + `Failed to force build rclnodejs from source: ${compileError.message}` + ); + } + } + + const rosDistro = process.env.ROS_DISTRO; + const ubuntuCodename = detectUbuntuCodename(); + + debug( + `Platform: ${process.platform}, Arch: ${process.arch}, Ubuntu: ${ubuntuCodename || 'unknown'}, ROS: ${rosDistro || 'unknown'}` + ); + + // Prebuilt binaries are only supported on Linux (Ubuntu) + if (process.platform === 'linux') { + // Try exact match prebuilt binary first + try { + const prebuiltModule = customFallbackLoader(); + if (prebuiltModule) { + nativeModule = prebuiltModule; + return nativeModule; + } + } catch (e) { + debug('Exact match prebuilt loading failed:', e.message); + } + + debug( + 'No exact match prebuilt binary found, falling back to build from source' + ); + } else { + debug( + `Platform ${process.platform} does not support prebuilt binaries, will try existing build or compile from source` + ); + } + + try { + // Try to find existing built binary first (works on all platforms) + // The 'bindings' module will search standard locations like: + // - build/Release/rclnodejs.node + // - build/Debug/rclnodejs.node + // - compiled/{node_version}/{platform}/{arch}/rclnodejs.node + // etc. + nativeModule = bindings('rclnodejs'); + debug('Found and loaded existing built binary'); + return nativeModule; + } catch { + debug('No existing built binary found, triggering compilation...'); + + // Trigger actual compilation + try { + debug('Running node-gyp rebuild...'); + execSync('npm run rebuild', { + stdio: 'inherit', + cwd: path.join(__dirname, '..'), + timeout: 300000, // 5 minute timeout + }); + + // Try to load the newly built binary + nativeModule = bindings('rclnodejs'); + debug('Successfully compiled and loaded from source'); + return nativeModule; + } catch (compileError) { + debug('Compilation failed:', compileError.message); + throw new Error( + `Failed to build rclnodejs from source: ${compileError.message}` + ); + } + } +} + +module.exports = loadNativeAddon(); diff --git a/lib/node.js b/lib/node.js index e9a344f8..7a85dc01 100644 --- a/lib/node.js +++ b/lib/node.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const ActionInterfaces = require('./action/interfaces.js'); const Client = require('./client.js'); diff --git a/lib/publisher.js b/lib/publisher.js index b4ae66f8..df7599db 100644 --- a/lib/publisher.js +++ b/lib/publisher.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const debug = require('debug')('rclnodejs:publisher'); const Entity = require('./entity.js'); diff --git a/lib/serialization.js b/lib/serialization.js index b8462f84..0f676124 100644 --- a/lib/serialization.js +++ b/lib/serialization.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); class Serialization { /** diff --git a/lib/service.js b/lib/service.js index bc70fc0c..ac70982d 100644 --- a/lib/service.js +++ b/lib/service.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:service'); diff --git a/lib/subscription.js b/lib/subscription.js index c73af1d3..74914c5e 100644 --- a/lib/subscription.js +++ b/lib/subscription.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:subscription'); diff --git a/lib/time.js b/lib/time.js index 176b8ded..a563e837 100644 --- a/lib/time.js +++ b/lib/time.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Duration = require('./duration.js'); const ClockType = require('./clock_type.js'); const S_TO_NS = 10n ** 9n; diff --git a/lib/time_source.js b/lib/time_source.js index fc6f5814..9b22d57b 100644 --- a/lib/time_source.js +++ b/lib/time_source.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const { Clock, ROSClock } = require('./clock.js'); const { ClockType } = Clock; const { Parameter, ParameterType } = require('./parameter.js'); diff --git a/lib/timer.js b/lib/timer.js index 823bb49b..3b09b99b 100644 --- a/lib/timer.js +++ b/lib/timer.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const DistroUtils = require('./distro.js'); /** diff --git a/lib/type_description_service.js b/lib/type_description_service.js index b2bd30bd..649e4b28 100644 --- a/lib/type_description_service.js +++ b/lib/type_description_service.js @@ -15,7 +15,7 @@ 'use strict'; const loader = require('./interface_loader.js'); -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); const Service = require('./service.js'); const { diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..1e5fbedf --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,37 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); + +/** + * Detect Ubuntu codename from /etc/os-release + * @returns {string|null} Ubuntu codename (e.g., 'noble', 'jammy') or null if not detectable + */ +function detectUbuntuCodename() { + if (process.platform !== 'linux') { + return null; + } + + try { + const osRelease = fs.readFileSync('/etc/os-release', 'utf8'); + const match = osRelease.match(/^VERSION_CODENAME=(.*)$/m); + return match ? match[1].trim() : null; + } catch { + return null; + } +} + +module.exports = { + detectUbuntuCodename, +}; diff --git a/lib/validator.js b/lib/validator.js index 0144906b..0d8c5a6b 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('./native_loader.js'); /** * An object - Representing a validator in ROS. diff --git a/package.json b/package.json index 3d2a1716..40d9530b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "generate-messages:dev": "node scripts/generate_messages.js --debug && npx --yes prettier --ignore-path --write generated/**/*.js", "generate-tsd-messages": "node scripts/generate_tsd.js", "clean": "node-gyp clean && npx rimraf ./generated", - "install": "npm run rebuild", + "install": "node scripts/install.js", "postinstall": "npm run generate-messages", "docs": "cd docs && make", "test": "nyc node --expose-gc ./scripts/run_test.js && tsd", @@ -30,7 +30,8 @@ "lint": "eslint && node ./scripts/cpplint.js", "format": "clang-format -i -style=file ./src/*.cpp ./src/*.h && npx --yes prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark,rostsd_gen}/**/*.{js,md,ts}\" ./*.{js,md,ts}", "prepare": "husky", - "coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" + "coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", + "prebuild": "prebuildify --napi --strip --target 16.20.2 --target electron@23.0.0 && node scripts/tag_prebuilds.js" }, "bin": { "generate-ros-messages": "./scripts/generate_messages.js" @@ -64,6 +65,7 @@ "lint-staged": "^16.2.0", "mocha": "^11.0.2", "nyc": "^17.1.0", + "prebuildify": "^6.0.1", "rimraf": "^6.0.1", "sinon": "^21.0.0", "tree-kill": "^1.2.2", @@ -80,9 +82,9 @@ "fs-extra": "^11.2.0", "is-close": "^1.3.3", "json-bigint": "^1.0.0", + "node-addon-api": "^8.3.1", "terser": "^5.39.0", - "walk": "^2.3.15", - "node-addon-api": "^8.3.1" + "walk": "^2.3.15" }, "husky": { "hooks": { diff --git a/rosidl_gen/deallocator.js b/rosidl_gen/deallocator.js index 9dca2094..cd44ba68 100644 --- a/rosidl_gen/deallocator.js +++ b/rosidl_gen/deallocator.js @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../lib/native_loader.js'); let deallocator = { delayFreeStructMember(refObj, type, name) { diff --git a/rosidl_gen/primitive_types.js b/rosidl_gen/primitive_types.js index d935d1ea..a59dd72a 100644 --- a/rosidl_gen/primitive_types.js +++ b/rosidl_gen/primitive_types.js @@ -16,7 +16,7 @@ const ref = require('../third_party/ref-napi'); const StructType = require('@rclnodejs/ref-struct-di')(ref); -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../lib/native_loader.js'); const StringRefStruct = StructType({ data: ref.types.CString, diff --git a/rosidl_gen/templates/message.dot b/rosidl_gen/templates/message.dot index b4e13583..6f4f33fd 100644 --- a/rosidl_gen/templates/message.dot +++ b/rosidl_gen/templates/message.dot @@ -224,7 +224,7 @@ function extractMemberNames(fields) { }} {{? willUseTypedArray}} -const rclnodejs = require('bindings')('rclnodejs'); +const rclnodejs = require('../../lib/native_loader.js'); {{?}} const ref = require('../../third_party/ref-napi'); const StructType = require('@rclnodejs/ref-struct-di')(ref); diff --git a/scripts/install.js b/scripts/install.js new file mode 100755 index 00000000..c04c896c --- /dev/null +++ b/scripts/install.js @@ -0,0 +1,113 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { detectUbuntuCodename } = require('../lib/utils'); + +function getRosDistro() { + return process.env.ROS_DISTRO || null; +} + +function checkPrebuiltBinary() { + const platform = process.platform; + const arch = process.arch; + + // Only Linux has prebuilt binaries + if (platform !== 'linux') { + console.log( + `Platform ${platform} does not have prebuilt binaries, will build from source` + ); + return false; + } + + const ubuntuCodename = detectUbuntuCodename(); + const rosDistro = getRosDistro(); + + if (!ubuntuCodename || !rosDistro) { + console.log( + 'Cannot detect Ubuntu codename or ROS distribution, will build from source' + ); + return false; + } + + // Check for the specific prebuilt binary + const prebuildDir = path.join( + __dirname, + '..', + 'prebuilds', + `${platform}-${arch}` + ); + const expectedBinary = `${rosDistro}-${ubuntuCodename}-${arch}-rclnodejs.node`; + const binaryPath = path.join(prebuildDir, expectedBinary); + + if (fs.existsSync(binaryPath)) { + console.log(`✓ Found prebuilt binary: ${expectedBinary}`); + console.log(` Platform: ${platform}, Arch: ${arch}`); + console.log(` Ubuntu: ${ubuntuCodename}, ROS: ${rosDistro}`); + return true; + } + + console.log( + `✗ No prebuilt binary found for ${rosDistro}-${ubuntuCodename}-${arch}` + ); + + // List available binaries for debugging + if (fs.existsSync(prebuildDir)) { + const availableBinaries = fs + .readdirSync(prebuildDir) + .filter((f) => f.endsWith('.node')); + if (availableBinaries.length > 0) { + console.log( + ' Available prebuilt binaries:', + availableBinaries.join(', ') + ); + } + } + + return false; +} + +function buildFromSource() { + console.log('\n=== Building rclnodejs from source ==='); + try { + execSync('npm run rebuild', { + stdio: 'inherit', + cwd: path.join(__dirname, '..'), + timeout: 600000, // 10 minute timeout + }); + console.log('✓ Successfully built from source\n'); + } catch (error) { + console.error('✗ Failed to build from source:', error.message); + process.exit(1); + } +} + +function main() { + console.log('\n=== Installing rclnodejs ===\n'); + + if (checkPrebuiltBinary()) { + console.log('✓ Installation complete - using prebuilt binary\n'); + } else { + console.log('→ Building from source...\n'); + buildFromSource(); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { checkPrebuiltBinary, buildFromSource }; diff --git a/scripts/npmjs-readme.md b/scripts/npmjs-readme.md index 43bc3512..34d64aec 100644 --- a/scripts/npmjs-readme.md +++ b/scripts/npmjs-readme.md @@ -31,6 +31,26 @@ npm i rclnodejs - **Note:** to install rclnodejs from GitHub: add `"rclnodejs":"RobotWebTools/rclnodejs#"` to your `package.json` dependency section. +### Prebuilt Binaries + +rclnodejs ships with prebuilt native binaries for common Linux configurations since `v1.5.2`, eliminating the need for compilation during installation. This significantly speeds up installation and reduces dependencies. + +**Supported Platforms:** + +- **Ubuntu 22.04 (Jammy)** - ROS 2 Humble +- **Ubuntu 24.04 (Noble)** - ROS 2 Jazzy, Kilted +- **Architectures:** x64, arm64 +- **Node.js:** >= 16.20.2 (N-API compatible) + +**Force Building from Source:** + +If you need to build from source even when a prebuilt binary is available, set the environment variable: + +```bash +export RCLNODEJS_FORCE_BUILD=1 +npm install rclnodejs +``` + ## Documentation API [documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html) is available online. diff --git a/scripts/tag_prebuilds.js b/scripts/tag_prebuilds.js new file mode 100755 index 00000000..7fe7fe69 --- /dev/null +++ b/scripts/tag_prebuilds.js @@ -0,0 +1,70 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); +const path = require('path'); +const { detectUbuntuCodename } = require('../lib/utils'); + +function tagPrebuilds() { + const rosDistro = process.env.ROS_DISTRO; + const ubuntuCodename = detectUbuntuCodename(); + const platform = process.platform; + const arch = process.arch; + + console.log( + `Tagging prebuilds with Ubuntu: ${ubuntuCodename || 'unknown'}, ROS2: ${rosDistro || 'unknown'}, Platform: ${platform}, Arch: ${arch}` + ); + + const prebuildDir = path.join( + __dirname, + '..', + 'prebuilds', + `${platform}-${arch}` + ); + + if (!fs.existsSync(prebuildDir)) { + console.log('No prebuilds directory found, skipping tagging'); + return; + } + + const files = fs.readdirSync(prebuildDir).filter((f) => f.endsWith('.node')); + + for (const file of files) { + const filePath = path.join(prebuildDir, file); + + // Create tagged version with format: {ros_distro}-{linux-codename}-{cpu-arch}-rclnodejs.node + if (rosDistro && ubuntuCodename) { + const taggedName = `${rosDistro}-${ubuntuCodename}-${arch}-rclnodejs.node`; + const taggedPath = path.join(prebuildDir, taggedName); + fs.copyFileSync(filePath, taggedPath); + console.log(`Created tagged binary: ${taggedName}`); + + // Remove the original generic binary file if it's the basic rclnodejs.node + if (file === 'rclnodejs.node') { + fs.unlinkSync(filePath); + console.log(`Removed generic binary: ${file}`); + } + } else { + console.log( + `Skipping tagging for ${file} - missing ROS_DISTRO or Ubuntu codename` + ); + } + } +} + +if (require.main === module) { + tagPrebuilds(); +} + +module.exports = { tagPrebuilds }; diff --git a/third_party/ref-napi/lib/ref.js b/third_party/ref-napi/lib/ref.js index 178798d1..da7604f7 100644 --- a/third_party/ref-napi/lib/ref.js +++ b/third_party/ref-napi/lib/ref.js @@ -3,11 +3,7 @@ const assert = require('assert'); const inspect = require('util').inspect; const debug = require('debug')('ref'); const os = require('os'); -const path = require('path'); -const addon = require('bindings')({ - bindings: 'rclnodejs', - module_root: path.resolve(__dirname, '..', '..', '..'), -}); +const addon = require('../../../lib/native_loader.js'); if (!addon || !addon.ref) { throw new Error('Failed to load ref bindings from rclnodejs addon');