单一可执行应用程序#

稳定性: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 上(可选)

    signtool 可以从已安装的Windows SDK 中使用。如果跳过此步骤,请忽略 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 的版本相同。

资产#

用户可以通过将键路径字典添加到配置中作为 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 } = 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() API 的文档。

启动快照支持#

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

useSnapshot 为真时,主脚本必须调用 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.getRawAsset()sea.getAssetAsBlob() 不同,此方法不返回副本。相反,它返回捆绑在可执行文件中的原始资产。

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

注入的主脚本中的 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 的部分
  • 如果 node 二进制文件是 ELF 文件,则将其注入名为 NODE_SEA_BLOB 的注释中

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

平台支持#

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

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

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