Skip to content

Commit a2021a2

Browse files
committed
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.
1 parent 91dadf2 commit a2021a2

File tree

9 files changed

+303
-3
lines changed

9 files changed

+303
-3
lines changed

doc/api/single-executable-applications.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ The configuration currently reads the following top-level fields:
179179
"disableExperimentalSEAWarning": true, // Default: false
180180
"useSnapshot": false, // Default: false
181181
"useCodeCache": true, // Default: false
182+
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
182183
"assets": { // Optional
183184
"a.dat": "/path/to/a.dat",
184185
"b.txt": "/path/to/b.txt"
@@ -276,6 +277,43 @@ execute the script, which would improve the startup performance.
276277
277278
**Note:** `import()` does not work when `useCodeCache` is `true`.
278279
280+
### Execution arguments
281+
282+
The `execArgv` field can be used to specify Node.js-specific
283+
arguments that will be automatically applied when the single
284+
executable application starts. This allows application developers
285+
to configure Node.js runtime options without requiring end users
286+
to be aware of these flags.
287+
288+
For example, the following configuration:
289+
290+
```json
291+
{
292+
"main": "/path/to/bundled/script.js",
293+
"output": "/path/to/write/the/generated/blob.blob",
294+
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
295+
}
296+
```
297+
298+
will instruct the SEA to be launched with the `--no-warnings` and
299+
`--max-old-space-size=2048` flags. In the scripts embedded in the executable, these flags
300+
can be accessed using the `process.execArgv` property:
301+
302+
```js
303+
// If the executable is launched with `sea user-arg1 user-arg2`
304+
console.log(process.execArgv);
305+
// Prints: ['--no-warnings', '--max-old-space-size=2048']
306+
console.log(process.argv);
307+
// Prints ['/path/to/sea', 'path/to/sea', 'user-arg1', 'user-arg2']
308+
```
309+
310+
The user-provided arguments are in the `process.argv` array starting from index 2,
311+
similar to what would happen if the application is started with:
312+
313+
```console
314+
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2
315+
```
316+
279317
## In the injected main script
280318
281319
### Single-executable application API

src/json_parser.cc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,46 @@ std::optional<JSONParser::StringDict> JSONParser::GetTopLevelStringDict(
154154
return result;
155155
}
156156

157+
std::optional<std::vector<std::string>> JSONParser::GetTopLevelStringList(
158+
std::string_view field) {
159+
Isolate* isolate = isolate_.get();
160+
v8::Locker locker(isolate);
161+
v8::Isolate::Scope isolate_scope(isolate);
162+
v8::HandleScope handle_scope(isolate);
163+
Local<Context> context = context_.Get(isolate);
164+
Context::Scope context_scope(context);
165+
Local<Object> content_object = content_.Get(isolate);
166+
Local<Value> value;
167+
bool has_field;
168+
// It's not a real script, so don't print the source line.
169+
errors::PrinterTryCatch bootstrapCatch(
170+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
171+
Local<Value> field_local;
172+
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
173+
return std::nullopt;
174+
}
175+
if (!content_object->Has(context, field_local).To(&has_field)) {
176+
return std::nullopt;
177+
}
178+
if (!has_field) {
179+
return std::vector<std::string>();
180+
}
181+
if (!content_object->Get(context, field_local).ToLocal(&value) ||
182+
!value->IsArray()) {
183+
return std::nullopt;
184+
}
185+
Local<Array> array = value.As<Array>();
186+
std::vector<std::string> result;
187+
uint32_t length = array->Length();
188+
for (uint32_t i = 0; i < length; ++i) {
189+
Local<Value> element;
190+
if (!array->Get(context, i).ToLocal(&element) || !element->IsString()) {
191+
return std::nullopt;
192+
}
193+
Utf8Value element_utf8(isolate, element);
194+
result.emplace_back(element_utf8.ToString());
195+
}
196+
return result;
197+
}
198+
157199
} // namespace node

