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
38 changes: 38 additions & 0 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
81 changes: 78 additions & 3 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -598,14 +668,19 @@ 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,
builds_snapshot_from_main
? 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);
Expand Down
2 changes: 2 additions & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum class SeaFlags : uint32_t {
kUseSnapshot = 1 << 1,
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
kIncludeExecArgv = 1 << 4,
};

struct SeaResource {
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/sea-exec-argv-empty.js
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions test/fixtures/sea-exec-argv.js
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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/
});
67 changes: 67 additions & 0 deletions test/sequential/test-single-executable-application-exec-argv.js
Original file line number Diff line number Diff line change
@@ -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,
});
Loading