Skip to content

Commit da4160e

Browse files
committed
doc,test: add documentation and test on how to use addons in SEA
1 parent b98ada2 commit da4160e

File tree

6 files changed

+136
-0
lines changed

6 files changed

+136
-0
lines changed

doc/api/single-executable-applications.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,38 @@ are equal to [`process.execPath`][].
476476
The value of `__dirname` in the injected main script is equal to the directory
477477
name of [`process.execPath`][].
478478
479+
### Using native addons in the injected main script
480+
481+
Native addons can be bundled as assets into the single-executable application
482+
by specifying them in the `assets` field of the configuration file used to
483+
generate the single-executable application preparation blob.
484+
The addon can then be loaded in the injected main script by writing the asset
485+
to a temporary file and loading it with `process.dlopen()`.
486+
487+
```json
488+
{
489+
"main": "/path/to/bundled/script.js",
490+
"output": "/path/to/write/the/generated/blob.blob",
491+
"assets": {
492+
"myaddon.node": "/path/to/myaddon/build/Release/myaddon.node"
493+
}
494+
}
495+
```
496+
497+
```js
498+
// script.js
499+
const fs = require('node:fs');
500+
const os = require('node:os');
501+
const path = require('node:path');
502+
const { getRawAsset } = require('node:sea');
503+
const addonPath = path.join(os.tmpdir(), 'myaddon.node');
504+
fs.writeFileSync(addonPath, new Uint8Array(getRawAsset('myaddon.node')));
505+
const myaddon = { exports: {} };
506+
process.dlopen(myaddon, addonPath);
507+
console.log(myaddon.exports);
508+
fs.rmSync(addonPath);
509+
```
510+
479511
## Notes
480512
481513
### Single executable application creation process

test/node-api/node-api.status

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ prefix node-api
1212
# https://github.com/nodejs/node/issues/43457
1313
test_fatal/test_threads: PASS,FLAKY
1414
test_fatal/test_threads_report: PASS,FLAKY
15+
16+
[$system==linux && $arch==ppc64]
17+
# https://github.com/nodejs/node/issues/59561
18+
test_sea_addon/test: SKIP

test/node-api/sea_addon

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include <node_api.h>
2+
#include <string.h>
3+
#include "../../js-native-api/common.h"
4+
5+
static napi_value Method(napi_env env, napi_callback_info info) {
6+
napi_value world;
7+
const char* str = "world";
8+
size_t str_len = strlen(str);
9+
NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, &world));
10+
return world;
11+
}
12+
13+
NAPI_MODULE_INIT() {
14+
napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("hello", Method);
15+
NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc));
16+
return exports;
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "binding",
5+
"sources": [ "binding.c" ]
6+
}
7+
]
8+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
// This tests that SEA can load addons packaged as assets by writing them to disk
3+
// and loading them via process.dlopen().
4+
const common = require('../../common');
5+
const { generateSEA, skipIfSingleExecutableIsNotSupported } = require('../../common/sea');
6+
7+
skipIfSingleExecutableIsNotSupported();
8+
9+
const assert = require('assert');
10+
11+
const tmpdir = require('../../common/tmpdir');
12+
const { copyFileSync, writeFileSync, existsSync, rmSync } = require('fs');
13+
const {
14+
spawnSyncAndExitWithoutError,
15+
spawnSyncAndAssert,
16+
} = require('../../common/child_process');
17+
const { join } = require('path');
18+
const configFile = tmpdir.resolve('sea-config.json');
19+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
20+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
21+
tmpdir.refresh();
22+
23+
// Copy test fixture to working directory
24+
const addonPath = join(__dirname, 'build', common.buildType, 'binding.node');
25+
const copiedAddonPath = tmpdir.resolve('binding.node');
26+
copyFileSync(addonPath, copiedAddonPath);
27+
writeFileSync(tmpdir.resolve('sea.js'), `
28+
const sea = require('node:sea');
29+
const fs = require('fs');
30+
const path = require('path');
31+
32+
const addonPath = path.join(process.cwd(), 'hello.node');
33+
fs.writeFileSync(addonPath, new Uint8Array(sea.getRawAsset('hello.node')));
34+
const mod = {exports: {}}
35+
process.dlopen(mod, addonPath);
36+
console.log('hello,', mod.exports.hello());
37+
`, 'utf-8');
38+
39+
writeFileSync(configFile, `
40+
{
41+
"main": "sea.js",
42+
"output": "sea-prep.blob",
43+
"disableExperimentalSEAWarning": true,
44+
"assets": {
45+
"hello.node": "binding.node"
46+
}
47+
}
48+
`, 'utf8');
49+
50+
spawnSyncAndExitWithoutError(
51+
process.execPath,
52+
['--experimental-sea-config', 'sea-config.json'],
53+
{ cwd: tmpdir.path },
54+
);
55+
assert(existsSync(seaPrepBlob));
56+
57+
generateSEA(outputFile, process.execPath, seaPrepBlob);
58+
59+
// Remove the copied addon after it's been packaged into the SEA blob
60+
rmSync(copiedAddonPath, { force: true });
61+
62+
spawnSyncAndAssert(
63+
outputFile,
64+
[],
65+
{
66+
env: {
67+
...process.env,
68+
NODE_DEBUG_NATIVE: 'SEA',
69+
},
70+
cwd: tmpdir.path,
71+
},
72+
{
73+
stdout: /hello, world/,
74+
},
75+
);

0 commit comments

Comments
 (0)