Skip to content

Commit 2bde005

Browse files
joyeecheungRafaelGSS
authored andcommitted
sea: use JSON configuration and blob content for SEA
PR-URL: #47125 Refs: nodejs/single-executable#58 Reviewed-By: Darshan Sen <[email protected]>
1 parent 37d1273 commit 2bde005

16 files changed

+639
-45
lines changed

doc/api/cli.md

+14
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,18 @@ added: v16.6.0
594594

595595
Use this flag to disable top-level await in REPL.
596596

597+
### `--experimental-sea-config`
598+
599+
<!-- YAML
600+
added: REPLACEME
601+
-->
602+
603+
> Stability: 1 - Experimental
604+
605+
Use this flag to generate a blob that can be injected into the Node.js
606+
binary to produce a [single executable application][]. See the documentation
607+
about [this configuration][`--experimental-sea-config`] for details.
608+
597609
### `--experimental-shadow-realm`
598610

599611
<!-- YAML
@@ -2556,6 +2568,7 @@ done
25562568
[`"type"`]: packages.md#type
25572569
[`--cpu-prof-dir`]: #--cpu-prof-dir
25582570
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
2571+
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
25592572
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
25602573
[`--heap-prof-dir`]: #--heap-prof-dir
25612574
[`--import`]: #--importmodule
@@ -2594,6 +2607,7 @@ done
25942607
[scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger
25952608
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
25962609
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
2610+
[single executable application]: single-executable-applications.md
25972611
[test reporters]: test.md#test-reporters
25982612
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
25992613
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014

doc/api/single-executable-applications.md

+65-28
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ This feature allows the distribution of a Node.js application conveniently to a
1010
system that does not have Node.js installed.
1111

1212
Node.js supports the creation of [single executable applications][] by allowing
13-
the injection of a JavaScript file into the `node` binary. During start up, the
14-
program checks if anything has been injected. If the script is found, it
15-
executes its contents. Otherwise Node.js operates as it normally does.
13+
the injection of a blob prepared by Node.js, which can contain a bundled script,
14+
into the `node` binary. During start up, the program checks if anything has been
15+
injected. If the blob is found, it executes the script in the blob. Otherwise
16+
Node.js operates as it normally does.
1617

17-
The single executable application feature only supports running a single
18-
embedded [CommonJS][] file.
18+
The single executable application feature currently only supports running a
19+
single embedded script using the [CommonJS][] module system.
1920

20-
A bundled JavaScript file can be turned into a single executable application
21-
with any tool which can inject resources into the `node` binary.
21+
Users can create a single executable application from their bundled script
22+
with the `node` binary itself and any tool which can inject resources into the
23+
binary.
2224

2325
Here are the steps for creating a single executable application using one such
2426
tool, [postject][]:
@@ -28,12 +30,24 @@ tool, [postject][]:
2830
$ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
2931
```
3032

31-
2. Create a copy of the `node` executable and name it according to your needs:
33+
2. Create a configuration file building a blob that can be injected into the
34+
single executable application (see
35+
[Generating single executable preparation blobs][] for details):
36+
```console
37+
$ echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json
38+
```
39+
40+
3. Generate the blob to be injected:
41+
```console
42+
$ node --experimental-sea-config sea-config.json
43+
```
44+
45+
4. Create a copy of the `node` executable and name it according to your needs:
3246
```console
3347
$ cp $(command -v node) hello
3448
```
3549

36-
3. Remove the signature of the binary:
50+
5. Remove the signature of the binary:
3751

3852
* On macOS:
3953

@@ -50,35 +64,35 @@ tool, [postject][]:
5064
$ signtool remove /s hello
5165
```
5266

53-
4. Inject the JavaScript file into the copied binary by running `postject` with
67+
6. Inject the blob into the copied binary by running `postject` with
5468
the following options:
5569

5670
* `hello` - The name of the copy of the `node` executable created in step 2.
57-
* `NODE_JS_CODE` - The name of the resource / note / section in the binary
58-
where the contents of the JavaScript file will be stored.
59-
* `hello.js` - The name of the JavaScript file created in step 1.
60-
* `--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
71+
* `NODE_SEA_BLOB` - The name of the resource / note / section in the binary
72+
where the contents of the blob will be stored.
73+
* `sea-prep.blob` - The name of the blob created in step 1.
74+
* `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
6175
[fuse][] used by the Node.js project to detect if a file has been injected.
62-
* `--macho-segment-name NODE_JS` (only needed on macOS) - The name of the
63-
segment in the binary where the contents of the JavaScript file will be
76+
* `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the
77+
segment in the binary where the contents of the blob will be
6478
stored.
6579

6680
To summarize, here is the required command for each platform:
6781

6882
* On systems other than macOS:
6983
```console
70-
$ npx postject hello NODE_JS_CODE hello.js \
71-
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2
84+
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
85+
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
7286
```
7387

7488
* On macOS:
7589
```console
76-
$ npx postject hello NODE_JS_CODE hello.js \
77-
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
78-
--macho-segment-name NODE_JS
90+
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
91+
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
92+
--macho-segment-name NODE_SEA
7993
```
8094

81-
5. Sign the binary:
95+
7. Sign the binary:
8296

8397
* On macOS:
8498

@@ -95,12 +109,33 @@ tool, [postject][]:
95109
$ signtool sign /fd SHA256 hello
96110
```
97111

98-
6. Run the binary:
112+
8. Run the binary:
99113
```console
100114
$ ./hello world
101115
Hello, world!
102116
```
103117

118+
## Generating single executable preparation blobs
119+
120+
Single executable preparation blobs that are injected into the application can
121+
be generated using the `--experimental-sea-config` flag of the Node.js binary
122+
that will be used to build the single executable. It takes a path to a
123+
configuration file in JSON format. If the path passed to it isn't absolute,
124+
Node.js will use the path relative to the current working directory.
125+
126+
The configuration currently reads the following top-level fields:
127+
128+
```json
129+
{
130+
"main": "/path/to/bundled/script.js",
131+
"output": "/path/to/write/the/generated/blob.blob"
132+
}
133+
```
134+
135+
If the paths are not absolute, Node.js will use the path relative to the
136+
current working directory. The version of the Node.js binary used to produce
137+
the blob must be the same as the one to which the blob will be injected.
138+
104139
## Notes
105140

106141
### `require(id)` in the injected module is not file based
@@ -135,15 +170,16 @@ of [`process.execPath`][].
135170
### Single executable application creation process
136171

137172
A tool aiming to create a single executable Node.js application must
138-
inject the contents of a JavaScript file into:
173+
inject the contents of the blob prepared with `--experimental-sea-config"`
174+
into:
139175

140-
* a resource named `NODE_JS_CODE` if the `node` binary is a [PE][] file
141-
* a section named `NODE_JS_CODE` in the `NODE_JS` segment if the `node` binary
176+
* a resource named `NODE_SEA_BLOB` if the `node` binary is a [PE][] file
177+
* a section named `NODE_SEA_BLOB` in the `NODE_SEA` segment if the `node` binary
142178
is a [Mach-O][] file
143-
* a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file
179+
* a note named `NODE_SEA_BLOB` if the `node` binary is an [ELF][] file
144180

145181
Search the binary for the
146-
`NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
182+
`NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
147183
last character to `1` to indicate that a resource has been injected.
148184

149185
### Platform support
@@ -165,6 +201,7 @@ to help us document them.
165201

166202
[CommonJS]: modules.md#modules-commonjs-modules
167203
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
204+
[Generating single executable preparation blobs]: #generating-single-executable-preparation-blobs
168205
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
169206
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
170207
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/

doc/contributing/maintaining-single-executable-application-support.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ To disable single executable application support, build Node.js with the
6666
## Implementation
6767

6868
When built with single executable application support, the Node.js process uses
69-
[`postject-api.h`][] to check if the `NODE_JS_CODE` section exists in the
69+
[`postject-api.h`][] to check if the `NODE_SEA_BLOB` section exists in the
7070
binary. If it is found, it passes the buffer to
7171
[`single_executable_application.js`][], which executes the contents of the
7272
embedded script.

node.gyp

+2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
'src/js_stream.cc',
8383
'src/json_utils.cc',
8484
'src/js_udp_wrap.cc',
85+
'src/json_parser.h',
86+
'src/json_parser.cc',
8587
'src/module_wrap.cc',
8688
'src/node.cc',
8789
'src/node_api.cc',

src/json_parser.cc

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "json_parser.h"
2+
#include "node_errors.h"
3+
#include "node_v8_platform-inl.h"
4+
#include "util-inl.h"
5+
6+
namespace node {
7+
using v8::ArrayBuffer;
8+
using v8::Context;
9+
using v8::Isolate;
10+
using v8::Local;
11+
using v8::Object;
12+
using v8::String;
13+
using v8::Value;
14+
15+
static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) {
16+
Isolate* isolate = Isolate::Allocate();
17+
CHECK_NOT_NULL(isolate);
18+
per_process::v8_platform.Platform()->RegisterIsolate(isolate,
19+
uv_default_loop());
20+
Isolate::CreateParams params;
21+
params.array_buffer_allocator = allocator;
22+
Isolate::Initialize(isolate, params);
23+
return isolate;
24+
}
25+
26+
void JSONParser::FreeIsolate(Isolate* isolate) {
27+
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
28+
isolate->Dispose();
29+
}
30+
31+
JSONParser::JSONParser()
32+
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
33+
isolate_(NewIsolate(allocator_.get())),
34+
handle_scope_(isolate_.get()),
35+
context_(isolate_.get(), Context::New(isolate_.get())),
36+
context_scope_(context_.Get(isolate_.get())) {}
37+
38+
bool JSONParser::Parse(const std::string& content) {
39+
DCHECK(!parsed_);
40+
41+
Isolate* isolate = isolate_.get();
42+
Local<Context> context = context_.Get(isolate);
43+
44+
// It's not a real script, so don't print the source line.
45+
errors::PrinterTryCatch bootstrapCatch(
46+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
47+
Local<Value> json_string_value;
48+
Local<Value> result_value;
49+
if (!ToV8Value(context, content).ToLocal(&json_string_value) ||
50+
!json_string_value->IsString() ||
51+
!v8::JSON::Parse(context, json_string_value.As<String>())
52+
.ToLocal(&result_value) ||
53+
!result_value->IsObject()) {
54+
return false;
55+
}
56+
content_.Reset(isolate, result_value.As<Object>());
57+
parsed_ = true;
58+
return true;
59+
}
60+
61+
std::optional<std::string> JSONParser::GetTopLevelField(
62+
const std::string& field) {
63+
Isolate* isolate = isolate_.get();
64+
Local<Context> context = context_.Get(isolate);
65+
Local<Object> content_object = content_.Get(isolate);
66+
Local<Value> value;
67+
// It's not a real script, so don't print the source line.
68+
errors::PrinterTryCatch bootstrapCatch(
69+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
70+
if (!content_object
71+
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
72+
.ToLocal(&value) ||
73+
!value->IsString()) {
74+
return {};
75+
}
76+
Utf8Value utf8_value(isolate, value);
77+
return utf8_value.ToString();
78+
}
79+
80+
} // namespace node

src/json_parser.h

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#ifndef SRC_JSON_PARSER_H_
2+
#define SRC_JSON_PARSER_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include <memory>
7+
#include <optional>
8+
#include <string>
9+
#include "util.h"
10+
#include "v8.h"
11+
12+
namespace node {
13+
// This is intended to be used to get some top-level fields out of a JSON
14+
// without having to spin up a full Node.js environment that unnecessarily
15+
// complicates things.
16+
class JSONParser {
17+
public:
18+
JSONParser();
19+
~JSONParser() {}
20+
bool Parse(const std::string& content);
21+
std::optional<std::string> GetTopLevelField(const std::string& field);
22+
23+
private:
24+
// We might want a lighter-weight JSON parser for this use case. But for now
25+
// using V8 is good enough.
26+
static void FreeIsolate(v8::Isolate* isolate);
27+
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
28+
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
29+
v8::HandleScope handle_scope_;
30+
v8::Global<v8::Context> context_;
31+
v8::Context::Scope context_scope_;
32+
v8::Global<v8::Object> content_;
33+
bool parsed_ = false;
34+
};
35+
} // namespace node
36+
37+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
38+
39+
#endif // SRC_JSON_PARSER_H_

src/node.cc

+5
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,11 @@ static ExitCode StartInternal(int argc, char** argv) {
12391239

12401240
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
12411241

1242+
std::string sea_config = per_process::cli_options->experimental_sea_config;
1243+
if (!sea_config.empty()) {
1244+
return sea::BuildSingleExecutableBlob(sea_config);
1245+
}
1246+
12421247
// --build-snapshot indicates that we are in snapshot building mode.
12431248
if (per_process::cli_options->per_isolate->build_snapshot) {
12441249
if (result->args().size() < 2) {

src/node_errors.cc

+23-5
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,19 @@ void PrintStackTrace(Isolate* isolate, Local<StackTrace> stack) {
246246
std::string FormatCaughtException(Isolate* isolate,
247247
Local<Context> context,
248248
Local<Value> err,
249-
Local<Message> message) {
249+
Local<Message> message,
250+
bool add_source_line = true) {
251+
std::string result;
250252
node::Utf8Value reason(isolate,
251253
err->ToDetailString(context)
252254
.FromMaybe(Local<String>()));
253-
bool added_exception_line = false;
254-
std::string source =
255-
GetErrorSource(isolate, context, message, &added_exception_line);
256-
std::string result = source + '\n' + reason.ToString() + '\n';
255+
if (add_source_line) {
256+
bool added_exception_line = false;
257+
std::string source =
258+
GetErrorSource(isolate, context, message, &added_exception_line);
259+
result = source + '\n';
260+
}
261+
result += reason.ToString() + '\n';
257262

258263
Local<v8::StackTrace> stack = message->GetStackTrace();
259264
if (!stack.IsEmpty()) result += FormatStackTrace(isolate, stack);
@@ -1209,6 +1214,19 @@ void TriggerUncaughtException(Isolate* isolate, const v8::TryCatch& try_catch) {
12091214
false /* from_promise */);
12101215
}
12111216

1217+
PrinterTryCatch::~PrinterTryCatch() {
1218+
if (!HasCaught()) {
1219+
return;
1220+
}
1221+
std::string str =
1222+
FormatCaughtException(isolate_,
1223+
isolate_->GetCurrentContext(),
1224+
Exception(),
1225+
Message(),
1226+
print_source_line_ == kPrintSourceLine);
1227+
PrintToStderrAndFlush(str);
1228+
}
1229+
12121230
} // namespace errors
12131231

12141232
} // namespace node

0 commit comments

Comments
 (0)