Skip to content

Commit 541dc75

Browse files
committed
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.
1 parent 70690be commit 541dc75

10 files changed

+372
-7
lines changed

doc/api/single-executable-applications.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ The configuration currently reads the following top-level fields:
180180
"useSnapshot": false, // Default: false
181181
"useCodeCache": true, // Default: false
182182
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
183+
"execArgvExtension": "env", // Default: "env", options: "none", "env", "cli"
183184
"assets": { // Optional
184185
"a.dat": "/path/to/a.dat",
185186
"b.txt": "/path/to/b.txt"
@@ -314,6 +315,42 @@ similar to what would happen if the application is started with:
314315
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2
315316
```
316317
318+
### Execution argument extension
319+
320+
The `execArgvExtension` field controls how additional execution arguments can be
321+
provided beyond those specified in the `execArgv` field. It accepts one of three string values:
322+
323+
* `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used,
324+
and the `NODE_OPTIONS` environment variable will be ignored.
325+
* `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments.
326+
This is the default behavior to maintain backward compatibility.
327+
* `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags
328+
will be parsed as execution arguments for Node.js instead of being passed to the user script.
329+
This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable.
330+
331+
For example, with `"execArgvExtension": "cli"`:
332+
333+
```json
334+
{
335+
"main": "/path/to/bundled/script.js",
336+
"output": "/path/to/write/the/generated/blob.blob",
337+
"execArgv": ["--no-warnings"],
338+
"execArgvExtension": "cli"
339+
}
340+
```
341+
342+
The executable can be launched as:
343+
344+
```console
345+
./my-sea --node-options="--trace-exit" user-arg1 user-arg2
346+
```
347+
348+
This would be equivalent to running:
349+
350+
```console
351+
node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2
352+
```
353+
317354
## In the injected main script
318355
319356
### Single-executable application API

src/node.cc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,17 @@ static ExitCode InitializeNodeWithArgsInternal(
940940
}
941941

942942
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
943-
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
943+
bool should_parse_node_options =
944+
!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv);
945+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
946+
if (sea::IsSingleExecutable()) {
947+
sea::SeaResource sea_resource = sea::FindSingleExecutableResource();
948+
if (sea_resource.exec_argv_extension != sea::SeaExecArgvExtension::kEnv) {
949+
should_parse_node_options = false;
950+
}
951+
}
952+
#endif
953+
if (should_parse_node_options) {
944954
// NODE_OPTIONS environment variable is preferred over the file one.
945955
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
946956
!node_options.empty()) {

src/node_sea.cc

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "node_errors.h"
88
#include "node_external_reference.h"
99
#include "node_internals.h"
10+
#include "node_options.h"
1011
#include "node_snapshot_builder.h"
1112
#include "node_union_bytes.h"
1213
#include "node_v8_platform-inl.h"
@@ -86,6 +87,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
8687
uint32_t flags = static_cast<uint32_t>(sea.flags);
8788
Debug("Write SEA flags %x\n", flags);
8889
written_total += WriteArithmetic<uint32_t>(flags);
90+
91+
Debug("Write SEA resource exec argv extension %u\n",
92+
static_cast<uint8_t>(sea.exec_argv_extension));
93+
written_total +=
94+
WriteArithmetic<uint8_t>(static_cast<uint8_t>(sea.exec_argv_extension));
8995
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
9096

9197
Debug("Write SEA code path %p, size=%zu\n",
@@ -158,6 +164,11 @@ SeaResource SeaDeserializer::Read() {
158164
CHECK_EQ(magic, kMagic);
159165
SeaFlags flags(static_cast<SeaFlags>(ReadArithmetic<uint32_t>()));
160166
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
167+
168+
uint8_t extension_value = ReadArithmetic<uint8_t>();
169+
SeaExecArgvExtension exec_argv_extension =
170+
static_cast<SeaExecArgvExtension>(extension_value);
171+
Debug("Read SEA resource exec argv extension %u\n", extension_value);
161172
CHECK_EQ(read_total, SeaResource::kHeaderSize);
162173

163174
std::string_view code_path =
@@ -212,7 +223,13 @@ SeaResource SeaDeserializer::Read() {
212223
exec_argv.emplace_back(arg);
213224
}
214225
}
215-
return {flags, code_path, code, code_cache, assets, exec_argv};
226+
return {flags,
227+
exec_argv_extension,
228+
code_path,
229+
code,
230+
code_cache,
231+
assets,
232+
exec_argv};
216233
}
217234

218235
std::string_view FindSingleExecutableBlob() {
@@ -297,26 +314,55 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
297314
if (IsSingleExecutable()) {
298315
static std::vector<char*> new_argv;
299316
static std::vector<std::string> exec_argv_storage;
317+
static std::vector<std::string> cli_extension_args;
300318

301319
SeaResource sea_resource = FindSingleExecutableResource();
302320

303321
new_argv.clear();
304322
exec_argv_storage.clear();
323+
cli_extension_args.clear();
324+
325+
// Handle CLI extension mode for --node-options
326+
if (sea_resource.exec_argv_extension == SeaExecArgvExtension::kCli) {
327+
// Extract --node-options and filter argv
328+
for (int i = 1; i < argc; ++i) {
329+
if (strncmp(argv[i], "--node-options=", 15) == 0) {
330+
std::string node_options = argv[i] + 15;
331+
std::vector<std::string> errors;
332+
cli_extension_args = ParseNodeOptionsEnvVar(node_options, &errors);
333+
// Remove this argument by shifting the rest
334+
for (int j = i; j < argc - 1; ++j) {
335+
argv[j] = argv[j + 1];
336+
}
337+
argc--;
338+
i--; // Adjust index since we removed an element
339+
}
340+
}
341+
}
305342

306-
// Reserve space for argv[0], exec argv, original argv, and nullptr
307-
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
343+
// Reserve space for argv[0], exec argv, cli extension args, original argv,
344+
// and nullptr
345+
new_argv.reserve(argc + sea_resource.exec_argv.size() +
346+
cli_extension_args.size() + 2);
308347
new_argv.emplace_back(argv[0]);
309348

310349
// Insert exec argv from SEA config
311350
if (!sea_resource.exec_argv.empty()) {
312-
exec_argv_storage.reserve(sea_resource.exec_argv.size());
351+
exec_argv_storage.reserve(sea_resource.exec_argv.size() +
352+
cli_extension_args.size());
313353
for (const auto& arg : sea_resource.exec_argv) {
314354
exec_argv_storage.emplace_back(arg);
315355
new_argv.emplace_back(exec_argv_storage.back().data());
316356
}
317357
}
318358

319-
// Add actual run time arguments.
359+
// Insert CLI extension args
360+
for (const auto& arg : cli_extension_args) {
361+
exec_argv_storage.emplace_back(arg);
362+
new_argv.emplace_back(exec_argv_storage.back().data());
363+
}
364+
365+
// Add actual run time arguments
320366
new_argv.insert(new_argv.end(), argv, argv + argc);
321367
new_argv.emplace_back(nullptr);
322368
argc = new_argv.size() - 1;
@@ -332,6 +378,7 @@ struct SeaConfig {
332378
std::string main_path;
333379
std::string output_path;
334380
SeaFlags flags = SeaFlags::kDefault;
381+
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
335382
std::unordered_map<std::string, std::string> assets;
336383
std::vector<std::string> exec_argv;
337384
};
@@ -475,6 +522,27 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
475522
result.flags |= SeaFlags::kIncludeExecArgv;
476523
result.exec_argv = std::move(exec_argv);
477524
}
525+
} else if (key == "execArgvExtension") {
526+
std::string_view extension_str;
527+
if (field.value().get_string().get(extension_str)) {
528+
FPrintF(stderr,
529+
"\"execArgvExtension\" field of %s is not a string\n",
530+
config_path);
531+
return std::nullopt;
532+
}
533+
if (extension_str == "none") {
534+
result.exec_argv_extension = SeaExecArgvExtension::kNone;
535+
} else if (extension_str == "env") {
536+
result.exec_argv_extension = SeaExecArgvExtension::kEnv;
537+
} else if (extension_str == "cli") {
538+
result.exec_argv_extension = SeaExecArgvExtension::kCli;
539+
} else {
540+
FPrintF(stderr,
541+
"\"execArgvExtension\" field of %s must be one of "
542+
"\"none\", \"env\", or \"cli\"\n",
543+
config_path);
544+
return std::nullopt;
545+
}
478546
}
479547
}
480548

