Skip to content

Commit c6e3d5d

Browse files
joyeecheungtargos
authored andcommitted
sea: support execArgv in sea config
The `execArgv` field can be used to specify Node.js-specific arguments that will be automatically applied when the single executable application starts. This allows application developers to configure Node.js runtime options without requiring end users to be aware of these flags. PR-URL: #59314 Refs: #51688 Refs: #55573 Refs: nodejs/single-executable#100 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Darshan Sen <raisinten@gmail.com>
1 parent b7ea39d commit c6e3d5d

7 files changed

+270
-3
lines changed

doc/api/single-executable-applications.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ The configuration currently reads the following top-level fields:
179179
"disableExperimentalSEAWarning": true, // Default: false
180180
"useSnapshot": false, // Default: false
181181
"useCodeCache": true, // Default: false
182+
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
182183
"assets": { // Optional
183184
"a.dat": "/path/to/a.dat",
184185
"b.txt": "/path/to/b.txt"
@@ -276,6 +277,43 @@ execute the script, which would improve the startup performance.
276277
277278
**Note:** `import()` does not work when `useCodeCache` is `true`.
278279
280+
### Execution arguments
281+
282+
The `execArgv` field can be used to specify Node.js-specific
283+
arguments that will be automatically applied when the single
284+
executable application starts. This allows application developers
285+
to configure Node.js runtime options without requiring end users
286+
to be aware of these flags.
287+
288+
For example, the following configuration:
289+
290+
```json
291+
{
292+
"main": "/path/to/bundled/script.js",
293+
"output": "/path/to/write/the/generated/blob.blob",
294+
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
295+
}
296+
```
297+
298+
will instruct the SEA to be launched with the `--no-warnings` and
299+
`--max-old-space-size=2048` flags. In the scripts embedded in the executable, these flags
300+
can be accessed using the `process.execArgv` property:
301+
302+
```js
303+
// If the executable is launched with `sea user-arg1 user-arg2`
304+
console.log(process.execArgv);
305+
// Prints: ['--no-warnings', '--max-old-space-size=2048']
306+
console.log(process.argv);
307+
// Prints ['/path/to/sea', 'path/to/sea', 'user-arg1', 'user-arg2']
308+
```
309+
310+
The user-provided arguments are in the `process.argv` array starting from index 2,
311+
similar to what would happen if the application is started with:
312+
313+
```console
314+
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2
315+
```
316+
279317
## In the injected main script
280318
281319
### Single-executable application API

src/node_sea.cc

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
123123
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
124124
}
125125
}
126+
127+
if (static_cast<bool>(sea.flags & SeaFlags::kIncludeExecArgv)) {
128+
Debug("Write SEA resource exec argv size %zu\n", sea.exec_argv.size());
129+
written_total += WriteArithmetic<size_t>(sea.exec_argv.size());
130+
for (const auto& arg : sea.exec_argv) {
131+
Debug("Write SEA resource exec arg %s at %p, size=%zu\n",
132+
arg.data(),
133+
arg.data(),
134+
arg.size());
135+
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
136+
}
137+
}
126138
return written_total;
127139
}
128140

@@ -185,7 +197,22 @@ SeaResource SeaDeserializer::Read() {
185197
assets.emplace(key, content);
186198
}
187199
}
188-
return {flags, code_path, code, code_cache, assets};
200+
201+
std::vector<std::string_view> exec_argv;
202+
if (static_cast<bool>(flags & SeaFlags::kIncludeExecArgv)) {
203+
size_t exec_argv_size = ReadArithmetic<size_t>();
204+
Debug("Read SEA resource exec args size %zu\n", exec_argv_size);
205+
exec_argv.reserve(exec_argv_size);
206+
for (size_t i = 0; i < exec_argv_size; ++i) {
207+
std::string_view arg = ReadStringView(StringLogMode::kAddressAndContent);
208+
Debug("Read SEA resource exec arg %s at %p, size=%zu\n",
209+
arg.data(),
210+
arg.data(),
211+
arg.size());
212+
exec_argv.emplace_back(arg);
213+
}
214+
}
215+
return {flags, code_path, code, code_cache, assets, exec_argv};
189216
}
190217

