Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src,doc: Experimental support for SEA #42334

Closed
wants to merge 10 commits into from
118 changes: 118 additions & 0 deletions doc/contributing/maintaining-single-executable-application-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Maintaining Single Executable Applications support

Support for [single executable applications](https://github.com/nodejs/node/blob/master/doc/contributing/technical-priorities.md#single-executable-applications)
is one of the key technical priorities identified for the success of Node.js.

## High level strategy

From the [next-10 discussions](https://github.com/nodejs/next-10/blob/main/meetings/summit-nov-2021.md#single-executable-applications)
there are 2 approaches the project believes are important to support:

* Compile with Node.js into executable (`boxnode` approach).
* Bundle into existing Node.js executable (`pkg` approach).

### Compile with node into executable

No additional code within the Node.js project is needed to support the
option of compiling a bundled application along with Node.js into a single
executable application.

### Bundle into existing Node.js executable

The project does not plan to provide the complete solution but instead the key
elements which are required in the Node.js executable in order to enable
bundling with the pre-built Node.js binaries. This includes:

* Looking for a segment within the executable that holds bundled code.
* Running the bundled code when such a segment is found.

It is left up to external tools/solutions to:

* Bundle code into a single script that can be executed with `-e` on
the command line.
* Generate a command line with appropriate options, including `-e` to
run the bundled script.
* Add a segment to an existing Node.js executable which contains
the command line and appropriate headers.
* Re-generate or removing signatures on the resulting executable
* Provide a virtual file system, and hooking it in if needed to
support native modules or reading file contents.

## Maintaining

### Compile with node into executable

The approach of compiling with node into an executable requires that we
maintain a stable [em-bedder API](https://nodejs.org/dist/latest/docs/api/embedding.html).

### Bundle into existing Node.js executable

The following header must be included in a segment in order to have it run
as a single executable application:

NODEJSSEAVVVVVVVVFFFFFFFFFAAAAAAAA

where:

* `VVVVVVVV` represents the version to be used to interpret the section,
for example `00000001`.
* `FFFFFFFF` represents the flags to be used in the process of starting
the bundled application. Currently this must be `00000000` to indicate that
no flags are set or `000000001` to indicate that a binary block
of data is included in addition to the arguments.
* `AAAAAAAA` is the number of arguments being provided

The characters in both `VVVVVVVV`, `FFFFFFFF` and `AAAAAAAA` are
restricted to being hexadecimal characters (`0` through `9` and
`A` through `F`) that form a 32-bit, big endian integer.

Following the header are AAAAAAAA strings, each terminated for 0x00
one for each of the parameters passed. These parameters are is treated
as a set of command line options that are used as a prefix to any
additional command line options passed when the executable is started.
For example, for a simple single hello world for version `00000001` could be:

```text
NODEJSSEA000000010000000000000002-e\0console.log('Hello from single binary')\0
```

If the flags are set to `00000001` then there must be at least one charater
of binary data following the argument strings and a pointer to this
data will be exposed through `process.seaBinaryData`.

Support for bundling into existing Node.js binaries is maintained
in `src/node_single_binary.*`.

Currently only POSIX-compliant platforms are supported. The goal
is to expand this to include Windows and macOS as well.

If a breaking change to the content after the header is required, the version
`VVVVVVVV` should be incremented. Support for a new format
may be introduced as a semver-minor provided that older versions
are still supported. Removing support for a version is semver-major.

The `FFFFFFFF` is a set of flags that is used to control the
process of starting the application. For example they might indicate
that some set of arguments should be suppressed on the command line.
Currently no flags are in use.

For test purposes [LIEF](https://github.com/lief-project/LIEF) can
be used to add a section in the required format. The following is a
simple example for using LIEF on Linux. It can be improved as it
currently replaces an existing section instead of adding a new
one:

```text
#!/usr/bin/env python
import lief
binary = lief.parse('node')

segment = lief.ELF.Segment()
segment.type = lief.ELF.SEGMENT_TYPES.LOAD
segment.flags = lief.ELF.SEGMENT_FLAGS.R
stringContent = "NODEJSSEA000000010000000000000002-e\0console.log('Hello from single binary')\0"
segment.content = bytearray(stringContent.encode())
segment = binary.replace(segment, binary[lief.ELF.SEGMENT_TYPES.NOTE])

binary.write("hello")
```
26 changes: 26 additions & 0 deletions lib/internal/main/single_executable_application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

const {
prepareMainThreadExecution
} = require('internal/bootstrap/pre_execution');

const { getOptionValue } = require('internal/options');

const {
evalModule,
evalScript,
readStdin
} = require('internal/process/execution');

prepareMainThreadExecution();
markBootstrapComplete();

const source = getOptionValue('--eval');
const print = getOptionValue('--print');
if (getOptionValue('--input-type') === 'module')
evalModule(source, print);
else
evalScript('[eval]',
source,
getOptionValue('--inspect-brk'),
print);
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@
'src/node_report_utils.cc',
'src/node_serdes.cc',
'src/node_shadow_realm.cc',
'src/node_single_executable_application.cc',
'src/node_snapshotable.cc',
'src/node_sockaddr.cc',
'src/node_stat_watcher.cc',
Expand Down
26 changes: 25 additions & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "node_process-inl.h"
#include "node_report.h"
#include "node_revert.h"
#include "node_single_executable_application.h"
#include "node_snapshot_builder.h"
#include "node_v8_platform-inl.h"
#include "node_version.h"
Expand Down Expand Up @@ -160,6 +161,9 @@ PVOID old_vectored_exception_handler;

// node_v8_platform-inl.h
struct V8Platform v8_platform;

bool single_executable_application = false;
char* sea_binary_data = nullptr;
} // namespace per_process

// The section in the OpenSSL configuration file to be loaded.
Expand Down Expand Up @@ -519,6 +523,21 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
return StartExecution(env, "internal/main/prof_process");
}

if (env->options()->has_eval_string &&
per_process::single_executable_application) {
if (per_process::sea_binary_data != nullptr) {
Isolate* isolate = env->isolate();
Local<v8::Context> context = env->context();
READONLY_PROPERTY(
env->process_object(),
"seaBinaryData",
ToV8Value(context,
reinterpret_cast<size_t>(per_process::sea_binary_data))
.ToLocalChecked());
}
return StartExecution(env, "internal/main/single_executable_application");
}

// -e/--eval without -i/--interactive
if (env->options()->has_eval_string && !env->options()->force_repl) {
return StartExecution(env, "internal/main/eval_string");
Expand Down Expand Up @@ -1026,7 +1045,12 @@ InitializationResult InitializeOncePerProcess(
argv = uv_setup_args(argc, argv);

InitializationResult result;
result.args = std::vector<std::string>(argv, argv + argc);
if (single_executable_application::CheckForSEA(
argc, argv, &(result.args), &per_process::sea_binary_data)) {
per_process::single_executable_application = true;
} else {
result.args = std::vector<std::string>(argv, argv + argc);
}
std::vector<std::string> errors;

// This needs to run *before* V8::Initialize().
Expand Down