NodeJs-Single Executable Applications

 后端   小卒子   2024-09-11 09:30   140
  nodejs

单一可执行应用程序

稳定性: 1.1 - 活跃开发中

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

Node.js 通过允许将由 Node.js 准备的 blob 注入到 node 二进制文件中,从而支持创建 单一可执行应用程序https://github.com/nodejs/single-executablehttps://github.com/nodejs/single-executable。该 blob 可以包含一个打包好的脚本。在启动时,程序会检查是否有注入内容。如果找到 blob,它会执行 blob 中的脚本。否则,Node.js 将按正常方式运行。

目前,单一可执行应用程序功能仅支持使用 CommonJSmodules.md#modules-commonjs-modulesmodules.md#modules-commonjs-modules 模块系统运行单个嵌入式脚本。

用户可以使用 node 二进制文件及任何能够将资源注入二进制文件的工具来创建单一可执行应用程序。

以下是使用其中一种工具 postjecthttps://github.com/nodejs/postjecthttps://github.com/nodejs/postject 创建单一可执行应用程序的步骤:

  1. 创建一个 JavaScript 文件:

    echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
    
  2. 创建一个配置文件来构建可以注入到单一可执行应用程序中的 blob(详细信息见 生成单一可执行准备 blob#生成单一可执行准备-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 上(可选):

    signtoolhttps://learn.microsoft.com/en-us/windows/win32/seccrypto/signtoolhttps://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool 可以从已安装的 Windows SDKhttps://developer.microsoft.com/en-us/windows/downloads/windows-sdk/https://developer.microsoft.com/en-us/windows/downloads/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 项目用于检测是否注入了文件的 fusehttps://www.electronjs.org/docs/latest/tutorial/fuseshttps://www.electronjs.org/docs/latest/tutorial/fuses。
    • --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

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

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

{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "disableExperimentalSEAWarning": true, // 默认: false
  "useSnapshot": false,  // 默认: false
  "useCodeCache": true, // 默认: false
  "assets": {  // 可选
    "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

用户可以通过在配置文件中添加一个 key-path 字典来包含资产,作为 assets 字段。在构建时,Node.js 将从指定的路径读取资产并将其打包到准备 blob 中。在生成的可执行文件中,用户可以使用 sea.getAsset()#seagetassetkey-encoding#seagetassetkey-encoding 和 sea.getAssetAsBlob()#seagetassetasblobkey-options#seagetassetasblobkey-options 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');
// 返回以 ArrayBuffer 形式的资产数据副本。
const image = getAsset('a.jpg');
// 返回解码为 UTF8 的资产字符串。
const text = getAsset('b.txt', 'utf8');
// 返回包含资产的 Blob。
const blob = getAssetAsBlob('a.jpg');
// 返回包含原始资产的 ArrayBuffer。
const raw = getRawAsset('a.jpg');

有关 sea.getAsset()#seagetassetkey-encoding#seagetassetkey-encoding, sea.getAssetAsBlob()#seagetassetasblobkey-options#seagetassetasblobkey-options 和 sea.getRawAsset()#seagetrawassetkey#seagetrawassetkey API 的更多信息,请参阅文档。

启动快照支持

可以使用 useSnapshot 字段来启用启动快照支持。在这种情况下,main 脚本不会在最终可执行文件启动时运行。相反,它将在构建机器上运行时运行。生成的准备 blob 将包括捕获 main 脚本初始化的状态的快照。最终的可执行文件将会在运行时反序列化快照。

useSnapshot 为 true 时,main 脚本必须

调用 v8.startupSnapshot.setDeserializeMainFunction()v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-datav8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data API 来配置在用户机器上启动时需要运行的代码。

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

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

启动快照脚本的一般约束也适用于用于构建单一可执行应用程序的 main 脚本,且主脚本可以使用 v8.startupSnapshot APIv8.md#startup-snapshot-apiv8.md#startup-snapshot-api 来适应这些约束。请参阅 Node.js 启动快照支持文档cli.md#--build-snapshotcli.md#--build-snapshot。

V8 代码缓存支持

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

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

注入的主脚本中的 API

单一可执行应用程序 API

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

sea.isSea()

  • 返回: {boolean} 是否在单一可执行应用程序中运行此脚本。

sea.getAsset(key[, encoding])

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

  • key {string} 资产在单一可执行应用程序配置中的键。
  • encoding {string} 如果指定,将以字符串形式解码资产。接受 TextDecoder 支持的任何编码。如果未指定,将返回包含资产副本的 ArrayBuffer
  • 返回: {string|ArrayBuffer}

sea.getAssetAsBlob(key[, options])

类似于 sea.getAsset()#seagetassetkey-encoding#seagetassetkey-encoding, 但返回结果为 Blobhttps://developer.mozilla.org/en-US/docs/Web/API/Blobhttps://developer.mozilla.org/en-US/docs/Web/API/Blob。
当找不到匹配的资产时,会抛出错误。

  • key {string} 资产在单一可执行应用程序配置中的键。
  • options {Object}
    • type {string} 可选的 mime 类型。
  • 返回: {Blob}

sea.getRawAsset(key)

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

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

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

  • key {string} 资产在单一可执行应用程序配置中的键。
  • 返回: {string|ArrayBuffer}

注入的主脚本中的 require(id)

注入的主脚本中的 require() 与非注入模块的 require()modules.md#requireidmodules.md#requireid 不同。它也没有非注入 require()modules.md#requireidmodules.md#requireid 的任何属性,除了 require.mainmodules.md#accessing-the-main-modulemodules.md#accessing-the-main-module. 它只能用于加载内置模块。尝试加载只能在文件系统中找到的模块会抛出错误。

用户可以将他们的应用程序打包成独立的 JavaScript 文件以注入到可执行文件中,这也确保了更确定的依赖关系图。

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

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

注入的主脚本中的 __filenamemodule.filename

注入的主脚本中的 __filenamemodule.filename 的值等于 process.execPathprocess.md#processexecpathprocess.md#processexecpath。

注入的主脚本中的 __dirname

注入的主脚本中的 __dirname 的值等于 process.execPathprocess.md#processexecpathprocess.md#processexecpath 的目录名。

注意事项

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

旨在创建单一可执行 Node.js 应用程序的工具必须将准备好的 blob 内容注入到:

  • 如果 node 二进制文件是 PEhttps://en.wikipedia.org/wiki/Portable_Executablehttps://en.wikipedia.org/wiki/Portable_Executable 文件,则注入名为 NODE_SEA_BLOB 的资源
  • 如果 node 二进制文件是 Mach-Ohttps://en.wikipedia.org/wiki/Mach-Ohttps://en.wikipedia.org/wiki/Mach-O 文件,则注入名为 NODE_SEA_BLOB 的部分到 NODE_SEA 段中
  • 如果 node 二进制文件是 ELFhttps://en.wikipedia.org/wiki/Executable_and_Linkable_Formathttps://en.wikipedia.org/wiki/Executable_and_Linkable_Format 文件,则注入名为 NODE_SEA_BLOB 的注释

在二进制文件中搜索 NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 fusehttps://www.electronjs.org/docs/latest/tutorial/fuseshttps://www.electronjs.org/docs/latest/tutorial/fuses 字符串,并将最后一个字符翻转为 1 以指示已注入资源。

平台支持

单一可执行支持在 CI 上定期测试的以下平台上:

  • Windows
  • macOS
  • Linux(所有 Node.js 支持的发行版支持的架构https://github.com/nodejs/node/blob/main/BUILDING.md#platform-listhttps://github.com/nodejs/node/blob/main/BUILDING.md#platform-list)

这是由于缺乏可以用于测试该功能的更好的工具。

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