Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it might be good to define "cli" in here very briefly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean defining the word "cli" as "command line interface"? I feel that for the target audience of this part of the documentation, that would be a bit superfluous - it seems unlikely that someone who is packaging an an application as a CLI SEA does not know what "cli" means.

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
Expand Down
12 changes: 11 additions & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
79 changes: 74 additions & 5 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();

// 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, original argv, and nullptr
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
// 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;
Expand All @@ -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;
};
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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()}
Expand Down
10 changes: 9 additions & 1 deletion src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/sea-exec-argv-extension-cli.js
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions test/fixtures/sea-exec-argv-extension-env.js
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions test/fixtures/sea-exec-argv-extension-none.js
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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/
});
Loading
Loading