diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 5e12e985b6567c..0a5e3ca569c8ad 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -421,6 +421,38 @@ are equal to [`process.execPath`][]. The value of `__dirname` in the injected main script is equal to the directory name of [`process.execPath`][]. +### Using native addons in the injected main script + +Native addons can be bundled as assets into the single-executable application +by specifying them in the `assets` field of the configuration file used to +generate the single-executable application preparation blob. +The addon can then be loaded in the injected main script by writing the asset +to a temporary file and loading it with `process.dlopen()`. + +```json +{ + "main": "/path/to/bundled/script.js", + "output": "/path/to/write/the/generated/blob.blob", + "assets": { + "myaddon.node": "/path/to/myaddon/build/Release/myaddon.node" + } +} +``` + +```js +// script.js +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { getRawAsset } = require('node:sea'); +const addonPath = path.join(os.tmpdir(), 'myaddon.node'); +fs.writeFileSync(addonPath, new Uint8Array(getRawAsset('myaddon.node'))); +const myaddon = { exports: {} }; +process.dlopen(myaddon, addonPath); +console.log(myaddon.exports); +fs.rmSync(addonPath); +``` + ## Notes ### Single executable application creation process diff --git a/test/node-api/node-api.status b/test/node-api/node-api.status index b8e18f95623a20..7f03605d67eeed 100644 --- a/test/node-api/node-api.status +++ b/test/node-api/node-api.status @@ -12,3 +12,7 @@ prefix node-api # https://github.com/nodejs/node/issues/43457 test_fatal/test_threads: PASS,FLAKY test_fatal/test_threads_report: PASS,FLAKY + +[$system==linux && $arch==ppc64] +# https://github.com/nodejs/node/issues/59561 +test_sea_addon/test: SKIP diff --git a/test/node-api/sea_addon b/test/node-api/sea_addon new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/node-api/test_sea_addon/binding.c b/test/node-api/test_sea_addon/binding.c new file mode 100644 index 00000000000000..8d3259e56e7b40 --- /dev/null +++ b/test/node-api/test_sea_addon/binding.c @@ -0,0 +1,17 @@ +#include +#include +#include "../../js-native-api/common.h" + +static napi_value Method(napi_env env, napi_callback_info info) { + napi_value world; + const char* str = "world"; + size_t str_len = strlen(str); + NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, &world)); + return world; +} + +NAPI_MODULE_INIT() { + napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("hello", Method); + NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc)); + return exports; +} diff --git a/test/node-api/test_sea_addon/binding.gyp b/test/node-api/test_sea_addon/binding.gyp new file mode 100644 index 00000000000000..62381d5e54f22b --- /dev/null +++ b/test/node-api/test_sea_addon/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "binding", + "sources": [ "binding.c" ] + } + ] +} diff --git a/test/node-api/test_sea_addon/test.js b/test/node-api/test_sea_addon/test.js new file mode 100644 index 00000000000000..d422c3eb2f73fa --- /dev/null +++ b/test/node-api/test_sea_addon/test.js @@ -0,0 +1,75 @@ +'use strict'; +// This tests that SEA can load addons packaged as assets by writing them to disk +// and loading them via process.dlopen(). +const common = require('../../common'); +const { generateSEA, skipIfSingleExecutableIsNotSupported } = require('../../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +const assert = require('assert'); + +const tmpdir = require('../../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync, rmSync } = require('fs'); +const { + spawnSyncAndExitWithoutError, + spawnSyncAndAssert, +} = require('../../common/child_process'); +const { join } = require('path'); +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); +tmpdir.refresh(); + +// Copy test fixture to working directory +const addonPath = join(__dirname, 'build', common.buildType, 'binding.node'); +const copiedAddonPath = tmpdir.resolve('binding.node'); +copyFileSync(addonPath, copiedAddonPath); +writeFileSync(tmpdir.resolve('sea.js'), ` +const sea = require('node:sea'); +const fs = require('fs'); +const path = require('path'); + +const addonPath = path.join(process.cwd(), 'hello.node'); +fs.writeFileSync(addonPath, new Uint8Array(sea.getRawAsset('hello.node'))); +const mod = {exports: {}} +process.dlopen(mod, addonPath); +console.log('hello,', mod.exports.hello()); +`, 'utf-8'); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": true, + "assets": { + "hello.node": "binding.node" + } +} +`, 'utf8'); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }, +); +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +// Remove the copied addon after it's been packaged into the SEA blob +rmSync(copiedAddonPath, { force: true }); + +spawnSyncAndAssert( + outputFile, + [], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'SEA', + }, + cwd: tmpdir.path, + }, + { + stdout: /hello, world/, + }, +);