191218
std::string_view FindSingleExecutableBlob() {
@@ -269,8 +296,27 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
269296
// entry point file path.
270297
if (IsSingleExecutable()) {
271298
static std::vector<char*> new_argv;
272-
new_argv.reserve(argc + 2);
299+
static std::vector<std::string> exec_argv_storage;
300+
301+
SeaResource sea_resource = FindSingleExecutableResource();
302+
303+
new_argv.clear();
304+
exec_argv_storage.clear();
305+
306+
// Reserve space for argv[0], exec argv, original argv, and nullptr
307+
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
273308
new_argv.emplace_back(argv[0]);
309+
310+
// Insert exec argv from SEA config
311+
if (!sea_resource.exec_argv.empty()) {
312+
exec_argv_storage.reserve(sea_resource.exec_argv.size());
313+
for (const auto& arg : sea_resource.exec_argv) {
314+
exec_argv_storage.emplace_back(arg);
315+
new_argv.emplace_back(exec_argv_storage.back().data());
316+
}
317+
}
318+
319+
// Add actual run time arguments.
274320
new_argv.insert(new_argv.end(), argv, argv + argc);
275321
new_argv.emplace_back(nullptr);
276322
argc = new_argv.size() - 1;
@@ -287,6 +333,7 @@ struct SeaConfig {
287333
std::string output_path;
288334
SeaFlags flags = SeaFlags::kDefault;
289335
std::unordered_map<std::string, std::string> assets;
336+
std::vector<std::string> exec_argv;
290337
};
291338

292339
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -405,6 +452,29 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
405452
if (!result.assets.empty()) {
406453
result.flags |= SeaFlags::kIncludeAssets;
407454
}
455+
} else if (key == "execArgv") {
456+
simdjson::ondemand::array exec_argv_array;
457+
if (field.value().get_array().get(exec_argv_array)) {
458+
FPrintF(stderr,
459+
"\"execArgv\" field of %s is not an array of strings\n",
460+
config_path);
461+
return std::nullopt;
462+
}
463+
std::vector<std::string> exec_argv;
464+
for (auto argv : exec_argv_array) {
465+
std::string_view argv_str;
466+
if (argv.get_string().get(argv_str)) {
467+
FPrintF(stderr,
468+
"\"execArgv\" field of %s is not an array of strings\n",
469+
config_path);
470+
return std::nullopt;
471+
}
472+
exec_argv.emplace_back(argv_str);
473+
}
474+
if (!exec_argv.empty()) {
475+
result.flags |= SeaFlags::kIncludeExecArgv;
476+
result.exec_argv = std::move(exec_argv);
477+
}
408478
}
409479
}
410480

@@ -598,14 +668,19 @@ ExitCode GenerateSingleExecutableBlob(
598668
for (auto const& [key, content] : assets) {
599669
assets_view.emplace(key, content);
600670
}
671+
std::vector<std::string_view> exec_argv_view;
672+
for (const auto& arg : config.exec_argv) {
673+
exec_argv_view.emplace_back(arg);
674+
}
601675
SeaResource sea{
602676
config.flags,
603677
config.main_path,
604678
builds_snapshot_from_main
605679
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
606680
: std::string_view{main_script.data(), main_script.size()},
607681
optional_sv_code_cache,
608-
assets_view};
682+
assets_view,
683+
exec_argv_view};
609684

610685
SeaSerializer serializer;
611686
serializer.Write(sea);

src/node_sea.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum class SeaFlags : uint32_t {
2828
kUseSnapshot = 1 << 1,
2929
kUseCodeCache = 1 << 2,
3030
kIncludeAssets = 1 << 3,
31+
kIncludeExecArgv = 1 << 4,
3132
};
3233