src/json_parser.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <optional>
88
#include <string>
99
#include <unordered_map>
10+
#include <vector>
1011
#include "util.h"
1112
#include "v8.h"
1213

@@ -23,6 +24,8 @@ class JSONParser {
2324
std::optional<std::string> GetTopLevelStringField(std::string_view field);
2425
std::optional<bool> GetTopLevelBoolField(std::string_view field);
2526
std::optional<StringDict> GetTopLevelStringDict(std::string_view field);
27+
std::optional<std::vector<std::string>> GetTopLevelStringList(
28+
std::string_view field);
2629

2730
private:
2831
// We might want a lighter-weight JSON parser for this use case. But for now

src/node_sea.cc

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
123123
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
124124
}
125125
}
126+
127+
if (static_cast<bool>(sea.flags & SeaFlags::kIncludeExecArgv)) {
128+
Debug("Write SEA resource exec argv size %zu\n", sea.exec_argv.size());
129+
written_total += WriteArithmetic<size_t>(sea.exec_argv.size());
130+
for (const auto& arg : sea.exec_argv) {
131+
Debug("Write SEA resource exec arg %s at %p, size=%zu\n",
132+
arg.data(),
133+
arg.data(),
134+
arg.size());
135+
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
136+
}
137+
}
126138
return written_total;
127139
}
128140

@@ -185,7 +197,22 @@ SeaResource SeaDeserializer::Read() {
185197
assets.emplace(key, content);
186198
}
187199
}
188-
return {flags, code_path, code, code_cache, assets};
200+
201+
std::vector<std::string_view> exec_argv;
202+
if (static_cast<bool>(flags & SeaFlags::kIncludeExecArgv)) {
203+
size_t exec_argv_size = ReadArithmetic<size_t>();
204+
Debug("Read SEA resource exec args size %zu\n", exec_argv_size);
205+
exec_argv.reserve(exec_argv_size);
206+
for (size_t i = 0; i < exec_argv_size; ++i) {
207+
std::string_view arg = ReadStringView(StringLogMode::kAddressAndContent);
208+
Debug("Read SEA resource exec arg %s at %p, size=%zu\n",
209+
arg.data(),
210+
arg.data(),
211+
arg.size());
212+
exec_argv.emplace_back(arg);
213+
}
214+
}
215+
return {flags, code_path, code, code_cache, assets, exec_argv};
189216
}
190217

191218
std::string_view FindSingleExecutableBlob() {
@@ -269,8 +296,27 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
269296
// entry point file path.
270297
if (IsSingleExecutable()) {
271298
static std::vector<char*> new_argv;
272-
new_argv.reserve(argc + 2);
299+
static std::vector<std::string> exec_argv_storage;
300+
301+
SeaResource sea_resource = FindSingleExecutableResource();
302+
303+
new_argv.clear();
304+
exec_argv_storage.clear();
305+
306+
// Reserve space for argv[0], exec argv, original argv, and nullptr
307+
new_argv.reserve(argc + sea_resource.exec_argv.size() + 2);
273308
new_argv.emplace_back(argv[0]);
309+
310+
// Insert exec argv from SEA config
311+
if (!sea_resource.exec_argv.empty()) {
312+
exec_argv_storage.reserve(sea_resource.exec_argv.size());
313+
for (const auto& arg : sea_resource.exec_argv) {
314+
exec_argv_storage.emplace_back(arg);
315+
new_argv.emplace_back(exec_argv_storage.back().data());
316+
}
317+
}
318+
319+
// Add actual run time arguments.
274320
new_argv.insert(new_argv.end(), argv, argv + argc);
275321
new_argv.emplace_back(nullptr);
276322
argc = new_argv.size() - 1;
@@ -287,6 +333,7 @@ struct SeaConfig {
287333
std::string output_path;
288334
SeaFlags flags = SeaFlags::kDefault;
289335
std::unordered_map<std::string, std::string> assets;
336+
std::vector<std::string> exec_argv;
290337
};
291338

292339
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -378,6 +425,17 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
378425
result.assets = std::move(assets_opt.value());
379426
}
380427

428+
auto exec_argv_opt = parser.GetTopLevelStringList("execArgv");
429+
if (!exec_argv_opt.has_value()) {
430+
FPrintF(stderr,
431+
"\"execArgv\" field of %s is not an array of strings\n",
432+
config_path);
433+
return std::nullopt;
434+
} else if (!exec_argv_opt.value().empty()) {
435+
result.flags |= SeaFlags::kIncludeExecArgv;
436+
result.exec_argv = std::move(exec_argv_opt.value());
437+
}
438+
381439
return result;
382440
}
383441

