From 6559b90984cee64c600f9e50dd18ab73d76af7eb Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 21 Oct 2022 17:42:52 +0530 Subject: [PATCH 01/73] 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 --- configure.py | 10 +++ doc/api/index.md | 1 + doc/api/single-executable-applications.md | 38 ++++++++ ...g-single-executable-application-support.md | 79 +++++++++++++++++ .../main/single_executable_application.js | 49 +++++++++++ node.gyp | 5 +- src/node.cc | 41 +++++++++ src/node_options.cc | 4 + src/util.cc | 29 +++++++ src/util.h | 4 + .../test-single-executable-application.js | 86 +++++++++++++++++++ typings/internalBinding/util.d.ts | 1 + 12 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 doc/api/single-executable-applications.md create mode 100644 doc/contributing/maintaining-single-executable-application-support.md create mode 100644 lib/internal/main/single_executable_application.js create mode 100644 test/parallel/test-single-executable-application.js diff --git a/configure.py b/configure.py index 215aee5d805e4c..3ddbbebd3f7f20 100755 --- a/configure.py +++ b/configure.py @@ -146,6 +146,12 @@ 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 9c35550f5daf81..81ef77491b1f1b 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 00000000000000..6c6a0b19059ae9 --- /dev/null +++ b/doc/api/single-executable-applications.md @@ -0,0 +1,38 @@ +# Single executable applications + + + +> Stability: 1 - Experimental: This feature is currently being designed and will +> still change. + + + +Node.js supports the creation of [single executable applications][] by allowing +the injection of a JavaScript file into the binary. During start up, the program +checks if a resource (on [PE][]) or section (on [Mach-O][] or [ELF][]) named +`NODE_JS_CODE` exists. If it is found, it executes its contents, otherwise it +operates like plain Node.js. + +This feature allows the distribution of a Node.js application conveniently to a +system that does not have Node.js installed. + +A bundled JavaScript file can be turned into a single executable application +with any other tool which can inject resources or sections, like [postject][]: + +```console +$ cat hello.js +console.log(`Hello, ${process.argv[2]}!`); +$ cp $(command -v node) hello +$ npx postject hello NODE_JS_CODE hello.js +$ ./hello world +Hello, world! +``` + +This currently only supports running a single embedded [CommonJS][] file. + +[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 +[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 00000000000000..f0d77865091116 --- /dev/null +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -0,0 +1,79 @@ +# 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 and use it at 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 yet to come: + +* Running an embedded ESM file. +* Running an archive of multiple files. +* Accepting [Node.js-specific CLI options][] embedded into the binary. +* [XCOFF][] executable format. + +## 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-specific 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 00000000000000..c4b3f0db553544 --- /dev/null +++ b/lib/internal/main/single_executable_application.js @@ -0,0 +1,49 @@ +'use strict'; +const { + prepareMainThreadExecution, + markBootstrapComplete, +} = require('internal/process/pre_execution'); +const { + privateSymbols: { + single_executable_application_code, + }, +} = internalBinding('util'); +const { Module, wrapSafe } = require('internal/modules/cjs/loader'); +const { createRequire } = Module; + +prepareMainThreadExecution(); +markBootstrapComplete(); + +// 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 = process[single_executable_application_code].toString(); +const compiledWrapper = wrapSafe(filename, contents); + +const customModule = new Module(filename, null); +customModule.filename = filename; +customModule.paths = Module._nodeModulePaths(customModule.path); + +const customExports = customModule.exports; + +const customRequire = createRequire(filename); +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 605bc811936a6f..57f1b2f5fce744 100644 --- a/node.gyp +++ b/node.gyp @@ -151,7 +151,8 @@ 'include_dirs': [ 'src', - 'deps/v8/include' + 'deps/v8/include', + 'test/fixtures/postject-copy/node_modules/postject/dist' ], 'sources': [ @@ -449,6 +450,7 @@ 'include_dirs': [ 'src', + 'test/fixtures/postject-copy/node_modules/postject/dist', '<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h ], 'dependencies': [ @@ -675,6 +677,7 @@ 'src/util-inl.h', # Dependency headers 'deps/v8/include/v8.h', + 'test/fixtures/postject-copy/node_modules/postject/dist/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 f92be4b089db87..4b91efc604a16a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -310,6 +310,27 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { first_argv = env->argv()[1]; } +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + size_t single_executable_application_size = 0; + const char* single_executable_application_code = + FindSingleExecutableCode(&single_executable_application_size); + if (single_executable_application_code != nullptr) { + Local buffer = + Buffer::New( + env->isolate(), + const_cast(single_executable_application_code), + single_executable_application_size, + [](char* data, void* hint) {}, + nullptr) + .ToLocalChecked(); + env->process_object() + ->SetPrivate( + env->context(), env->single_executable_application_code(), buffer) + .Check(); + return StartExecution(env, "internal/main/single_executable_application"); + } +#endif + if (first_argv == "inspect") { return StartExecution(env, "internal/main/inspect"); } @@ -1250,6 +1271,26 @@ static ExitCode StartInternal(int argc, char** argv) { } int Start(int argc, char** argv) { +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + // Repeats argv[0] at position 1 on argv as a replacement for the missing + // entry point file path. + if (FindSingleExecutableCode() != nullptr) { + 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; + } +#endif + return static_cast(StartInternal(argc, argv)); } diff --git a/src/node_options.cc b/src/node_options.cc index c1f97a5d9207eb..75352cea2b3bb9 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -300,6 +300,10 @@ void Parse( // TODO(addaleax): Make that unnecessary. DebugOptionsParser::DebugOptionsParser() { +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (FindSingleExecutableCode() != nullptr) return; +#endif + AddOption("--inspect-port", "set host:port for inspector", &DebugOptions::host_port, diff --git a/src/util.cc b/src/util.cc index 006eb068982d75..b2c63a040e7356 100644 --- a/src/util.cc +++ b/src/util.cc @@ -31,6 +31,10 @@ #include "string_bytes.h" #include "uv.h" +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +#include "postject-api.h" +#endif + #ifdef _WIN32 #include // _S_IREAD _S_IWRITE #include @@ -53,6 +57,12 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames. +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +static bool single_executable_application_code_loaded = false; +static size_t single_executable_application_size = 0; +static const char* single_executable_application_code = nullptr; +#endif + namespace node { using v8::ArrayBufferView; @@ -592,4 +602,23 @@ Local UnionBytes::ToStringChecked(Isolate* isolate) const { } } +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +const char* FindSingleExecutableCode(size_t* size) { + // TODO(RaisinTen): Use a fuse when https://github.com/nodejs/postject/pull/59 + // lands. + if (single_executable_application_code_loaded == false) { + single_executable_application_code = + static_cast(postject_find_resource( + "NODE_JS_CODE", &single_executable_application_size, nullptr)); + single_executable_application_code_loaded = true; + } + + if (size != nullptr) { + *size = single_executable_application_size; + } + + return single_executable_application_code; +} +#endif + } // namespace node diff --git a/src/util.h b/src/util.h index d0e4bd41ec92c8..39e10ca0db469b 100644 --- a/src/util.h +++ b/src/util.h @@ -948,6 +948,10 @@ void SetConstructorFunction(v8::Isolate* isolate, SetConstructorFunctionFlag flag = SetConstructorFunctionFlag::SET_CLASS_NAME); +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +const char* FindSingleExecutableCode(size_t* size = nullptr); +#endif + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js new file mode 100644 index 00000000000000..78777debe774b0 --- /dev/null +++ b/test/parallel/test-single-executable-application.js @@ -0,0 +1,86 @@ +'use strict'; +const common = require('../common'); + +if (!process.config.variables.single_executable_application) + common.skip('Single Executable Application support has been disabled.'); + +if (process.config.variables.asan) + common.skip('ASAN builds fail with a SEGV on unknown address due to ' + + 'a READ memory access from FindSingleExecutableCode'); + +// This tests the creation of a single executable application. + +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync } = require('fs'); +const { execSync } = require('child_process'); +const { join } = require('path'); +const { strictEqual } = require('assert'); + +const inputFile = join(tmpdir.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', +}; +`); + +writeFileSync(inputFile, ` +require('../common'); + +const { deepStrictEqual, strictEqual } = require('assert'); +const { dirname } = require('path'); + +deepStrictEqual(process.argv, [process.execPath, process.execPath, '-a', '--b=c', 'd']); + +strictEqual(__filename, process.execPath); +strictEqual(__dirname, dirname(process.execPath)); +strictEqual(module.exports, exports); +strictEqual(require.main, module); + +const requirable = require('./requirable.js'); +deepStrictEqual(requirable, { + hello: 'world', +}); + +console.log('Hello, world!'); +`); +copyFileSync(process.execPath, outputFile); +if (process.platform === 'win32') { + execSync(`test\\fixtures\\postject-copy\\node_modules\\.bin\\postject.cmd ${outputFile} NODE_JS_CODE ${inputFile}`); +} else { + execSync(`test/fixtures/postject-copy/node_modules/.bin/postject ${outputFile} NODE_JS_CODE ${inputFile}`); +} + +// Verifying code signing using a self-signed certificate. +if (process.platform === 'darwin') { + let codesignFound = false; + try { + execSync('command -v codesign'); + codesignFound = true; + } catch (err) { + console.log(err.message); + } + if (codesignFound) { + execSync(`codesign --sign - ${outputFile}`); + execSync(`codesign --verify ${outputFile}`); + } +} else if (process.platform === 'win32') { + let signtoolFound = false; + try { + execSync('where signtool'); + signtoolFound = true; + } catch (err) { + console.log(err.message); + } + if (signtoolFound) { + execSync(`signtool sign /fd SHA256 ${outputFile}`); + execSync(`signtool verify /pa SHA256 ${outputFile}`); + } +} + +const singleExecutableApplicationOutput = execSync(`${outputFile} -a --b=c d`); +strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world!\n'); diff --git a/typings/internalBinding/util.d.ts b/typings/internalBinding/util.d.ts index 002f8eb26d93e6..3bf3578bfc7dac 100644 --- a/typings/internalBinding/util.d.ts +++ b/typings/internalBinding/util.d.ts @@ -16,6 +16,7 @@ declare function InternalBinding(binding: 'util'): { napi_wrapper: 5; untransferable_object_private_symbol: 6; exiting_aliased_Uint32Array: 7; + single_executable_application_code: 8; kPending: 0; kFulfilled: 1; From 444ca790f97d7e598a64d4872820b4dcc054b9cd Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 23 Dec 2022 13:57:27 +0530 Subject: [PATCH 02/73] test: fix `'node': No such file or directory` error on CI `node` does not exist on some of the CI systems. Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 78777debe774b0..59b3ebbe9b3f92 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -49,11 +49,8 @@ deepStrictEqual(requirable, { console.log('Hello, world!'); `); copyFileSync(process.execPath, outputFile); -if (process.platform === 'win32') { - execSync(`test\\fixtures\\postject-copy\\node_modules\\.bin\\postject.cmd ${outputFile} NODE_JS_CODE ${inputFile}`); -} else { - execSync(`test/fixtures/postject-copy/node_modules/.bin/postject ${outputFile} NODE_JS_CODE ${inputFile}`); -} +const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); +execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile}`); // Verifying code signing using a self-signed certificate. if (process.platform === 'darwin') { From a30e3fc2eaf7cd6a2f14f67f4700810be57d2baf Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 23 Dec 2022 16:38:20 +0530 Subject: [PATCH 03/73] test: fix `Cannot find module '../common'` error on Jenkins CI This happened because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 59b3ebbe9b3f92..802626fc88620d 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -29,7 +29,9 @@ module.exports = { `); writeFileSync(inputFile, ` -require('../common'); +// Although, require('../common') works locally, that couldn't be used here +// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. +require('${join(__dirname, '..', 'common')}'); const { deepStrictEqual, strictEqual } = require('assert'); const { dirname } = require('path'); From 5caad766b6296d999a03a1a0803a55e5f5e68cde Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 23 Dec 2022 17:35:11 +0530 Subject: [PATCH 04/73] test: skip test on failing platforms Signed-off-by: Darshan Sen --- .../test-single-executable-application.js | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 802626fc88620d..78d092e6940adf 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -8,6 +8,12 @@ if (process.config.variables.asan) common.skip('ASAN builds fail with a SEGV on unknown address due to ' + 'a READ memory access from FindSingleExecutableCode'); +if (process.platform === 'aix') + common.skip('XCOFF binary format not supported.'); + +if (process.platform === 'freebsd') + common.skip('Running the resultant binary fails with `Exec format error`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); @@ -54,32 +60,11 @@ copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile}`); -// Verifying code signing using a self-signed certificate. if (process.platform === 'darwin') { - let codesignFound = false; - try { - execSync('command -v codesign'); - codesignFound = true; - } catch (err) { - console.log(err.message); - } - if (codesignFound) { - execSync(`codesign --sign - ${outputFile}`); - execSync(`codesign --verify ${outputFile}`); - } -} else if (process.platform === 'win32') { - let signtoolFound = false; - try { - execSync('where signtool'); - signtoolFound = true; - } catch (err) { - console.log(err.message); - } - if (signtoolFound) { - execSync(`signtool sign /fd SHA256 ${outputFile}`); - execSync(`signtool verify /pa SHA256 ${outputFile}`); - } + execSync(`codesign --sign - ${outputFile}`); + execSync(`codesign --verify ${outputFile}`); } +// TODO(RaisinTen): Test code signing on Windows. const singleExecutableApplicationOutput = execSync(`${outputFile} -a --b=c d`); strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world!\n'); From 1c5968def576b3e842a06cc27a3fd6050012c3d3 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 23 Dec 2022 19:03:55 +0530 Subject: [PATCH 05/73] test: make sure that double backslashes in Windows paths aren't removed Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 78d092e6940adf..968a3b22ebb509 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -34,10 +34,17 @@ module.exports = { }; `); +let commonPathForSea = join(__dirname, '..', 'common'); +if (process.platform === 'win32') { + // Otherwise, the double backslashes get replaced with single backslashes in + // the generated file. + commonPathForSea = commonPathForSea.replace(/\\/g, '\\\\'); +} + writeFileSync(inputFile, ` // Although, require('../common') works locally, that couldn't be used here // because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. -require('${join(__dirname, '..', 'common')}'); +require('${commonPathForSea}'); const { deepStrictEqual, strictEqual } = require('assert'); const { dirname } = require('path'); From 290d69f36b646a9f5f29c33d38a23280d1b3a1ca Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 3 Jan 2023 16:54:46 +0530 Subject: [PATCH 06/73] src: move argv replacement to another function Refs: https://github.com/nodejs/node/pull/45038#discussion_r1056886009 Signed-off-by: Darshan Sen --- src/node.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node.cc b/src/node.cc index 4b91efc604a16a..2b8921e664cd0b 100644 --- a/src/node.cc +++ b/src/node.cc @@ -122,6 +122,7 @@ #include #include +#include #include namespace node { @@ -1270,7 +1271,7 @@ static ExitCode StartInternal(int argc, char** argv) { return LoadSnapshotDataAndRun(&snapshot_data, result.get()); } -int Start(int argc, char** argv) { +static std::tuple FixupArgsForSEA(int argc, char** argv) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION // Repeats argv[0] at position 1 on argv as a replacement for the missing // entry point file path. @@ -1290,7 +1291,11 @@ int Start(int argc, char** argv) { argv = new_argv; } #endif + return { argc, argv }; +} +int Start(int argc, char** argv) { + std::tie(argc, argv) = FixupArgsForSEA(argc, argv); return static_cast(StartInternal(argc, argv)); } From da1b134f8c12fa81891ff9cf44748007d4306674 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 3 Jan 2023 17:16:56 +0530 Subject: [PATCH 07/73] doc: fix info about using notes on ELF Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 6c6a0b19059ae9..cc6ccaeb26653e 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -9,9 +9,9 @@ Node.js supports the creation of [single executable applications][] by allowing the injection of a JavaScript file into the binary. During start up, the program -checks if a resource (on [PE][]) or section (on [Mach-O][] or [ELF][]) named -`NODE_JS_CODE` exists. If it is found, it executes its contents, otherwise it -operates like plain Node.js. +checks if a resource (on [PE][]) or section (on [Mach-O][]) or note (on [ELF][]) +named `NODE_JS_CODE` exists. If it is found, it executes its contents, otherwise +it operates like plain Node.js. This feature allows the distribution of a Node.js application conveniently to a system that does not have Node.js installed. From f08ca63282f01f56e8ae1002b5d5c4a09486bf74 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 3 Jan 2023 17:40:17 +0530 Subject: [PATCH 08/73] test: test code signing on Windows Signed-off-by: Darshan Sen --- .../test-single-executable-application.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 968a3b22ebb509..ea8ebe298a334a 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -70,8 +70,29 @@ execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${input if (process.platform === 'darwin') { execSync(`codesign --sign - ${outputFile}`); execSync(`codesign --verify ${outputFile}`); +} else if (process.platform === 'win32') { + let signtoolFound = false; + try { + execSync('where signtool'); + signtoolFound = true; + } catch (err) { + console.log(err.message); + } + if (signtoolFound) { + let certificatesFound = false; + try { + execSync(`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) { + execSync(`signtool verify /pa SHA256 ${outputFile}`); + } + } } -// TODO(RaisinTen): Test code signing on Windows. const singleExecutableApplicationOutput = execSync(`${outputFile} -a --b=c d`); strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world!\n'); From afc5d75c1f6626c797c213769f1d208902ef1969 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 3 Jan 2023 18:01:10 +0530 Subject: [PATCH 09/73] lib,test: emit experimental warning Signed-off-by: Darshan Sen --- lib/internal/main/single_executable_application.js | 3 +++ test/parallel/test-single-executable-application.js | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js index c4b3f0db553544..7be59a057f567f 100644 --- a/lib/internal/main/single_executable_application.js +++ b/lib/internal/main/single_executable_application.js @@ -8,12 +8,15 @@ const { single_executable_application_code, }, } = internalBinding('util'); +const { emitExperimentalWarning } = require('internal/util'); const { Module, wrapSafe } = require('internal/modules/cjs/loader'); const { createRequire } = Module; prepareMainThreadExecution(); markBootstrapComplete(); +emitExperimentalWarning('Single executable application'); + // This is roughly the same as: // // const mod = new Module(filename); diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index ea8ebe298a334a..d22e7f2cc14385 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -44,7 +44,11 @@ if (process.platform === 'win32') { writeFileSync(inputFile, ` // Although, require('../common') works locally, that couldn't be used here // because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. -require('${commonPathForSea}'); +const { expectWarning } = require('${commonPathForSea}'); + +expectWarning('ExperimentalWarning', + 'Single executable application is an experimental feature and ' + + 'might change at any time'); const { deepStrictEqual, strictEqual } = require('assert'); const { dirname } = require('path'); From da74406cfdc7d6ddefe518ea9ebef8cf7560d765 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 3 Jan 2023 18:03:55 +0530 Subject: [PATCH 10/73] fixup! src: move argv replacement to another function Signed-off-by: Darshan Sen --- src/node.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node.cc b/src/node.cc index 2b8921e664cd0b..124fd884e68c35 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1291,7 +1291,7 @@ static std::tuple FixupArgsForSEA(int argc, char** argv) { argv = new_argv; } #endif - return { argc, argv }; + return {argc, argv}; } int Start(int argc, char** argv) { From 412d59255b0ac4e9a45d939d34b35604e50058e0 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:13:45 +0530 Subject: [PATCH 11/73] test: skip on debug builds on Linux Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index d22e7f2cc14385..7c5fa49e86a9d4 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -14,6 +14,9 @@ if (process.platform === 'aix') if (process.platform === 'freebsd') common.skip('Running the resultant binary fails with `Exec format error`.'); +if (process.platform === 'linux' && process.config.variables.is_debug === 1) + common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 10b0fcd7a8fa25e896302b176433d6625983fdf2 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:17:35 +0530 Subject: [PATCH 12/73] test: skip on shared builds Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 7c5fa49e86a9d4..42e187fbbd2d09 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -17,6 +17,11 @@ if (process.platform === 'freebsd') 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`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 3addc198d3d946953d0c2519c84409a17ad10e65 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:27:12 +0530 Subject: [PATCH 13/73] test: skip on --without-ssl and --shared-openssl Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 42e187fbbd2d09..a8021ba123702a 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -22,6 +22,9 @@ if (process.config.variables.node_shared) '`/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.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`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 232ece58f9abbbeb16c2cea2f12acb816eb70cfb Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:31:07 +0530 Subject: [PATCH 14/73] test: skip on test-ibm-rhel8-s390x-1 Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index a8021ba123702a..00baf2d3b1b39a 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -25,6 +25,9 @@ if (process.config.variables.node_shared) 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.env.NODE_NAME === 'test-ibm-rhel8-s390x-1') + common.skip('Running the resultant binary fails with `memory access out of bounds`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From f24e37c0dc877069b412229c9397d779dcc3ba34 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:40:45 +0530 Subject: [PATCH 15/73] test: skip on smartos Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 00baf2d3b1b39a..86bc06fb8eedbb 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -28,6 +28,9 @@ if (!process.config.variables.node_use_openssl || process.config.variables.node_ if (process.env.NODE_NAME === 'test-ibm-rhel8-s390x-1') common.skip('Running the resultant binary fails with `memory access out of bounds`.'); +if (process.env.NODE_NAME === 'test-equinix_mnx-smartos20-x64-4') + common.skip('Injection fails with `Can\'t convert PT_NOTE.virtual_address into an offset (0x0)`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 8126bb388536f1bf3a1ec6a9c5e0771fb0737a8a Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 4 Jan 2023 18:50:48 +0530 Subject: [PATCH 16/73] test: skip when cross-compiling Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 86bc06fb8eedbb..958d14a0d53cd4 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -31,6 +31,9 @@ if (process.env.NODE_NAME === 'test-ibm-rhel8-s390x-1') if (process.env.NODE_NAME === 'test-equinix_mnx-smartos20-x64-4') common.skip('Injection fails with `Can\'t convert PT_NOTE.virtual_address into an offset (0x0)`.'); +if (process.config.variables.want_separate_host_toolset !== 0) + common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.'); + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 3c5c2cd3fb8888d6a3da243690fafc5da06b2fa8 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 5 Jan 2023 16:39:28 +0530 Subject: [PATCH 17/73] src: speed up case where no resource is present Signed-off-by: Darshan Sen --- src/node.cc | 2 +- src/node_options.cc | 2 +- src/util.cc | 6 ++++-- src/util.h | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/node.cc b/src/node.cc index 124fd884e68c35..e260e3dcc56a90 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1275,7 +1275,7 @@ static std::tuple FixupArgsForSEA(int argc, char** argv) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION // Repeats argv[0] at position 1 on argv as a replacement for the missing // entry point file path. - if (FindSingleExecutableCode() != nullptr) { + if (IsSingleExecutable()) { char** new_argv = new char*[argc + 2]; int new_argc = 0; new_argv[new_argc++] = argv[0]; diff --git a/src/node_options.cc b/src/node_options.cc index 75352cea2b3bb9..4b4357a4f46813 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -301,7 +301,7 @@ void Parse( DebugOptionsParser::DebugOptionsParser() { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (FindSingleExecutableCode() != nullptr) return; + if (IsSingleExecutable()) return; #endif AddOption("--inspect-port", diff --git a/src/util.cc b/src/util.cc index b2c63a040e7356..889760e4f85816 100644 --- a/src/util.cc +++ b/src/util.cc @@ -603,9 +603,11 @@ Local UnionBytes::ToStringChecked(Isolate* isolate) const { } #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +bool IsSingleExecutable() { + return postject_has_resource(); +} + const char* FindSingleExecutableCode(size_t* size) { - // TODO(RaisinTen): Use a fuse when https://github.com/nodejs/postject/pull/59 - // lands. if (single_executable_application_code_loaded == false) { single_executable_application_code = static_cast(postject_find_resource( diff --git a/src/util.h b/src/util.h index 39e10ca0db469b..2803becd20266e 100644 --- a/src/util.h +++ b/src/util.h @@ -949,6 +949,7 @@ void SetConstructorFunction(v8::Isolate* isolate, SetConstructorFunctionFlag::SET_CLASS_NAME); #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +bool IsSingleExecutable(); const char* FindSingleExecutableCode(size_t* size = nullptr); #endif From b47b68df2e95cb5a2466c64566a017e88923d326 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 5 Jan 2023 16:42:06 +0530 Subject: [PATCH 18/73] src: use custom fuse string Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 9 +++++++-- src/util.cc | 1 + test/parallel/test-single-executable-application.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index cc6ccaeb26653e..90d24d1fd60e9c 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -17,13 +17,18 @@ This feature allows the distribution of a Node.js application conveniently to a system that does not have Node.js installed. A bundled JavaScript file can be turned into a single executable application -with any other tool which can inject resources or sections, like [postject][]: +with any other tool which can inject resources into the Node.js binary. The tool +should also 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. + +One such tool is [postject][]: ```console $ cat hello.js console.log(`Hello, ${process.argv[2]}!`); $ cp $(command -v node) hello -$ npx postject hello NODE_JS_CODE hello.js +$ npx postject hello NODE_JS_CODE hello.js --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 $ ./hello world Hello, world! ``` diff --git a/src/util.cc b/src/util.cc index 889760e4f85816..9e788c824a4f10 100644 --- a/src/util.cc +++ b/src/util.cc @@ -32,6 +32,7 @@ #include "uv.h" #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION +#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0" #include "postject-api.h" #endif diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 958d14a0d53cd4..d3d4a7b14d4a56 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -89,7 +89,7 @@ console.log('Hello, world!'); `); copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); -execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile}`); +execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0`); if (process.platform === 'darwin') { execSync(`codesign --sign - ${outputFile}`); From 2fe91c64ccc3615b0b55f974756ac6d7e42010a2 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 5 Jan 2023 17:02:35 +0530 Subject: [PATCH 19/73] test: do not skip on asan builds Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index d3d4a7b14d4a56..05ffbc7c02847b 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -4,10 +4,6 @@ const common = require('../common'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); -if (process.config.variables.asan) - common.skip('ASAN builds fail with a SEGV on unknown address due to ' + - 'a READ memory access from FindSingleExecutableCode'); - if (process.platform === 'aix') common.skip('XCOFF binary format not supported.'); From e0a9af3e0fb5631f83a5383227706bfb5b408ea8 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 9 Jan 2023 17:07:10 +0530 Subject: [PATCH 20/73] src,doc,test: remove :0 from sentinel fuse string Imports https://github.com/nodejs/postject/pull/59/commits/6da6e04cb5ba175333a660db8d72d54897f27930. Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 2 +- src/util.cc | 2 +- test/parallel/test-single-executable-application.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 90d24d1fd60e9c..46559789a1bc60 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -28,7 +28,7 @@ One such tool is [postject][]: $ cat hello.js console.log(`Hello, ${process.argv[2]}!`); $ cp $(command -v node) hello -$ npx postject hello NODE_JS_CODE hello.js --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 +$ npx postject hello NODE_JS_CODE hello.js --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 $ ./hello world Hello, world! ``` diff --git a/src/util.cc b/src/util.cc index 9e788c824a4f10..21a992e46f869d 100644 --- a/src/util.cc +++ b/src/util.cc @@ -32,7 +32,7 @@ #include "uv.h" #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION -#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0" +#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2" #include "postject-api.h" #endif diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 05ffbc7c02847b..490cd7ef127b16 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -85,7 +85,7 @@ console.log('Hello, world!'); `); copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); -execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0`); +execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2`); if (process.platform === 'darwin') { execSync(`codesign --sign - ${outputFile}`); From 665e5f8a8e1350d42f55a73675861890fc26c42c Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 9 Jan 2023 17:10:19 +0530 Subject: [PATCH 21/73] test: skip on asan builds Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 490cd7ef127b16..2e4df6e33a77e7 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); +if (process.config.variables.asan) + common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.'); + if (process.platform === 'aix') common.skip('XCOFF binary format not supported.'); From fe6d4882f1b44a4a88e89141cc3aa13101c30fa2 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 19 Jan 2023 16:51:28 +0530 Subject: [PATCH 22/73] src: change segment name from `__POSTJECT` to `NODE_JS` on macOS Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 7 ++++--- src/util.cc | 5 ++++- test/parallel/test-single-executable-application.js | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 46559789a1bc60..4f079cffd18425 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -10,8 +10,8 @@ Node.js supports the creation of [single executable applications][] by allowing the injection of a JavaScript file into the binary. During start up, the program checks if a resource (on [PE][]) or section (on [Mach-O][]) or note (on [ELF][]) -named `NODE_JS_CODE` exists. If it is found, it executes its contents, otherwise -it operates like plain Node.js. +named `NODE_JS_CODE` exists (on macOS, in the `NODE_JS` segment). If it is +found, it executes its contents, otherwise it operates like plain Node.js. This feature allows the distribution of a Node.js application conveniently to a system that does not have Node.js installed. @@ -28,7 +28,8 @@ One such tool is [postject][]: $ cat hello.js console.log(`Hello, ${process.argv[2]}!`); $ 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 # Also add `--macho-segment-name NODE_JS` on macOS. $ ./hello world Hello, world! ``` diff --git a/src/util.cc b/src/util.cc index 21a992e46f869d..8ed6ff3c069415 100644 --- a/src/util.cc +++ b/src/util.cc @@ -610,9 +610,12 @@ bool IsSingleExecutable() { const char* FindSingleExecutableCode(size_t* size) { if (single_executable_application_code_loaded == false) { + postject_options options; + postject_options_init(&options); + options.macho_segment_name = "NODE_JS"; single_executable_application_code = static_cast(postject_find_resource( - "NODE_JS_CODE", &single_executable_application_size, nullptr)); + "NODE_JS_CODE", &single_executable_application_size, &options)); single_executable_application_code_loaded = true; } diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 2e4df6e33a77e7..88986b0b58aadb 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -88,7 +88,11 @@ console.log('Hello, world!'); `); copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); -execSync(`${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2`); +let postjectCommand = `${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; +if (process.platform === 'darwin') { + postjectCommand += ' --macho-segment-name NODE_JS'; +} +execSync(postjectCommand); if (process.platform === 'darwin') { execSync(`codesign --sign - ${outputFile}`); From 8c5ba962feac2327ccc8f5dd1fdb823e3de4ba64 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 30 Jan 2023 17:59:13 +0530 Subject: [PATCH 23/73] test: only run test on Ubuntu for Linux Signed-off-by: Darshan Sen --- .../test-single-executable-application.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 88986b0b58aadb..d7c9ae9fea6aa4 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -1,18 +1,16 @@ 'use strict'; const common = require('../common'); +const os = require('os'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); +if (['darwin', 'win32', 'linux'].indexOf(process.platform) === -1) + common.skip(`Unsupported platform ${process.platform}.`); + if (process.config.variables.asan) common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.'); -if (process.platform === 'aix') - common.skip('XCOFF binary format not supported.'); - -if (process.platform === 'freebsd') - common.skip('Running the resultant binary fails with `Exec format error`.'); - if (process.platform === 'linux' && process.config.variables.is_debug === 1) common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.'); @@ -24,15 +22,15 @@ if (process.config.variables.node_shared) 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.env.NODE_NAME === 'test-ibm-rhel8-s390x-1') - common.skip('Running the resultant binary fails with `memory access out of bounds`.'); - -if (process.env.NODE_NAME === 'test-equinix_mnx-smartos20-x64-4') - common.skip('Injection fails with `Can\'t convert PT_NOTE.virtual_address into an offset (0x0)`.'); - 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') { + if (!/Ubuntu/.test(os.version())) { + common.skip('Only supported Linux distribution is Ubuntu.'); + } +} + // This tests the creation of a single executable application. const tmpdir = require('../common/tmpdir'); From 285567945747caa7ac0f22124af20f9b9fda1bcb Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 31 Jan 2023 10:39:49 +0530 Subject: [PATCH 24/73] test: skip on non-x64 Linux archictectures Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index d7c9ae9fea6aa4..8ae7bb2e2221b1 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -1,6 +1,5 @@ 'use strict'; const common = require('../common'); -const os = require('os'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); @@ -25,11 +24,8 @@ if (!process.config.variables.node_use_openssl || process.config.variables.node_ 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') { - if (!/Ubuntu/.test(os.version())) { - common.skip('Only supported Linux distribution is Ubuntu.'); - } -} +if (process.platform === 'linux' && process.arch !== 'x64') + common.skip(`Unsupported architecture for Linux - ${process.arch}.`); // This tests the creation of a single executable application. From 9daeaf00acf1cd3106c8b2886ffecab08beb74ed Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Tue, 31 Jan 2023 16:12:02 +0530 Subject: [PATCH 25/73] test: add back ubuntu check for linux Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 8ae7bb2e2221b1..010a1ecae133bf 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -1,5 +1,6 @@ 'use strict'; const common = require('../common'); +const os = require('os'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); @@ -24,8 +25,15 @@ if (!process.config.variables.node_use_openssl || process.config.variables.node_ 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' && process.arch !== 'x64') - common.skip(`Unsupported architecture for Linux - ${process.arch}.`); +if (process.platform === 'linux') { + if (!/Ubuntu/.test(os.version())) { + common.skip('Only supported Linux distribution is Ubuntu.'); + } + + if (process.arch !== 'x64') { + common.skip(`Unsupported architecture for Linux - ${process.arch}.`); + } +} // This tests the creation of a single executable application. From bf4e51972afe0f81ed5da94aaaac0f9c4a10b779 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 2 Feb 2023 20:42:35 +0530 Subject: [PATCH 26/73] src: call IsSingleExecutable() before FindSingleExecutableCode() This also serves as a workaround for https://github.com/nodejs/postject/issues/70. Signed-off-by: Darshan Sen --- src/node.cc | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/node.cc b/src/node.cc index e260e3dcc56a90..1bbaf09616ba2f 100644 --- a/src/node.cc +++ b/src/node.cc @@ -312,23 +312,25 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - size_t single_executable_application_size = 0; - const char* single_executable_application_code = - FindSingleExecutableCode(&single_executable_application_size); - if (single_executable_application_code != nullptr) { - Local buffer = - Buffer::New( - env->isolate(), - const_cast(single_executable_application_code), - single_executable_application_size, - [](char* data, void* hint) {}, - nullptr) - .ToLocalChecked(); - env->process_object() - ->SetPrivate( - env->context(), env->single_executable_application_code(), buffer) - .Check(); - return StartExecution(env, "internal/main/single_executable_application"); + if (IsSingleExecutable()) { + size_t single_executable_application_size = 0; + const char* single_executable_application_code = + FindSingleExecutableCode(&single_executable_application_size); + if (single_executable_application_code != nullptr) { + Local buffer = + Buffer::New( + env->isolate(), + const_cast(single_executable_application_code), + single_executable_application_size, + [](char* data, void* hint) {}, + nullptr) + .ToLocalChecked(); + env->process_object() + ->SetPrivate( + env->context(), env->single_executable_application_code(), buffer) + .Check(); + return StartExecution(env, "internal/main/single_executable_application"); + } } #endif From a4ac3551ef885c804a237fd1233c7f2dbd954737 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 2 Feb 2023 21:06:54 +0530 Subject: [PATCH 27/73] lib: expose require without fs access Refs: https://github.com/nodejs/node/pull/45038#discussion_r1091997956 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 34 +++++++++++++++++++ .../main/single_executable_application.js | 11 ++++-- .../test-single-executable-application.js | 19 ++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 4f079cffd18425..f38c9424fcbc66 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -36,9 +36,43 @@ Hello, world! This currently only supports running a single embedded [CommonJS][] file. +## Notes + +### `require(id)` in the injected module is not file based + +This is not the same as [`require()`][]. This also does not have any of the +properties that [`require()`][] has except [`require.main`][]. It is used to +import only built-in modules. Attempting to import a module available on the +file system will throw an error. + +Since the injected JavaScript file would be bundled into a standalone module by +default in most cases, there shouldn't be any need for a file based `require()` +API. Not having a file based `require()` API in the single-executable +application should also safeguard users from some security vulnerabilities. + +However, if a file based `require()` is still needed, that can also be achieved: + +```js +const { Module: { createRequire } } = require('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`][]. + [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 [postject]: https://github.com/nodejs/postject [single executable applications]: https://github.com/nodejs/single-executable diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js index 7be59a057f567f..8fad64d411eee4 100644 --- a/lib/internal/main/single_executable_application.js +++ b/lib/internal/main/single_executable_application.js @@ -10,7 +10,7 @@ const { } = internalBinding('util'); const { emitExperimentalWarning } = require('internal/util'); const { Module, wrapSafe } = require('internal/modules/cjs/loader'); -const { createRequire } = Module; +const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); prepareMainThreadExecution(); markBootstrapComplete(); @@ -37,7 +37,14 @@ customModule.paths = Module._nodeModulePaths(customModule.path); const customExports = customModule.exports; -const customRequire = createRequire(filename); +function customRequire(path) { + if (!Module.isBuiltin(path)) { + throw new ERR_UNKNOWN_BUILTIN_MODULE(path); + } + + return require(path); +} + customRequire.main = customModule; const customFilename = customModule.filename; diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 010a1ecae133bf..c1e0ff42aaf31a 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -63,25 +63,36 @@ if (process.platform === 'win32') { } writeFileSync(inputFile, ` +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 } = require('${commonPathForSea}'); +const { expectWarning } = createdRequire('${commonPathForSea}'); expectWarning('ExperimentalWarning', 'Single executable application is an experimental feature and ' + 'might change at any time'); -const { deepStrictEqual, strictEqual } = require('assert'); +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); -strictEqual(require.main, module); -const requirable = require('./requirable.js'); +throws(() => require('./requirable.js'), { + code: 'ERR_UNKNOWN_BUILTIN_MODULE', +}); + +const requirable = createdRequire('./requirable.js'); deepStrictEqual(requirable, { hello: 'world', }); From 13dd5036bde983ea6231bd94f1b0fffa5bc64109 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 3 Feb 2023 17:56:48 +0530 Subject: [PATCH 28/73] src: wrap macho_segment_name assignment into a pre-processor for macOS Refs: https://github.com/nodejs/node/pull/45038#discussion_r1095552926 Signed-off-by: Darshan Sen --- src/util.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util.cc b/src/util.cc index 8ed6ff3c069415..b965e38b1abf8c 100644 --- a/src/util.cc +++ b/src/util.cc @@ -610,12 +610,18 @@ bool IsSingleExecutable() { const char* FindSingleExecutableCode(size_t* size) { if (single_executable_application_code_loaded == false) { +#ifdef __APPLE__ postject_options options; postject_options_init(&options); options.macho_segment_name = "NODE_JS"; single_executable_application_code = static_cast(postject_find_resource( "NODE_JS_CODE", &single_executable_application_size, &options)); +#else + single_executable_application_code = + static_cast(postject_find_resource( + "NODE_JS_CODE", &single_executable_application_size, nullptr)); +#endif single_executable_application_code_loaded = true; } From ea4a1390d3a3212ea0d6ada807167cf85b308916 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 3 Feb 2023 19:23:39 +0530 Subject: [PATCH 29/73] test: detect Ubuntu by parsing '/etc/os-release' `os.version()` might contain the `Ubuntu` substring. Signed-off-by: Darshan Sen --- .../test-single-executable-application.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index c1e0ff42aaf31a..ca7ffd14557fd8 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -1,6 +1,13 @@ 'use strict'; const common = require('../common'); -const os = require('os'); + +// This tests the creation of a single executable application. + +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, readFileSync, writeFileSync } = require('fs'); +const { execSync } = 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.'); @@ -26,7 +33,12 @@ 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') { - if (!/Ubuntu/.test(os.version())) { + try { + const osReleaseText = readFileSync('/etc/os-release'); + if (/^NAME="Ubuntu"/.test(osReleaseText)) { + throw new Error('Not Ubuntu.'); + } + } catch { common.skip('Only supported Linux distribution is Ubuntu.'); } @@ -35,14 +47,6 @@ if (process.platform === 'linux') { } } -// This tests the creation of a single executable application. - -const tmpdir = require('../common/tmpdir'); -const { copyFileSync, writeFileSync } = require('fs'); -const { execSync } = require('child_process'); -const { join } = require('path'); -const { strictEqual } = require('assert'); - const inputFile = join(tmpdir.path, 'sea.js'); const requirableFile = join(tmpdir.path, 'requirable.js'); const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea'); From 5664222d79274f047820ecfe2c8fb5547b2a8d4a Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 3 Feb 2023 19:41:18 +0530 Subject: [PATCH 30/73] test: set utf8 encoding while reading Co-authored-by: Antoine du Hamel --- test/parallel/test-single-executable-application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index ca7ffd14557fd8..0bff451bf42e06 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -34,7 +34,7 @@ if (process.config.variables.want_separate_host_toolset !== 0) if (process.platform === 'linux') { try { - const osReleaseText = readFileSync('/etc/os-release'); + const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' }); if (/^NAME="Ubuntu"/.test(osReleaseText)) { throw new Error('Not Ubuntu.'); } From 3a42a29301bf1837fa47ff389e9bdd535fceaa29 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 6 Feb 2023 10:52:04 +0530 Subject: [PATCH 31/73] fixup! test: detect Ubuntu by parsing '/etc/os-release' Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 0bff451bf42e06..67deb32cb05a0c 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -35,7 +35,7 @@ if (process.config.variables.want_separate_host_toolset !== 0) if (process.platform === 'linux') { try { const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' }); - if (/^NAME="Ubuntu"/.test(osReleaseText)) { + if (!/^NAME="Ubuntu"/.test(osReleaseText)) { throw new Error('Not Ubuntu.'); } } catch { From 0eb3deeee77f0a0046cb08d0e7922cb70f5fd5cd Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 6 Feb 2023 10:56:57 +0530 Subject: [PATCH 32/73] Apply suggestions from code review Co-authored-by: Antoine du Hamel --- doc/api/single-executable-applications.md | 2 +- test/parallel/test-single-executable-application.js | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index f38c9424fcbc66..6cca482f7e1a52 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -53,7 +53,7 @@ application should also safeguard users from some security vulnerabilities. However, if a file based `require()` is still needed, that can also be achieved: ```js -const { Module: { createRequire } } = require('module'); +const { createRequire } = require('node:module'); require = createRequire(__filename); ``` diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 67deb32cb05a0c..86549fdc3d1588 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -12,7 +12,7 @@ const { strictEqual } = require('assert'); if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); -if (['darwin', 'win32', 'linux'].indexOf(process.platform) === -1) +if (!['darwin', 'win32', 'linux'].includes(process.platform)) common.skip(`Unsupported platform ${process.platform}.`); if (process.config.variables.asan) @@ -60,11 +60,6 @@ module.exports = { `); let commonPathForSea = join(__dirname, '..', 'common'); -if (process.platform === 'win32') { - // Otherwise, the double backslashes get replaced with single backslashes in - // the generated file. - commonPathForSea = commonPathForSea.replace(/\\/g, '\\\\'); -} writeFileSync(inputFile, ` const { Module: { createRequire } } = require('module'); @@ -72,7 +67,7 @@ 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('${commonPathForSea}'); +const { expectWarning } = createdRequire(${JSON.stringify(commonPathForSea)}); expectWarning('ExperimentalWarning', 'Single executable application is an experimental feature and ' + From 925775c569371badf4942b393e7028b9db1dc661 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Mon, 6 Feb 2023 11:14:00 +0530 Subject: [PATCH 33/73] test: fix lint error Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 86549fdc3d1588..31550f192bef16 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -59,15 +59,13 @@ module.exports = { }; `); -let commonPathForSea = join(__dirname, '..', 'common'); - writeFileSync(inputFile, ` 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(${JSON.stringify(commonPathForSea)}); +const { expectWarning } = createdRequire(${JSON.stringify(join(__dirname, '..', 'common'))}); expectWarning('ExperimentalWarning', 'Single executable application is an experimental feature and ' + From e841b2681dbf747a6712c13ea9e93099341f69a1 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 18:11:35 +0530 Subject: [PATCH 34/73] doc: use "`node` binary" instead of just "binary" Refs: https://github.com/nodejs/node/pull/45038/files#r1099874091 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 6cca482f7e1a52..61e0b2cc3ae62a 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -8,10 +8,10 @@ Node.js supports the creation of [single executable applications][] by allowing -the injection of a JavaScript file into the binary. During start up, the program -checks if a resource (on [PE][]) or section (on [Mach-O][]) or note (on [ELF][]) -named `NODE_JS_CODE` exists (on macOS, in the `NODE_JS` segment). If it is -found, it executes its contents, otherwise it operates like plain Node.js. +the injection of a JavaScript file into the `node` binary. During start up, the +program checks if a resource (on [PE][]) or section (on [Mach-O][]) or note (on +[ELF][]) named `NODE_JS_CODE` exists (on macOS, in the `NODE_JS` segment). If it +is found, it executes its contents, otherwise it operates like plain Node.js. This feature allows the distribution of a Node.js application conveniently to a system that does not have Node.js installed. From aca1db736b5635bbbf19a4c34486759633e6ee6b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 19:00:30 +0530 Subject: [PATCH 35/73] doc: add note about linux support Refs: https://github.com/nodejs/node/pull/45038#discussion_r1099880089 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 10 ++++++++++ ...aintaining-single-executable-application-support.md | 1 + 2 files changed, 11 insertions(+) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 61e0b2cc3ae62a..ae7ed4e75e969f 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -67,6 +67,16 @@ equal to [`process.execPath`][]. The value of `__dirname` in the injected module is equal to the directory name of [`process.execPath`][]. +### Linux support + +AMD64 Ubuntu is the only Linux distribution where single-executable support is +tested regularly on CI currently. This is because the tool that is used to test +the creation of single-executables, [postject][], has some known issues on other +architectures/distributions which results in the creation of a binary that runs +into segmentation faults. + +However, using a different tool for the resource injection part might work. + [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 diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md index f0d77865091116..8866e30b8a9ff0 100644 --- a/doc/contributing/maintaining-single-executable-application-support.md +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -53,6 +53,7 @@ for the following features are yet to come: * Running an archive of multiple files. * Accepting [Node.js-specific CLI options][] embedded into the binary. * [XCOFF][] executable format. +* Run tests on Linux architectures/distributions other than AMD64 Ubuntu. ## Disabling single executable application support From e51708d99dd98cb9bedfda37a09cae9cdbefbc7b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 19:32:07 +0530 Subject: [PATCH 36/73] src: use external string Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100002717 Signed-off-by: Darshan Sen --- .../main/single_executable_application.js | 2 +- src/node.cc | 32 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js index 8fad64d411eee4..21e01f5b6b6eaf 100644 --- a/lib/internal/main/single_executable_application.js +++ b/lib/internal/main/single_executable_application.js @@ -28,7 +28,7 @@ emitExperimentalWarning('Single executable application'); // TODO(RaisinTen): Find a way to deduplicate this. const filename = process.execPath; -const contents = process[single_executable_application_code].toString(); +const contents = process[single_executable_application_code]; const compiledWrapper = wrapSafe(filename, contents); const customModule = new Module(filename, null); diff --git a/src/node.cc b/src/node.cc index 1bbaf09616ba2f..ca145ee9e68827 100644 --- a/src/node.cc +++ b/src/node.cc @@ -267,6 +267,23 @@ MaybeLocal StartExecution(Environment* env, const char* main_script_id) { return scope.EscapeMaybe(realm->ExecuteBootstrapper(main_script_id)); } +class ExternalOneByteStringSingleExecutableCode : + public v8::String::ExternalOneByteStringResource { + public: + explicit ExternalOneByteStringSingleExecutableCode( + const char* data, size_t size) + : data_(data), + size_(size) {} + + const char* data() const override { return data_; } + + size_t length() const override { return size_; } + + private: + const char* data_; + size_t size_; +}; + MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { InternalCallbackScope callback_scope( env, @@ -317,17 +334,14 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { const char* single_executable_application_code = FindSingleExecutableCode(&single_executable_application_size); if (single_executable_application_code != nullptr) { - Local buffer = - Buffer::New( - env->isolate(), - const_cast(single_executable_application_code), - single_executable_application_size, - [](char* data, void* hint) {}, - nullptr) - .ToLocalChecked(); + v8::Local code = v8::String::NewExternalOneByte( + env->isolate(), + new ExternalOneByteStringSingleExecutableCode( + single_executable_application_code, + single_executable_application_size)).ToLocalChecked(); env->process_object() ->SetPrivate( - env->context(), env->single_executable_application_code(), buffer) + env->context(), env->single_executable_application_code(), code) .Check(); return StartExecution(env, "internal/main/single_executable_application"); } From 2ad8e30354089bc882460afce253489382871f0f Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 20:28:59 +0530 Subject: [PATCH 37/73] src: move all SEA code to per_process::sea in node_sea.cc Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100004848 Signed-off-by: Darshan Sen --- node.gyp | 2 + src/node.cc | 62 ++-------------------- src/node_options.cc | 3 +- src/node_sea.cc | 126 ++++++++++++++++++++++++++++++++++++++++++++ src/node_sea.h | 32 +++++++++++ src/util.cc | 41 -------------- src/util.h | 5 -- 7 files changed, 167 insertions(+), 104 deletions(-) create mode 100644 src/node_sea.cc create mode 100644 src/node_sea.h diff --git a/node.gyp b/node.gyp index 57f1b2f5fce744..021d58ff1ebf5f 100644 --- a/node.gyp +++ b/node.gyp @@ -525,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', @@ -635,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', diff --git a/src/node.cc b/src/node.cc index ca145ee9e68827..bb924973b3caac 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" @@ -258,7 +259,6 @@ void Environment::InitializeDiagnostics() { } } -static MaybeLocal StartExecution(Environment* env, const char* main_script_id) { EscapableHandleScope scope(env->isolate()); CHECK_NOT_NULL(main_script_id); @@ -267,23 +267,6 @@ MaybeLocal StartExecution(Environment* env, const char* main_script_id) { return scope.EscapeMaybe(realm->ExecuteBootstrapper(main_script_id)); } -class ExternalOneByteStringSingleExecutableCode : - public v8::String::ExternalOneByteStringResource { - public: - explicit ExternalOneByteStringSingleExecutableCode( - const char* data, size_t size) - : data_(data), - size_(size) {} - - const char* data() const override { return data_; } - - size_t length() const override { return size_; } - - private: - const char* data_; - size_t size_; -}; - MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { InternalCallbackScope callback_scope( env, @@ -329,22 +312,8 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (IsSingleExecutable()) { - size_t single_executable_application_size = 0; - const char* single_executable_application_code = - FindSingleExecutableCode(&single_executable_application_size); - if (single_executable_application_code != nullptr) { - v8::Local code = v8::String::NewExternalOneByte( - env->isolate(), - new ExternalOneByteStringSingleExecutableCode( - single_executable_application_code, - single_executable_application_size)).ToLocalChecked(); - env->process_object() - ->SetPrivate( - env->context(), env->single_executable_application_code(), code) - .Check(); - return StartExecution(env, "internal/main/single_executable_application"); - } + if (per_process::sea::IsSingleExecutable()) { + return per_process::sea::StartSingleExecutableExecution(env); } #endif @@ -1287,31 +1256,10 @@ static ExitCode StartInternal(int argc, char** argv) { return LoadSnapshotDataAndRun(&snapshot_data, result.get()); } -static std::tuple FixupArgsForSEA(int argc, char** argv) { +int Start(int argc, char** argv) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - // 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; - } + std::tie(argc, argv) = per_process::sea::FixupArgsForSEA(argc, argv); #endif - return {argc, argv}; -} - -int Start(int argc, char** argv) { - std::tie(argc, argv) = FixupArgsForSEA(argc, argv); return static_cast(StartInternal(argc, argv)); } diff --git a/src/node_options.cc b/src/node_options.cc index 4b4357a4f46813..0320fbe7d5d3c2 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 @@ -301,7 +302,7 @@ void Parse( DebugOptionsParser::DebugOptionsParser() { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (IsSingleExecutable()) return; + if (per_process::sea::IsSingleExecutable()) return; #endif AddOption("--inspect-port", diff --git a/src/node_sea.cc b/src/node_sea.cc new file mode 100644 index 00000000000000..d9c3c1a7f85604 --- /dev/null +++ b/src/node_sea.cc @@ -0,0 +1,126 @@ +#include "node_sea.h" + +#include "env-inl.h" +#include "node_internals.h" +#include "v8-local-handle.h" +#include "v8-primitive.h" +#include "v8-value.h" + +#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2" +#include "postject-api.h" + +#include + +#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) + +using v8::Local; +using v8::MaybeLocal; +using v8::String; +using v8::Value; + +namespace { + +bool single_executable_application_code_loaded = false; +size_t single_executable_application_size = 0; +const char* single_executable_application_code = nullptr; + +const char* FindSingleExecutableCode(size_t* size) { + if (single_executable_application_code_loaded == false) { +#ifdef __APPLE__ + postject_options options; + postject_options_init(&options); + options.macho_segment_name = "NODE_JS"; + single_executable_application_code = + static_cast(postject_find_resource( + "NODE_JS_CODE", &single_executable_application_size, &options)); +#else + single_executable_application_code = + static_cast(postject_find_resource( + "NODE_JS_CODE", &single_executable_application_size, nullptr)); +#endif + single_executable_application_code_loaded = true; + } + + if (size != nullptr) { + *size = single_executable_application_size; + } + + return single_executable_application_code; +} + +class ExternalOneByteStringSingleExecutableCode + : public String::ExternalOneByteStringResource { + public: + explicit ExternalOneByteStringSingleExecutableCode(const char* data, + size_t size) + : data_(data), size_(size) {} + + const char* data() const override { return data_; } + + size_t length() const override { return size_; } + + private: + const char* data_; + size_t size_; +}; + +} // namespace + +namespace node { +namespace per_process { +namespace sea { + +bool IsSingleExecutable() { + return postject_has_resource(); +} + +MaybeLocal StartSingleExecutableExecution(Environment* env) { + size_t size = 0; + const char* code = FindSingleExecutableCode(&size); + + if (code == nullptr) { + return {}; + } + + Local code_external_string = + String::NewExternalOneByte( + env->isolate(), + new ExternalOneByteStringSingleExecutableCode(code, size)) + .ToLocalChecked(); + + env->process_object() + ->SetPrivate(env->context(), + env->single_executable_application_code(), + code_external_string) + .Check(); + + return StartExecution(env, "internal/main/single_executable_application"); +} + +std::tuple 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}; +} + +} // namespace sea +} // namespace per_process +} // namespace node + +#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) diff --git a/src/node_sea.h b/src/node_sea.h new file mode 100644 index 00000000000000..1c23ce27d101ff --- /dev/null +++ b/src/node_sea.h @@ -0,0 +1,32 @@ +#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 "v8-local-handle.h" +#include "v8-value.h" + +#include + +namespace node { + +class Environment; + +namespace per_process { +namespace sea { + +bool IsSingleExecutable(); +v8::MaybeLocal StartSingleExecutableExecution(Environment* env); +std::tuple FixupArgsForSEA(int argc, char** argv); + +} // namespace sea +} // namespace per_process +} // namespace node + +#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_SEA_H_ diff --git a/src/util.cc b/src/util.cc index b965e38b1abf8c..006eb068982d75 100644 --- a/src/util.cc +++ b/src/util.cc @@ -31,11 +31,6 @@ #include "string_bytes.h" #include "uv.h" -#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION -#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2" -#include "postject-api.h" -#endif - #ifdef _WIN32 #include // _S_IREAD _S_IWRITE #include @@ -58,12 +53,6 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames. -#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION -static bool single_executable_application_code_loaded = false; -static size_t single_executable_application_size = 0; -static const char* single_executable_application_code = nullptr; -#endif - namespace node { using v8::ArrayBufferView; @@ -603,34 +592,4 @@ Local UnionBytes::ToStringChecked(Isolate* isolate) const { } } -#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION -bool IsSingleExecutable() { - return postject_has_resource(); -} - -const char* FindSingleExecutableCode(size_t* size) { - if (single_executable_application_code_loaded == false) { -#ifdef __APPLE__ - postject_options options; - postject_options_init(&options); - options.macho_segment_name = "NODE_JS"; - single_executable_application_code = - static_cast(postject_find_resource( - "NODE_JS_CODE", &single_executable_application_size, &options)); -#else - single_executable_application_code = - static_cast(postject_find_resource( - "NODE_JS_CODE", &single_executable_application_size, nullptr)); -#endif - single_executable_application_code_loaded = true; - } - - if (size != nullptr) { - *size = single_executable_application_size; - } - - return single_executable_application_code; -} -#endif - } // namespace node diff --git a/src/util.h b/src/util.h index 2803becd20266e..d0e4bd41ec92c8 100644 --- a/src/util.h +++ b/src/util.h @@ -948,11 +948,6 @@ void SetConstructorFunction(v8::Isolate* isolate, SetConstructorFunctionFlag flag = SetConstructorFunctionFlag::SET_CLASS_NAME); -#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION -bool IsSingleExecutable(); -const char* FindSingleExecutableCode(size_t* size = nullptr); -#endif - } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 2c6bb38edea5b55940642e71c9f51c82e95d753a Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 20:45:19 +0530 Subject: [PATCH 38/73] src: add TODO for non-ASCII character inputs Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100011151 Signed-off-by: Darshan Sen --- src/node_sea.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node_sea.cc b/src/node_sea.cc index d9c3c1a7f85604..b8483e2240eaca 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -76,6 +76,7 @@ bool IsSingleExecutable() { MaybeLocal StartSingleExecutableExecution(Environment* env) { size_t size = 0; + // TODO(RaisinTen): Add support for non-ASCII character inputs. const char* code = FindSingleExecutableCode(&size); if (code == nullptr) { From eceb1f7b1d9d69d716634183afe25b4b3585fc88 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 21:23:16 +0530 Subject: [PATCH 39/73] src: create sea binding Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100023057 Signed-off-by: Darshan Sen --- .../main/single_executable_application.js | 8 +-- src/node.cc | 3 +- src/node_binding.cc | 1 + src/node_external_reference.h | 1 + src/node_sea.cc | 55 ++++++++++++------- src/node_sea.h | 7 --- typings/internalBinding/util.d.ts | 1 - 7 files changed, 40 insertions(+), 36 deletions(-) diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js index 21e01f5b6b6eaf..4ce40774a0606a 100644 --- a/lib/internal/main/single_executable_application.js +++ b/lib/internal/main/single_executable_application.js @@ -3,11 +3,7 @@ const { prepareMainThreadExecution, markBootstrapComplete, } = require('internal/process/pre_execution'); -const { - privateSymbols: { - single_executable_application_code, - }, -} = internalBinding('util'); +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'); @@ -28,7 +24,7 @@ emitExperimentalWarning('Single executable application'); // TODO(RaisinTen): Find a way to deduplicate this. const filename = process.execPath; -const contents = process[single_executable_application_code]; +const contents = getSingleExecutableCode(); const compiledWrapper = wrapSafe(filename, contents); const customModule = new Module(filename, null); diff --git a/src/node.cc b/src/node.cc index bb924973b3caac..8a34a01e1c4181 100644 --- a/src/node.cc +++ b/src/node.cc @@ -259,6 +259,7 @@ void Environment::InitializeDiagnostics() { } } +static MaybeLocal StartExecution(Environment* env, const char* main_script_id) { EscapableHandleScope scope(env->isolate()); CHECK_NOT_NULL(main_script_id); @@ -313,7 +314,7 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION if (per_process::sea::IsSingleExecutable()) { - return per_process::sea::StartSingleExecutableExecution(env); + return StartExecution(env, "internal/main/single_executable_application"); } #endif diff --git a/src/node_binding.cc b/src/node_binding.cc index 243a44994fb300..db607ea298edf5 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 c3ab57c0bb0f98..38ba3b21a74a69 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_sea.cc b/src/node_sea.cc index b8483e2240eaca..21b9c2d8b026e3 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -1,10 +1,9 @@ #include "node_sea.h" #include "env-inl.h" +#include "node_external_reference.h" #include "node_internals.h" -#include "v8-local-handle.h" -#include "v8-primitive.h" -#include "v8-value.h" +#include "v8.h" #define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2" #include "postject-api.h" @@ -13,8 +12,10 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) +using v8::Context; +using v8::FunctionCallbackInfo; using v8::Local; -using v8::MaybeLocal; +using v8::Object; using v8::String; using v8::Value; @@ -64,23 +65,15 @@ class ExternalOneByteStringSingleExecutableCode size_t size_; }; -} // namespace - -namespace node { -namespace per_process { -namespace sea { - -bool IsSingleExecutable() { - return postject_has_resource(); -} +void GetSingleExecutableCode(const FunctionCallbackInfo& args) { + node::Environment* env = node::Environment::GetCurrent(args); -MaybeLocal StartSingleExecutableExecution(Environment* env) { size_t size = 0; // TODO(RaisinTen): Add support for non-ASCII character inputs. const char* code = FindSingleExecutableCode(&size); if (code == nullptr) { - return {}; + return; } Local code_external_string = @@ -89,13 +82,17 @@ MaybeLocal StartSingleExecutableExecution(Environment* env) { new ExternalOneByteStringSingleExecutableCode(code, size)) .ToLocalChecked(); - env->process_object() - ->SetPrivate(env->context(), - env->single_executable_application_code(), - code_external_string) - .Check(); + args.GetReturnValue().Set(code_external_string); +} + +} // namespace - return StartExecution(env, "internal/main/single_executable_application"); +namespace node { +namespace per_process { +namespace sea { + +bool IsSingleExecutable() { + return postject_has_resource(); } std::tuple FixupArgsForSEA(int argc, char** argv) { @@ -120,8 +117,24 @@ std::tuple FixupArgsForSEA(int argc, char** argv) { return {argc, argv}; } +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethod( + context, target, "getSingleExecutableCode", GetSingleExecutableCode); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(GetSingleExecutableCode); +} + } // namespace sea } // namespace per_process } // namespace node +NODE_BINDING_CONTEXT_AWARE_INTERNAL(sea, node::per_process::sea::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + sea, node::per_process::sea::RegisterExternalReferences) + #endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) diff --git a/src/node_sea.h b/src/node_sea.h index 1c23ce27d101ff..c0a28a1d42b7dd 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -5,20 +5,13 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) -#include "v8-local-handle.h" -#include "v8-value.h" - #include namespace node { - -class Environment; - namespace per_process { namespace sea { bool IsSingleExecutable(); -v8::MaybeLocal StartSingleExecutableExecution(Environment* env); std::tuple FixupArgsForSEA(int argc, char** argv); } // namespace sea diff --git a/typings/internalBinding/util.d.ts b/typings/internalBinding/util.d.ts index 3bf3578bfc7dac..002f8eb26d93e6 100644 --- a/typings/internalBinding/util.d.ts +++ b/typings/internalBinding/util.d.ts @@ -16,7 +16,6 @@ declare function InternalBinding(binding: 'util'): { napi_wrapper: 5; untransferable_object_private_symbol: 6; exiting_aliased_Uint32Array: 7; - single_executable_application_code: 8; kPending: 0; kFulfilled: 1; From 732b4e0f7bd8709f4bd630c3b727ed142ec4e223 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 8 Feb 2023 21:50:34 +0530 Subject: [PATCH 40/73] src: add a TODO to reuse LoadEnvironment Refs: https://github.com/nodejs/node/pull/45038#discussion_r1090626101 Signed-off-by: Darshan Sen --- src/node.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/node.cc b/src/node.cc index 8a34a01e1c4181..01e1b64354ec20 100644 --- a/src/node.cc +++ b/src/node.cc @@ -314,6 +314,12 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION if (per_process::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 From a1b2643e906696801e9272f62165a9d9f93029c2 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 10:38:26 +0530 Subject: [PATCH 41/73] src: use UnionBytes insteda of extending from ExternalOneByteStringSingleExecutableCode Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100403261 Signed-off-by: Darshan Sen --- src/node_sea.cc | 34 ++++++------------- .../test-single-executable-application.js | 4 +-- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/node_sea.cc b/src/node_sea.cc index 21b9c2d8b026e3..04661386727be2 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -3,12 +3,16 @@ #include "env-inl.h" #include "node_external_reference.h" #include "node_internals.h" +#include "node_union_bytes.h" +#include "simdutf.h" #include "v8.h" #define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2" #include "postject-api.h" +#include #include +#include #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) @@ -49,40 +53,24 @@ const char* FindSingleExecutableCode(size_t* size) { return single_executable_application_code; } -class ExternalOneByteStringSingleExecutableCode - : public String::ExternalOneByteStringResource { - public: - explicit ExternalOneByteStringSingleExecutableCode(const char* data, - size_t size) - : data_(data), size_(size) {} - - const char* data() const override { return data_; } - - size_t length() const override { return size_; } - - private: - const char* data_; - size_t size_; -}; - void GetSingleExecutableCode(const FunctionCallbackInfo& args) { node::Environment* env = node::Environment::GetCurrent(args); size_t size = 0; - // TODO(RaisinTen): Add support for non-ASCII character inputs. const char* code = FindSingleExecutableCode(&size); if (code == nullptr) { return; } - Local code_external_string = - String::NewExternalOneByte( - env->isolate(), - new ExternalOneByteStringSingleExecutableCode(code, size)) - .ToLocalChecked(); + size_t expected_u16_length = simdutf::utf16_length_from_utf8(code, size); + auto out = std::make_shared>(expected_u16_length); + size_t u16_length = simdutf::convert_utf8_to_utf16( + code, size, reinterpret_cast(out->data())); + out->resize(u16_length); - args.GetReturnValue().Set(code_external_string); + args.GetReturnValue().Set( + node::UnionBytes(out).ToStringChecked(env->isolate())); } } // namespace diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 31550f192bef16..b82ff57cfd98a8 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -94,7 +94,7 @@ deepStrictEqual(requirable, { hello: 'world', }); -console.log('Hello, world!'); +console.log('Hello, world! 😊'); `); copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); @@ -132,4 +132,4 @@ if (process.platform === 'darwin') { } const singleExecutableApplicationOutput = execSync(`${outputFile} -a --b=c d`); -strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world!\n'); +strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n'); From b62393b225f3454e28ef99257d83144cdcb6520b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 10:58:47 +0530 Subject: [PATCH 42/73] src: avoid storing sea statics globally Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100404282 Signed-off-by: Darshan Sen --- src/node_sea.cc | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/node_sea.cc b/src/node_sea.cc index 04661386727be2..7dfc1daab5e6fe 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -11,6 +11,7 @@ #include "postject-api.h" #include +#include #include #include @@ -20,53 +21,44 @@ using v8::Context; using v8::FunctionCallbackInfo; using v8::Local; using v8::Object; -using v8::String; using v8::Value; namespace { -bool single_executable_application_code_loaded = false; -size_t single_executable_application_size = 0; -const char* single_executable_application_code = nullptr; - -const char* FindSingleExecutableCode(size_t* size) { - if (single_executable_application_code_loaded == false) { +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"; - single_executable_application_code = - static_cast(postject_find_resource( - "NODE_JS_CODE", &single_executable_application_size, &options)); + const char* code = static_cast( + postject_find_resource("NODE_JS_CODE", &size, &options)); #else - single_executable_application_code = - static_cast(postject_find_resource( - "NODE_JS_CODE", &single_executable_application_size, nullptr)); + const char* code = static_cast( + postject_find_resource("NODE_JS_CODE", &size, nullptr)); #endif - single_executable_application_code_loaded = true; - } - - if (size != nullptr) { - *size = single_executable_application_size; - } - - return single_executable_application_code; + return {code, size}; + }(); + return sea_code; } void GetSingleExecutableCode(const FunctionCallbackInfo& args) { node::Environment* env = node::Environment::GetCurrent(args); - size_t size = 0; - const char* code = FindSingleExecutableCode(&size); + const std::string_view sea_code = FindSingleExecutableCode(); - if (code == nullptr) { + if (sea_code.empty()) { return; } - size_t expected_u16_length = simdutf::utf16_length_from_utf8(code, size); + size_t expected_u16_length = + simdutf::utf16_length_from_utf8(sea_code.data(), sea_code.size()); auto out = std::make_shared>(expected_u16_length); - size_t u16_length = simdutf::convert_utf8_to_utf16( - code, size, reinterpret_cast(out->data())); + size_t u16_length = + simdutf::convert_utf8_to_utf16(sea_code.data(), + sea_code.size(), + reinterpret_cast(out->data())); out->resize(u16_length); args.GetReturnValue().Set( From 4d118cf46d5683319c09f3ffdc037b0cf9838efa Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 11:00:51 +0530 Subject: [PATCH 43/73] doc: move the second para above the first one Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100590587 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index ae7ed4e75e969f..bcf93a831aff58 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -7,15 +7,15 @@ +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 a resource (on [PE][]) or section (on [Mach-O][]) or note (on [ELF][]) named `NODE_JS_CODE` exists (on macOS, in the `NODE_JS` segment). If it is found, it executes its contents, otherwise it operates like plain Node.js. -This feature allows the distribution of a Node.js application conveniently to a -system that does not have Node.js installed. - A bundled JavaScript file can be turned into a single executable application with any other tool which can inject resources into the Node.js binary. The tool should also search the binary for the From b040491dc789e0da48914f727ddd0506e5135263 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 11:03:09 +0530 Subject: [PATCH 44/73] doc: https://github.com/nodejs/node/pull/45038#discussion_r1100745618 Signed-off-by: Darshan Sen --- .../maintaining-single-executable-application-support.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md index 8866e30b8a9ff0..4db6867820abf5 100644 --- a/doc/contributing/maintaining-single-executable-application-support.md +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -38,7 +38,8 @@ It is left up to external tools/solutions 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 and use it at runtime. +arbitrary read-only resources into the binary such as those needed for bundling +the application into the runtime. ## Planning From b4ab7f03eafd4f112b458d05d377ee10d0a265d1 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 11:04:14 +0530 Subject: [PATCH 45/73] doc: https://github.com/nodejs/node/pull/45038#discussion_r1100746143 Signed-off-by: Darshan Sen --- .../maintaining-single-executable-application-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md index 4db6867820abf5..551acb27e96133 100644 --- a/doc/contributing/maintaining-single-executable-application-support.md +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -48,7 +48,7 @@ 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 yet to come: +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. From 17a019d5e607011cfe32cc5314a894d0dcc43a28 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 11:07:34 +0530 Subject: [PATCH 46/73] build: add TODO comment to node.gyp Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100752158 Signed-off-by: Darshan Sen --- node.gyp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node.gyp b/node.gyp index 021d58ff1ebf5f..615b116ba67aa6 100644 --- a/node.gyp +++ b/node.gyp @@ -679,6 +679,8 @@ 'src/util-inl.h', # Dependency headers 'deps/v8/include/v8.h', + # TODO(mhdawson): Move this file along with the license file to + # `deps/postject`. 'test/fixtures/postject-copy/node_modules/postject/dist/postject-api.h' # javascript files to make for an even more pleasant IDE experience '<@(library_files)', From ec052a1fec8a7e39bc301ca3c62ea5b0081a3519 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 19:08:54 +0530 Subject: [PATCH 47/73] doc: https://github.com/nodejs/node/pull/45038#discussion_r1101352130 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index bcf93a831aff58..621963ab58c299 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -3,7 +3,7 @@ > Stability: 1 - Experimental: This feature is currently being designed and will -> still change. +> change. From c3c1ace343e713461b51966f491add1cbe1936f9 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 19:58:31 +0530 Subject: [PATCH 48/73] test: skip asan build only on linux Refs: https://github.com/nodejs/node/pull/45038#discussion_r1101531865 Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index b82ff57cfd98a8..4dec966ec3c400 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -15,7 +15,7 @@ if (!process.config.variables.single_executable_application) if (!['darwin', 'win32', 'linux'].includes(process.platform)) common.skip(`Unsupported platform ${process.platform}.`); -if (process.config.variables.asan) +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) From 2aee17fefd889782e2beba1f4d381fccf9f4164c Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 20:36:22 +0530 Subject: [PATCH 49/73] test: move sea script to test/fixtures Refs: https://github.com/nodejs/node/pull/45038#discussion_r1101361319 Signed-off-by: Darshan Sen --- test/fixtures/sea.js | 35 +++++++++++++++ .../test-single-executable-application.js | 44 +++---------------- 2 files changed, 41 insertions(+), 38 deletions(-) create mode 100644 test/fixtures/sea.js diff --git a/test/fixtures/sea.js b/test/fixtures/sea.js new file mode 100644 index 00000000000000..efdc32708b9898 --- /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 index 4dec966ec3c400..28e33db2590fae 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -3,6 +3,7 @@ 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 { execSync } = require('child_process'); @@ -47,10 +48,14 @@ if (process.platform === 'linux') { } } -const inputFile = join(tmpdir.path, 'sea.js'); +const inputFile = fixtures.path('sea.js'); const requirableFile = join(tmpdir.path, 'requirable.js'); const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea'); +// Although, require('../common') works locally, that couldn't be used here +// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. +process.env.COMMON_DIRECTORY = JSON.stringify(join(__dirname, '..', 'common')).slice(1, -1); + tmpdir.refresh(); writeFileSync(requirableFile, ` @@ -59,43 +64,6 @@ module.exports = { }; `); -writeFileSync(inputFile, ` -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(${JSON.stringify(join(__dirname, '..', 'common'))}); - -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! 😊'); -`); copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); let postjectCommand = `${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; From d711de10a18fcbd4de584a50e9a95f48979afea8 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 21:02:04 +0530 Subject: [PATCH 50/73] test: use execFileSync instead of execSync Refs: https://github.com/nodejs/node/pull/45038#discussion_r1101362601 Signed-off-by: Darshan Sen --- .../test-single-executable-application.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 28e33db2590fae..0251aa8800a753 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -6,7 +6,7 @@ const common = require('../common'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const { copyFileSync, readFileSync, writeFileSync } = require('fs'); -const { execSync } = require('child_process'); +const { execFileSync } = require('child_process'); const { join } = require('path'); const { strictEqual } = require('assert'); @@ -52,10 +52,6 @@ const inputFile = fixtures.path('sea.js'); const requirableFile = join(tmpdir.path, 'requirable.js'); const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea'); -// Although, require('../common') works locally, that couldn't be used here -// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. -process.env.COMMON_DIRECTORY = JSON.stringify(join(__dirname, '..', 'common')).slice(1, -1); - tmpdir.refresh(); writeFileSync(requirableFile, ` @@ -66,19 +62,22 @@ module.exports = { copyFileSync(process.execPath, outputFile); const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); -let postjectCommand = `${process.execPath} ${postjectFile} ${outputFile} NODE_JS_CODE ${inputFile} --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; -if (process.platform === 'darwin') { - postjectCommand += ' --macho-segment-name NODE_JS'; -} -execSync(postjectCommand); +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') { - execSync(`codesign --sign - ${outputFile}`); - execSync(`codesign --verify ${outputFile}`); + execFileSync('codesign', [ '--sign', '-', outputFile ]); + execFileSync('codesign', [ '--verify', outputFile ]); } else if (process.platform === 'win32') { let signtoolFound = false; try { - execSync('where signtool'); + execFileSync('where', [ 'signtool' ]); signtoolFound = true; } catch (err) { console.log(err.message); @@ -86,7 +85,7 @@ if (process.platform === 'darwin') { if (signtoolFound) { let certificatesFound = false; try { - execSync(`signtool sign /fd SHA256 ${outputFile}`); + 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)) { @@ -94,10 +93,13 @@ if (process.platform === 'darwin') { } } if (certificatesFound) { - execSync(`signtool verify /pa SHA256 ${outputFile}`); + execFileSync('signtool', 'verify', '/pa', 'SHA256', outputFile); } } } -const singleExecutableApplicationOutput = execSync(`${outputFile} -a --b=c d`); +const singleExecutableApplicationOutput = execFileSync( + outputFile, + [ '-a', '--b=c', 'd' ], + { env: { COMMON_DIRECTORY: JSON.stringify(join(__dirname, '..', 'common')).slice(1, -1) } }); strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n'); From e8819fbe767b6dbe9252f3c677cad68950a1c38e Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 9 Feb 2023 22:00:14 +0530 Subject: [PATCH 51/73] Update test/parallel/test-single-executable-application.js Co-authored-by: Anna Henningsen --- test/parallel/test-single-executable-application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 0251aa8800a753..92d34ae4a0ecdf 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -101,5 +101,5 @@ if (process.platform === 'darwin') { const singleExecutableApplicationOutput = execFileSync( outputFile, [ '-a', '--b=c', 'd' ], - { env: { COMMON_DIRECTORY: JSON.stringify(join(__dirname, '..', 'common')).slice(1, -1) } }); + { env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } }); strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n'); From 5c7bdfdd0624fc3a6bdcee0072286eb971496fdf Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 10:48:25 +0530 Subject: [PATCH 52/73] Apply suggestions from code review Co-authored-by: Joyee Cheung --- doc/api/single-executable-applications.md | 30 +++++++++++-------- .../main/single_executable_application.js | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 621963ab58c299..0700b155b9239c 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -28,8 +28,13 @@ One such tool is [postject][]: $ cat hello.js console.log(`Hello, ${process.argv[2]}!`); $ cp $(command -v node) hello +# On systems other than macOS: $ npx postject hello NODE_JS_CODE hello.js \ - --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 # Also add `--macho-segment-name NODE_JS` on macOS. + --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 +# On macOS: +$ npx postject hello NODE_JS_CODE hello.js \ + --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ + --macho-segment-name NODE_JS $ ./hello world Hello, world! ``` @@ -40,15 +45,15 @@ This currently only supports running a single embedded [CommonJS][] file. ### `require(id)` in the injected module is not file based -This is not the same as [`require()`][]. This also does not have any of the -properties that [`require()`][] has except [`require.main`][]. It is used to -import only built-in modules. Attempting to import a module available on the +`require()` in the injected module is not the same as the [`require()`][] available to +modules that are not injected. This 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. -Since the injected JavaScript file would be bundled into a standalone module by -default in most cases, there shouldn't be any need for a file based `require()` -API. Not having a file based `require()` API in the single-executable -application should also safeguard users from some security vulnerabilities. +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: @@ -70,12 +75,11 @@ of [`process.execPath`][]. ### Linux support AMD64 Ubuntu is the only Linux distribution where single-executable support is -tested regularly on CI currently. This is because the tool that is used to test -the creation of single-executables, [postject][], has some known issues on other -architectures/distributions which results in the creation of a binary that runs -into segmentation faults. +tested regularly on CI currently, due to lack of better tools to generate +single-executables that can be used to test this feature on other platforms. -However, using a different tool for the resource injection part might work. +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 diff --git a/lib/internal/main/single_executable_application.js b/lib/internal/main/single_executable_application.js index 4ce40774a0606a..d9604cff720d2f 100644 --- a/lib/internal/main/single_executable_application.js +++ b/lib/internal/main/single_executable_application.js @@ -8,7 +8,7 @@ const { emitExperimentalWarning } = require('internal/util'); const { Module, wrapSafe } = require('internal/modules/cjs/loader'); const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); -prepareMainThreadExecution(); +prepareMainThreadExecution(false, true); markBootstrapComplete(); emitExperimentalWarning('Single executable application'); From 44930d7307b6efca74a49eb112424e07e47fe39d Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 10:52:28 +0530 Subject: [PATCH 53/73] src: remove per_process nesting for the sea namespace Refs: https://github.com/nodejs/node/pull/45038#discussion_r1102164537 Signed-off-by: Darshan Sen --- src/node.cc | 4 ++-- src/node_options.cc | 2 +- src/node_sea.cc | 7 ++----- src/node_sea.h | 2 -- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/node.cc b/src/node.cc index 01e1b64354ec20..c696af77a79733 100644 --- a/src/node.cc +++ b/src/node.cc @@ -313,7 +313,7 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (per_process::sea::IsSingleExecutable()) { + if (sea::IsSingleExecutable()) { // TODO(addaleax): Find a way to reuse: // // LoadEnvironment(Environment*, const char*) @@ -1265,7 +1265,7 @@ static ExitCode StartInternal(int argc, char** argv) { int Start(int argc, char** argv) { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - std::tie(argc, argv) = per_process::sea::FixupArgsForSEA(argc, argv); + std::tie(argc, argv) = sea::FixupArgsForSEA(argc, argv); #endif return static_cast(StartInternal(argc, argv)); } diff --git a/src/node_options.cc b/src/node_options.cc index 0320fbe7d5d3c2..e7b0ba1073fbb4 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -302,7 +302,7 @@ void Parse( DebugOptionsParser::DebugOptionsParser() { #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - if (per_process::sea::IsSingleExecutable()) return; + if (sea::IsSingleExecutable()) return; #endif AddOption("--inspect-port", diff --git a/src/node_sea.cc b/src/node_sea.cc index 7dfc1daab5e6fe..e78fd430da8b9b 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -68,7 +68,6 @@ void GetSingleExecutableCode(const FunctionCallbackInfo& args) { } // namespace namespace node { -namespace per_process { namespace sea { bool IsSingleExecutable() { @@ -110,11 +109,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { } } // namespace sea -} // namespace per_process } // namespace node -NODE_BINDING_CONTEXT_AWARE_INTERNAL(sea, node::per_process::sea::Initialize) -NODE_BINDING_EXTERNAL_REFERENCE( - sea, node::per_process::sea::RegisterExternalReferences) +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 index c0a28a1d42b7dd..97bf0115e0f0d4 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -8,14 +8,12 @@ #include namespace node { -namespace per_process { namespace sea { bool IsSingleExecutable(); std::tuple FixupArgsForSEA(int argc, char** argv); } // namespace sea -} // namespace per_process } // namespace node #endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) From 1747349ff5ba6aa9c402f7c2bcd2f8a02201c2b6 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 10:54:45 +0530 Subject: [PATCH 54/73] fixup! Apply suggestions from code review Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 0700b155b9239c..4c67612cc3ef51 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -45,11 +45,11 @@ This currently only supports running a single embedded [CommonJS][] file. ### `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. This 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. +`require()` in the injected module is not the same as the [`require()`][] +available to modules that are not injected. This 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. @@ -78,8 +78,9 @@ AMD64 Ubuntu is the only Linux distribution where single-executable support is tested regularly on CI currently, due to 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. +Suggestions for other resource injection tools/workflows are welcomed, please +start a discussion at +to help us document them. [CommonJS]: modules.md#modules-commonjs-modules [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format From 308418e22ac826efd87291168e643ac6059f67a0 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 11:14:53 +0530 Subject: [PATCH 55/73] doc: move technical details below Refs: https://github.com/nodejs/node/pull/45038/files#r1102129105 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 4c67612cc3ef51..33cff0ff507a8c 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -12,15 +12,11 @@ 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 a resource (on [PE][]) or section (on [Mach-O][]) or note (on -[ELF][]) named `NODE_JS_CODE` exists (on macOS, in the `NODE_JS` segment). If it -is found, it executes its contents, otherwise it operates like plain Node.js. +program checks if anything has been injected. If the script is found, it +executes its contents, otherwise it operates like plain Node.js. A bundled JavaScript file can be turned into a single executable application -with any other tool which can inject resources into the Node.js binary. The tool -should also 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. +with any tool which can inject resources into the `node` binary. One such tool is [postject][]: @@ -72,6 +68,20 @@ equal to [`process.execPath`][]. 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 is supposed to +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 + +The tool should also 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. + ### Linux support AMD64 Ubuntu is the only Linux distribution where single-executable support is @@ -89,5 +99,6 @@ to help us document them. [`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 From daa8f4d14f19c77402a1790784dc0ed0e66d50cb Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 11:38:15 +0530 Subject: [PATCH 56/73] src: fix bug from UnionBytes going out of scope Refs: https://github.com/nodejs/node/pull/45038#discussion_r1102163348 Signed-off-by: Darshan Sen --- src/node_sea.cc | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/node_sea.cc b/src/node_sea.cc index e78fd430da8b9b..3c8227ba7ae5ff 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -46,23 +46,27 @@ const std::string_view FindSingleExecutableCode() { void GetSingleExecutableCode(const FunctionCallbackInfo& args) { node::Environment* env = node::Environment::GetCurrent(args); - const std::string_view sea_code = FindSingleExecutableCode(); + static const std::string_view sea_code = FindSingleExecutableCode(); if (sea_code.empty()) { return; } - size_t expected_u16_length = - simdutf::utf16_length_from_utf8(sea_code.data(), sea_code.size()); - auto out = std::make_shared>(expected_u16_length); - size_t u16_length = - simdutf::convert_utf8_to_utf16(sea_code.data(), - sea_code.size(), - reinterpret_cast(out->data())); - out->resize(u16_length); + 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>(expected_u16_length); + size_t u16_length = simdutf::convert_utf8_to_utf16( + sea_code.data(), + sea_code.size(), + reinterpret_cast(out->data())); + out->resize(u16_length); + return node::UnionBytes{out}; + }(); args.GetReturnValue().Set( - node::UnionBytes(out).ToStringChecked(env->isolate())); + sea_code_union_bytes.ToStringChecked(env->isolate())); } } // namespace From 132bfcd18bfeb5c37a158b114d54173dde5ccc07 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 19:27:12 +0530 Subject: [PATCH 57/73] src: add TODO for using 1 byte strings for ASCII-only sources Refs: https://github.com/nodejs/node/pull/45038#discussion_r1102784221 Signed-off-by: Darshan Sen --- src/node_sea.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/node_sea.cc b/src/node_sea.cc index 3c8227ba7ae5ff..530492fec2a07a 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -52,6 +52,9 @@ void GetSingleExecutableCode(const FunctionCallbackInfo& args) { 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 = From a0117da64022b9a6383bf69d65f18cf70be66862 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 19:57:34 +0530 Subject: [PATCH 58/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 33cff0ff507a8c..1cd1384de993d9 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -13,7 +13,7 @@ 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 it operates like plain Node.js. +executes its contents. Otherwise it operates like plain Node.js. A bundled JavaScript file can be turned into a single executable application with any tool which can inject resources into the `node` binary. From dfa4272edf5e8b5713234963473c218497959b06 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:04:13 +0530 Subject: [PATCH 59/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 1cd1384de993d9..e16bba24706b0b 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -42,7 +42,7 @@ This currently only supports running a single embedded [CommonJS][] file. ### `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. This also does not have any of the +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. From aef7e269fd3c446ba45c42d4198a4febe977e745 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:09:27 +0530 Subject: [PATCH 60/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index e16bba24706b0b..9e89fadfe38e38 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -70,7 +70,7 @@ of [`process.execPath`][]. ### Single executable application creation process -A tool aiming to create a single executable Node.js application is supposed to +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 From d63ebf1be3d14afe73b231a8508fc856fe4c40bb Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:11:10 +0530 Subject: [PATCH 61/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 9e89fadfe38e38..c626ecd261e5f9 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -78,7 +78,7 @@ inject the contents of a JavaScript file into: is a [Mach-O][] file * a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file -The tool should also search the binary for the +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. From 6f6b7c0d57f6c1006ff867a83535e475b428fbbc Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:20:17 +0530 Subject: [PATCH 62/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index c626ecd261e5f9..33f7a62db461e8 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -35,7 +35,8 @@ $ ./hello world Hello, world! ``` -This currently only supports running a single embedded [CommonJS][] file. +The single executable application feature only supports running a single embedded +[CommonJS][] file. ## Notes From 04842d0b8c7697469d8261fc5325f68d03ec3d30 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:20:43 +0530 Subject: [PATCH 63/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 33f7a62db461e8..b5d1f522e2365e 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -89,7 +89,7 @@ AMD64 Ubuntu is the only Linux distribution where single-executable support is tested regularly on CI currently, due to 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 +Suggestions for other resource injection tools/workflows are welcomed. Please start a discussion at to help us document them. From aaa4b1bfb8ce858f0d863c65e59daf7fea6796a7 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:21:46 +0530 Subject: [PATCH 64/73] fixup! Update doc/api/single-executable-applications.md Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index b5d1f522e2365e..6208bb4f0e8b01 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -35,8 +35,8 @@ $ ./hello world Hello, world! ``` -The single executable application feature only supports running a single embedded -[CommonJS][] file. +The single executable application feature only supports running a single +embedded [CommonJS][] file. ## Notes From 0ad40165c2ec69322c8bc19395dbff4b0a1da94d Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 20:23:28 +0530 Subject: [PATCH 65/73] Update doc/api/single-executable-applications.md Co-authored-by: Rich Trott --- doc/api/single-executable-applications.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 6208bb4f0e8b01..a7b05d463eece7 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -2,8 +2,7 @@ -> Stability: 1 - Experimental: This feature is currently being designed and will -> change. +> Stability: 1 - Experimental: This feature is being designed and will change. From 7ebf905b17296a413f2f57bea620807daf936e07 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 10 Feb 2023 21:48:32 +0530 Subject: [PATCH 66/73] doc: add explanation to clarify the steps Refs: https://github.com/nodejs/node/pull/45038#discussion_r1102891154 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 68 ++++++++++++++++------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index a7b05d463eece7..fe5d519b6b2ba9 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -14,28 +14,58 @@ 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 it operates like plain Node.js. +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. -One such tool is [postject][]: - -```console -$ cat hello.js -console.log(`Hello, ${process.argv[2]}!`); -$ cp $(command -v node) hello -# On systems other than macOS: -$ npx postject hello NODE_JS_CODE hello.js \ - --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 -# On macOS: -$ npx postject hello NODE_JS_CODE hello.js \ - --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ - --macho-segment-name NODE_JS -$ ./hello world -Hello, world! -``` - -The single executable application feature only supports running a single -embedded [CommonJS][] file. +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 From eecf141fc49257791e8fd01bb6114de391914099 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sat, 11 Feb 2023 10:44:45 +0530 Subject: [PATCH 67/73] build: use postect-api.h from deps Refs: https://github.com/nodejs/node/pull/45038#discussion_r1100752158 Signed-off-by: Darshan Sen --- node.gyp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/node.gyp b/node.gyp index 615b116ba67aa6..6d1b2bf36902cf 100644 --- a/node.gyp +++ b/node.gyp @@ -152,7 +152,7 @@ 'include_dirs': [ 'src', 'deps/v8/include', - 'test/fixtures/postject-copy/node_modules/postject/dist' + 'deps/postject' ], 'sources': [ @@ -450,7 +450,7 @@ 'include_dirs': [ 'src', - 'test/fixtures/postject-copy/node_modules/postject/dist', + 'deps/postject', '<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h ], 'dependencies': [ @@ -679,9 +679,7 @@ 'src/util-inl.h', # Dependency headers 'deps/v8/include/v8.h', - # TODO(mhdawson): Move this file along with the license file to - # `deps/postject`. - 'test/fixtures/postject-copy/node_modules/postject/dist/postject-api.h' + 'deps/postject/postject-api.h' # javascript files to make for an even more pleasant IDE experience '<@(library_files)', '<@(deps_files)', From 8201ef93b8b45cbac4de3fed5b351a6d8bc83497 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sun, 12 Feb 2023 10:15:42 +0530 Subject: [PATCH 68/73] Apply suggestions from code review Co-authored-by: Colin Ihrig --- doc/api/single-executable-applications.md | 2 +- .../maintaining-single-executable-application-support.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index fe5d519b6b2ba9..71d8748be7eb7f 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -12,7 +12,7 @@ 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 it operates like plain Node.js. +executes its contents. Otherwise Node.js operates as it normally does. The single executable application feature only supports running a single embedded [CommonJS][] file. diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md index 551acb27e96133..7acc53c6d01cf7 100644 --- a/doc/contributing/maintaining-single-executable-application-support.md +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -38,7 +38,7 @@ It is left up to external tools/solutions 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 +arbitrary read-only resources into the binary such as those needed for bundling the application into the runtime. ## Planning @@ -52,7 +52,7 @@ 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. -* Accepting [Node.js-specific CLI options][] embedded into the binary. +* Embedding [Node.js CLI options][] into the binary. * [XCOFF][] executable format. * Run tests on Linux architectures/distributions other than AMD64 Ubuntu. From c276f2e05eebd678ed834f22f1774018871a9b41 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sun, 12 Feb 2023 10:21:30 +0530 Subject: [PATCH 69/73] fixup! Apply suggestions from code review Signed-off-by: Darshan Sen --- .../maintaining-single-executable-application-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contributing/maintaining-single-executable-application-support.md b/doc/contributing/maintaining-single-executable-application-support.md index 7acc53c6d01cf7..e3957230f3001e 100644 --- a/doc/contributing/maintaining-single-executable-application-support.md +++ b/doc/contributing/maintaining-single-executable-application-support.md @@ -70,7 +70,7 @@ binary. If it is found, it passes the buffer to embedded script. [Next-10 discussions]: https://github.com/nodejs/next-10/blob/main/meetings/summit-nov-2021.md#single-executable-applications -[Node.js-specific CLI options]: https://nodejs.org/api/cli.html +[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 From 7fff0382ab2cc0e1cf01355318c4a0a41ee07370 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sun, 12 Feb 2023 10:24:38 +0530 Subject: [PATCH 70/73] doc: clarify platform support Refs: https://github.com/nodejs/node/pull/45038#discussion_r1103678086 Signed-off-by: Darshan Sen --- doc/api/single-executable-applications.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 71d8748be7eb7f..ef0604ce618f3e 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -112,11 +112,17 @@ 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. -### Linux support +### Platform support -AMD64 Ubuntu is the only Linux distribution where single-executable support is -tested regularly on CI currently, due to lack of better tools to generate -single-executables that can be used to test this feature on other platforms. +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 From 824945b62d3d6cc84a548a443aa96518e29f4f91 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sun, 12 Feb 2023 10:27:50 +0530 Subject: [PATCH 71/73] test: use fixtures helper for postject CLI path Refs: https://github.com/nodejs/node/pull/45038#discussion_r1103669739 Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index 92d34ae4a0ecdf..fdd651c8a3c994 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -61,7 +61,7 @@ module.exports = { `); copyFileSync(process.execPath, outputFile); -const postjectFile = join(__dirname, '..', 'fixtures', 'postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); +const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js'); execFileSync(process.execPath, [ postjectFile, outputFile, From b4a22bac373179305cdd42ab72ad77bbe460cd6a Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sun, 12 Feb 2023 10:41:58 +0530 Subject: [PATCH 72/73] src: explain POSTJECT_SENTINEL_FUSE macro Refs: https://github.com/nodejs/node/pull/45038#discussion_r1103670449 Signed-off-by: Darshan Sen --- src/node_sea.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/node_sea.cc b/src/node_sea.cc index 530492fec2a07a..18b661ce4ff31d 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -7,8 +7,14 @@ #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 #include From e454d1b1aa519709f5932874e7dce411942c29c4 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Fri, 17 Feb 2023 19:19:47 +0530 Subject: [PATCH 73/73] test: skip on --with-intl=system-icu Signed-off-by: Darshan Sen --- test/parallel/test-single-executable-application.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js index fdd651c8a3c994..bc0eb763de748c 100644 --- a/test/parallel/test-single-executable-application.js +++ b/test/parallel/test-single-executable-application.js @@ -27,6 +27,11 @@ if (process.config.variables.node_shared) '`/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`.');