mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
sea: implement execArgvExtension
This implements the execArgvExtension configuration field for SEA, which takes one of three string values to specify whether and how execution arguments can be extended for the SEA at run time: * `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used, and the `NODE_OPTIONS` environment variable will be ignored. * `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments. This is the default behavior to maintain backward compatibility. * `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags will be parsed as execution arguments for Node.js instead of being passed to the user script. This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable. PR-URL: https://github.com/nodejs/node/pull/59560 Fixes: https://github.com/nodejs/node/issues/55573 Fixes: https://github.com/nodejs/single-executable/issues/100 Refs: https://github.com/nodejs/node/issues/51688 Reviewed-By: Xuguang Mei <meixuguang@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
@@ -180,6 +180,7 @@ The configuration currently reads the following top-level fields:
|
||||
"useSnapshot": false, // Default: false
|
||||
"useCodeCache": true, // Default: false
|
||||
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
|
||||
"execArgvExtension": "env", // Default: "env", options: "none", "env", "cli"
|
||||
"assets": { // Optional
|
||||
"a.dat": "/path/to/a.dat",
|
||||
"b.txt": "/path/to/b.txt"
|
||||
@@ -314,6 +315,42 @@ similar to what would happen if the application is started with:
|
||||
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2
|
||||
```
|
||||
|
||||
### Execution argument extension
|
||||
|
||||
The `execArgvExtension` field controls how additional execution arguments can be
|
||||
provided beyond those specified in the `execArgv` field. It accepts one of three string values:
|
||||
|
||||
* `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used,
|
||||
and the `NODE_OPTIONS` environment variable will be ignored.
|
||||
* `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments.
|
||||
This is the default behavior to maintain backward compatibility.
|
||||
* `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags
|
||||
will be parsed as execution arguments for Node.js instead of being passed to the user script.
|
||||
This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable.
|
||||
|
||||
For example, with `"execArgvExtension": "cli"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"main": "/path/to/bundled/script.js",
|
||||
"output": "/path/to/write/the/generated/blob.blob",
|
||||
"execArgv": ["--no-warnings"],
|
||||
"execArgvExtension": "cli"
|
||||
}
|
||||
```
|
||||
|
||||
The executable can be launched as:
|
||||
|
||||
```console
|
||||
./my-sea --node-options="--trace-exit" user-arg1 user-arg2
|
||||
```
|
||||
|
||||
This would be equivalent to running:
|
||||
|
||||
```console
|
||||
node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2
|
||||
```
|
||||
|
||||
## In the injected main script
|
||||
|
||||
### Single-executable application API
|
||||
|
||||
12
src/node.cc
12
src/node.cc
@@ -940,7 +940,17 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
}
|
||||
|
||||
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
|
||||
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
|
||||
bool should_parse_node_options =
|
||||
!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv);
|
||||
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||
if (sea::IsSingleExecutable()) {
|
||||
sea::SeaResource sea_resource = sea::FindSingleExecutableResource();
|
||||
if (sea_resource.exec_argv_extension != sea::SeaExecArgvExtension::kEnv) {
|
||||
should_parse_node_options = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (should_parse_node_options) {
|
||||
// NODE_OPTIONS environment variable is preferred over the file one.
|
||||
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
|
||||
!node_options.empty()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "node_errors.h"
|
||||
#include "node_external_reference.h"
|
||||
#include "node_internals.h"
|
||||
#include "node_options.h"
|
||||
#include "node_snapshot_builder.h"
|
||||
#include "node_union_bytes.h"
|
||||
#include "node_v8_platform-inl.h"
|
||||
@@ -86,6 +87,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
|
||||
uint32_t flags = static_cast<uint32_t>(sea.flags);
|
||||
Debug("Write SEA flags %x\n", flags);
|
||||
written_total += WriteArithmetic<uint32_t>(flags);
|
||||
|
||||
Debug("Write SEA resource exec argv extension %u\n",
|
||||
static_cast<uint8_t>(sea.exec_argv_extension));
|
||||
written_total +=
|
||||
WriteArithmetic<uint8_t>(static_cast<uint8_t>(sea.exec_argv_extension));
|
||||
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
|
||||
|
||||
Debug("Write SEA code path %p, size=%zu\n",
|
||||
@@ -158,6 +164,11 @@ SeaResource SeaDeserializer::Read() {
|
||||
CHECK_EQ(magic, kMagic);
|
||||
SeaFlags flags(static_cast<SeaFlags>(ReadArithmetic<uint32_t>()));
|
||||
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
|
||||
|
||||
uint8_t extension_value = ReadArithmetic<uint8_t>();
|
||||
SeaExecArgvExtension exec_argv_extension =
|
||||
static_cast<SeaExecArgvExtension>(extension_value);
|
||||
Debug("Read SEA resource exec argv extension %u\n", extension_value);
|
||||
CHECK_EQ(read_total, SeaResource::kHeaderSize);
|
||||
|
||||
std::string_view code_path =
|
||||
@@ -212,7 +223,13 @@ SeaResource SeaDeserializer::Read() {
|
||||
exec_argv.emplace_back(arg);
|
||||
}
|
||||
}
|
||||
return {flags, code_path, code, code_cache, assets, exec_argv};
|
||||
return {flags,
|
||||
exec_argv_extension,
|
||||
code_path,
|
||||
code,
|
||||
code_cache,
|
||||
assets,
|
||||
exec_argv};
|
||||
}
|
||||
|
||||
std::string_view FindSingleExecutableBlob() {
|
||||
@@ -297,26 +314,55 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
|
||||
if (IsSingleExecutable()) {
|
||||
static std::vector<char*> new_argv;
|
||||
static std::vector<std::string> exec_argv_storage;
|
||||
static std::vector<std::string> cli_extension_args;
|
||||
|
||||
SeaResource sea_resource = FindSingleExecutableResource();
|
||||
|
||||
new_argv.clear();
|
||||
exec_argv_storage.clear();
|
||||
cli_extension_args.clear();
|
||||
|
||||
// Reserve space for argv[0], exec argv, original argv, and nullptr
|
||||
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
|
||||
// Handle CLI extension mode for --node-options
|
||||
if (sea_resource.exec_argv_extension == SeaExecArgvExtension::kCli) {
|
||||
// Extract --node-options and filter argv
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (strncmp(argv[i], "--node-options=", 15) == 0) {
|
||||
std::string node_options = argv[i] + 15;
|
||||
std::vector<std::string> errors;
|
||||
cli_extension_args = ParseNodeOptionsEnvVar(node_options, &errors);
|
||||
// Remove this argument by shifting the rest
|
||||
for (int j = i; j < argc - 1; ++j) {
|
||||
argv[j] = argv[j + 1];
|
||||
}
|
||||
argc--;
|
||||
i--; // Adjust index since we removed an element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve space for argv[0], exec argv, cli extension args, original argv,
|
||||
// and nullptr
|
||||
new_argv.reserve(argc + sea_resource.exec_argv.size() +
|
||||
cli_extension_args.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());
|
||||
exec_argv_storage.reserve(sea_resource.exec_argv.size() +
|
||||
cli_extension_args.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.
|
||||
// Insert CLI extension args
|
||||
for (const auto& arg : cli_extension_args) {
|
||||
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;
|
||||
@@ -332,6 +378,7 @@ struct SeaConfig {
|
||||
std::string main_path;
|
||||
std::string output_path;
|
||||
SeaFlags flags = SeaFlags::kDefault;
|
||||
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
|
||||
std::unordered_map<std::string, std::string> assets;
|
||||
std::vector<std::string> exec_argv;
|
||||
};
|
||||
@@ -475,6 +522,27 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
|
||||
result.flags |= SeaFlags::kIncludeExecArgv;
|
||||
result.exec_argv = std::move(exec_argv);
|
||||
}
|
||||
} else if (key == "execArgvExtension") {
|
||||
std::string_view extension_str;
|
||||
if (field.value().get_string().get(extension_str)) {
|
||||
FPrintF(stderr,
|
||||
"\"execArgvExtension\" field of %s is not a string\n",
|
||||
config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (extension_str == "none") {
|
||||
result.exec_argv_extension = SeaExecArgvExtension::kNone;
|
||||
} else if (extension_str == "env") {
|
||||
result.exec_argv_extension = SeaExecArgvExtension::kEnv;
|
||||
} else if (extension_str == "cli") {
|
||||
result.exec_argv_extension = SeaExecArgvExtension::kCli;
|
||||
} else {
|
||||
FPrintF(stderr,
|
||||
"\"execArgvExtension\" field of %s must be one of "
|
||||
"\"none\", \"env\", or \"cli\"\n",
|
||||
config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +742,7 @@ ExitCode GenerateSingleExecutableBlob(
|
||||
}
|
||||
SeaResource sea{
|
||||
config.flags,
|
||||
config.exec_argv_extension,
|
||||
config.main_path,
|
||||
builds_snapshot_from_main
|
||||
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
|
||||
|
||||
@@ -31,8 +31,15 @@ enum class SeaFlags : uint32_t {
|
||||
kIncludeExecArgv = 1 << 4,
|
||||
};
|
||||
|
||||
enum class SeaExecArgvExtension : uint8_t {
|
||||
kNone = 0,
|
||||
kEnv = 1,
|
||||
kCli = 2,
|
||||
};
|
||||
|
||||
struct SeaResource {
|
||||
SeaFlags flags = SeaFlags::kDefault;
|
||||
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
|
||||
std::string_view code_path;
|
||||
std::string_view main_code_or_snapshot;
|
||||
std::optional<std::string_view> code_cache;
|
||||
@@ -42,7 +49,8 @@ struct SeaResource {
|
||||
bool use_snapshot() const;
|
||||
bool use_code_cache() const;
|
||||
|
||||
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
|
||||
static constexpr size_t kHeaderSize =
|
||||
sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension);
|
||||
};
|
||||
|
||||
bool IsSingleExecutable();
|
||||
|
||||
14
test/fixtures/sea-exec-argv-extension-cli.js
vendored
Normal file
14
test/fixtures/sea-exec-argv-extension-cli.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
const assert = require('assert');
|
||||
|
||||
console.log('process.argv:', JSON.stringify(process.argv));
|
||||
console.log('process.execArgv:', JSON.stringify(process.execArgv));
|
||||
|
||||
// Should have execArgv from SEA config + CLI --node-options
|
||||
assert.deepStrictEqual(process.execArgv, ['--no-warnings', '--max-old-space-size=1024']);
|
||||
|
||||
assert.deepStrictEqual(process.argv.slice(2), [
|
||||
'user-arg1',
|
||||
'user-arg2'
|
||||
]);
|
||||
|
||||
console.log('execArgvExtension cli test passed');
|
||||
19
test/fixtures/sea-exec-argv-extension-env.js
vendored
Normal file
19
test/fixtures/sea-exec-argv-extension-env.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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));
|
||||
|
||||
// Should have execArgv from SEA config.
|
||||
// Note that flags from NODE_OPTIONS are not included in process.execArgv no matter it's
|
||||
// an SEA or not, but we can test whether it works by checking that the warning emitted
|
||||
// above was silenced.
|
||||
assert.deepStrictEqual(process.execArgv, ['--no-warnings']);
|
||||
|
||||
assert.deepStrictEqual(process.argv.slice(2), [
|
||||
'user-arg1',
|
||||
'user-arg2'
|
||||
]);
|
||||
|
||||
console.log('execArgvExtension env test passed');
|
||||
14
test/fixtures/sea-exec-argv-extension-none.js
vendored
Normal file
14
test/fixtures/sea-exec-argv-extension-none.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
const assert = require('assert');
|
||||
|
||||
console.log('process.argv:', JSON.stringify(process.argv));
|
||||
console.log('process.execArgv:', JSON.stringify(process.execArgv));
|
||||
|
||||
// Should only have execArgv from SEA config, no NODE_OPTIONS
|
||||
assert.deepStrictEqual(process.execArgv, ['--no-warnings']);
|
||||
|
||||
assert.deepStrictEqual(process.argv.slice(2), [
|
||||
'user-arg1',
|
||||
'user-arg2'
|
||||
]);
|
||||
|
||||
console.log('execArgvExtension none test passed');
|
||||
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
|
||||
const {
|
||||
generateSEA,
|
||||
skipIfSingleExecutableIsNotSupported,
|
||||
} = require('../common/sea');
|
||||
|
||||
skipIfSingleExecutableIsNotSupported();
|
||||
|
||||
// This tests the execArgvExtension "cli" mode 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-extension-cli.js'), tmpdir.resolve('sea.js'));
|
||||
|
||||
writeFileSync(configFile, `
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-prep.blob",
|
||||
"disableExperimentalSEAWarning": true,
|
||||
"execArgv": ["--no-warnings"],
|
||||
"execArgvExtension": "cli"
|
||||
}
|
||||
`);
|
||||
|
||||
spawnSyncAndExitWithoutError(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', 'sea-config.json'],
|
||||
{ cwd: tmpdir.path });
|
||||
|
||||
assert(existsSync(seaPrepBlob));
|
||||
|
||||
generateSEA(outputFile, process.execPath, seaPrepBlob);
|
||||
|
||||
// Test that --node-options works with execArgvExtension: "cli"
|
||||
spawnSyncAndAssert(
|
||||
outputFile,
|
||||
['--node-options=--max-old-space-size=1024', 'user-arg1', 'user-arg2'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: '--max-old-space-size=2048', // Should be ignored
|
||||
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
|
||||
NODE_DEBUG_NATIVE: 'SEA',
|
||||
}
|
||||
},
|
||||
{
|
||||
stdout: /execArgvExtension cli test passed/
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
|
||||
const {
|
||||
generateSEA,
|
||||
skipIfSingleExecutableIsNotSupported,
|
||||
} = require('../common/sea');
|
||||
|
||||
skipIfSingleExecutableIsNotSupported();
|
||||
|
||||
// This tests the execArgvExtension "env" mode (default) 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-extension-env.js'), tmpdir.resolve('sea.js'));
|
||||
|
||||
writeFileSync(configFile, `
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-prep.blob",
|
||||
"disableExperimentalSEAWarning": true,
|
||||
"execArgv": ["--no-warnings"],
|
||||
"execArgvExtension": "env"
|
||||
}
|
||||
`);
|
||||
|
||||
spawnSyncAndExitWithoutError(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', 'sea-config.json'],
|
||||
{ cwd: tmpdir.path });
|
||||
|
||||
assert(existsSync(seaPrepBlob));
|
||||
|
||||
generateSEA(outputFile, process.execPath, seaPrepBlob);
|
||||
|
||||
// Test that NODE_OPTIONS works with execArgvExtension: "env" (default behavior)
|
||||
spawnSyncAndAssert(
|
||||
outputFile,
|
||||
['user-arg1', 'user-arg2'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: '--max-old-space-size=512',
|
||||
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
|
||||
NODE_DEBUG_NATIVE: 'SEA',
|
||||
}
|
||||
},
|
||||
{
|
||||
stdout: /execArgvExtension env test passed/,
|
||||
stderr(output) {
|
||||
assert.doesNotMatch(output, /This warning should not be shown in the output/);
|
||||
return true;
|
||||
},
|
||||
trim: true
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
|
||||
const {
|
||||
generateSEA,
|
||||
skipIfSingleExecutableIsNotSupported,
|
||||
} = require('../common/sea');
|
||||
|
||||
skipIfSingleExecutableIsNotSupported();
|
||||
|
||||
// This tests the execArgvExtension "none" mode 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-extension-none.js'), tmpdir.resolve('sea.js'));
|
||||
|
||||
writeFileSync(configFile, `
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-prep.blob",
|
||||
"disableExperimentalSEAWarning": true,
|
||||
"execArgv": ["--no-warnings"],
|
||||
"execArgvExtension": "none"
|
||||
}
|
||||
`);
|
||||
|
||||
spawnSyncAndExitWithoutError(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', 'sea-config.json'],
|
||||
{ cwd: tmpdir.path });
|
||||
|
||||
assert(existsSync(seaPrepBlob));
|
||||
|
||||
generateSEA(outputFile, process.execPath, seaPrepBlob);
|
||||
|
||||
// Test that NODE_OPTIONS is ignored with execArgvExtension: "none"
|
||||
spawnSyncAndAssert(
|
||||
outputFile,
|
||||
['user-arg1', 'user-arg2'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: '--max-old-space-size=2048',
|
||||
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
|
||||
NODE_DEBUG_NATIVE: 'SEA',
|
||||
}
|
||||
},
|
||||
{
|
||||
stdout: /execArgvExtension none test passed/
|
||||
});
|
||||
Reference in New Issue
Block a user