| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 956f786 commit 8ea8354
13 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -146,6 +146,12 @@ | |||
| 146 | 146 | default=None, | |
| 147 | 147 | help='use on deprecated SunOS systems that do not support ifaddrs.h') | |
| 148 | 148 | ||
| 149 | + parser.add_argument('--disable-single-executable-application', | ||
| 150 | + action='store_true', | ||
| 151 | + dest='disable_single_executable_application', | ||
| 152 | + default=None, | ||
| 153 | + help='Disable Single Executable Application support.') | ||
| 154 | + | ||
| 149 | 155 | parser.add_argument("--fully-static", | |
| 150 | 156 | action="store_true", | |
| 151 | 157 | dest="fully_static", | |
@@ -1402,6 +1408,10 @@ def configure_node(o): | |||
| 1402 | 1408 | if options.no_ifaddrs: | |
| 1403 | 1409 | o['defines'] += ['SUNOS_NO_IFADDRS'] | |
| 1404 | 1410 | ||
| 1411 | + o['variables']['single_executable_application'] = b(not options.disable_single_executable_application) | ||
| 1412 | + if options.disable_single_executable_application: | ||
| 1413 | + o['defines'] += ['DISABLE_SINGLE_EXECUTABLE_APPLICATION'] | ||
| 1414 | + | ||
| 1405 | 1415 | # By default, enable ETW on Windows. | |
| 1406 | 1416 | if flavor == 'win': | |
| 1407 | 1417 | o['variables']['node_use_etw'] = b(not options.without_etw) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -52,6 +52,7 @@ | |||
| 52 | 52 | * [Readline](readline.md) | |
| 53 | 53 | * [REPL](repl.md) | |
| 54 | 54 | * [Report](report.md) | |
| 55 | + * [Single executable applications](single-executable-applications.md) | ||
| 55 | 56 | * [Stream](stream.md) | |
| 56 | 57 | * [String decoder](string_decoder.md) | |
| 57 | 58 | * [Test runner](test.md) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,140 @@ | |||
| 1 | + # Single executable applications | ||
| 2 | + | ||
| 3 | + <!--introduced_in=REPLACEME--> | ||
| 4 | + | ||
| 5 | + > Stability: 1 - Experimental: This feature is being designed and will change. | ||
| 6 | + | ||
| 7 | + <!-- source_link=lib/internal/main/single_executable_application.js --> | ||
| 8 | + | ||
| 9 | + This feature allows the distribution of a Node.js application conveniently to a | ||
| 10 | + system that does not have Node.js installed. | ||
| 11 | + | ||
| 12 | + Node.js supports the creation of [single executable applications][] by allowing | ||
| 13 | + the injection of a JavaScript file into the `node` binary. During start up, the | ||
| 14 | + program checks if anything has been injected. If the script is found, it | ||
| 15 | + executes its contents. Otherwise Node.js operates as it normally does. | ||
| 16 | + | ||
| 17 | + The single executable application feature only supports running a single | ||
| 18 | + embedded [CommonJS][] file. | ||
| 19 | + | ||
| 20 | + A bundled JavaScript file can be turned into a single executable application | ||
| 21 | + with any tool which can inject resources into the `node` binary. | ||
| 22 | + | ||
| 23 | + Here are the steps for creating a single executable application using one such | ||
| 24 | + tool, [postject][]: | ||
| 25 | + | ||
| 26 | + 1. Create a JavaScript file: | ||
| 27 | + ```console | ||
| 28 | + $ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js | ||
| 29 | + ``` | ||
| 30 | + | ||
| 31 | + 2. Create a copy of the `node` executable and name it according to your needs: | ||
| 32 | + ```console | ||
| 33 | + $ cp $(command -v node) hello | ||
| 34 | + ``` | ||
| 35 | + | ||
| 36 | + 3. Inject the JavaScript file into the copied binary by running `postject` with | ||
| 37 | + the following options: | ||
| 38 | + | ||
| 39 | + * `hello` - The name of the copy of the `node` executable created in step 2. | ||
| 40 | + * `NODE_JS_CODE` - The name of the resource / note / section in the binary | ||
| 41 | + where the contents of the JavaScript file will be stored. | ||
| 42 | + * `hello.js` - The name of the JavaScript file created in step 1. | ||
| 43 | + * `--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The | ||
| 44 | + [fuse][] used by the Node.js project to detect if a file has been injected. | ||
| 45 | + * `--macho-segment-name NODE_JS` (only needed on macOS) - The name of the | ||
| 46 | + segment in the binary where the contents of the JavaScript file will be | ||
| 47 | + stored. | ||
| 48 | + | ||
| 49 | + To summarize, here is the required command for each platform: | ||
| 50 | + | ||
| 51 | + * On systems other than macOS: | ||
| 52 | + ```console | ||
| 53 | + $ npx postject hello NODE_JS_CODE hello.js \ | ||
| 54 | + --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 | ||
| 55 | + ``` | ||
| 56 | + | ||
| 57 | + * On macOS: | ||
| 58 | + ```console | ||
| 59 | + $ npx postject hello NODE_JS_CODE hello.js \ | ||
| 60 | + --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ | ||
| 61 | + --macho-segment-name NODE_JS | ||
| 62 | + ``` | ||
| 63 | + | ||
| 64 | + 4. Run the binary: | ||
| 65 | + ```console | ||
| 66 | + $ ./hello world | ||
| 67 | + Hello, world! | ||
| 68 | + ``` | ||
| 69 | + | ||
| 70 | + ## Notes | ||
| 71 | + | ||
| 72 | + ### `require(id)` in the injected module is not file based | ||
| 73 | + | ||
| 74 | + `require()` in the injected module is not the same as the [`require()`][] | ||
| 75 | + available to modules that are not injected. It also does not have any of the | ||
| 76 | + properties that non-injected [`require()`][] has except [`require.main`][]. It | ||
| 77 | + can only be used to load built-in modules. Attempting to load a module that can | ||
| 78 | + only be found in the file system will throw an error. | ||
| 79 | + | ||
| 80 | + Instead of relying on a file based `require()`, users can bundle their | ||
| 81 | + application into a standalone JavaScript file to inject into the executable. | ||
| 82 | + This also ensures a more deterministic dependency graph. | ||
| 83 | + | ||
| 84 | + However, if a file based `require()` is still needed, that can also be achieved: | ||
| 85 | + | ||
| 86 | + ```js | ||
| 87 | + const { createRequire } = require('node:module'); | ||
| 88 | + require = createRequire(__filename); | ||
| 89 | + ``` | ||
| 90 | + | ||
| 91 | + ### `__filename` and `module.filename` in the injected module | ||
| 92 | + | ||
| 93 | + The values of `__filename` and `module.filename` in the injected module are | ||
| 94 | + equal to [`process.execPath`][]. | ||
| 95 | + | ||
| 96 | + ### `__dirname` in the injected module | ||
| 97 | + | ||
| 98 | + The value of `__dirname` in the injected module is equal to the directory name | ||
| 99 | + of [`process.execPath`][]. | ||
| 100 | + | ||
| 101 | + ### Single executable application creation process | ||
| 102 | + | ||
| 103 | + A tool aiming to create a single executable Node.js application must | ||
| 104 | + inject the contents of a JavaScript file into: | ||
| 105 | + | ||
| 106 | + * a resource named `NODE_JS_CODE` if the `node` binary is a [PE][] file | ||
| 107 | + * a section named `NODE_JS_CODE` in the `NODE_JS` segment if the `node` binary | ||
| 108 | + is a [Mach-O][] file | ||
| 109 | + * a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file | ||
| 110 | + | ||
| 111 | + Search the binary for the | ||
| 112 | + `NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the | ||
| 113 | + last character to `1` to indicate that a resource has been injected. | ||
| 114 | + | ||
| 115 | + ### Platform support | ||
| 116 | + | ||
| 117 | + Single-executable support is tested regularly on CI only on the following | ||
| 118 | + platforms: | ||
| 119 | + | ||
| 120 | + * Windows | ||
| 121 | + * macOS | ||
| 122 | + * Linux (AMD64 only) | ||
| 123 | + | ||
| 124 | + This is due to a lack of better tools to generate single-executables that can be | ||
| 125 | + used to test this feature on other platforms. | ||
| 126 | + | ||
| 127 | + Suggestions for other resource injection tools/workflows are welcomed. Please | ||
| 128 | + start a discussion at <https://github.com/nodejs/single-executable/discussions> | ||
| 129 | + to help us document them. | ||
| 130 | + | ||
| 131 | + [CommonJS]: modules.md#modules-commonjs-modules | ||
| 132 | + [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format | ||
| 133 | + [Mach-O]: https://en.wikipedia.org/wiki/Mach-O | ||
| 134 | + [PE]: https://en.wikipedia.org/wiki/Portable_Executable | ||
| 135 | + [`process.execPath`]: process.md#processexecpath | ||
| 136 | + [`require()`]: modules.md#requireid | ||
| 137 | + [`require.main`]: modules.md#accessing-the-main-module | ||
| 138 | + [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses | ||
| 139 | + [postject]: https://github.com/nodejs/postject | ||
| 140 | + [single executable applications]: https://github.com/nodejs/single-executable | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,81 @@ | |||
| 1 | + # Maintaining Single Executable Applications support | ||
| 2 | + | ||
| 3 | + Support for [single executable applications][] is one of the key technical | ||
| 4 | + priorities identified for the success of Node.js. | ||
| 5 | + | ||
| 6 | + ## High level strategy | ||
| 7 | + | ||
| 8 | + From the [Next-10 discussions][] there are 2 approaches the project believes are | ||
| 9 | + important to support: | ||
| 10 | + | ||
| 11 | + ### Compile with Node.js into executable | ||
| 12 | + | ||
| 13 | + This is the approach followed by [boxednode][]. | ||
| 14 | + | ||
| 15 | + No additional code within the Node.js project is needed to support the | ||
| 16 | + option of compiling a bundled application along with Node.js into a single | ||
| 17 | + executable application. | ||
| 18 | + | ||
| 19 | + ### Bundle into existing Node.js executable | ||
| 20 | + | ||
| 21 | + This is the approach followed by [pkg][]. | ||
| 22 | + | ||
| 23 | + The project does not plan to provide the complete solution but instead the key | ||
| 24 | + elements which are required in the Node.js executable in order to enable | ||
| 25 | + bundling with the pre-built Node.js binaries. This includes: | ||
| 26 | + | ||
| 27 | + * Looking for a segment within the executable that holds bundled code. | ||
| 28 | + * Running the bundled code when such a segment is found. | ||
| 29 | + | ||
| 30 | + It is left up to external tools/solutions to: | ||
| 31 | + | ||
| 32 | + * Bundle code into a single script. | ||
| 33 | + * Generate a command line with appropriate options. | ||
| 34 | + * Add a segment to an existing Node.js executable which contains | ||
| 35 | + the command line and appropriate headers. | ||
| 36 | + * Re-generate or removing signatures on the resulting executable | ||
| 37 | + * Provide a virtual file system, and hooking it in if needed to | ||
| 38 | + support native modules or reading file contents. | ||
| 39 | + | ||
| 40 | + However, the project also maintains a separate tool, [postject][], for injecting | ||
| 41 | + arbitrary read-only resources into the binary such as those needed for bundling | ||
| 42 | + the application into the runtime. | ||
| 43 | + | ||
| 44 | + ## Planning | ||
| 45 | + | ||
| 46 | + Planning for this feature takes place in the [single-executable repository][]. | ||
| 47 | + | ||
| 48 | + ## Upcoming features | ||
| 49 | + | ||
| 50 | + Currently, only running a single embedded CommonJS file is supported but support | ||
| 51 | + for the following features are in the list of work we'd like to get to: | ||
| 52 | + | ||
| 53 | + * Running an embedded ESM file. | ||
| 54 | + * Running an archive of multiple files. | ||
| 55 | + * Embedding [Node.js CLI options][] into the binary. | ||
| 56 | + * [XCOFF][] executable format. | ||
| 57 | + * Run tests on Linux architectures/distributions other than AMD64 Ubuntu. | ||
| 58 | + | ||
| 59 | + ## Disabling single executable application support | ||
| 60 | + | ||
| 61 | + To disable single executable application support, build Node.js with the | ||
| 62 | + `--disable-single-executable-application` configuration option. | ||
| 63 | + | ||
| 64 | + ## Implementation | ||
| 65 | + | ||
| 66 | + When built with single executable application support, the Node.js process uses | ||
| 67 | + [`postject-api.h`][] to check if the `NODE_JS_CODE` section exists in the | ||
| 68 | + binary. If it is found, it passes the buffer to | ||
| 69 | + [`single_executable_application.js`][], which executes the contents of the | ||
| 70 | + embedded script. | ||
| 71 | + | ||
| 72 | + [Next-10 discussions]: https://github.com/nodejs/next-10/blob/main/meetings/summit-nov-2021.md#single-executable-applications | ||
| 73 | + [Node.js CLI options]: https://nodejs.org/api/cli.html | ||
| 74 | + [XCOFF]: https://www.ibm.com/docs/en/aix/7.2?topic=formats-xcoff-object-file-format | ||
| 75 | + [`postject-api.h`]: https://github.com/nodejs/node/blob/71951a0e86da9253d7c422fa2520ee9143e557fa/test/fixtures/postject-copy/node_modules/postject/dist/postject-api.h | ||
| 76 | + [`single_executable_application.js`]: https://github.com/nodejs/node/blob/main/lib/internal/main/single_executable_application.js | ||
| 77 | + [boxednode]: https://github.com/mongodb-js/boxednode | ||
| 78 | + [pkg]: https://github.com/vercel/pkg | ||
| 79 | + [postject]: https://github.com/nodejs/postject | ||
| 80 | + [single executable applications]: https://github.com/nodejs/node/blob/main/doc/contributing/technical-priorities.md#single-executable-applications | ||
| 81 | + [single-executable repository]: https://github.com/nodejs/single-executable | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,55 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const { | ||
| 3 | + prepareMainThreadExecution, | ||
| 4 | + markBootstrapComplete, | ||
| 5 | + } = require('internal/process/pre_execution'); | ||
| 6 | + const { getSingleExecutableCode } = internalBinding('sea'); | ||
| 7 | + const { emitExperimentalWarning } = require('internal/util'); | ||
| 8 | + const { Module, wrapSafe } = require('internal/modules/cjs/loader'); | ||
| 9 | + const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); | ||
| 10 | + | ||
| 11 | + prepareMainThreadExecution(false, true); | ||
| 12 | + markBootstrapComplete(); | ||
| 13 | + | ||
| 14 | + emitExperimentalWarning('Single executable application'); | ||
| 15 | + | ||
| 16 | + // This is roughly the same as: | ||
| 17 | + // | ||
| 18 | + // const mod = new Module(filename); | ||
| 19 | + // mod._compile(contents, filename); | ||
| 20 | + // | ||
| 21 | + // but the code has been duplicated because currently there is no way to set the | ||
| 22 | + // value of require.main to module. | ||
| 23 | + // | ||
| 24 | + // TODO(RaisinTen): Find a way to deduplicate this. | ||
| 25 | + | ||
| 26 | + const filename = process.execPath; | ||
| 27 | + const contents = getSingleExecutableCode(); | ||
| 28 | + const compiledWrapper = wrapSafe(filename, contents); | ||
| 29 | + | ||
| 30 | + const customModule = new Module(filename, null); | ||
| 31 | + customModule.filename = filename; | ||
| 32 | + customModule.paths = Module._nodeModulePaths(customModule.path); | ||
| 33 | + | ||
| 34 | + const customExports = customModule.exports; | ||
| 35 | + | ||
| 36 | + function customRequire(path) { | ||
| 37 | + if (!Module.isBuiltin(path)) { | ||
| 38 | + throw new ERR_UNKNOWN_BUILTIN_MODULE(path); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + return require(path); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + customRequire.main = customModule; | ||
| 45 | + | ||
| 46 | + const customFilename = customModule.filename; | ||
| 47 | + | ||
| 48 | + const customDirname = customModule.path; | ||
| 49 | + | ||
| 50 | + compiledWrapper( | ||
| 51 | + customExports, | ||
| 52 | + customRequire, | ||
| 53 | + customModule, | ||
| 54 | + customFilename, | ||
| 55 | + customDirname); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -153,7 +153,8 @@ | |||
| 153 | 153 | ||
| 154 | 154 | 'include_dirs': [ | |
| 155 | 155 | 'src', | |
| 156 | - 'deps/v8/include' | ||
| 156 | + 'deps/v8/include', | ||
| 157 | + 'deps/postject' | ||
| 157 | 158 | ], | |
| 158 | 159 | ||
| 159 | 160 | 'sources': [ | |
@@ -458,6 +459,7 @@ | |||
| 458 | 459 | ||
| 459 | 460 | 'include_dirs': [ | |
| 460 | 461 | 'src', | |
| 462 | + 'deps/postject', | ||
| 461 | 463 | '<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h | |
| 462 | 464 | ], | |
| 463 | 465 | 'dependencies': [ | |
@@ -531,6 +533,7 @@ | |||
| 531 | 533 | 'src/node_report.cc', | |
| 532 | 534 | 'src/node_report_module.cc', | |
| 533 | 535 | 'src/node_report_utils.cc', | |
| 536 | + 'src/node_sea.cc', | ||
| 534 | 537 | 'src/node_serdes.cc', | |
| 535 | 538 | 'src/node_shadow_realm.cc', | |
| 536 | 539 | 'src/node_snapshotable.cc', | |
@@ -641,6 +644,7 @@ | |||
| 641 | 644 | 'src/node_report.h', | |
| 642 | 645 | 'src/node_revert.h', | |
| 643 | 646 | 'src/node_root_certs.h', | |
| 647 | + 'src/node_sea.h', | ||
| 644 | 648 | 'src/node_shadow_realm.h', | |
| 645 | 649 | 'src/node_snapshotable.h', | |
| 646 | 650 | 'src/node_snapshot_builder.h', | |
@@ -683,6 +687,7 @@ | |||
| 683 | 687 | 'src/util-inl.h', | |
| 684 | 688 | # Dependency headers | |
| 685 | 689 | 'deps/v8/include/v8.h', | |
| 690 | + 'deps/postject/postject-api.h' | ||
| 686 | 691 | # javascript files to make for an even more pleasant IDE experience | |
| 687 | 692 | '<@(library_files)', | |
| 688 | 693 | '<@(deps_files)', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,6 +39,7 @@ | |||
| 39 | 39 | #include "node_realm-inl.h" | |
| 40 | 40 | #include "node_report.h" | |
| 41 | 41 | #include "node_revert.h" | |
| 42 | + #include "node_sea.h" | ||
| 42 | 43 | #include "node_snapshot_builder.h" | |
| 43 | 44 | #include "node_v8_platform-inl.h" | |
| 44 | 45 | #include "node_version.h" | |
@@ -126,6 +127,7 @@ | |||
| 126 | 127 | #include <cstring> | |
| 127 | 128 | ||
| 128 | 129 | #include <string> | |
| 130 | + #include <tuple> | ||
| 129 | 131 | #include <vector> | |
| 130 | 132 | ||
| 131 | 133 | namespace node { | |
@@ -321,6 +323,18 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) { | |||
| 321 | 323 | first_argv = env->argv()[1]; | |
| 322 | 324 | } | |
| 323 | 325 | ||
| 326 | + #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION | ||
| 327 | + if (sea::IsSingleExecutable()) { | ||
| 328 | + // TODO(addaleax): Find a way to reuse: | ||
| 329 | + // | ||
| 330 | + // LoadEnvironment(Environment*, const char*) | ||
| 331 | + // | ||
| 332 | + // instead and not add yet another main entry point here because this | ||
| 333 | + // already duplicates existing code. | ||
| 334 | + return StartExecution(env, "internal/main/single_executable_application"); | ||
| 335 | + } | ||
| 336 | + #endif | ||
| 337 | + | ||
| 324 | 338 | if (first_argv == "inspect") { | |
| 325 | 339 | return StartExecution(env, "internal/main/inspect"); | |
| 326 | 340 | } | |
@@ -1187,6 +1201,10 @@ int LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr, | |||
| 1187 | 1201 | } | |
| 1188 | 1202 | ||
| 1189 | 1203 | int Start(int argc, char** argv) { | |
| 1204 | + #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION | ||
| 1205 | + std::tie(argc, argv) = sea::FixupArgsForSEA(argc, argv); | ||
| 1206 | + #endif | ||
| 1207 | + | ||
| 1190 | 1208 | CHECK_GT(argc, 0); | |
| 1191 | 1209 | ||
| 1192 | 1210 | // Hack around with the argv pointer. Used for process.title = "blah". | |
| Back | FazBrowse Home | New Git URL |
0 commit comments