@@ -674,6 +742,7 @@ ExitCode GenerateSingleExecutableBlob(
674742
}
675743
SeaResource sea{
676744
config.flags,
745+
config.exec_argv_extension,
677746
config.main_path,
678747
builds_snapshot_from_main
679748
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}

src/node_sea.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,15 @@ enum class SeaFlags : uint32_t {
3131
kIncludeExecArgv = 1 << 4,
3232
};
3333

34+
enum class SeaExecArgvExtension : uint8_t {
35+
kNone = 0,
36+
kEnv = 1,
37+
kCli = 2,
38+
};
39+
3440
struct SeaResource {
3541
SeaFlags flags = SeaFlags::kDefault;
42+
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
3643
std::string_view code_path;
3744
std::string_view main_code_or_snapshot;
3845
std::optional<std::string_view> code_cache;
@@ -42,7 +49,8 @@ struct SeaResource {
4249
bool use_snapshot() const;
4350
bool use_code_cache() const;
4451

45-
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
52+
static constexpr size_t kHeaderSize =
53+
sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension);
4654
};
4755

4856
bool IsSingleExecutable();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const assert = require('assert');
2+
3+
console.log('process.argv:', JSON.stringify(process.argv));
4+
console.log('process.execArgv:', JSON.stringify(process.execArgv));
5+
6+
// Should have execArgv from SEA config + CLI --node-options
7+
assert.deepStrictEqual(process.execArgv, ['--no-warnings', '--max-old-space-size=1024']);
8+
9+
assert.deepStrictEqual(process.argv.slice(2), [
10+
'user-arg1',
11+
'user-arg2'
12+
]);
13+
14+
console.log('execArgvExtension cli test passed');
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const assert = require('assert');
2+
3+
process.emitWarning('This warning should not be shown in the output', 'TestWarning');
4+
5+
console.log('process.argv:', JSON.stringify(process.argv));
6+
console.log('process.execArgv:', JSON.stringify(process.execArgv));
7+
8+
// Should have execArgv from SEA config.
9+
// Note that flags from NODE_OPTIONS are not included in process.execArgv no matter it's
10+
// an SEA or not, but we can test whether it works by checking that the warning emitted
11+
// above was silenced.
12+
assert.deepStrictEqual(process.execArgv, ['--no-warnings']);
13+
14+
assert.deepStrictEqual(process.argv.slice(2), [
15+
'user-arg1',
16+
'user-arg2'
17+
]);
18+
19+
console.log('execArgvExtension env test passed');
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const assert = require('assert');
2+
3+
console.log('process.argv:', JSON.stringify(process.argv));
4+
console.log('process.execArgv:', JSON.stringify(process.execArgv));
5+
6+
// Should only have execArgv from SEA config, no NODE_OPTIONS
7+
assert.deepStrictEqual(process.execArgv, ['--no-warnings']);
8+
9+
assert.deepStrictEqual(process.argv.slice(2), [
10+
'user-arg1',
11+
'user-arg2'
12+
]);
13+
14+
console.log('execArgvExtension none test passed');
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const {
6+
generateSEA,
7+
skipIfSingleExecutableIsNotSupported,
8+
} = require('../common/sea');
9+
10+
skipIfSingleExecutableIsNotSupported();
11+
12+
// This tests the execArgvExtension "cli" mode in single executable applications.
13+
14+
const fixtures = require('../common/fixtures');
15+
const tmpdir = require('../common/tmpdir');
16+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
17+
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
18+
const { join } = require('path');
19+
const assert = require('assert');
20+
21+
const configFile = tmpdir.resolve('sea-config.json');
22+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
23+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
24+
25+
tmpdir.refresh();
26+
27+
// Copy test fixture to working directory
28+
copyFileSync(fixtures.path('sea-exec-argv-extension-cli.js'), tmpdir.resolve('sea.js'));
29+
30+
writeFileSync(configFile, `
31+
{
32+
"main": "sea.js",
33+
"output": "sea-prep.blob",
34+
"disableExperimentalSEAWarning": true,
35+
"execArgv": ["--no-warnings"],
36+
"execArgvExtension": "cli"
37+
}
38+
`);
39+
40+
spawnSyncAndExitWithoutError(
41+
process.execPath,
42+
['--experimental-sea-config', 'sea-config.json'],
43+
{ cwd: tmpdir.path });
44+
45+
assert(existsSync(seaPrepBlob));
46+
47+
generateSEA(outputFile, process.execPath, seaPrepBlob);
48+
49+
// Test that --node-options works with execArgvExtension: "cli"
50+
spawnSyncAndAssert(
51+
outputFile,
52+
['--node-options=--max-old-space-size=1024', 'user-arg1', 'user-arg2'],
53+
{
54+
env: {
55+
...process.env,
56+
NODE_OPTIONS: '--max-old-space-size=2048', // Should be ignored
57+
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
58+
NODE_DEBUG_NATIVE: 'SEA',
59+
}
60+
},
61+
{
62+
stdout: /execArgvExtension cli test passed/
63+
});

0 commit comments

Comments
 (0)