| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 37d1273 commit 2bde005
16 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -594,6 +594,18 @@ added: v16.6.0 | |||
| 594 | 594 | ||
| 595 | 595 | Use this flag to disable top-level await in REPL. | |
| 596 | 596 | ||
| 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 | + | ||
| 597 | 609 | ### `--experimental-shadow-realm` | |
| 598 | 610 | ||
| 599 | 611 | <!-- YAML | |
@@ -2556,6 +2568,7 @@ done | |||
| 2556 | 2568 | [`"type"`]: packages.md#type | |
| 2557 | 2569 | [`--cpu-prof-dir`]: #--cpu-prof-dir | |
| 2558 | 2570 | [`--diagnostic-dir`]: #--diagnostic-dirdirectory | |
| 2571 | + [`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs | ||
| 2559 | 2572 | [`--experimental-wasm-modules`]: #--experimental-wasm-modules | |
| 2560 | 2573 | [`--heap-prof-dir`]: #--heap-prof-dir | |
| 2561 | 2574 | [`--import`]: #--importmodule | |
@@ -2594,6 +2607,7 @@ done | |||
| 2594 | 2607 | [scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger | |
| 2595 | 2608 | [security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure | |
| 2596 | 2609 | [semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space | |
| 2610 | + [single executable application]: single-executable-applications.md | ||
| 2597 | 2611 | [test reporters]: test.md#test-reporters | |
| 2598 | 2612 | [timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | |
| 2599 | 2613 | [tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,15 +10,17 @@ This feature allows the distribution of a Node.js application conveniently to a | |||
| 10 | 10 | system that does not have Node.js installed. | |
| 11 | 11 | ||
| 12 | 12 | 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. | ||
| 16 | 17 | ||
| 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. | ||
| 19 | 20 | ||
| 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. | ||
| 22 | 24 | ||
| 23 | 25 | Here are the steps for creating a single executable application using one such | |
| 24 | 26 | tool, [postject][]: | |
@@ -28,12 +30,24 @@ tool, [postject][]: | |||
| 28 | 30 | $ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js | |
| 29 | 31 | ``` | |
| 30 | 32 | ||
| 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: | ||
| 32 | 46 | ```console | |
| 33 | 47 | $ cp $(command -v node) hello | |
| 34 | 48 | ``` | |
| 35 | 49 | ||
| 36 | - 3. Remove the signature of the binary: | ||
| 50 | + 5. Remove the signature of the binary: | ||
| 37 | 51 | ||
| 38 | 52 | * On macOS: | |
| 39 | 53 | ||
@@ -50,35 +64,35 @@ tool, [postject][]: | |||
| 50 | 64 | $ signtool remove /s hello | |
| 51 | 65 | ``` | |
| 52 | 66 | ||
| 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 | ||
| 54 | 68 | the following options: | |
| 55 | 69 | ||
| 56 | 70 | * `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 | ||
| 61 | 75 | [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 | ||
| 64 | 78 | stored. | |
| 65 | 79 | ||
| 66 | 80 | To summarize, here is the required command for each platform: | |
| 67 | 81 | ||
| 68 | 82 | * On systems other than macOS: | |
| 69 | 83 | ```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 | ||
| 72 | 86 | ``` | |
| 73 | 87 | ||
| 74 | 88 | * On macOS: | |
| 75 | 89 | ```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 | ||
| 79 | 93 | ``` | |
| 80 | 94 | ||
| 81 | - 5. Sign the binary: | ||
| 95 | + 7. Sign the binary: | ||
| 82 | 96 | ||
| 83 | 97 | * On macOS: | |
| 84 | 98 | ||
@@ -95,12 +109,33 @@ tool, [postject][]: | |||
| 95 | 109 | $ signtool sign /fd SHA256 hello | |
| 96 | 110 | ``` | |
| 97 | 111 | ||
| 98 | - 6. Run the binary: | ||
| 112 | + 8. Run the binary: | ||
| 99 | 113 | ```console | |
| 100 | 114 | $ ./hello world | |
| 101 | 115 | Hello, world! | |
| 102 | 116 | ``` | |
| 103 | 117 | ||
| 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 | + | ||
| 104 | 139 | ## Notes | |
| 105 | 140 | ||
| 106 | 141 | ### `require(id)` in the injected module is not file based | |
@@ -135,15 +170,16 @@ of [`process.execPath`][]. | |||
| 135 | 170 | ### Single executable application creation process | |
| 136 | 171 | ||
| 137 | 172 | 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: | ||
| 139 | 175 | ||
| 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 | ||
| 142 | 178 | 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 | ||
| 144 | 180 | ||
| 145 | 181 | 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 | ||
| 147 | 183 | last character to `1` to indicate that a resource has been injected. | |
| 148 | 184 | ||
| 149 | 185 | ### Platform support | |
@@ -165,6 +201,7 @@ to help us document them. | |||
| 165 | 201 | ||
| 166 | 202 | [CommonJS]: modules.md#modules-commonjs-modules | |
| 167 | 203 | [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format | |
| 204 | + [Generating single executable preparation blobs]: #generating-single-executable-preparation-blobs | ||
| 168 | 205 | [Mach-O]: https://en.wikipedia.org/wiki/Mach-O | |
| 169 | 206 | [PE]: https://en.wikipedia.org/wiki/Portable_Executable | |
| 170 | 207 | [Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -66,7 +66,7 @@ To disable single executable application support, build Node.js with the | |||
| 66 | 66 | ## Implementation | |
| 67 | 67 | ||
| 68 | 68 | 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 | ||
| 70 | 70 | binary. If it is found, it passes the buffer to | |
| 71 | 71 | [`single_executable_application.js`][], which executes the contents of the | |
| 72 | 72 | embedded script. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -82,6 +82,8 @@ | |||
| 82 | 82 | 'src/js_stream.cc', | |
| 83 | 83 | 'src/json_utils.cc', | |
| 84 | 84 | 'src/js_udp_wrap.cc', | |
| 85 | + 'src/json_parser.h', | ||
| 86 | + 'src/json_parser.cc', | ||
| 85 | 87 | 'src/module_wrap.cc', | |
| 86 | 88 | 'src/node.cc', | |
| 87 | 89 | 'src/node_api.cc', | |
| Original file line number | Diff line number | Diff 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 | ||
| Original file line number | Diff line number | Diff 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_ | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1239,6 +1239,11 @@ static ExitCode StartInternal(int argc, char** argv) { | |||
| 1239 | 1239 | ||
| 1240 | 1240 | uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME); | |
| 1241 | 1241 | ||
| 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 | + | ||
| 1242 | 1247 | // --build-snapshot indicates that we are in snapshot building mode. | |
| 1243 | 1248 | if (per_process::cli_options->per_isolate->build_snapshot) { | |
| 1244 | 1249 | if (result->args().size() < 2) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -246,14 +246,19 @@ void PrintStackTrace(Isolate* isolate, Local<StackTrace> stack) { | |||
| 246 | 246 | std::string FormatCaughtException(Isolate* isolate, | |
| 247 | 247 | Local<Context> context, | |
| 248 | 248 | Local<Value> err, | |
| 249 | - Local<Message> message) { | ||
| 249 | + Local<Message> message, | ||
| 250 | + bool add_source_line = true) { | ||
| 251 | + std::string result; | ||
| 250 | 252 | node::Utf8Value reason(isolate, | |
| 251 | 253 | err->ToDetailString(context) | |
| 252 | 254 | .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'; | ||
| 257 | 262 | ||
| 258 | 263 | Local<v8::StackTrace> stack = message->GetStackTrace(); | |
| 259 | 264 | if (!stack.IsEmpty()) result += FormatStackTrace(isolate, stack); | |
@@ -1209,6 +1214,19 @@ void TriggerUncaughtException(Isolate* isolate, const v8::TryCatch& try_catch) { | |||
| 1209 | 1214 | false /* from_promise */); | |
| 1210 | 1215 | } | |
| 1211 | 1216 | ||
| 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 | + | ||
| 1212 | 1230 | } // namespace errors | |
| 1213 | 1231 | ||
| 1214 | 1232 | } // namespace node | |
| Back | FazBrowse Home | New Git URL |
0 commit comments