src: add initial support for single executable applications
Compile a JavaScript file into a single executable application: ```console $ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js $ cp $(command -v node) hello $ npx postject hello NODE_JS_CODE hello.js \ --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 $ npx postject hello NODE_JS_CODE hello.js \ --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ --macho-segment-name NODE_JS $ ./hello world Hello, world! ``` Signed-off-by: Darshan Sen <raisinten@gmail.com> PR-URL: https://github.com/nodejs/node/pull/45038 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
parent
2472b6742c
commit
9bbde3d7ba
10
configure.py
10
configure.py
@ -146,6 +146,12 @@ parser.add_argument('--no-ifaddrs',
|
|||||||
default=None,
|
default=None,
|
||||||
help='use on deprecated SunOS systems that do not support ifaddrs.h')
|
help='use on deprecated SunOS systems that do not support ifaddrs.h')
|
||||||
|
|
||||||
|
parser.add_argument('--disable-single-executable-application',
|
||||||
|
action='store_true',
|
||||||
|
dest='disable_single_executable_application',
|
||||||
|
default=None,
|
||||||
|
help='Disable Single Executable Application support.')
|
||||||
|
|
||||||
parser.add_argument("--fully-static",
|
parser.add_argument("--fully-static",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
dest="fully_static",
|
dest="fully_static",
|
||||||
@ -1357,6 +1363,10 @@ def configure_node(o):
|
|||||||
if options.no_ifaddrs:
|
if options.no_ifaddrs:
|
||||||
o['defines'] += ['SUNOS_NO_IFADDRS']
|
o['defines'] += ['SUNOS_NO_IFADDRS']
|
||||||
|
|
||||||
|
o['variables']['single_executable_application'] = b(not options.disable_single_executable_application)
|
||||||
|
if options.disable_single_executable_application:
|
||||||
|
o['defines'] += ['DISABLE_SINGLE_EXECUTABLE_APPLICATION']
|
||||||
|
|
||||||
o['variables']['node_with_ltcg'] = b(options.with_ltcg)
|
o['variables']['node_with_ltcg'] = b(options.with_ltcg)
|
||||||
if flavor != 'win' and options.with_ltcg:
|
if flavor != 'win' and options.with_ltcg:
|
||||||
raise Exception('Link Time Code Generation is only supported on Windows.')
|
raise Exception('Link Time Code Generation is only supported on Windows.')
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
* [Readline](readline.md)
|
* [Readline](readline.md)
|
||||||
* [REPL](repl.md)
|
* [REPL](repl.md)
|
||||||
* [Report](report.md)
|
* [Report](report.md)
|
||||||
|
* [Single executable applications](single-executable-applications.md)
|
||||||
* [Stream](stream.md)
|
* [Stream](stream.md)
|
||||||
* [String decoder](string_decoder.md)
|
* [String decoder](string_decoder.md)
|
||||||
* [Test runner](test.md)
|
* [Test runner](test.md)
|
||||||
|
140
doc/api/single-executable-applications.md
Normal file
140
doc/api/single-executable-applications.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Single executable applications
|
||||||
|
|
||||||
|
<!--introduced_in=REPLACEME-->
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental: This feature is being designed and will change.
|
||||||
|
|
||||||
|
<!-- source_link=lib/internal/main/single_executable_application.js -->
|
||||||
|
|
||||||
|
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 single executable application feature only supports running a single
|
||||||
|
embedded [CommonJS][] file.
|
||||||
|
|
||||||
|
A bundled JavaScript file can be turned into a single executable application
|
||||||
|
with any tool which can inject resources into the `node` binary.
|
||||||
|
|
||||||
|
Here are the steps for creating a single executable application using one such
|
||||||
|
tool, [postject][]:
|
||||||
|
|
||||||
|
1. Create a JavaScript file:
|
||||||
|
```console
|
||||||
|
$ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a copy of the `node` executable and name it according to your needs:
|
||||||
|
```console
|
||||||
|
$ cp $(command -v node) hello
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Inject the JavaScript file 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
|
||||||
|
[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
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
* On macOS:
|
||||||
|
```console
|
||||||
|
$ npx postject hello NODE_JS_CODE hello.js \
|
||||||
|
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
|
||||||
|
--macho-segment-name NODE_JS
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the binary:
|
||||||
|
```console
|
||||||
|
$ ./hello world
|
||||||
|
Hello, world!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### `require(id)` in the injected module is not file based
|
||||||
|
|
||||||
|
`require()` in the injected module is not the same as the [`require()`][]
|
||||||
|
available to modules that are not injected. It also does not have any of the
|
||||||
|
properties that non-injected [`require()`][] has except [`require.main`][]. It
|
||||||
|
can only be used to load built-in modules. Attempting to load a module that can
|
||||||
|
only be found in the file system will throw an error.
|
||||||
|
|
||||||
|
Instead of relying on a file based `require()`, users can bundle their
|
||||||
|
application into a standalone JavaScript file to inject into the executable.
|
||||||
|
This also ensures a more deterministic dependency graph.
|
||||||
|
|
||||||
|
However, if a file based `require()` is still needed, that can also be achieved:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { createRequire } = require('node:module');
|
||||||
|
require = createRequire(__filename);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `__filename` and `module.filename` in the injected module
|
||||||
|
|
||||||
|
The values of `__filename` and `module.filename` in the injected module are
|
||||||
|
equal to [`process.execPath`][].
|
||||||
|
|
||||||
|
### `__dirname` in the injected module
|
||||||
|
|
||||||
|
The value of `__dirname` in the injected module is equal to the directory name
|
||||||
|
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:
|
||||||
|
|
||||||
|
* 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
|
||||||
|
is a [Mach-O][] file
|
||||||
|
* a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file
|
||||||
|
|
||||||
|
Search the binary for the
|
||||||
|
`NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
|
||||||
|
last character to `1` to indicate that a resource has been injected.
|
||||||
|
|
||||||
|
### Platform support
|
||||||
|
|
||||||
|
Single-executable support is tested regularly on CI only on the following
|
||||||
|
platforms:
|
||||||
|
|
||||||
|
* Windows
|
||||||
|
* macOS
|
||||||
|
* Linux (AMD64 only)
|
||||||
|
|
||||||
|
This is due to a lack of better tools to generate single-executables that can be
|
||||||
|
used to test this feature on other platforms.
|
||||||
|
|
||||||
|
Suggestions for other resource injection tools/workflows are welcomed. Please
|
||||||
|
start a discussion at <https://github.com/nodejs/single-executable/discussions>
|
||||||
|
to help us document them.
|
||||||
|
|
||||||
|
[CommonJS]: modules.md#modules-commonjs-modules
|
||||||
|
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||||
|
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
|
||||||
|
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
|
||||||
|
[`process.execPath`]: process.md#processexecpath
|
||||||
|
[`require()`]: modules.md#requireid
|
||||||
|
[`require.main`]: modules.md#accessing-the-main-module
|
||||||
|
[fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses
|
||||||
|
[postject]: https://github.com/nodejs/postject
|
||||||
|
[single executable applications]: https://github.com/nodejs/single-executable
|
@ -0,0 +1,81 @@
|
|||||||
|
# Maintaining Single Executable Applications support
|
||||||
|
|
||||||
|
Support for [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][] there are 2 approaches the project believes are
|
||||||
|
important to support:
|
||||||
|
|
||||||
|
### Compile with Node.js into executable
|
||||||
|
|
||||||
|
This is the approach followed by [boxednode][].
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
This is the approach followed by [pkg][].
|
||||||
|
|
||||||
|
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.
|
||||||
|
* Generate a command line with appropriate options.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
However, the project also maintains a separate tool, [postject][], for injecting
|
||||||
|
arbitrary read-only resources into the binary such as those needed for bundling
|
||||||
|
the application into the runtime.
|
||||||
|
|
||||||
|
## Planning
|
||||||
|
|
||||||
|
Planning for this feature takes place in the [single-executable repository][].
|
||||||
|
|
||||||
|
## Upcoming features
|
||||||
|
|
||||||
|
Currently, only running a single embedded CommonJS file is supported but support
|
||||||
|
for the following features are in the list of work we'd like to get to:
|
||||||
|
|
||||||
|
* Running an embedded ESM file.
|
||||||
|
* Running an archive of multiple files.
|
||||||
|
* Embedding [Node.js CLI options][] into the binary.
|
||||||
|
* [XCOFF][] executable format.
|
||||||
|
* Run tests on Linux architectures/distributions other than AMD64 Ubuntu.
|
||||||
|
|
||||||
|
## Disabling single executable application support
|
||||||
|
|
||||||
|
To disable single executable application support, build Node.js with the
|
||||||
|
`--disable-single-executable-application` configuration option.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
binary. If it is found, it passes the buffer to
|
||||||
|
[`single_executable_application.js`][], which executes the contents of the
|
||||||
|
embedded script.
|
||||||
|
|
||||||
|
[Next-10 discussions]: https://github.com/nodejs/next-10/blob/main/meetings/summit-nov-2021.md#single-executable-applications
|
||||||
|
[Node.js CLI options]: https://nodejs.org/api/cli.html
|
||||||
|
[XCOFF]: https://www.ibm.com/docs/en/aix/7.2?topic=formats-xcoff-object-file-format
|
||||||
|
[`postject-api.h`]: https://github.com/nodejs/node/blob/71951a0e86da9253d7c422fa2520ee9143e557fa/test/fixtures/postject-copy/node_modules/postject/dist/postject-api.h
|
||||||
|
[`single_executable_application.js`]: https://github.com/nodejs/node/blob/main/lib/internal/main/single_executable_application.js
|
||||||
|
[boxednode]: https://github.com/mongodb-js/boxednode
|
||||||
|
[pkg]: https://github.com/vercel/pkg
|
||||||
|
[postject]: https://github.com/nodejs/postject
|
||||||
|
[single executable applications]: https://github.com/nodejs/node/blob/main/doc/contributing/technical-priorities.md#single-executable-applications
|
||||||
|
[single-executable repository]: https://github.com/nodejs/single-executable
|
55
lib/internal/main/single_executable_application.js
Normal file
55
lib/internal/main/single_executable_application.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
const {
|
||||||
|
prepareMainThreadExecution,
|
||||||
|
markBootstrapComplete,
|
||||||
|
} = require('internal/process/pre_execution');
|
||||||
|
const { getSingleExecutableCode } = internalBinding('sea');
|
||||||
|
const { emitExperimentalWarning } = require('internal/util');
|
||||||
|
const { Module, wrapSafe } = require('internal/modules/cjs/loader');
|
||||||
|
const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors');
|
||||||
|
|
||||||
|
prepareMainThreadExecution(false, true);
|
||||||
|
markBootstrapComplete();
|
||||||
|
|
||||||
|
emitExperimentalWarning('Single executable application');
|
||||||
|
|
||||||
|
// This is roughly the same as:
|
||||||
|
//
|
||||||
|
// const mod = new Module(filename);
|
||||||
|
// mod._compile(contents, filename);
|
||||||
|
//
|
||||||
|
// but the code has been duplicated because currently there is no way to set the
|
||||||
|
// value of require.main to module.
|
||||||
|
//
|
||||||
|
// TODO(RaisinTen): Find a way to deduplicate this.
|
||||||
|
|
||||||
|
const filename = process.execPath;
|
||||||
|
const contents = getSingleExecutableCode();
|
||||||
|
const compiledWrapper = wrapSafe(filename, contents);
|
||||||
|
|
||||||
|
const customModule = new Module(filename, null);
|
||||||
|
customModule.filename = filename;
|
||||||
|
customModule.paths = Module._nodeModulePaths(customModule.path);
|
||||||
|
|
||||||
|
const customExports = customModule.exports;
|
||||||
|
|
||||||
|
function customRequire(path) {
|
||||||
|
if (!Module.isBuiltin(path)) {
|
||||||
|
throw new ERR_UNKNOWN_BUILTIN_MODULE(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return require(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
customRequire.main = customModule;
|
||||||
|
|
||||||
|
const customFilename = customModule.filename;
|
||||||
|
|
||||||
|
const customDirname = customModule.path;
|
||||||
|
|
||||||
|
compiledWrapper(
|
||||||
|
customExports,
|
||||||
|
customRequire,
|
||||||
|
customModule,
|
||||||
|
customFilename,
|
||||||
|
customDirname);
|
7
node.gyp
7
node.gyp
@ -151,7 +151,8 @@
|
|||||||
|
|
||||||
'include_dirs': [
|
'include_dirs': [
|
||||||
'src',
|
'src',
|
||||||
'deps/v8/include'
|
'deps/v8/include',
|
||||||
|
'deps/postject'
|
||||||
],
|
],
|
||||||
|
|
||||||
'sources': [
|
'sources': [
|
||||||
@ -449,6 +450,7 @@
|
|||||||
|
|
||||||
'include_dirs': [
|
'include_dirs': [
|
||||||
'src',
|
'src',
|
||||||
|
'deps/postject',
|
||||||
'<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h
|
'<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h
|
||||||
],
|
],
|
||||||
'dependencies': [
|
'dependencies': [
|
||||||
@ -523,6 +525,7 @@
|
|||||||
'src/node_report.cc',
|
'src/node_report.cc',
|
||||||
'src/node_report_module.cc',
|
'src/node_report_module.cc',
|
||||||
'src/node_report_utils.cc',
|
'src/node_report_utils.cc',
|
||||||
|
'src/node_sea.cc',
|
||||||
'src/node_serdes.cc',
|
'src/node_serdes.cc',
|
||||||
'src/node_shadow_realm.cc',
|
'src/node_shadow_realm.cc',
|
||||||
'src/node_snapshotable.cc',
|
'src/node_snapshotable.cc',
|
||||||
@ -633,6 +636,7 @@
|
|||||||
'src/node_report.h',
|
'src/node_report.h',
|
||||||
'src/node_revert.h',
|
'src/node_revert.h',
|
||||||
'src/node_root_certs.h',
|
'src/node_root_certs.h',
|
||||||
|
'src/node_sea.h',
|
||||||
'src/node_shadow_realm.h',
|
'src/node_shadow_realm.h',
|
||||||
'src/node_snapshotable.h',
|
'src/node_snapshotable.h',
|
||||||
'src/node_snapshot_builder.h',
|
'src/node_snapshot_builder.h',
|
||||||
@ -675,6 +679,7 @@
|
|||||||
'src/util-inl.h',
|
'src/util-inl.h',
|
||||||
# Dependency headers
|
# Dependency headers
|
||||||
'deps/v8/include/v8.h',
|
'deps/v8/include/v8.h',
|
||||||
|
'deps/postject/postject-api.h'
|
||||||
# javascript files to make for an even more pleasant IDE experience
|
# javascript files to make for an even more pleasant IDE experience
|
||||||
'<@(library_files)',
|
'<@(library_files)',
|
||||||
'<@(deps_files)',
|
'<@(deps_files)',
|
||||||
|
17
src/node.cc
17
src/node.cc
@ -39,6 +39,7 @@
|
|||||||
#include "node_realm-inl.h"
|
#include "node_realm-inl.h"
|
||||||
#include "node_report.h"
|
#include "node_report.h"
|
||||||
#include "node_revert.h"
|
#include "node_revert.h"
|
||||||
|
#include "node_sea.h"
|
||||||
#include "node_snapshot_builder.h"
|
#include "node_snapshot_builder.h"
|
||||||
#include "node_v8_platform-inl.h"
|
#include "node_v8_platform-inl.h"
|
||||||
#include "node_version.h"
|
#include "node_version.h"
|
||||||
@ -122,6 +123,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
@ -310,6 +312,18 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
|
|||||||
first_argv = env->argv()[1];
|
first_argv = env->argv()[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||||
|
if (sea::IsSingleExecutable()) {
|
||||||
|
// TODO(addaleax): Find a way to reuse:
|
||||||
|
//
|
||||||
|
// LoadEnvironment(Environment*, const char*)
|
||||||
|
//
|
||||||
|
// instead and not add yet another main entry point here because this
|
||||||
|
// already duplicates existing code.
|
||||||
|
return StartExecution(env, "internal/main/single_executable_application");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (first_argv == "inspect") {
|
if (first_argv == "inspect") {
|
||||||
return StartExecution(env, "internal/main/inspect");
|
return StartExecution(env, "internal/main/inspect");
|
||||||
}
|
}
|
||||||
@ -1250,6 +1264,9 @@ static ExitCode StartInternal(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Start(int argc, char** argv) {
|
int Start(int argc, char** argv) {
|
||||||
|
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||||
|
std::tie(argc, argv) = sea::FixupArgsForSEA(argc, argv);
|
||||||
|
#endif
|
||||||
return static_cast<int>(StartInternal(argc, argv));
|
return static_cast<int>(StartInternal(argc, argv));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
V(process_wrap) \
|
V(process_wrap) \
|
||||||
V(process_methods) \
|
V(process_methods) \
|
||||||
V(report) \
|
V(report) \
|
||||||
|
V(sea) \
|
||||||
V(serdes) \
|
V(serdes) \
|
||||||
V(signal_wrap) \
|
V(signal_wrap) \
|
||||||
V(spawn_sync) \
|
V(spawn_sync) \
|
||||||
|
@ -87,6 +87,7 @@ class ExternalReferenceRegistry {
|
|||||||
V(url) \
|
V(url) \
|
||||||
V(util) \
|
V(util) \
|
||||||
V(pipe_wrap) \
|
V(pipe_wrap) \
|
||||||
|
V(sea) \
|
||||||
V(serdes) \
|
V(serdes) \
|
||||||
V(string_decoder) \
|
V(string_decoder) \
|
||||||
V(stream_wrap) \
|
V(stream_wrap) \
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include "node_binding.h"
|
#include "node_binding.h"
|
||||||
#include "node_external_reference.h"
|
#include "node_external_reference.h"
|
||||||
#include "node_internals.h"
|
#include "node_internals.h"
|
||||||
|
#include "node_sea.h"
|
||||||
#if HAVE_OPENSSL
|
#if HAVE_OPENSSL
|
||||||
#include "openssl/opensslv.h"
|
#include "openssl/opensslv.h"
|
||||||
#endif
|
#endif
|
||||||
@ -300,6 +301,10 @@ void Parse(
|
|||||||
// TODO(addaleax): Make that unnecessary.
|
// TODO(addaleax): Make that unnecessary.
|
||||||
|
|
||||||
DebugOptionsParser::DebugOptionsParser() {
|
DebugOptionsParser::DebugOptionsParser() {
|
||||||
|
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||||
|
if (sea::IsSingleExecutable()) return;
|
||||||
|
#endif
|
||||||
|
|
||||||
AddOption("--inspect-port",
|
AddOption("--inspect-port",
|
||||||
"set host:port for inspector",
|
"set host:port for inspector",
|
||||||
&DebugOptions::host_port,
|
&DebugOptions::host_port,
|
||||||
|
130
src/node_sea.cc
Normal file
130
src/node_sea.cc
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#include "node_sea.h"
|
||||||
|
|
||||||
|
#include "env-inl.h"
|
||||||
|
#include "node_external_reference.h"
|
||||||
|
#include "node_internals.h"
|
||||||
|
#include "node_union_bytes.h"
|
||||||
|
#include "simdutf.h"
|
||||||
|
#include "v8.h"
|
||||||
|
|
||||||
|
// The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by
|
||||||
|
// the Node.js project that is present only once in the entire binary. It is
|
||||||
|
// 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"
|
||||||
|
#include "postject-api.h"
|
||||||
|
#undef POSTJECT_SENTINEL_FUSE
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||||
|
|
||||||
|
using v8::Context;
|
||||||
|
using v8::FunctionCallbackInfo;
|
||||||
|
using v8::Local;
|
||||||
|
using v8::Object;
|
||||||
|
using v8::Value;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const std::string_view FindSingleExecutableCode() {
|
||||||
|
static const std::string_view sea_code = []() -> std::string_view {
|
||||||
|
size_t size;
|
||||||
|
#ifdef __APPLE__
|
||||||
|
postject_options options;
|
||||||
|
postject_options_init(&options);
|
||||||
|
options.macho_segment_name = "NODE_JS";
|
||||||
|
const char* code = static_cast<const char*>(
|
||||||
|
postject_find_resource("NODE_JS_CODE", &size, &options));
|
||||||
|
#else
|
||||||
|
const char* code = static_cast<const char*>(
|
||||||
|
postject_find_resource("NODE_JS_CODE", &size, nullptr));
|
||||||
|
#endif
|
||||||
|
return {code, size};
|
||||||
|
}();
|
||||||
|
return sea_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetSingleExecutableCode(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
node::Environment* env = node::Environment::GetCurrent(args);
|
||||||
|
|
||||||
|
static const std::string_view sea_code = FindSingleExecutableCode();
|
||||||
|
|
||||||
|
if (sea_code.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(joyeecheung): Use one-byte strings for ASCII-only source to save
|
||||||
|
// memory/binary size - using UTF16 by default results in twice of the size
|
||||||
|
// than necessary.
|
||||||
|
static const node::UnionBytes sea_code_union_bytes =
|
||||||
|
[]() -> node::UnionBytes {
|
||||||
|
size_t expected_u16_length =
|
||||||
|
simdutf::utf16_length_from_utf8(sea_code.data(), sea_code.size());
|
||||||
|
auto out = std::make_shared<std::vector<uint16_t>>(expected_u16_length);
|
||||||
|
size_t u16_length = simdutf::convert_utf8_to_utf16(
|
||||||
|
sea_code.data(),
|
||||||
|
sea_code.size(),
|
||||||
|
reinterpret_cast<char16_t*>(out->data()));
|
||||||
|
out->resize(u16_length);
|
||||||
|
return node::UnionBytes{out};
|
||||||
|
}();
|
||||||
|
|
||||||
|
args.GetReturnValue().Set(
|
||||||
|
sea_code_union_bytes.ToStringChecked(env->isolate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
namespace sea {
|
||||||
|
|
||||||
|
bool IsSingleExecutable() {
|
||||||
|
return postject_has_resource();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
|
||||||
|
// Repeats argv[0] at position 1 on argv as a replacement for the missing
|
||||||
|
// entry point file path.
|
||||||
|
if (IsSingleExecutable()) {
|
||||||
|
char** new_argv = new char*[argc + 2];
|
||||||
|
int new_argc = 0;
|
||||||
|
new_argv[new_argc++] = argv[0];
|
||||||
|
new_argv[new_argc++] = argv[0];
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
new_argv[new_argc++] = argv[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
new_argv[new_argc] = nullptr;
|
||||||
|
|
||||||
|
argc = new_argc;
|
||||||
|
argv = new_argv;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {argc, argv};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Initialize(Local<Object> target,
|
||||||
|
Local<Value> unused,
|
||||||
|
Local<Context> context,
|
||||||
|
void* priv) {
|
||||||
|
SetMethod(
|
||||||
|
context, target, "getSingleExecutableCode", GetSingleExecutableCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
|
registry->Register(GetSingleExecutableCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sea
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
NODE_BINDING_CONTEXT_AWARE_INTERNAL(sea, node::sea::Initialize)
|
||||||
|
NODE_BINDING_EXTERNAL_REFERENCE(sea, node::sea::RegisterExternalReferences)
|
||||||
|
|
||||||
|
#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
23
src/node_sea.h
Normal file
23
src/node_sea.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#ifndef SRC_NODE_SEA_H_
|
||||||
|
#define SRC_NODE_SEA_H_
|
||||||
|
|
||||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
||||||
|
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||||
|
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
namespace sea {
|
||||||
|
|
||||||
|
bool IsSingleExecutable();
|
||||||
|
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv);
|
||||||
|
|
||||||
|
} // namespace sea
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||||
|
|
||||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
||||||
|
#endif // SRC_NODE_SEA_H_
|
35
test/fixtures/sea.js
vendored
Normal file
35
test/fixtures/sea.js
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const { Module: { createRequire } } = require('module');
|
||||||
|
const createdRequire = createRequire(__filename);
|
||||||
|
|
||||||
|
// Although, require('../common') works locally, that couldn't be used here
|
||||||
|
// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI.
|
||||||
|
const { expectWarning } = createdRequire(process.env.COMMON_DIRECTORY);
|
||||||
|
|
||||||
|
expectWarning('ExperimentalWarning',
|
||||||
|
'Single executable application is an experimental feature and ' +
|
||||||
|
'might change at any time');
|
||||||
|
|
||||||
|
const { deepStrictEqual, strictEqual, throws } = require('assert');
|
||||||
|
const { dirname } = require('path');
|
||||||
|
|
||||||
|
deepStrictEqual(process.argv, [process.execPath, process.execPath, '-a', '--b=c', 'd']);
|
||||||
|
|
||||||
|
strictEqual(require.cache, undefined);
|
||||||
|
strictEqual(require.extensions, undefined);
|
||||||
|
strictEqual(require.main, module);
|
||||||
|
strictEqual(require.resolve, undefined);
|
||||||
|
|
||||||
|
strictEqual(__filename, process.execPath);
|
||||||
|
strictEqual(__dirname, dirname(process.execPath));
|
||||||
|
strictEqual(module.exports, exports);
|
||||||
|
|
||||||
|
throws(() => require('./requirable.js'), {
|
||||||
|
code: 'ERR_UNKNOWN_BUILTIN_MODULE',
|
||||||
|
});
|
||||||
|
|
||||||
|
const requirable = createdRequire('./requirable.js');
|
||||||
|
deepStrictEqual(requirable, {
|
||||||
|
hello: 'world',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Hello, world! 😊');
|
110
test/parallel/test-single-executable-application.js
Normal file
110
test/parallel/test-single-executable-application.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
|
||||||
|
// This tests the creation of a single executable application.
|
||||||
|
|
||||||
|
const fixtures = require('../common/fixtures');
|
||||||
|
const tmpdir = require('../common/tmpdir');
|
||||||
|
const { copyFileSync, readFileSync, writeFileSync } = require('fs');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const { join } = require('path');
|
||||||
|
const { strictEqual } = require('assert');
|
||||||
|
|
||||||
|
if (!process.config.variables.single_executable_application)
|
||||||
|
common.skip('Single Executable Application support has been disabled.');
|
||||||
|
|
||||||
|
if (!['darwin', 'win32', 'linux'].includes(process.platform))
|
||||||
|
common.skip(`Unsupported platform ${process.platform}.`);
|
||||||
|
|
||||||
|
if (process.platform === 'linux' && process.config.variables.asan)
|
||||||
|
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
|
||||||
|
|
||||||
|
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
|
||||||
|
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
|
||||||
|
|
||||||
|
if (process.config.variables.node_shared)
|
||||||
|
common.skip('Running the resultant binary fails with ' +
|
||||||
|
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
|
||||||
|
'libnode.so.112: cannot open shared object file: No such file or directory`.');
|
||||||
|
|
||||||
|
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
|
||||||
|
common.skip('Running the resultant binary fails with ' +
|
||||||
|
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
|
||||||
|
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
|
||||||
|
|
||||||
|
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
|
||||||
|
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
|
||||||
|
|
||||||
|
if (process.config.variables.want_separate_host_toolset !== 0)
|
||||||
|
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
try {
|
||||||
|
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
|
||||||
|
if (!/^NAME="Ubuntu"/.test(osReleaseText)) {
|
||||||
|
throw new Error('Not Ubuntu.');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
common.skip('Only supported Linux distribution is Ubuntu.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.arch !== 'x64') {
|
||||||
|
common.skip(`Unsupported architecture for Linux - ${process.arch}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputFile = fixtures.path('sea.js');
|
||||||
|
const requirableFile = join(tmpdir.path, 'requirable.js');
|
||||||
|
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
|
||||||
|
|
||||||
|
tmpdir.refresh();
|
||||||
|
|
||||||
|
writeFileSync(requirableFile, `
|
||||||
|
module.exports = {
|
||||||
|
hello: 'world',
|
||||||
|
};
|
||||||
|
`);
|
||||||
|
|
||||||
|
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' ] : [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
execFileSync('codesign', [ '--sign', '-', outputFile ]);
|
||||||
|
execFileSync('codesign', [ '--verify', outputFile ]);
|
||||||
|
} else if (process.platform === 'win32') {
|
||||||
|
let signtoolFound = false;
|
||||||
|
try {
|
||||||
|
execFileSync('where', [ 'signtool' ]);
|
||||||
|
signtoolFound = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
if (signtoolFound) {
|
||||||
|
let certificatesFound = false;
|
||||||
|
try {
|
||||||
|
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', outputFile ]);
|
||||||
|
certificatesFound = true;
|
||||||
|
} catch (err) {
|
||||||
|
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (certificatesFound) {
|
||||||
|
execFileSync('signtool', 'verify', '/pa', 'SHA256', outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleExecutableApplicationOutput = execFileSync(
|
||||||
|
outputFile,
|
||||||
|
[ '-a', '--b=c', 'd' ],
|
||||||
|
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
|
||||||
|
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');
|
Loading…
x
Reference in New Issue
Block a user