summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarshan Sen <raisinten@gmail.com>2023-02-18 08:19:18 +0530
committerGitHub <noreply@github.com>2023-02-18 02:49:18 +0000
commit9bbde3d7baef584f14569ef79f116e9d288c7aaa (patch)
tree8222435eb1eb88f41b6f710af7948e3496b6854f
parent2472b6742c0cea5eaf4a01aac3566b45f06da59f (diff)
downloadnode-new-9bbde3d7baef584f14569ef79f116e9d288c7aaa.tar.gz
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>
-rwxr-xr-xconfigure.py10
-rw-r--r--doc/api/index.md1
-rw-r--r--doc/api/single-executable-applications.md140
-rw-r--r--doc/contributing/maintaining-single-executable-application-support.md81
-rw-r--r--lib/internal/main/single_executable_application.js55
-rw-r--r--node.gyp7
-rw-r--r--src/node.cc17
-rw-r--r--src/node_binding.cc1
-rw-r--r--src/node_external_reference.h1
-rw-r--r--src/node_options.cc5
-rw-r--r--src/node_sea.cc130
-rw-r--r--src/node_sea.h23
-rw-r--r--test/fixtures/sea.js35
-rw-r--r--test/parallel/test-single-executable-application.js110
14 files changed, 615 insertions, 1 deletions
diff --git a/configure.py b/configure.py
index 215aee5d80..3ddbbebd3f 100755
--- a/configure.py
+++ b/configure.py
@@ -146,6 +146,12 @@ parser.add_argument('--no-ifaddrs',
default=None,
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",
action="store_true",
dest="fully_static",
@@ -1357,6 +1363,10 @@ def configure_node(o):
if options.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)
if flavor != 'win' and options.with_ltcg:
raise Exception('Link Time Code Generation is only supported on Windows.')
diff --git a/doc/api/index.md b/doc/api/index.md
index 9c35550f5d..81ef77491b 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -52,6 +52,7 @@
* [Readline](readline.md)
* [REPL](repl.md)
* [Report](report.md)
+* [Single executable applications](single-executable-applications.md)
* [Stream](stream.md)
* [String decoder](string_decoder.md)
* [Test runner](test.md)
diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md
new file mode 100644
index 0000000000..ef0604ce61
--- /dev/null
+++ b/doc/api/single-executable-applications.md
@@ -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
diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md
new file mode 100644
index 0000000000..e3957230f3
--- /dev/null
+++ b/doc/contributing/maintaining-single-executable-application-support.md
@@ -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
diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js
new file mode 100644
index 0000000000..d9604cff72
--- /dev/null
+++ b/lib/internal/main/single_executable_application.js
@@ -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);
diff --git a/node.gyp b/node.gyp
index 605bc81193..6d1b2bf369 100644
--- a/node.gyp
+++ b/node.gyp
@@ -151,7 +151,8 @@
'include_dirs': [
'src',
- 'deps/v8/include'
+ 'deps/v8/include',
+ 'deps/postject'
],
'sources': [
@@ -449,6 +450,7 @@
'include_dirs': [
'src',
+ 'deps/postject',
'<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h
],
'dependencies': [
@@ -523,6 +525,7 @@
'src/node_report.cc',
'src/node_report_module.cc',
'src/node_report_utils.cc',
+ 'src/node_sea.cc',
'src/node_serdes.cc',
'src/node_shadow_realm.cc',
'src/node_snapshotable.cc',
@@ -633,6 +636,7 @@
'src/node_report.h',
'src/node_revert.h',
'src/node_root_certs.h',
+ 'src/node_sea.h',
'src/node_shadow_realm.h',
'src/node_snapshotable.h',
'src/node_snapshot_builder.h',
@@ -675,6 +679,7 @@
'src/util-inl.h',
# Dependency headers
'deps/v8/include/v8.h',
+ 'deps/postject/postject-api.h'
# javascript files to make for an even more pleasant IDE experience
'<@(library_files)',
'<@(deps_files)',
diff --git a/src/node.cc b/src/node.cc
index 994a2f6770..f79f4421f8 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -39,6 +39,7 @@
#include "node_realm-inl.h"
#include "node_report.h"
#include "node_revert.h"
+#include "node_sea.h"
#include "node_snapshot_builder.h"
#include "node_v8_platform-inl.h"
#include "node_version.h"
@@ -122,6 +123,7 @@
#include <cstring>
#include <string>
+#include <tuple>
#include <vector>
namespace node {
@@ -310,6 +312,18 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
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") {
return StartExecution(env, "internal/main/inspect");
}
@@ -1250,6 +1264,9 @@ static ExitCode StartInternal(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));
}
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 243a44994f..db607ea298 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -62,6 +62,7 @@
V(process_wrap) \
V(process_methods) \
V(report) \
+ V(sea) \
V(serdes) \
V(signal_wrap) \
V(spawn_sync) \
diff --git a/src/node_external_reference.h b/src/node_external_reference.h
index c3ab57c0bb..38ba3b21a7 100644
--- a/src/node_external_reference.h
+++ b/src/node_external_reference.h
@@ -87,6 +87,7 @@ class ExternalReferenceRegistry {
V(url) \
V(util) \
V(pipe_wrap) \
+ V(sea) \
V(serdes) \
V(string_decoder) \
V(stream_wrap) \
diff --git a/src/node_options.cc b/src/node_options.cc
index c1f97a5d92..e7b0ba1073 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -5,6 +5,7 @@
#include "node_binding.h"
#include "node_external_reference.h"
#include "node_internals.h"
+#include "node_sea.h"
#if HAVE_OPENSSL
#include "openssl/opensslv.h"
#endif
@@ -300,6 +301,10 @@ void Parse(
// TODO(addaleax): Make that unnecessary.
DebugOptionsParser::DebugOptionsParser() {
+#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
+ if (sea::IsSingleExecutable()) return;
+#endif
+
AddOption("--inspect-port",
"set host:port for inspector",
&DebugOptions::host_port,
diff --git a/src/node_sea.cc b/src/node_sea.cc
new file mode 100644
index 0000000000..18b661ce4f
--- /dev/null
+++ b/src/node_sea.cc
@@ -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)
diff --git a/src/node_sea.h b/src/node_sea.h
new file mode 100644
index 0000000000..97bf0115e0
--- /dev/null
+++ b/src/node_sea.h
@@ -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_
diff --git a/test/fixtures/sea.js b/test/fixtures/sea.js
new file mode 100644
index 0000000000..efdc32708b
--- /dev/null
+++ b/test/fixtures/sea.js
@@ -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! 😊');
diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js
new file mode 100644
index 0000000000..bc0eb763de
--- /dev/null
+++ b/test/parallel/test-single-executable-application.js
@@ -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');