@@ -546,14 +604,19 @@ ExitCode GenerateSingleExecutableBlob(
546604
for (auto const& [key, content] : assets) {
547605
assets_view.emplace(key, content);
548606
}
607+
std::vector<std::string_view> exec_argv_view;
608+
for (const auto& arg : config.exec_argv) {
609+
exec_argv_view.emplace_back(arg);
610+
}
549611
SeaResource sea{
550612
config.flags,
551613
config.main_path,
552614
builds_snapshot_from_main
553615
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
554616
: std::string_view{main_script.data(), main_script.size()},
555617
optional_sv_code_cache,
556-
assets_view};
618+
assets_view,
619+
exec_argv_view};
557620

558621
SeaSerializer serializer;
559622
serializer.Write(sea);

src/node_sea.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum class SeaFlags : uint32_t {
2828
kUseSnapshot = 1 << 1,
2929
kUseCodeCache = 1 << 2,
3030
kIncludeAssets = 1 << 3,
31+
kIncludeExecArgv = 1 << 4,
3132
};
3233

3334
struct SeaResource {
@@ -36,6 +37,7 @@ struct SeaResource {
3637
std::string_view main_code_or_snapshot;
3738
std::optional<std::string_view> code_cache;
3839
std::unordered_map<std::string_view, std::string_view> assets;
40+
std::vector<std::string_view> exec_argv;
3941

4042
bool use_snapshot() const;
4143
bool use_code_cache() const;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const assert = require('assert');
2+
3+
console.log('process.argv:', JSON.stringify(process.argv));
4+
assert.strictEqual(process.argv[2], 'user-arg');
5+
assert.deepStrictEqual(process.execArgv, []);
6+
console.log('empty execArgv test passed');

test/fixtures/sea-exec-argv.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
assert.deepStrictEqual(process.execArgv, [ '--no-warnings', '--max-old-space-size=2048' ]);
9+
10+
// We start from 2, because in SEA, the index 1 would be the same as the execPath
11+
// to accommodate the general expectation that index 1 is the path to script for
12+
// applications.
13+
assert.deepStrictEqual(process.argv.slice(2), [
14+
'user-arg1',
15+
'user-arg2'
16+
]);
17+
18+
console.log('multiple execArgv test passed');
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 execArgv functionality with empty array 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-empty.js'), tmpdir.resolve('sea.js'));
29+
30+
writeFileSync(configFile, `
31+
{
32+
"main": "sea.js",
33+
"output": "sea-prep.blob",
34+
"disableExperimentalSEAWarning": true,
35+
"execArgv": []
36+
}
37+
`);
38+
39+
spawnSyncAndExitWithoutError(
40+
process.execPath,
41+
['--experimental-sea-config', 'sea-config.json'],
42+
{ cwd: tmpdir.path });
43+
44+
assert(existsSync(seaPrepBlob));
45+
46+
generateSEA(outputFile, process.execPath, seaPrepBlob);
47+
48+
// Test that empty execArgv work correctly
49+
spawnSyncAndAssert(
50+
outputFile,
51+
['user-arg'],
52+
{
53+
env: {
54+
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
55+
NODE_DEBUG_NATIVE: 'SEA',
56+
...process.env,
57+
}
58+
},
59+
{
60+
stdout: /empty execArgv test passed/
61+
});

0 commit comments

Comments
 (0)