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: https://github.com/nodejs/node/pull/59314
Refs: https://github.com/nodejs/node/issues/51688
Refs: https://github.com/nodejs/node/issues/55573
Refs: https://github.com/nodejs/single-executable/issues/100
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
Joyee Cheung
2025-08-17 10:31:32 +02:00
committed by GitHub
parent 7f3a150388
commit 3fc70198e0
7 changed files with 270 additions and 3 deletions

View File

@@ -179,6 +179,7 @@ The configuration currently reads the following top-level fields:
"disableExperimentalSEAWarning": true, // Default: false
"useSnapshot": false, // Default: false
"useCodeCache": true, // Default: false
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
"assets": { // Optional
"a.dat": "/path/to/a.dat",
"b.txt": "/path/to/b.txt"
@@ -276,6 +277,43 @@ execute the script, which would improve the startup performance.
**Note:** `import()` does not work when `useCodeCache` is `true`.
### Execution arguments
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.
For example, the following configuration:
```json
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob",
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
}
```
will instruct the SEA to be launched with the `--no-warnings` and
`--max-old-space-size=2048` flags. In the scripts embedded in the executable, these flags
can be accessed using the `process.execArgv` property:
```js
// If the executable is launched with `sea user-arg1 user-arg2`
console.log(process.execArgv);
// Prints: ['--no-warnings', '--max-old-space-size=2048']
console.log(process.argv);
// Prints ['/path/to/sea', 'path/to/sea', 'user-arg1', 'user-arg2']
```
The user-provided arguments are in the `process.argv` array starting from index 2,
similar to what would happen if the application is started with:
```console
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2
```
## In the injected main script
### Single-executable application API

View File

@@ -123,6 +123,18 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
}
}
if (static_cast<bool>(sea.flags & SeaFlags::kIncludeExecArgv)) {
Debug("Write SEA resource exec argv size %zu\n", sea.exec_argv.size());
written_total += WriteArithmetic<size_t>(sea.exec_argv.size());
for (const auto& arg : sea.exec_argv) {
Debug("Write SEA resource exec arg %s at %p, size=%zu\n",
arg.data(),
arg.data(),
arg.size());
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
}
}
return written_total;
}
@@ -185,7 +197,22 @@ SeaResource SeaDeserializer::Read() {
assets.emplace(key, content);
}
}
return {flags, code_path, code, code_cache, assets};
std::vector<std::string_view> exec_argv;
if (static_cast<bool>(flags & SeaFlags::kIncludeExecArgv)) {
size_t exec_argv_size = ReadArithmetic<size_t>();
Debug("Read SEA resource exec args size %zu\n", exec_argv_size);
exec_argv.reserve(exec_argv_size);
for (size_t i = 0; i < exec_argv_size; ++i) {
std::string_view arg = ReadStringView(StringLogMode::kAddressAndContent);
Debug("Read SEA resource exec arg %s at %p, size=%zu\n",
arg.data(),
arg.data(),
arg.size());
exec_argv.emplace_back(arg);
}
}
return {flags, code_path, code, code_cache, assets, exec_argv};
}
std::string_view FindSingleExecutableBlob() {
@@ -269,8 +296,27 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
// entry point file path.
if (IsSingleExecutable()) {
static std::vector<char*> new_argv;
new_argv.reserve(argc + 2);
static std::vector<std::string> exec_argv_storage;
SeaResource sea_resource = FindSingleExecutableResource();
new_argv.clear();
exec_argv_storage.clear();
// Reserve space for argv[0], exec argv, original argv, and nullptr
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
new_argv.emplace_back(argv[0]);
// Insert exec argv from SEA config
if (!sea_resource.exec_argv.empty()) {
exec_argv_storage.reserve(sea_resource.exec_argv.size());
for (const auto& arg : sea_resource.exec_argv) {
exec_argv_storage.emplace_back(arg);
new_argv.emplace_back(exec_argv_storage.back().data());
}
}
// Add actual run time arguments.
new_argv.insert(new_argv.end(), argv, argv + argc);
new_argv.emplace_back(nullptr);
argc = new_argv.size() - 1;
@@ -287,6 +333,7 @@ struct SeaConfig {
std::string output_path;
SeaFlags flags = SeaFlags::kDefault;
std::unordered_map<std::string, std::string> assets;
std::vector<std::string> exec_argv;
};
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -405,6 +452,29 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
if (!result.assets.empty()) {
result.flags |= SeaFlags::kIncludeAssets;
}
} else if (key == "execArgv") {
simdjson::ondemand::array exec_argv_array;
if (field.value().get_array().get(exec_argv_array)) {
FPrintF(stderr,
"\"execArgv\" field of %s is not an array of strings\n",
config_path);
return std::nullopt;
}
std::vector<std::string> exec_argv;
for (auto argv : exec_argv_array) {
std::string_view argv_str;
if (argv.get_string().get(argv_str)) {
FPrintF(stderr,
"\"execArgv\" field of %s is not an array of strings\n",
config_path);
return std::nullopt;
}
exec_argv.emplace_back(argv_str);
}
if (!exec_argv.empty()) {
result.flags |= SeaFlags::kIncludeExecArgv;
result.exec_argv = std::move(exec_argv);
}
}
}
@@ -598,6 +668,10 @@ ExitCode GenerateSingleExecutableBlob(
for (auto const& [key, content] : assets) {
assets_view.emplace(key, content);
}
std::vector<std::string_view> exec_argv_view;
for (const auto& arg : config.exec_argv) {
exec_argv_view.emplace_back(arg);
}
SeaResource sea{
config.flags,
config.main_path,
@@ -605,7 +679,8 @@ ExitCode GenerateSingleExecutableBlob(
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
: std::string_view{main_script.data(), main_script.size()},
optional_sv_code_cache,
assets_view};
assets_view,
exec_argv_view};
SeaSerializer serializer;
serializer.Write(sea);

View File

@@ -28,6 +28,7 @@ enum class SeaFlags : uint32_t {
kUseSnapshot = 1 << 1,
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
kIncludeExecArgv = 1 << 4,
};
struct SeaResource {
@@ -36,6 +37,7 @@ struct SeaResource {
std::string_view main_code_or_snapshot;
std::optional<std::string_view> code_cache;
std::unordered_map<std::string_view, std::string_view> assets;
std::vector<std::string_view> exec_argv;
bool use_snapshot() const;
bool use_code_cache() const;

6
test/fixtures/sea-exec-argv-empty.js vendored Normal file
View File

@@ -0,0 +1,6 @@
const assert = require('assert');
console.log('process.argv:', JSON.stringify(process.argv));
assert.strictEqual(process.argv[2], 'user-arg');
assert.deepStrictEqual(process.execArgv, []);
console.log('empty execArgv test passed');

18
test/fixtures/sea-exec-argv.js vendored Normal file
View File

@@ -0,0 +1,18 @@
const assert = require('assert');
process.emitWarning('This warning should not be shown in the output', 'TestWarning');
console.log('process.argv:', JSON.stringify(process.argv));
console.log('process.execArgv:', JSON.stringify(process.execArgv));
assert.deepStrictEqual(process.execArgv, [ '--no-warnings', '--max-old-space-size=2048' ]);
// We start from 2, because in SEA, the index 1 would be the same as the execPath
// to accommodate the general expectation that index 1 is the path to script for
// applications.
assert.deepStrictEqual(process.argv.slice(2), [
'user-arg1',
'user-arg2'
]);
console.log('multiple execArgv test passed');

View File

@@ -0,0 +1,61 @@
'use strict';
require('../common');
const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
// This tests the execArgv functionality with empty array in single executable applications.
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
const { join } = require('path');
const assert = require('assert');
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
copyFileSync(fixtures.path('sea-exec-argv-empty.js'), tmpdir.resolve('sea.js'));
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"execArgv": []
}
`);
spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{ cwd: tmpdir.path });
assert(existsSync(seaPrepBlob));
generateSEA(outputFile, process.execPath, seaPrepBlob);
// Test that empty execArgv work correctly
spawnSyncAndAssert(
outputFile,
['user-arg'],
{
env: {
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
}
},
{
stdout: /empty execArgv test passed/
});

View File

@@ -0,0 +1,67 @@
'use strict';
require('../common');
const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
// This tests the execArgv functionality with multiple arguments in single executable applications.
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
const { join } = require('path');
const assert = require('assert');
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
copyFileSync(fixtures.path('sea-exec-argv.js'), tmpdir.resolve('sea.js'));
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
}
`);
spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{ cwd: tmpdir.path });
assert(existsSync(seaPrepBlob));
generateSEA(outputFile, process.execPath, seaPrepBlob);
// Test that multiple execArgv are properly applied
spawnSyncAndAssert(
outputFile,
['user-arg1', 'user-arg2'],
{
env: {
...process.env,
NODE_NO_WARNINGS: '0',
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
NODE_DEBUG_NATIVE: 'SEA',
}
},
{
stdout: /multiple execArgv test passed/,
stderr(output) {
assert.doesNotMatch(output, /This warning should not be shown in the output/);
return true;
},
trim: true,
});