单一可执行应用程序#

稳定性:1.1 - 活跃开发

源代码: src/node_sea.cc

此功能允许将 Node.js 应用程序方便地分发到未安装 Node.js 的系统。

Node.js 支持创建单一可执行应用程序,它允许将一个由 Node.js 准备的、可以包含打包脚本的 blob 注入到 node 二进制文件中。在启动时,程序会检查是否有任何内容被注入。如果找到该 blob,它将执行 blob 中的脚本。否则,Node.js 将像往常一样运行。

单一可执行应用程序功能目前仅支持使用 CommonJS 模块系统运行单个嵌入式脚本。

用户可以使用 node 二进制文件本身以及任何可以将资源注入二进制文件的工具,从他们打包好的脚本创建单一可执行应用程序。

以下是使用其中一个工具 postject 创建单一可执行应用程序的步骤:

  1. 创建一个 JavaScript 文件

    echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js 
  2. 创建一个配置文件,用于构建一个可以注入到单一可执行应用程序中的 blob(详情请参阅生成单一可执行文件准备 blob

    echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json 
  3. 生成要注入的 blob

    node --experimental-sea-config sea-config.json 
  4. 创建 node 可执行文件的副本,并根据你的需要命名

    • 在 Windows 以外的系统上
    cp $(command -v node) hello 
    • 在 Windows 上
    node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" 

    .exe 扩展名是必需的。

  5. 移除二进制文件的签名(仅限 macOS 和 Windows)

    • 在 macOS 上
    codesign --remove-signature hello 
    • 在 Windows 上(可选)

    可以从已安装的 Windows SDK 中使用 signtool。如果跳过此步骤,请忽略 postject 发出的任何与签名相关的警告。

    signtool remove /s hello.exe 
  6. 通过运行带有以下选项的 postject,将 blob 注入到复制的二进制文件中

    • hello / hello.exe - 在第 4 步中创建的 node 可执行文件副本的名称。
    • NODE_SEA_BLOB - 二进制文件中用于存储 blob 内容的资源 / note / section 的名称。
    • sea-prep.blob - 在第 1 步中创建的 blob 的名称。
    • --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 - Node.js 项目用于检测文件是否已被注入的保险丝(fuse)
    • --macho-segment-name NODE_SEA (仅在 macOS 上需要) - 二进制文件中用于存储 blob 内容的段(segment)的名称。

    总结一下,以下是各平台所需的命令:

    • 在 Linux 上

      npx postject hello NODE_SEA_BLOB sea-prep.blob \
          --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 
    • 在 Windows - PowerShell 上

      npx postject hello.exe NODE_SEA_BLOB sea-prep.blob `
          --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 
    • 在 Windows - 命令提示符上

      npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^
          --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 
    • 在 macOS 上

      npx postject hello NODE_SEA_BLOB sea-prep.blob \
          --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
          --macho-segment-name NODE_SEA 
  7. 对二进制文件进行签名(仅限 macOS 和 Windows)

    • 在 macOS 上
    codesign --sign - hello 
    • 在 Windows 上(可选)

    需要有证书才能进行此操作。但是,未签名的二进制文件仍然可以运行。

    signtool sign /fd SHA256 hello.exe 
  8. 运行二进制文件

    • 在 Windows 以外的系统上
    $ ./hello world
    Hello, world! 
    • 在 Windows 上
    $ .\hello.exe world
    Hello, world! 

生成单一可执行文件准备 blob#

可以使用 Node.js 二进制文件的 --experimental-sea-config 标志来生成注入到应用程序中的单一可执行文件准备 blob。该标志接受一个 JSON 格式配置文件的路径。如果传递给它的路径不是绝对路径,Node.js 将使用相对于当前工作目录的路径。

该配置目前读取以下顶级字段:

{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "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"
  }
} 

如果路径不是绝对路径,Node.js 将使用相对于当前工作目录的路径。用于生成 blob 的 Node.js 二进制文件的版本必须与将要注入 blob 的版本相同。

注意:当生成跨平台的 SEA(例如,在 darwin-arm64 上为 linux-x64 生成 SEA)时,必须将 useCodeCacheuseSnapshot 设置为 false,以避免生成不兼容的可执行文件。由于代码缓存和快照只能在它们被编译的相同平台上加载,生成的可执行文件在尝试加载在不同平台上构建的代码缓存或快照时可能会在启动时崩溃。

资源#

用户可以通过将一个键-路径字典作为 assets 字段添加到配置中来包含资源。在构建时,Node.js 会从指定的路径读取资源,并将它们打包到准备 blob 中。在生成的可执行文件中,用户可以使用 sea.getAsset()sea.getAssetAsBlob() API 来检索资源。

{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "assets": {
    "a.jpg": "/path/to/a.jpg",
    "b.txt": "/path/to/b.txt"
  }
} 

单一可执行应用程序可以按如下方式访问资源:

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'); 

更多信息请参阅 sea.getAsset()sea.getAssetAsBlob()sea.getRawAsset()sea.getAssetKeys() API 的文档。

启动快照支持#

useSnapshot 字段可用于启用启动快照支持。在这种情况下,当最终的可执行文件启动时,main 脚本将不会被执行。相反,它会在构建机器上生成单一可执行应用程序准备 blob 时运行。生成的准备 blob 随后会包含一个捕获了由 main 脚本初始化的状态的快照。注入了准备 blob 的最终可执行文件将在运行时反序列化该快照。

useSnapshot 为 true 时,主脚本必须调用 v8.startupSnapshot.setDeserializeMainFunction() API 来配置需要在用户启动最终可执行文件时运行的代码。

应用程序在单一可执行应用程序中使用快照的典型模式是:

  1. 在构建时,在构建机器上,运行主脚本来初始化堆,使其达到准备好接收用户输入的状态。该脚本还应该使用 v8.startupSnapshot.setDeserializeMainFunction() 配置一个主函数。这个函数将被编译并序列化到快照中,但在构建时不会被调用。
  2. 在运行时,主函数将在用户机器上反序列化后的堆之上运行,以处理用户输入并生成输出。

启动快照脚本的一般约束也适用于用于为单一可执行应用程序构建快照的主脚本,并且主脚本可以使用 v8.startupSnapshot API 来适应这些约束。请参阅关于 Node.js 中启动快照支持的文档

V8 代码缓存支持#

当在配置中将 useCodeCache 设置为 true 时,在生成单一可执行文件准备 blob 期间,Node.js 将编译 main 脚本以生成 V8 代码缓存。生成的代码缓存将成为准备 blob 的一部分,并被注入到最终的可执行文件中。当单一可执行应用程序启动时,Node.js 将使用代码缓存来加速编译,而不是从头编译 main 脚本,然后执行脚本,这将提高启动性能。

注意:useCodeCachetrue 时,import() 不起作用。

执行参数#

execArgv 字段可用于指定 Node.js 特定的参数,这些参数将在单一可执行应用程序启动时自动应用。这允许应用程序开发人员配置 Node.js 运行时选项,而无需最终用户了解这些标志。

例如,以下配置:

{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "execArgv": ["--no-warnings", "--max-old-space-size=2048"]
} 

将指示 SEA 以 --no-warnings--max-old-space-size=2048 标志启动。在嵌入可执行文件的脚本中,可以使用 process.execArgv 属性访问这些标志:

// 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'] 

用户提供的参数位于 process.argv 数组中,从索引 2 开始,这与使用以下命令启动应用程序时的情况类似:

node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2 

执行参数扩展#

execArgvExtension 字段控制如何提供除 execArgv 字段中指定的参数之外的额外执行参数。它接受三个字符串值之一:

  • "none": 不允许扩展。只使用在 execArgv 中指定的参数,并且 NODE_OPTIONS 环境变量将被忽略。
  • "env": (默认值) NODE_OPTIONS 环境变量可以扩展执行参数。这是为了保持向后兼容性的默认行为。
  • "cli": 可执行文件可以与 --node-options="--flag1 --flag2" 一起启动,这些标志将被解析为 Node.js 的执行参数,而不是传递给用户脚本。这允许使用 NODE_OPTIONS 环境变量不支持的参数。

例如,使用 "execArgvExtension": "cli"

{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "execArgv": ["--no-warnings"],
  "execArgvExtension": "cli"
} 

可执行文件可以这样启动:

./my-sea --node-options="--trace-exit" user-arg1 user-arg2 

这等同于运行:

node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 

在注入的主脚本中#

单一可执行应用程序 API#

node:sea 内置模块允许从嵌入到可执行文件中的 JavaScript 主脚本与单一可执行应用程序进行交互。

sea.isSea()#
  • 返回:<boolean> 此脚本是否在单一可执行应用程序内运行。

sea.getAsset(key[, encoding])#

此方法可用于检索在构建时配置为打包到单一可执行应用程序中的资源。当找不到匹配的资源时,会抛出一个错误。

  • key <string> 单一可执行应用程序配置中 assets 字段指定的字典中资源的键。
  • encoding <string> 如果指定,资源将被解码为字符串。接受任何 TextDecoder 支持的编码。如果未指定,将返回一个包含资源副本的 ArrayBuffer
  • 返回:<string> | <ArrayBuffer>

sea.getAssetAsBlob(key[, options])#

类似于 sea.getAsset(),但返回一个 <Blob> 格式的结果。当找不到匹配的资源时,会抛出一个错误。

  • key <string> 单一可执行应用程序配置中 assets 字段指定的字典中资源的键。
  • options <Object>
    • type <string> blob 的可选 mime 类型。
  • 返回:<Blob>

sea.getRawAsset(key)#

此方法可用于检索在构建时配置为打包到单一可执行应用程序中的资源。当找不到匹配的资源时,会抛出一个错误。

sea.getAsset()sea.getAssetAsBlob() 不同,此方法不返回副本。相反,它返回打包在可执行文件内部的原始资源。

目前,用户应避免写入返回的数组缓冲区。如果注入的 section 未标记为可写或未正确对齐,写入返回的数组缓冲区很可能会导致崩溃。

  • key <string> 单一可执行应用程序配置中 assets 字段指定的字典中资源的键。
  • 返回:<ArrayBuffer>

sea.getAssetKeys()#

  • 返回 <string[]> 一个包含嵌入在可执行文件中的所有资源键的数组。如果没有嵌入资源,则返回一个空数组。

此方法可用于检索嵌入到单一可执行应用程序中的所有资源键的数组。如果不是在单一可执行应用程序内运行,则会抛出错误。

注入的主脚本中的 require(id) 不是基于文件的#

注入的主脚本中的 require() 与非注入模块可用的 require() 不同。它也没有非注入的 require() 所具有的任何属性,除了 require.main。它只能用于加载内置模块。尝试加载只能在文件系统中找到的模块将抛出错误。

用户可以将其应用程序打包成一个独立的 JavaScript 文件注入到可执行文件中,而不是依赖于基于文件的 require()。这也确保了更具确定性的依赖关系图。

然而,如果仍然需要基于文件的 require(),也可以实现:

const { createRequire } = require('node:module');
require = createRequire(__filename); 

注入的主脚本中的 __filenamemodule.filename#

在注入的主脚本中,__filenamemodule.filename 的值等于 process.execPath

注入的主脚本中的 __dirname#

在注入的主脚本中,__dirname 的值等于 process.execPath 的目录名。

注意#

单一可执行应用程序的创建过程#

一个旨在创建单一可执行 Node.js 应用程序的工具必须将使用 --experimental-sea-config" 准备的 blob 内容注入到:

  • 如果 node 二进制文件是 PE 文件,则注入到名为 NODE_SEA_BLOB 的资源中
  • 如果 node 二进制文件是 Mach-O 文件,则注入到 NODE_SEA 段中名为 NODE_SEA_BLOB 的节(section)中
  • 如果 node 二进制文件是 ELF 文件,则注入到名为 NODE_SEA_BLOB 的 note 中

在二进制文件中搜索 NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 保险丝(fuse) 字符串,并将最后一个字符翻转为 1,以表明资源已被注入。

平台支持#

单一可执行文件支持仅在以下平台上在 CI 中进行定期测试:

这是由于缺乏更好的工具来生成可在其他平台上测试此功能的单一可执行文件。

欢迎对其他资源注入工具/工作流程提出建议。请在 https://github.com/nodejs/single-executable/discussions 发起讨论,以帮助我们记录它们。