mirror of
https://github.com/nodejs/node.git
synced 2025-12-28 07:50:41 +00:00
sea: use JSON configuration and blob content for SEA
PR-URL: https://github.com/nodejs/node/pull/47125 Refs: https://github.com/nodejs/single-executable/discussions/58 Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
parent
cfb654cc31
commit
491a5c968f
@ -594,6 +594,18 @@ added: v16.6.0
|
||||
|
||||
Use this flag to disable top-level await in REPL.
|
||||
|
||||
### `--experimental-sea-config`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Use this flag to generate a blob that can be injected into the Node.js
|
||||
binary to produce a [single executable application][]. See the documentation
|
||||
about [this configuration][`--experimental-sea-config`] for details.
|
||||
|
||||
### `--experimental-shadow-realm`
|
||||
|
||||
<!-- YAML
|
||||
@ -2556,6 +2568,7 @@ done
|
||||
[`"type"`]: packages.md#type
|
||||
[`--cpu-prof-dir`]: #--cpu-prof-dir
|
||||
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
|
||||
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
|
||||
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
|
||||
[`--heap-prof-dir`]: #--heap-prof-dir
|
||||
[`--import`]: #--importmodule
|
||||
@ -2594,6 +2607,7 @@ done
|
||||
[scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger
|
||||
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
|
||||
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
|
||||
[single executable application]: single-executable-applications.md
|
||||
[test reporters]: test.md#test-reporters
|
||||
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014
|
||||
|
||||
@ -10,15 +10,17 @@ This feature allows the distribution of a Node.js application conveniently to a
|
||||
system that does not have Node.js installed.
|
||||
|
||||
Node.js supports the creation of [single executable applications][] by allowing
|
||||
the injection of a JavaScript file into the `node` binary. During start up, the
|
||||
program checks if anything has been injected. If the script is found, it
|
||||
executes its contents. Otherwise Node.js operates as it normally does.
|
||||
the injection of a blob prepared by Node.js, which can contain a bundled script,
|
||||
into the `node` binary. During start up, the program checks if anything has been
|
||||
injected. If the blob is found, it executes the script in the blob. Otherwise
|
||||
Node.js operates as it normally does.
|
||||
|
||||
The single executable application feature only supports running a single
|
||||
embedded [CommonJS][] file.
|
||||
The single executable application feature currently only supports running a
|
||||
single embedded script using the [CommonJS][] module system.
|
||||
|
||||
A bundled JavaScript file can be turned into a single executable application
|
||||
with any tool which can inject resources into the `node` binary.
|
||||
Users can create a single executable application from their bundled script
|
||||
with the `node` binary itself and any tool which can inject resources into the
|
||||
binary.
|
||||
|
||||
Here are the steps for creating a single executable application using one such
|
||||
tool, [postject][]:
|
||||
@ -28,12 +30,24 @@ tool, [postject][]:
|
||||
$ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
|
||||
```
|
||||
|
||||
2. Create a copy of the `node` executable and name it according to your needs:
|
||||
2. Create a configuration file building a blob that can be injected into the
|
||||
single executable application (see
|
||||
[Generating single executable preparation blobs][] for details):
|
||||
```console
|
||||
$ echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json
|
||||
```
|
||||
|
||||
3. Generate the blob to be injected:
|
||||
```console
|
||||
$ node --experimental-sea-config sea-config.json
|
||||
```
|
||||
|
||||
4. Create a copy of the `node` executable and name it according to your needs:
|
||||
```console
|
||||
$ cp $(command -v node) hello
|
||||
```
|
||||
|
||||
3. Remove the signature of the binary:
|
||||
5. Remove the signature of the binary:
|
||||
|
||||
* On macOS:
|
||||
|
||||
@ -50,35 +64,35 @@ tool, [postject][]:
|
||||
$ signtool remove /s hello
|
||||
```
|
||||
|
||||
4. Inject the JavaScript file into the copied binary by running `postject` with
|
||||
6. Inject the blob into the copied binary by running `postject` with
|
||||
the following options:
|
||||
|
||||
* `hello` - The name of the copy of the `node` executable created in step 2.
|
||||
* `NODE_JS_CODE` - The name of the resource / note / section in the binary
|
||||
where the contents of the JavaScript file will be stored.
|
||||
* `hello.js` - The name of the JavaScript file created in step 1.
|
||||
* `--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
|
||||
* `NODE_SEA_BLOB` - The name of the resource / note / section in the binary
|
||||
where the contents of the blob will be stored.
|
||||
* `sea-prep.blob` - The name of the blob created in step 1.
|
||||
* `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
|
||||
[fuse][] used by the Node.js project to detect if a file has been injected.
|
||||
* `--macho-segment-name NODE_JS` (only needed on macOS) - The name of the
|
||||
segment in the binary where the contents of the JavaScript file will be
|
||||
* `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the
|
||||
segment in the binary where the contents of the blob will be
|
||||
stored.
|
||||
|
||||
To summarize, here is the required command for each platform:
|
||||
|
||||
* On systems other than macOS:
|
||||
```console
|
||||
$ npx postject hello NODE_JS_CODE hello.js \
|
||||
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2
|
||||
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
|
||||
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
|
||||
```
|
||||
|
||||
* On macOS:
|
||||
```console
|
||||
$ npx postject hello NODE_JS_CODE hello.js \
|
||||
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
|
||||
--macho-segment-name NODE_JS
|
||||
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
|
||||
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
|
||||
--macho-segment-name NODE_SEA
|
||||
```
|
||||
|
||||
5. Sign the binary:
|
||||
7. Sign the binary:
|
||||
|
||||
* On macOS:
|
||||
|
||||
@ -95,12 +109,33 @@ tool, [postject][]:
|
||||
$ signtool sign /fd SHA256 hello
|
||||
```
|
||||
|
||||
6. Run the binary:
|
||||
8. Run the binary:
|
||||
```console
|
||||
$ ./hello world
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
## Generating single executable preparation blobs
|
||||
|
||||
Single executable preparation blobs that are injected into the application can
|
||||
be generated using the `--experimental-sea-config` flag of the Node.js binary
|
||||
that will be used to build the single executable. It takes a path to a
|
||||
configuration file in JSON format. If the path passed to it isn't absolute,
|
||||
Node.js will use the path relative to the current working directory.
|
||||
|
||||
The configuration currently reads the following top-level fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"main": "/path/to/bundled/script.js",
|
||||
"output": "/path/to/write/the/generated/blob.blob"
|
||||
}
|
||||
```
|
||||
|
||||
If the paths are not absolute, Node.js will use the path relative to the
|
||||
current working directory. The version of the Node.js binary used to produce
|
||||
the blob must be the same as the one to which the blob will be injected.
|
||||
|
||||
## Notes
|
||||
|
||||
### `require(id)` in the injected module is not file based
|
||||
@ -135,15 +170,16 @@ of [`process.execPath`][].
|
||||
### Single executable application creation process
|
||||
|
||||
A tool aiming to create a single executable Node.js application must
|
||||
inject the contents of a JavaScript file into:
|
||||
inject the contents of the blob prepared with `--experimental-sea-config"`
|
||||
into:
|
||||
|
||||
* a resource named `NODE_JS_CODE` if the `node` binary is a [PE][] file
|
||||
* a section named `NODE_JS_CODE` in the `NODE_JS` segment if the `node` binary
|
||||
* a resource named `NODE_SEA_BLOB` if the `node` binary is a [PE][] file
|
||||
* a section named `NODE_SEA_BLOB` in the `NODE_SEA` segment if the `node` binary
|
||||
is a [Mach-O][] file
|
||||
* a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file
|
||||
* a note named `NODE_SEA_BLOB` if the `node` binary is an [ELF][] file
|
||||
|
||||
Search the binary for the
|
||||
`NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
|
||||
`NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
|
||||
last character to `1` to indicate that a resource has been injected.
|
||||
|
||||
### Platform support
|
||||
@ -165,6 +201,7 @@ to help us document them.
|
||||
|
||||
[CommonJS]: modules.md#modules-commonjs-modules
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[Generating single executable preparation blobs]: #generating-single-executable-preparation-blobs
|
||||
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
|
||||
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
|
||||
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
|
||||
|
||||
@ -66,7 +66,7 @@ To disable single executable application support, build Node.js with the
|
||||
## Implementation
|
||||
|
||||
When built with single executable application support, the Node.js process uses
|
||||
[`postject-api.h`][] to check if the `NODE_JS_CODE` section exists in the
|
||||
[`postject-api.h`][] to check if the `NODE_SEA_BLOB` section exists in the
|
||||
binary. If it is found, it passes the buffer to
|
||||
[`single_executable_application.js`][], which executes the contents of the
|
||||
embedded script.
|
||||
|
||||
2
node.gyp
2
node.gyp
@ -82,6 +82,8 @@
|
||||
'src/js_stream.cc',
|
||||
'src/json_utils.cc',
|
||||
'src/js_udp_wrap.cc',
|
||||
'src/json_parser.h',
|
||||
'src/json_parser.cc',
|
||||
'src/module_wrap.cc',
|
||||
'src/node.cc',
|
||||
'src/node_api.cc',
|
||||
|
||||
80
src/json_parser.cc
Normal file
80
src/json_parser.cc
Normal file
@ -0,0 +1,80 @@
|
||||
#include "json_parser.h"
|
||||
#include "node_errors.h"
|
||||
#include "node_v8_platform-inl.h"
|
||||
#include "util-inl.h"
|
||||
|
||||
namespace node {
|
||||
using v8::ArrayBuffer;
|
||||
using v8::Context;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::Object;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) {
|
||||
Isolate* isolate = Isolate::Allocate();
|
||||
CHECK_NOT_NULL(isolate);
|
||||
per_process::v8_platform.Platform()->RegisterIsolate(isolate,
|
||||
uv_default_loop());
|
||||
Isolate::CreateParams params;
|
||||
params.array_buffer_allocator = allocator;
|
||||
Isolate::Initialize(isolate, params);
|
||||
return isolate;
|
||||
}
|
||||
|
||||
void JSONParser::FreeIsolate(Isolate* isolate) {
|
||||
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
|
||||
isolate->Dispose();
|
||||
}
|
||||
|
||||
JSONParser::JSONParser()
|
||||
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
|
||||
isolate_(NewIsolate(allocator_.get())),
|
||||
handle_scope_(isolate_.get()),
|
||||
context_(isolate_.get(), Context::New(isolate_.get())),
|
||||
context_scope_(context_.Get(isolate_.get())) {}
|
||||
|
||||
bool JSONParser::Parse(const std::string& content) {
|
||||
DCHECK(!parsed_);
|
||||
|
||||
Isolate* isolate = isolate_.get();
|
||||
Local<Context> context = context_.Get(isolate);
|
||||
|
||||
// It's not a real script, so don't print the source line.
|
||||
errors::PrinterTryCatch bootstrapCatch(
|
||||
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
|
||||
Local<Value> json_string_value;
|
||||
Local<Value> result_value;
|
||||
if (!ToV8Value(context, content).ToLocal(&json_string_value) ||
|
||||
!json_string_value->IsString() ||
|
||||
!v8::JSON::Parse(context, json_string_value.As<String>())
|
||||
.ToLocal(&result_value) ||
|
||||
!result_value->IsObject()) {
|
||||
return false;
|
||||
}
|
||||
content_.Reset(isolate, result_value.As<Object>());
|
||||
parsed_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> JSONParser::GetTopLevelField(
|
||||
const std::string& field) {
|
||||
Isolate* isolate = isolate_.get();
|
||||
Local<Context> context = context_.Get(isolate);
|
||||
Local<Object> content_object = content_.Get(isolate);
|
||||
Local<Value> value;
|
||||
// It's not a real script, so don't print the source line.
|
||||
errors::PrinterTryCatch bootstrapCatch(
|
||||
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
|
||||
if (!content_object
|
||||
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
|
||||
.ToLocal(&value) ||
|
||||
!value->IsString()) {
|
||||
return {};
|
||||
}
|
||||
Utf8Value utf8_value(isolate, value);
|
||||
return utf8_value.ToString();
|
||||
}
|
||||
|
||||
} // namespace node
|
||||
39
src/json_parser.h
Normal file
39
src/json_parser.h
Normal file
@ -0,0 +1,39 @@
|
||||
#ifndef SRC_JSON_PARSER_H_
|
||||
#define SRC_JSON_PARSER_H_
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include "util.h"
|
||||
#include "v8.h"
|
||||
|
||||
namespace node {
|
||||
// This is intended to be used to get some top-level fields out of a JSON
|
||||
// without having to spin up a full Node.js environment that unnecessarily
|
||||
// complicates things.
|
||||
class JSONParser {
|
||||
public:
|
||||
JSONParser();
|
||||
~JSONParser() {}
|
||||
bool Parse(const std::string& content);
|
||||
std::optional<std::string> GetTopLevelField(const std::string& field);
|
||||
|
||||
private:
|
||||
// We might want a lighter-weight JSON parser for this use case. But for now
|
||||
// using V8 is good enough.
|
||||
static void FreeIsolate(v8::Isolate* isolate);
|
||||
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
|
||||
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
|
||||
v8::HandleScope handle_scope_;
|
||||
v8::Global<v8::Context> context_;
|
||||
v8::Context::Scope context_scope_;
|
||||
v8::Global<v8::Object> content_;
|
||||
bool parsed_ = false;
|
||||
};
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#endif // SRC_JSON_PARSER_H_
|
||||
@ -1239,6 +1239,11 @@ static ExitCode StartInternal(int argc, char** argv) {
|
||||
|
||||
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
|
||||
|
||||
std::string sea_config = per_process::cli_options->experimental_sea_config;
|
||||
if (!sea_config.empty()) {
|
||||
return sea::BuildSingleExecutableBlob(sea_config);
|
||||
}
|
||||
|
||||
// --build-snapshot indicates that we are in snapshot building mode.
|
||||
if (per_process::cli_options->per_isolate->build_snapshot) {
|
||||
if (result->args().size() < 2) {
|
||||
|
||||
@ -246,14 +246,19 @@ void PrintStackTrace(Isolate* isolate, Local<StackTrace> stack) {
|
||||
std::string FormatCaughtException(Isolate* isolate,
|
||||
Local<Context> context,
|
||||
Local<Value> err,
|
||||
Local<Message> message) {
|
||||
Local<Message> message,
|
||||
bool add_source_line = true) {
|
||||
std::string result;
|
||||
node::Utf8Value reason(isolate,
|
||||
err->ToDetailString(context)
|
||||
.FromMaybe(Local<String>()));
|
||||
bool added_exception_line = false;
|
||||
std::string source =
|
||||
GetErrorSource(isolate, context, message, &added_exception_line);
|
||||
std::string result = source + '\n' + reason.ToString() + '\n';
|
||||
if (add_source_line) {
|
||||
bool added_exception_line = false;
|
||||
std::string source =
|
||||
GetErrorSource(isolate, context, message, &added_exception_line);
|
||||
result = source + '\n';
|
||||
}
|
||||
result += reason.ToString() + '\n';
|
||||
|
||||
Local<v8::StackTrace> stack = message->GetStackTrace();
|
||||
if (!stack.IsEmpty()) result += FormatStackTrace(isolate, stack);
|
||||
@ -1209,6 +1214,19 @@ void TriggerUncaughtException(Isolate* isolate, const v8::TryCatch& try_catch) {
|
||||
false /* from_promise */);
|
||||
}
|
||||
|
||||
PrinterTryCatch::~PrinterTryCatch() {
|
||||
if (!HasCaught()) {
|
||||
return;
|
||||
}
|
||||
std::string str =
|
||||
FormatCaughtException(isolate_,
|
||||
isolate_->GetCurrentContext(),
|
||||
Exception(),
|
||||
Message(),
|
||||
print_source_line_ == kPrintSourceLine);
|
||||
PrintToStderrAndFlush(str);
|
||||
}
|
||||
|
||||
} // namespace errors
|
||||
|
||||
} // namespace node
|
||||
|
||||
@ -275,6 +275,22 @@ void PerIsolateMessageListener(v8::Local<v8::Message> message,
|
||||
|
||||
void DecorateErrorStack(Environment* env,
|
||||
const errors::TryCatchScope& try_catch);
|
||||
|
||||
class PrinterTryCatch : public v8::TryCatch {
|
||||
public:
|
||||
enum PrintSourceLine { kPrintSourceLine, kDontPrintSourceLine };
|
||||
explicit PrinterTryCatch(v8::Isolate* isolate,
|
||||
PrintSourceLine print_source_line)
|
||||
: v8::TryCatch(isolate),
|
||||
isolate_(isolate),
|
||||
print_source_line_(print_source_line) {}
|
||||
~PrinterTryCatch();
|
||||
|
||||
private:
|
||||
v8::Isolate* isolate_;
|
||||
PrintSourceLine print_source_line_;
|
||||
};
|
||||
|
||||
} // namespace errors
|
||||
|
||||
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
|
||||
|
||||
@ -991,6 +991,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
|
||||
kAllowedInEnvvar);
|
||||
Implies("--node-memory-debug", "--debug-arraybuffer-allocations");
|
||||
Implies("--node-memory-debug", "--verify-base-objects");
|
||||
|
||||
AddOption("--experimental-sea-config",
|
||||
"Generate a blob that can be embedded into the single executable "
|
||||
"application",
|
||||
&PerProcessOptions::experimental_sea_config);
|
||||
}
|
||||
|
||||
inline std::string RemoveBrackets(const std::string& host) {
|
||||
|
||||
@ -264,6 +264,7 @@ class PerProcessOptions : public Options {
|
||||
bool print_help = false;
|
||||
bool print_v8_help = false;
|
||||
bool print_version = false;
|
||||
std::string experimental_sea_config;
|
||||
|
||||
#ifdef NODE_HAVE_I18N_SUPPORT
|
||||
std::string icu_data_dir;
|
||||
|
||||
114
src/node_sea.cc
114
src/node_sea.cc
@ -1,6 +1,8 @@
|
||||
#include "node_sea.h"
|
||||
|
||||
#include "debug_utils-inl.h"
|
||||
#include "env-inl.h"
|
||||
#include "json_parser.h"
|
||||
#include "node_external_reference.h"
|
||||
#include "node_internals.h"
|
||||
#include "node_union_bytes.h"
|
||||
@ -10,7 +12,7 @@
|
||||
// used by the postject_has_resource() function to efficiently detect if a
|
||||
// resource has been injected. See
|
||||
// https://github.com/nodejs/postject/blob/35343439cac8c488f2596d7c4c1dddfec1fddcae/postject-api.h#L42-L45.
|
||||
#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
|
||||
#define POSTJECT_SENTINEL_FUSE "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
|
||||
#include "postject-api.h"
|
||||
#undef POSTJECT_SENTINEL_FUSE
|
||||
|
||||
@ -21,6 +23,7 @@
|
||||
|
||||
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||
|
||||
using node::ExitCode;
|
||||
using v8::Context;
|
||||
using v8::FunctionCallbackInfo;
|
||||
using v8::Local;
|
||||
@ -30,6 +33,12 @@ using v8::Value;
|
||||
namespace node {
|
||||
namespace sea {
|
||||
|
||||
// A special number that will appear at the beginning of the single executable
|
||||
// preparation blobs ready to be injected into the binary. We use this to check
|
||||
// that the data given to us are intended for building single executable
|
||||
// applications.
|
||||
static const uint32_t kMagic = 0x143da20;
|
||||
|
||||
std::string_view FindSingleExecutableCode() {
|
||||
CHECK(IsSingleExecutable());
|
||||
static const std::string_view sea_code = []() -> std::string_view {
|
||||
@ -37,14 +46,17 @@ std::string_view FindSingleExecutableCode() {
|
||||
#ifdef __APPLE__
|
||||
postject_options options;
|
||||
postject_options_init(&options);
|
||||
options.macho_segment_name = "NODE_JS";
|
||||
options.macho_segment_name = "NODE_SEA";
|
||||
const char* code = static_cast<const char*>(
|
||||
postject_find_resource("NODE_JS_CODE", &size, &options));
|
||||
postject_find_resource("NODE_SEA_BLOB", &size, &options));
|
||||
#else
|
||||
const char* code = static_cast<const char*>(
|
||||
postject_find_resource("NODE_JS_CODE", &size, nullptr));
|
||||
postject_find_resource("NODE_SEA_BLOB", &size, nullptr));
|
||||
#endif
|
||||
return {code, size};
|
||||
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
|
||||
CHECK_EQ(first_word, kMagic);
|
||||
// TODO(joyeecheung): do more checks here e.g. matching the versions.
|
||||
return {code + sizeof(first_word), size - sizeof(first_word)};
|
||||
}();
|
||||
return sea_code;
|
||||
}
|
||||
@ -73,6 +85,98 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
|
||||
return {argc, argv};
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct SeaConfig {
|
||||
std::string main_path;
|
||||
std::string output_path;
|
||||
};
|
||||
|
||||
std::optional<SeaConfig> ParseSingleExecutableConfig(
|
||||
const std::string& config_path) {
|
||||
std::string config;
|
||||
int r = ReadFileSync(&config, config_path.c_str());
|
||||
if (r != 0) {
|
||||
const char* err = uv_strerror(r);
|
||||
FPrintF(stderr,
|
||||
"Cannot read single executable configuration from %s: %s\n",
|
||||
config_path,
|
||||
err);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SeaConfig result;
|
||||
JSONParser parser;
|
||||
if (!parser.Parse(config)) {
|
||||
FPrintF(stderr, "Cannot parse JSON from %s\n", config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.main_path = parser.GetTopLevelField("main").value_or(std::string());
|
||||
if (result.main_path.empty()) {
|
||||
FPrintF(stderr,
|
||||
"\"main\" field of %s is not a non-empty string\n",
|
||||
config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.output_path =
|
||||
parser.GetTopLevelField("output").value_or(std::string());
|
||||
if (result.output_path.empty()) {
|
||||
FPrintF(stderr,
|
||||
"\"output\" field of %s is not a non-empty string\n",
|
||||
config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool GenerateSingleExecutableBlob(const SeaConfig& config) {
|
||||
std::string main_script;
|
||||
// TODO(joyeecheung): unify the file utils.
|
||||
int r = ReadFileSync(&main_script, config.main_path.c_str());
|
||||
if (r != 0) {
|
||||
const char* err = uv_strerror(r);
|
||||
FPrintF(stderr, "Cannot read main script %s:%s\n", config.main_path, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> sink;
|
||||
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
|
||||
sink.reserve(sizeof(kMagic) + main_script.size());
|
||||
const char* pos = reinterpret_cast<const char*>(&kMagic);
|
||||
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
|
||||
sink.insert(
|
||||
sink.end(), main_script.data(), main_script.data() + main_script.size());
|
||||
|
||||
uv_buf_t buf = uv_buf_init(sink.data(), sink.size());
|
||||
r = WriteFileSync(config.output_path.c_str(), buf);
|
||||
if (r != 0) {
|
||||
const char* err = uv_strerror(r);
|
||||
FPrintF(stderr, "Cannot write output to %s:%s\n", config.output_path, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
FPrintF(stderr,
|
||||
"Wrote single executable preparation blob to %s\n",
|
||||
config.output_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ExitCode BuildSingleExecutableBlob(const std::string& config_path) {
|
||||
std::optional<SeaConfig> config_opt =
|
||||
ParseSingleExecutableConfig(config_path);
|
||||
if (!config_opt.has_value() ||
|
||||
!GenerateSingleExecutableBlob(config_opt.value())) {
|
||||
return ExitCode::kGenericUserError;
|
||||
}
|
||||
|
||||
return ExitCode::kNoFailure;
|
||||
}
|
||||
|
||||
void Initialize(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include "node_exit_code.h"
|
||||
|
||||
namespace node {
|
||||
namespace sea {
|
||||
@ -14,7 +15,7 @@ namespace sea {
|
||||
bool IsSingleExecutable();
|
||||
std::string_view FindSingleExecutableCode();
|
||||
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv);
|
||||
|
||||
node::ExitCode BuildSingleExecutableBlob(const std::string& config_path);
|
||||
} // namespace sea
|
||||
} // namespace node
|
||||
|
||||
|
||||
@ -5,10 +5,11 @@ const common = require('../common');
|
||||
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { copyFileSync, readFileSync, writeFileSync } = require('fs');
|
||||
const { copyFileSync, readFileSync, writeFileSync, existsSync } = require('fs');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { join } = require('path');
|
||||
const { strictEqual } = require('assert');
|
||||
const assert = require('assert');
|
||||
|
||||
if (!process.config.variables.single_executable_application)
|
||||
common.skip('Single Executable Application support has been disabled.');
|
||||
@ -51,6 +52,8 @@ if (process.platform === 'linux') {
|
||||
|
||||
const inputFile = fixtures.path('sea.js');
|
||||
const requirableFile = join(tmpdir.path, 'requirable.js');
|
||||
const configFile = join(tmpdir.path, 'sea-config.json');
|
||||
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
|
||||
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
|
||||
|
||||
tmpdir.refresh();
|
||||
@ -61,15 +64,30 @@ module.exports = {
|
||||
};
|
||||
`);
|
||||
|
||||
writeFileSync(configFile, `
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-prep.blob"
|
||||
}
|
||||
`);
|
||||
|
||||
// Copy input to working directory
|
||||
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
|
||||
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
|
||||
cwd: tmpdir.path
|
||||
});
|
||||
|
||||
assert(existsSync(seaPrepBlob));
|
||||
|
||||
copyFileSync(process.execPath, outputFile);
|
||||
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
|
||||
execFileSync(process.execPath, [
|
||||
postjectFile,
|
||||
outputFile,
|
||||
'NODE_JS_CODE',
|
||||
inputFile,
|
||||
'--sentinel-fuse', 'NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
|
||||
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_JS' ] : [],
|
||||
'NODE_SEA_BLOB',
|
||||
seaPrepBlob,
|
||||
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
|
||||
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
|
||||
]);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
|
||||
204
test/parallel/test-single-executable-blob-config-errors.js
Normal file
204
test/parallel/test-single-executable-blob-config-errors.js
Normal file
@ -0,0 +1,204 @@
|
||||
// This tests invalid options for --experimental-sea-config.
|
||||
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { writeFileSync, mkdirSync } = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
const assert = require('assert');
|
||||
const { join } = require('path');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = 'non-existent-relative.json';
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert.match(
|
||||
stderr,
|
||||
/Cannot read single executable configuration from non-existent-relative\.json/
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'non-existent-absolute.json');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`Cannot read single executable configuration from ${config}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'invalid.json');
|
||||
writeFileSync(config, '\n{\n"main"', 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert.match(stderr, /SyntaxError: Expected ':' after property name/);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`Cannot parse JSON from ${config}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'empty.json');
|
||||
writeFileSync(config, '{}', 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`"main" field of ${config} is not a non-empty string`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'no-main.json');
|
||||
writeFileSync(config, '{"output": "test.blob"}', 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`"main" field of ${config} is not a non-empty string`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'no-output.json');
|
||||
writeFileSync(config, '{"main": "bundle.js"}', 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`"output" field of ${config} is not a non-empty string`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'nonexistent-main-relative.json');
|
||||
writeFileSync(config, '{"main": "bundle.js", "output": "sea.blob"}', 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert.match(stderr, /Cannot read main script bundle\.js/);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'nonexistent-main-absolute.json');
|
||||
const main = join(tmpdir.path, 'bundle.js');
|
||||
const configJson = JSON.stringify({
|
||||
main,
|
||||
output: 'sea.blob'
|
||||
});
|
||||
writeFileSync(config, configJson, 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`Cannot read main script ${main}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'output-is-dir-absolute.json');
|
||||
const main = join(tmpdir.path, 'bundle.js');
|
||||
const output = join(tmpdir.path, 'output-dir');
|
||||
mkdirSync(output);
|
||||
writeFileSync(main, 'console.log("hello")', 'utf-8');
|
||||
const configJson = JSON.stringify({
|
||||
main,
|
||||
output,
|
||||
});
|
||||
writeFileSync(config, configJson, 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert(
|
||||
stderr.includes(
|
||||
`Cannot write output to ${output}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'output-is-dir-relative.json');
|
||||
const main = join(tmpdir.path, 'bundle.js');
|
||||
const output = join(tmpdir.path, 'output-dir');
|
||||
mkdirSync(output);
|
||||
writeFileSync(main, 'console.log("hello")', 'utf-8');
|
||||
const configJson = JSON.stringify({
|
||||
main,
|
||||
output: 'output-dir'
|
||||
});
|
||||
writeFileSync(config, configJson, 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
|
||||
const stderr = child.stderr.toString();
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert.match(stderr, /Cannot write output to output-dir/);
|
||||
}
|
||||
50
test/parallel/test-single-executable-blob-config.js
Normal file
50
test/parallel/test-single-executable-blob-config.js
Normal file
@ -0,0 +1,50 @@
|
||||
// This tests valid options for --experimental-sea-config.
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { writeFileSync, existsSync } = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
const assert = require('assert');
|
||||
const { join } = require('path');
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'absolute.json');
|
||||
const main = join(tmpdir.path, 'bundle.js');
|
||||
const output = join(tmpdir.path, 'output.blob');
|
||||
writeFileSync(main, 'console.log("hello")', 'utf-8');
|
||||
const configJson = JSON.stringify({
|
||||
main,
|
||||
output,
|
||||
});
|
||||
writeFileSync(config, configJson, 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
assert.strictEqual(child.status, 0);
|
||||
assert(existsSync(output));
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = join(tmpdir.path, 'relative.json');
|
||||
const main = join(tmpdir.path, 'bundle.js');
|
||||
const output = join(tmpdir.path, 'output.blob');
|
||||
writeFileSync(main, 'console.log("hello")', 'utf-8');
|
||||
const configJson = JSON.stringify({
|
||||
main: 'bundle.js',
|
||||
output: 'output.blob'
|
||||
});
|
||||
writeFileSync(config, configJson, 'utf8');
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-sea-config', config], {
|
||||
cwd: tmpdir.path,
|
||||
});
|
||||
|
||||
assert.strictEqual(child.status, 0);
|
||||
assert(existsSync(output));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user