From d39f420652a51ee6f8d56ed3394e8e8fd3933ffe Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 30 Jul 2025 16:01:47 +0100 Subject: [PATCH 1/2] 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. --- doc/api/single-executable-applications.md | 38 +++++++++ src/node_sea.cc | 82 ++++++++++++++++++- src/node_sea.h | 2 + test/fixtures/sea-exec-argv-empty.js | 6 ++ test/fixtures/sea-exec-argv.js | 18 ++++ ...-executable-application-exec-argv-empty.js | 61 ++++++++++++++ ...single-executable-application-exec-argv.js | 67 +++++++++++++++ 7 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/sea-exec-argv-empty.js create mode 100644 test/fixtures/sea-exec-argv.js create mode 100644 test/sequential/test-single-executable-application-exec-argv-empty.js create mode 100644 test/sequential/test-single-executable-application-exec-argv.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 27c57283a7b483..5e12e985b6567c 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -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 diff --git a/src/node_sea.cc b/src/node_sea.cc index 461862d67907c2..3ea1637ead1303 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -123,6 +123,18 @@ size_t SeaSerializer::Write(const SeaResource& sea) { written_total += WriteStringView(content, StringLogMode::kAddressOnly); } } + + if (static_cast(sea.flags & SeaFlags::kIncludeExecArgv)) { + Debug("Write SEA resource exec argv size %zu\n", sea.exec_argv.size()); + written_total += WriteArithmetic(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 exec_argv; + if (static_cast(flags & SeaFlags::kIncludeExecArgv)) { + size_t exec_argv_size = ReadArithmetic(); + 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 FixupArgsForSEA(int argc, char** argv) { // entry point file path. if (IsSingleExecutable()) { static std::vector new_argv; - new_argv.reserve(argc + 2); + static std::vector 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 assets; + std::vector exec_argv; }; std::optional ParseSingleExecutableConfig( @@ -405,6 +452,30 @@ std::optional 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; + } + simdjson::ondemand::value exec_argv_value; + std::vector 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 +669,10 @@ ExitCode GenerateSingleExecutableBlob( for (auto const& [key, content] : assets) { assets_view.emplace(key, content); } + std::vector 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 +680,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); diff --git a/src/node_sea.h b/src/node_sea.h index f3b3c34d26a969..5ba41064304fdf 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -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 code_cache; std::unordered_map assets; + std::vector exec_argv; bool use_snapshot() const; bool use_code_cache() const; diff --git a/test/fixtures/sea-exec-argv-empty.js b/test/fixtures/sea-exec-argv-empty.js new file mode 100644 index 00000000000000..1c98860ba34495 --- /dev/null +++ b/test/fixtures/sea-exec-argv-empty.js @@ -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'); diff --git a/test/fixtures/sea-exec-argv.js b/test/fixtures/sea-exec-argv.js new file mode 100644 index 00000000000000..7f0f8ece575138 --- /dev/null +++ b/test/fixtures/sea-exec-argv.js @@ -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'); diff --git a/test/sequential/test-single-executable-application-exec-argv-empty.js b/test/sequential/test-single-executable-application-exec-argv-empty.js new file mode 100644 index 00000000000000..953d0d0e9f871b --- /dev/null +++ b/test/sequential/test-single-executable-application-exec-argv-empty.js @@ -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/ + }); diff --git a/test/sequential/test-single-executable-application-exec-argv.js b/test/sequential/test-single-executable-application-exec-argv.js new file mode 100644 index 00000000000000..f24934a0fffc6f --- /dev/null +++ b/test/sequential/test-single-executable-application-exec-argv.js @@ -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, + }); From 9246fa8e22dbbe5dd00e4d48eeb755101b55f847 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 12 Aug 2025 20:02:39 +0200 Subject: [PATCH 2/2] fixup! sea: support execArgv in sea config --- src/node_sea.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node_sea.cc b/src/node_sea.cc index 3ea1637ead1303..bcc49c149e2374 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -460,7 +460,6 @@ std::optional ParseSingleExecutableConfig( config_path); return std::nullopt; } - simdjson::ondemand::value exec_argv_value; std::vector exec_argv; for (auto argv : exec_argv_array) { std::string_view argv_str;