单文件可执行应用程序#

稳定性: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 内容的资源/注释/部分的名称。
    • sea-prep.blob - 在步骤 1 中创建的 Blob 的名称。
    • --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 - Node.js 项目用于检测文件是否已被注入的熔丝
    • --macho-segment-name NODE_SEA(仅在 macOS 上需要)- 二进制文件中将存储 Blob 内容的段的名称。

    总结一下,这是每个平台所需的命令

    • 在 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
  "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 } = require('node:sea');
// 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() 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() 不起作用。

在注入的主脚本中#

单可执行应用程序 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() 不同,此方法不返回副本。 相反,它返回捆绑在可执行文件中的原始资源。

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

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

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

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

用户可以将其应用程序捆绑到独立的 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 的部分
  • 如果 node 二进制文件是 ELF 文件,则注入到名为 NODE_SEA_BLOB 的注释中

在二进制文件中搜索 NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 fuse 字符串,并将最后一个字符翻转为 1,以指示已注入资源。

平台支持#

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

  • Windows
  • macOS
  • Linux (Node.js 支持 的所有发行版,除了 Alpine,以及 Node.js 支持 的所有架构,除了 s390x)

这是因为缺少更好的工具来生成单可执行文件,这些文件可用于在其他平台上测试此功能。

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