doc,test: add documentation and test on how to use addons in SEA

PR-URL: https://github.com/nodejs/node/pull/59582
Fixes: https://github.com/nodejs/node/issues/60611
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Darshan Sen <raisinten@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Joyee Cheung authored and GitHub committed 2025-11-17 04:41:11 +00:00
1 parent 8131d386cc
commit 61cf89cb35
5 files changed
+150

No files matched your search

+38
View File
@@ -476,6 +476,43 @@ 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);
```
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
@@ -528,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
+12
View File
@@ -12,3 +12,15 @@ 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
[$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
+17
View File
@@ -0,0 +1,17 @@
#include <node_api.h>
#include <string.h>
#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;
}
+8
View File
@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "binding",
"sources": [ "binding.c" ]
}
]
}
+75
View File
@@ -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(${JSON.stringify(tmpdir.path)}, '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/,
},
);