From da4160ee34ad2588aff826f58f31298c3cdf39fd Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 8 Nov 2025 20:59:10 +0100 Subject: [PATCH 1/3] doc,test: add documentation and test on how to use addons in SEA --- doc/api/single-executable-applications.md | 32 ++++++++++ test/node-api/node-api.status | 4 ++ test/node-api/sea_addon | 0 test/node-api/test_sea_addon/binding.c | 17 +++++ test/node-api/test_sea_addon/binding.gyp | 8 +++ test/node-api/test_sea_addon/test.js | 75 +++++++++++++++++++++++ 6 files changed, 136 insertions(+) create mode 100644 test/node-api/sea_addon create mode 100644 test/node-api/test_sea_addon/binding.c create mode 100644 test/node-api/test_sea_addon/binding.gyp create mode 100644 test/node-api/test_sea_addon/test.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index acf2552d933b80..3c8a4ed5960475 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -476,6 +476,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/, + }, +); From 9a73fa3579759d8f08bc9d8060250831c3a7b924 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 8 Nov 2025 21:04:50 +0100 Subject: [PATCH 2/3] fixup! doc,test: add documentation and test on how to use addons in SEA --- doc/api/single-executable-applications.md | 6 ++++++ test/node-api/node-api.status | 4 ++++ test/node-api/sea_addon | 0 test/node-api/test_sea_addon/test.js | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) delete mode 100644 test/node-api/sea_addon diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 3c8a4ed5960475..b0ee939b4be1db 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -508,6 +508,11 @@ console.log(myaddon.exports); fs.rmSync(addonPath); ``` +Known caveat: if the single-executable application is produced by postject running on a Linux arm64 docker container, +[the produced ELF binary does not have the correct hash table to load the addons][postject-linux-arm64-issue] and +will crash on `process.dlopen()`. Build the single-executable application on other platforms, or at least on +a non-container Linux arm64 environment to work around this issue. + ## Notes ### Single executable application creation process @@ -560,6 +565,7 @@ to help us document them. [documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses [postject]: https://github.com/nodejs/postject +[postject-linux-arm64-issue]: https://github.com/nodejs/postject/issues/105 [signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool [single executable applications]: https://github.com/nodejs/single-executable [supported by Node.js]: https://github.com/nodejs/node/blob/main/BUILDING.md#platform-list diff --git a/test/node-api/node-api.status b/test/node-api/node-api.status index 7f03605d67eeed..9a67fccf6ee45f 100644 --- a/test/node-api/node-api.status +++ b/test/node-api/node-api.status @@ -16,3 +16,7 @@ test_fatal/test_threads_report: PASS,FLAKY [$system==linux && $arch==ppc64] # https://github.com/nodejs/node/issues/59561 test_sea_addon/test: SKIP + +[$system==linux && $arch==arm64] +# https://github.com/nodejs/postject/issues/105 +test_sea_addon/test: SKIP diff --git a/test/node-api/sea_addon b/test/node-api/sea_addon deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/test/node-api/test_sea_addon/test.js b/test/node-api/test_sea_addon/test.js index d422c3eb2f73fa..386ece80955c9d 100644 --- a/test/node-api/test_sea_addon/test.js +++ b/test/node-api/test_sea_addon/test.js @@ -29,7 +29,7 @@ const sea = require('node:sea'); const fs = require('fs'); const path = require('path'); -const addonPath = path.join(process.cwd(), 'hello.node'); +const addonPath = path.join(${JSON.stringify(tmpdir.path)}, 'hello.node'); fs.writeFileSync(addonPath, new Uint8Array(sea.getRawAsset('hello.node'))); const mod = {exports: {}} process.dlopen(mod, addonPath); From 84ed79b1efb61a6bcd436d3977880e8da3fb37a5 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 13 Nov 2025 11:08:00 +0100 Subject: [PATCH 3/3] fixup! fixup! doc,test: add documentation and test on how to use addons in SEA --- test/node-api/node-api.status | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/node-api/node-api.status b/test/node-api/node-api.status index 9a67fccf6ee45f..151687e3549f4f 100644 --- a/test/node-api/node-api.status +++ b/test/node-api/node-api.status @@ -20,3 +20,7 @@ test_sea_addon/test: SKIP [$system==linux && $arch==arm64] # https://github.com/nodejs/postject/issues/105 test_sea_addon/test: SKIP + +[$system==macos && $arch==x64] +# https://github.com/nodejs/node/issues/59553 +test_sea_addon/test: SKIP