3334
struct SeaResource {
@@ -36,6 +37,7 @@ struct SeaResource {
3637
std::string_view main_code_or_snapshot;
3738
std::optional<std::string_view> code_cache;
3839
std::unordered_map<std::string_view, std::string_view> assets;
40+
std::vector<std::string_view> exec_argv;
3941

4042
bool use_snapshot() const;
4143
bool use_code_cache() const;

test/fixtures/sea-exec-argv-empty.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const assert = require('assert');
2+
3+
console.log('process.argv:', JSON.stringify(process.argv));
4+
assert.strictEqual(process.argv[2], 'user-arg');
5+
assert.deepStrictEqual(process.execArgv, []);
6+
console.log('empty execArgv test passed');

test/fixtures/sea-exec-argv.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const assert = require('assert');
2+
3+
process.emitWarning('This warning should not be shown in the output', 'TestWarning');
4+
5+
console.log('process.argv:', JSON.stringify(process.argv));
6+
console.log('process.execArgv:', JSON.stringify(process.execArgv));
7+
8+
assert.deepStrictEqual(process.execArgv, [ '--no-warnings', '--max-old-space-size=2048' ]);
9+
10+
// We start from 2, because in SEA, the index 1 would be the same as the execPath
11+
// to accommodate the general expectation that index 1 is the path to script for
12+
// applications.
13+
assert.deepStrictEqual(process.argv.slice(2), [
14+
'user-arg1',
15+
'user-arg2'
16+
]);
17+
18+
console.log('multiple execArgv test passed');
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const {
6+
generateSEA,
7+
skipIfSingleExecutableIsNotSupported,
8+
} = require('../common/sea');
9+
10+
skipIfSingleExecutableIsNotSupported();
11+
12+
// This tests the execArgv functionality with empty array in single executable applications.
13+
14+
const fixtures = require('../common/fixtures');
15+
const tmpdir = require('../common/tmpdir');
16+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
17+
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
18+
const { join } = require('path');
19+
const assert = require('assert');
20+
21+
const configFile = tmpdir.resolve('sea-config.json');
22+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
23+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
24+
25+
tmpdir.refresh();
26+
27+
// Copy test fixture to working directory
28+
copyFileSync(fixtures.path('sea-exec-argv-empty.js'), tmpdir.resolve('sea.js'));
29+
30+
writeFileSync(configFile, `
31+
{
32+
"main": "sea.js",
33+
"output": "sea-prep.blob",
34+
"disableExperimentalSEAWarning": true,
35+
"execArgv": []
36+
}
37+
`);
38+
39+
spawnSyncAndExitWithoutError(
40+
process.execPath,
41+
['--experimental-sea-config', 'sea-config.json'],
42+
{ cwd: tmpdir.path });
43+
44+
assert(existsSync(seaPrepBlob));
45+
46+
generateSEA(outputFile, process.execPath, seaPrepBlob);
47+
48+
// Test that empty execArgv work correctly
49+
spawnSyncAndAssert(
50+
outputFile,
51+
['user-arg'],
52+
{
53+
env: {
54+
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
55+
NODE_DEBUG_NATIVE: 'SEA',
56+
...process.env,
57+
}
58+
},
59+
{
60+
stdout: /empty execArgv test passed/
61+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const {
6+
generateSEA,
7+
skipIfSingleExecutableIsNotSupported,
8+
} = require('../common/sea');
9+
10+
skipIfSingleExecutableIsNotSupported();
11+
12+
// This tests the execArgv functionality with multiple arguments in single executable applications.
13+
14+
const fixtures = require('../common/fixtures');
15+
const tmpdir = require('../common/tmpdir');
16+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
17+
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
18+
const { join } = require('path');
19+
const assert = require('assert');
20+
21+
const configFile = tmpdir.resolve('sea-config.json');
22+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
23+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
24+
25+
tmpdir.refresh();
26+
27+
// Copy test fixture to working directory
28+
copyFileSync(fixtures.path('sea-exec-argv.js'), tmpdir.resolve('sea.js'));
29+
30+
writeFileSync(configFile, `
31+
{
32+
"main": "sea.js",
33+
"output": "sea-prep.blob",
34+
"disableExperimentalSEAWarning": true,
35+
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
36+
}
37+
`);
38+
39+
spawnSyncAndExitWithoutError(
40+
process.execPath,
41+
['--experimental-sea-config', 'sea-config.json'],
42+
{ cwd: tmpdir.path });
43+
44+
assert(existsSync(seaPrepBlob));
45+
46+
generateSEA(outputFile, process.execPath, seaPrepBlob);
47+
48+
// Test that multiple execArgv are properly applied
49+
spawnSyncAndAssert(
50+
outputFile,
51+
['user-arg1', 'user-arg2'],
52+
{
53+
env: {
54+
...process.env,
55+
NODE_NO_WARNINGS: '0',
56+
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
57+
NODE_DEBUG_NATIVE: 'SEA',
58+
}
59+
},
60+
{
61+
stdout: /multiple execArgv test passed/,
62+
stderr(output) {
63+
assert.doesNotMatch(output, /This warning should not be shown in the output/);
64+
return true;
65+
},
66+
trim: true,
67+
});

0 commit comments

Comments
 (0)