単一実行可能アプリケーション#

安定性: 1.1 - アクティブな開発

ソースコード: src/node_sea.cc

この機能を使用すると、Node.js がインストールされていないシステムに Node.js アプリケーションを便利に配布できます。

Node.js は、バンドルされたスクリプトを含むことができる Node.js によって準備された blob を node バイナリにインジェクトできるようにすることで、単一の実行可能アプリケーションの作成をサポートします。起動中に、プログラムは何もインジェクトされているかを確認します。blob が見つかった場合、blob 内のスクリプトを実行します。それ以外の場合、Node.js は通常どおり動作します。

単一実行可能アプリケーション機能は現在、CommonJSモジュールシステムを使用した単一の埋め込みスクリプトの実行のみをサポートしています。

ユーザーは、バンドルされたスクリプトから、node バイナリ自体と、リソースをバイナリにインジェクトできるツールを使用して、単一実行可能アプリケーションを作成できます。

そのようなツールの 1 つである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 プロジェクトで使用されるfuse
    • --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 の生成#

アプリケーションにインジェクトされる単一実行可能準備 blob は、単一実行可能ファイルのビルドに使用される Node.js バイナリの --experimental-sea-config フラグを使用して生成できます。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() API と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() API とsea.getAssetAsBlob() API のドキュメントを参照してください。

スタートアップスナップショットのサポート#

useSnapshot フィールドを使用すると、スタートアップスナップショットのサポートを有効にできます。この場合、最終的な実行可能ファイルが起動されたときに、main スクリプトは実行されません。代わりに、単一実行可能アプリケーション準備 blob がビルドマシンで生成されたときに実行されます。生成された準備 blob には、main スクリプトによって初期化された状態をキャプチャしたスナップショットが含まれます。準備 blob がインジェクトされた最終的な実行可能ファイルは、実行時にスナップショットをデシリアライズします。

useSnapshot が true の場合、メインスクリプトはユーザーによって最終的な実行可能ファイルが起動されたときに実行する必要があるコードを構成するために、v8.startupSnapshot.setDeserializeMainFunction() API を呼び出す必要があります。

単一実行可能アプリケーションでスナップショットを使用するアプリケーションの典型的なパターンは次のとおりです

  1. ビルド時に、ビルドマシンで、メインスクリプトが実行され、ヒープがユーザー入力を受け入れる準備ができている状態に初期化されます。スクリプトでは、v8.startupSnapshot.setDeserializeMainFunction() を使用してメイン関数を構成する必要があります。この関数は、コンパイルされ、スナップショットにシリアライズされますが、ビルド時には呼び出されません。
  2. 実行時には、メイン関数はユーザーマシン上のデシリアライズされたヒープ上で実行され、ユーザー入力を処理して出力を生成します。

スタートアップスナップショットスクリプトの一般的な制約は、単一実行可能アプリケーションのスナップショットを構築するために使用されるメインスクリプトにも適用され、メインスクリプトはv8.startupSnapshot API を使用してこれらの制約に対応できます。Node.js のスタートアップスナップショットのサポートに関するドキュメントを参照してください。

V8 コードキャッシュのサポート#

構成で useCodeCachetrue に設定されている場合、単一実行可能準備 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() とは異なり、このメソッドはコピーを返しません。代わりに、実行可能ファイル内にバンドルされた生のアセットを返します。

今のところ、ユーザーは返された ArrayBuffer への書き込みを避けるべきです。挿入されたセクションが書き込み可能としてマークされていないか、適切にアラインされていない場合、返された ArrayBuffer への書き込みはクラッシュにつながる可能性があります。

  • key <string> 単一実行可能アプリケーション構成の assets フィールドで指定された辞書内のアセットのキー。
  • 戻り値: <string> | <ArrayBuffer>

注入されたメインスクリプト内の require(id) はファイルベースではありません#

注入されたメインスクリプト内の require() は、注入されていないモジュールで使用できる require() とは異なります。また、注入されていない require() が持つプロパティのうち、require.main 以外は何も持っていません。組み込みモジュールをロードするためにのみ使用できます。ファイルシステムでのみ見つけることができるモジュールをロードしようとすると、エラーがスローされます。

ファイルベースの require() に依存する代わりに、アプリケーションをスタンドアロンの JavaScript ファイルにバンドルして、実行可能ファイルに注入できます。これにより、より決定論的な依存関係グラフも保証されます。

ただし、ファイルベースの require() がまだ必要な場合は、それも実現できます。

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

注入されたメインスクリプト内の __filename および module.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 でディスカッションを開始してください。