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..bcc49c149e2374 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,29 @@ 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; + } + 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 +668,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 +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); 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, + });