| # Single executable applications |
| |
| <!--introduced_in=v19.7.0--> |
| |
| <!-- YAML |
| added: |
| - v19.7.0 |
| - v18.16.0 |
| changes: |
| - version: v25.5.0 |
| pr-url: https://github.com/nodejs/node/pull/61167 |
| description: Added built-in single executable application generation via the CLI flag `--build-sea`. |
| - version: v20.6.0 |
| pr-url: https://github.com/nodejs/node/pull/46824 |
| description: Added support for "useSnapshot". |
| - version: v20.6.0 |
| pr-url: https://github.com/nodejs/node/pull/48191 |
| description: Added support for "useCodeCache". |
| --> |
| |
| > Stability: 1.1 - Active development |
| |
| <!-- source_link=src/node_sea.cc --> |
| |
| 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 blob prepared by Node.js, which can contain a bundled script, |
| into the `node` binary. During start up, the program checks if anything has been |
| injected. If the blob is found, it executes the script in the blob. Otherwise |
| Node.js operates as it normally does. |
| |
| The single executable application feature supports running a |
| single embedded script using the [CommonJS][] or the [ECMAScript Modules][] module system. |
| |
| Users can create a single executable application from their bundled script |
| with the `node` binary itself and any tool which can inject resources into the |
| binary. |
| |
| 1. Create a JavaScript file: |
| ```bash |
| echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js |
| ``` |
| |
| 2. Create a configuration file building a blob that can be injected into the |
| single executable application (see |
| [Generating single executable preparation blobs][] for details): |
| |
| * On systems other than Windows: |
| |
| ```bash |
| echo '{ "main": "hello.js", "output": "sea" }' > sea-config.json |
| ``` |
| |
| * On Windows: |
| |
| ```bash |
| echo '{ "main": "hello.js", "output": "sea.exe" }' > sea-config.json |
| ``` |
| |
| The `.exe` extension is necessary. |
| |
| 3. Generate the target executable: |
| ```bash |
| node --build-sea sea-config.json |
| ``` |
| |
| 4. Sign the binary (macOS and Windows only): |
| |
| * On macOS: |
| |
| ```bash |
| codesign --sign - hello |
| ``` |
| |
| * On Windows (optional): |
| |
| A certificate needs to be present for this to work. However, the unsigned |
| binary would still be runnable. |
| |
| ```powershell |
| signtool sign /fd SHA256 hello.exe |
| ``` |
| |
| 5. Run the binary: |
| |
| * On systems other than Windows |
| |
| ```console |
| $ ./hello world |
| Hello, world! |
| ``` |
| |
| * On Windows |
| |
| ```console |
| $ .\hello.exe world |
| Hello, world! |
| ``` |
| |
| ## Generating single executable applications with `--build-sea` |
| |
| To generate a single executable application directly, the `--build-sea` flag can be |
| used. It takes a path to a configuration file in JSON format. If the path passed to it |
| isn't absolute, Node.js will use the path relative to the current working directory. |
| |
| The configuration currently reads the following top-level fields: |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| "mainFormat": "commonjs", // Default: "commonjs", options: "commonjs", "module" |
| "executable": "/path/to/node/binary", // Optional, if not specified, uses the current Node.js binary |
| "output": "/path/to/write/the/generated/executable", |
| "disableExperimentalSEAWarning": true, // Default: false |
| "useSnapshot": false, // Default: false |
| "useCodeCache": true, // Default: false |
| "execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional |
| "execArgvExtension": "env", // Default: "env", options: "none", "env", "cli" |
| "assets": { // Optional |
| "a.dat": "/path/to/a.dat", |
| "b.txt": "/path/to/b.txt" |
| } |
| } |
| ``` |
| |
| If the paths are not absolute, Node.js will use the path relative to the |
| current working directory. The version of the Node.js binary used to produce |
| the blob must be the same as the one to which the blob will be injected. |
| |
| Note: When generating cross-platform SEAs (e.g., generating a SEA |
| for `linux-x64` on `darwin-arm64`), `useCodeCache` and `useSnapshot` |
| must be set to false to avoid generating incompatible executables. |
| Since code cache and snapshots can only be loaded on the same platform |
| where they are compiled, the generated executable might crash on startup when |
| trying to load code cache or snapshots built on a different platform. |
| |
| ### Assets |
| |
| Users can include assets by adding a key-path dictionary to the configuration |
| as the `assets` field. At build time, Node.js would read the assets from the |
| specified paths and bundle them into the preparation blob. In the generated |
| executable, users can retrieve the assets using the [`sea.getAsset()`][] and |
| [`sea.getAssetAsBlob()`][] APIs. |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| "output": "/path/to/write/the/generated/executable", |
| "assets": { |
| "a.jpg": "/path/to/a.jpg", |
| "b.txt": "/path/to/b.txt" |
| } |
| } |
| ``` |
| |
| The single-executable application can access the assets as follows: |
| |
| ```cjs |
| const { getAsset, getAssetAsBlob, getRawAsset, getAssetKeys } = require('node:sea'); |
| // Get all asset keys. |
| const keys = getAssetKeys(); |
| console.log(keys); // ['a.jpg', 'b.txt'] |
| // Returns a copy of the data in an ArrayBuffer. |
| const image = getAsset('a.jpg'); |
| // Returns a string decoded from the asset as UTF8. |
| const text = getAsset('b.txt', 'utf8'); |
| // Returns a Blob containing the asset. |
| const blob = getAssetAsBlob('a.jpg'); |
| // Returns an ArrayBuffer containing the raw asset without copying. |
| const raw = getRawAsset('a.jpg'); |
| ``` |
| |
| See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][], |
| [`sea.getRawAsset()`][] and [`sea.getAssetKeys()`][] APIs for more information. |
| |
| ### Startup snapshot support |
| |
| The `useSnapshot` field can be used to enable startup snapshot support. In this |
| case, the `main` script would not be executed when the final executable is launched. |
| Instead, it would be run when the single executable application preparation |
| blob is generated on the building machine. The generated preparation blob would |
| then include a snapshot capturing the states initialized by the `main` script. |
| The final executable, with the preparation blob injected, would deserialize |
| the snapshot at run time. |
| |
| When `useSnapshot` is true, the main script must invoke the |
| [`v8.startupSnapshot.setDeserializeMainFunction()`][] API to configure code |
| that needs to be run when the final executable is launched by the users. |
| |
| The typical pattern for an application to use snapshot in a single executable |
| application is: |
| |
| 1. At build time, on the building machine, the main script is run to |
| initialize the heap to a state that's ready to take user input. The script |
| should also configure a main function with |
| [`v8.startupSnapshot.setDeserializeMainFunction()`][]. This function will be |
| compiled and serialized into the snapshot, but not invoked at build time. |
| 2. At run time, the main function will be run on top of the deserialized heap |
| on the user machine to process user input and generate output. |
| |
| The general constraints of the startup snapshot scripts also apply to the main |
| script when it's used to build snapshot for the single executable application, |
| and the main script can use the [`v8.startupSnapshot` API][] to adapt to |
| these constraints. See |
| [documentation about startup snapshot support in Node.js][]. |
| |
| ### V8 code cache support |
| |
| When `useCodeCache` is set to `true` in the configuration, during the generation |
| of the single executable preparation blob, Node.js will compile the `main` |
| script to generate the V8 code cache. The generated code cache would be part of |
| the preparation blob and get injected into the final executable. When the single |
| executable application is launched, instead of compiling the `main` script from |
| scratch, Node.js would use the code cache to speed up the compilation, then |
| execute the script, which would improve the startup performance. |
| |
| **Note:** `import()` does not work when `useCodeCache` is `true`. |
| |
| ### Execution arguments |
| |
| The `execArgv` field can be used to specify Node.js-specific |
| arguments that will be automatically applied when the single |
| executable application starts. This allows application developers |
| to configure Node.js runtime options without requiring end users |
| to be aware of these flags. |
| |
| For example, the following configuration: |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| "output": "/path/to/write/the/generated/executable", |
| "execArgv": ["--no-warnings", "--max-old-space-size=2048"] |
| } |
| ``` |
| |
| will instruct the SEA to be launched with the `--no-warnings` and |
| `--max-old-space-size=2048` flags. In the scripts embedded in the executable, these flags |
| can be accessed using the `process.execArgv` property: |
| |
| ```js |
| // If the executable is launched with `sea user-arg1 user-arg2` |
| console.log(process.execArgv); |
| // Prints: ['--no-warnings', '--max-old-space-size=2048'] |
| console.log(process.argv); |
| // Prints ['/path/to/sea', 'path/to/sea', 'user-arg1', 'user-arg2'] |
| ``` |
| |
| The user-provided arguments are in the `process.argv` array starting from index 2, |
| similar to what would happen if the application is started with: |
| |
| ```console |
| node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2 |
| ``` |
| |
| ### Execution argument extension |
| |
| The `execArgvExtension` field controls how additional execution arguments can be |
| provided beyond those specified in the `execArgv` field. It accepts one of three string values: |
| |
| * `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used, |
| and the `NODE_OPTIONS` environment variable will be ignored. |
| * `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments. |
| This is the default behavior to maintain backward compatibility. |
| * `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags |
| will be parsed as execution arguments for Node.js instead of being passed to the user script. |
| This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable. |
| |
| For example, with `"execArgvExtension": "cli"`: |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| "output": "/path/to/write/the/generated/executable", |
| "execArgv": ["--no-warnings"], |
| "execArgvExtension": "cli" |
| } |
| ``` |
| |
| The executable can be launched as: |
| |
| ```console |
| ./my-sea --node-options="--trace-exit" user-arg1 user-arg2 |
| ``` |
| |
| This would be equivalent to running: |
| |
| ```console |
| node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 |
| ``` |
| |
| ## Single-executable application API |
| |
| The `node:sea` builtin allows interaction with the single-executable application |
| from the JavaScript main script embedded into the executable. |
| |
| ### `sea.isSea()` |
| |
| <!-- YAML |
| added: |
| - v21.7.0 |
| - v20.12.0 |
| --> |
| |
| * Returns: {boolean} Whether this script is running inside a single-executable |
| application. |
| |
| ### `sea.getAsset(key[, encoding])` |
| |
| <!-- YAML |
| added: |
| - v21.7.0 |
| - v20.12.0 |
| --> |
| |
| This method can be used to retrieve the assets configured to be bundled into the |
| single-executable application at build time. |
| An error is thrown when no matching asset can be found. |
| |
| * `key` {string} the key for the asset in the dictionary specified by the |
| `assets` field in the single-executable application configuration. |
| * `encoding` {string} If specified, the asset will be decoded as |
| a string. Any encoding supported by the `TextDecoder` is accepted. |
| If unspecified, an `ArrayBuffer` containing a copy of the asset would be |
| returned instead. |
| * Returns: {string|ArrayBuffer} |
| |
| ### `sea.getAssetAsBlob(key[, options])` |
| |
| <!-- YAML |
| added: |
| - v21.7.0 |
| - v20.12.0 |
| --> |
| |
| Similar to [`sea.getAsset()`][], but returns the result in a {Blob}. |
| An error is thrown when no matching asset can be found. |
| |
| * `key` {string} the key for the asset in the dictionary specified by the |
| `assets` field in the single-executable application configuration. |
| * `options` {Object} |
| * `type` {string} An optional mime type for the blob. |
| * Returns: {Blob} |
| |
| ### `sea.getRawAsset(key)` |
| |
| <!-- YAML |
| added: |
| - v21.7.0 |
| - v20.12.0 |
| --> |
| |
| This method can be used to retrieve the assets configured to be bundled into the |
| single-executable application at build time. |
| An error is thrown when no matching asset can be found. |
| |
| Unlike `sea.getAsset()` or `sea.getAssetAsBlob()`, this method does not |
| return a copy. Instead, it returns the raw asset bundled inside the executable. |
| |
| For now, users should avoid writing to the returned array buffer. If the |
| injected section is not marked as writable or not aligned properly, |
| writes to the returned array buffer is likely to result in a crash. |
| |
| * `key` {string} the key for the asset in the dictionary specified by the |
| `assets` field in the single-executable application configuration. |
| * Returns: {ArrayBuffer} |
| |
| ### `sea.getAssetKeys()` |
| |
| <!-- YAML |
| added: |
| - v24.8.0 |
| - v22.20.0 |
| --> |
| |
| * Returns {string\[]} An array containing all the keys of the assets |
| embedded in the executable. If no assets are embedded, returns an empty array. |
| |
| This method can be used to retrieve an array of all the keys of assets |
| embedded into the single-executable application. |
| An error is thrown when not running inside a single-executable application. |
| |
| ## In the injected main script |
| |
| ### Module format of the injected main script |
| |
| To specify how Node.js should interpret the injected main script, use the |
| `mainFormat` field in the single-executable application configuration. |
| The accepted values are: |
| |
| * `"commonjs"`: The injected main script is treated as a CommonJS module. |
| * `"module"`: The injected main script is treated as an ECMAScript module. |
| |
| If the `mainFormat` field is not specified, it defaults to `"commonjs"`. |
| |
| Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"` |
| or `"useCodeCache"`. |
| |
| ### Module loading in the injected main script |
| |
| In the injected main script, module loading does not read from the file system. |
| By default, both `require()` and `import` statements would only be able to load |
| the built-in modules. Attempting to load a module that can only be found in the |
| file system will throw an error. |
| |
| Users can bundle their application into a standalone JavaScript file to inject |
| into the executable. This also ensures a more deterministic dependency graph. |
| |
| To load modules from the file system in the injected main script, users can |
| create a `require` function that can load from the file system using |
| `module.createRequire()`. For example, in a CommonJS entry point: |
| |
| ```js |
| const { createRequire } = require('node:module'); |
| require = createRequire(__filename); |
| ``` |
| |
| ### `require()` in the injected main script |
| |
| `require()` in the injected main script is not the same as the [`require()`][] |
| available to modules that are not injected. |
| Currently, it does not have any of the properties that non-injected |
| [`require()`][] has except [`require.main`][]. |
| |
| ### `__filename` and `module.filename` in the injected main script |
| |
| The values of `__filename` and `module.filename` in the injected main script |
| are equal to [`process.execPath`][]. |
| |
| ### `__dirname` in the injected main script |
| |
| The value of `__dirname` in the injected main script is equal to the directory |
| name of [`process.execPath`][]. |
| |
| ### `import.meta` in the injected main script |
| |
| When using `"mainFormat": "module"`, `import.meta` is available in the |
| injected main script with the following properties: |
| |
| * `import.meta.url`: A `file:` URL corresponding to [`process.execPath`][]. |
| * `import.meta.filename`: Equal to [`process.execPath`][]. |
| * `import.meta.dirname`: The directory name of [`process.execPath`][]. |
| * `import.meta.main`: `true`. |
| |
| `import.meta.resolve` is currently not supported. |
| |
| ### `import()` in the injected main script |
| |
| <!-- TODO(joyeecheung): support and document module.registerHooks --> |
| |
| When using `"mainFormat": "module"`, `import()` can be used to dynamically |
| load built-in modules. Attempting to use `import()` to load modules from |
| the file system will throw an error. |
| |
| ### Using native addons in the injected main script |
| |
| Native addons can be bundled as assets into the single-executable application |
| by specifying them in the `assets` field of the configuration file used to |
| generate the single-executable application preparation blob. |
| The addon can then be loaded in the injected main script by writing the asset |
| to a temporary file and loading it with `process.dlopen()`. |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| "output": "/path/to/write/the/generated/executable", |
| "assets": { |
| "myaddon.node": "/path/to/myaddon/build/Release/myaddon.node" |
| } |
| } |
| ``` |
| |
| ```js |
| // script.js |
| const fs = require('node:fs'); |
| const os = require('node:os'); |
| const path = require('node:path'); |
| const { getRawAsset } = require('node:sea'); |
| const addonPath = path.join(os.tmpdir(), 'myaddon.node'); |
| fs.writeFileSync(addonPath, new Uint8Array(getRawAsset('myaddon.node'))); |
| const myaddon = { exports: {} }; |
| process.dlopen(myaddon, addonPath); |
| console.log(myaddon.exports); |
| fs.rmSync(addonPath); |
| ``` |
| |
| Known caveat: if the single-executable application is produced by postject running on a Linux arm64 docker container, |
| [the produced ELF binary does not have the correct hash table to load the addons][postject-linux-arm64-issue] and |
| will crash on `process.dlopen()`. Build the single-executable application on other platforms, or at least on |
| a non-container Linux arm64 environment to work around this issue. |
| |
| ## Notes |
| |
| ### Single executable application creation process |
| |
| The process documented here is subject to change. |
| |
| #### 1. Generating single executable preparation blobs |
| |
| To build a single executable application, Node.js would first generate a blob |
| that contains all the necessary information to run the bundled script. |
| When using `--build-sea`, this step is done internally along with the injection. |
| |
| ##### Dumping the preparation blob to disk |
| |
| Before `--build-sea` was introduced, an older workflow was introduced to write the |
| preparation blob to disk for injection by external tools. This can still |
| be used for verification purposes. |
| |
| To dump the preparation blob to disk for verification, use `--experimental-sea-config`. |
| This writes a file that can be injected into a Node.js binary using tools like [postject][]. |
| |
| The configuration is similar to that of `--build-sea`, except that the |
| `output` field specifies the path to write the generated blob file instead of |
| the final executable. |
| |
| ```json |
| { |
| "main": "/path/to/bundled/script.js", |
| // Instead of the final executable, this is the path to write the blob. |
| "output": "/path/to/write/the/generated/blob.blob" |
| } |
| ``` |
| |
| #### 2. Injecting the preparation blob into the `node` binary |
| |
| To complete the creation of a single executable application, the generated blob |
| needs to be injected into a copy of the `node` binary, as documented below. |
| |
| When using `--build-sea`, this step is done internally along with the blob generation. |
| |
| * If the `node` binary is a [PE][] file, the blob should be injected as a resource |
| named `NODE_SEA_BLOB`. |
| * If the `node` binary is a [Mach-O][] file, the blob should be injected as a section |
| named `NODE_SEA_BLOB` in the `NODE_SEA` segment. |
| * If the `node` binary is an [ELF][] file, the blob should be injected as a note |
| named `NODE_SEA_BLOB`. |
| |
| Then, the SEA building process searches the binary for the |
| `NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the |
| last character to `1` to indicate that a resource has been injected. |
| |
| ##### Injecting the preparation blob manually |
| |
| Before `--build-sea` was introduced, an older workflow was introduced to allow |
| external tools to inject the generated blob into a copy of the `node` binary. |
| |
| For example, with [postject][]: |
| |
| 1. Create a copy of the `node` executable and name it according to your needs: |
| |
| * On systems other than Windows: |
| |
| ```bash |
| cp $(command -v node) hello |
| ``` |
| |
| * On Windows: |
| |
| ```text |
| node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" |
| ``` |
| |
| The `.exe` extension is necessary. |
| |
| 2. Remove the signature of the binary (macOS and Windows only): |
| |
| * On macOS: |
| |
| ```bash |
| codesign --remove-signature hello |
| ``` |
| |
| * On Windows (optional): |
| |
| [signtool][] can be used from the installed [Windows SDK][]. If this step is |
| skipped, ignore any signature-related warning from postject. |
| |
| ```powershell |
| signtool remove /s hello.exe |
| ``` |
| |
| 3. Inject the blob into the copied binary by running `postject` with |
| the following options: |
| |
| * `hello` / `hello.exe` - The name of the copy of the `node` executable |
| created in step 4. |
| * `NODE_SEA_BLOB` - The name of the resource / note / section in the binary |
| where the contents of the blob will be stored. |
| * `sea-prep.blob` - The name of the blob created in step 1. |
| * `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The |
| [fuse][] used by the Node.js project to detect if a file has been injected. |
| * `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the |
| segment in the binary where the contents of the blob will be |
| stored. |
| |
| To summarize, here is the required command for each platform: |
| |
| * On Linux: |
| ```bash |
| npx postject hello NODE_SEA_BLOB sea-prep.blob \ |
| --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 |
| ``` |
| |
| * On Windows - PowerShell: |
| ```powershell |
| npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` |
| --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 |
| ``` |
| |
| * On Windows - Command Prompt: |
| ```text |
| npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ |
| --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 |
| ``` |
| |
| * On macOS: |
| ```bash |
| npx postject hello NODE_SEA_BLOB sea-prep.blob \ |
| --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ |
| --macho-segment-name NODE_SEA |
| ``` |
| |
| ### Platform support |
| |
| Single-executable support is tested regularly on CI only on the following |
| platforms: |
| |
| * Windows |
| * macOS |
| * Linux (all distributions [supported by Node.js][] except Alpine and all |
| architectures [supported by Node.js][] except s390x) |
| |
| This is due to a lack of better tools to generate single-executables that can be |
| used to test this feature on other platforms. |
| |
| Suggestions for other resource injection tools/workflows are welcomed. Please |
| start a discussion at <https://github.com/nodejs/single-executable/discussions> |
| to help us document them. |
| |
| [CommonJS]: modules.md#modules-commonjs-modules |
| [ECMAScript Modules]: esm.md#modules-ecmascript-modules |
| [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format |
| [Generating single executable preparation blobs]: #1-generating-single-executable-preparation-blobs |
| [Mach-O]: https://en.wikipedia.org/wiki/Mach-O |
| [PE]: https://en.wikipedia.org/wiki/Portable_Executable |
| [Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ |
| [`process.execPath`]: process.md#processexecpath |
| [`require()`]: modules.md#requireid |
| [`require.main`]: modules.md#accessing-the-main-module |
| [`sea.getAsset()`]: #seagetassetkey-encoding |
| [`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options |
| [`sea.getAssetKeys()`]: #seagetassetkeys |
| [`sea.getRawAsset()`]: #seagetrawassetkey |
| [`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data |
| [`v8.startupSnapshot` API]: v8.md#startup-snapshot-api |
| [documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot |
| [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses |
| [postject]: https://github.com/nodejs/postject |
| [postject-linux-arm64-issue]: https://github.com/nodejs/postject/issues/105 |
| [signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool |
| [single executable applications]: https://github.com/nodejs/single-executable |
| [supported by Node.js]: https://github.com/nodejs/node/blob/main/BUILDING.md#platform-list |