シングル実行可能アプリケーション#

安定性: 1.1 - Active development

ソースコード: 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 プロジェクトがファイルがインジェクトされたかどうかを検出するために使用するヒューズ
    • --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
  "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 コードキャッシュのサポート#

設定で useCodeCachetrue に設定されている場合、シングル実行可能アプリケーション準備 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 フィールドで指定された引数に加えて、追加の実行引数をどのように提供できるかを制御します。これは次の3つの文字列値のいずれかを受け入れます。

  • "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() とは異なり、このメソッドはコピーを返しません。代わりに、実行可能ファイル内にバンドルされた生のアセットを返します。

現時点では、返された配列バッファへの書き込みは避けるべきです。インジェクトされたセクションが書き込み可能としてマークされていないか、適切にアライメントされていない場合、返された配列バッファへの書き込みはクラッシュを引き起こす可能性が高いです。

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

sea.getAssetKeys()#

  • 戻り値 <string[]> 実行可能ファイルに埋め込まれたすべてのアセットのキーを含む配列。アセットが埋め込まれていない場合は、空の配列を返します。

このメソッドは、シングル実行可能アプリケーションに埋め込まれたすべてのアセットのキーの配列を取得するために使用できます。シングル実行可能アプリケーション内で実行されていない場合、エラーがスローされます。

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

インジェクトされたメインスクリプト内の require() は、インジェクトされていないモジュールで利用可能な require() とは異なります。また、インジェクトされていない require() が持つプロパティは、require.main を除いて何も持っていません。これはビルトインモジュールのロードにのみ使用できます。ファイルシステムでしか見つからないモジュールをロードしようとすると、エラーがスローされます。

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

ただし、それでもファイルベースの 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 というヒューズ文字列を検索し、最後の文字を 1 に反転させて、リソースがインジェクトされたことを示します。

プラットフォームサポート#

シングル実行可能ファイルのサポートは、CI で以下のプラットフォームでのみ定期的にテストされています。

これは、他のプラットフォームでこの機能をテストするために使用できる、より優れたシングル実行可能ファイル生成ツールがないためです。

他のリソースインジェクションツールやワークフローの提案を歓迎します。それらを文書化するのを手伝うために、https://github.com/nodejs/single-executable/discussions でディスカッションを開始してください。