mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
6
test/fixtures/sea-exec-argv-empty.js
vendored
Normal 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
18
test/fixtures/sea-exec-argv.js
vendored
Normal 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');
|
||||
@@ -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/
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user