單一可執行應用程式#

穩定性: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 發起討論,以幫助我們記錄它們。