同梱した Node.js スクリプトを実行する (SEA)

Recipe ID: shell-011

Node.js で書かれたスクリプトをバイナリ化し、Sidecar として実行する方法を解説します。
ここでは Node.js 20 以降で標準提供されている Single Executable Applications (SEA) 機能を使用して、Node.js 実行環境ごと単一のファイルにまとめて配布する方法を紹介します。

前提条件

1. Node.js: バージョン 20 以上が必要です。
2. プラグインのインストール:

npm run tauri add shell

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
    "permissions": [
        ...,
        "shell:allow-open",
        {
            "identifier": "shell:allow-execute",
            "allow": [
                {
                    "name": "binaries/app",
                    "sidecar": true,
                    "args": true
                }
            ]
        }
    ]
}

手順

1. Node.js スクリプトのバイナリ化 (SEA)

Node.js の SEA 機能を使うと、スクリプト (.js) を Node.js バイナリ自体に注入(Inject)して、単一の実行可能ファイルを作成できます。

Step 1: JS ファイルの準備

まず、実行したいスクリプト app.js を用意します。

// app.js
console.log("Hello from Node.js SEA Sidecar!");

Step 2: 設定ファイルの作成

sea-config.json を作成します。

{
  "main": "app.js",
  "output": "sea-prep.blob"
}

Step 3: Blob の生成

以下のコマンドを実行し、スクリプトを実行可能な形式 (Blob) に変換します。

node --experimental-sea-config sea-config.json

これで sea-prep.blob が生成されます。

Step 4: 実行可能ファイルの作成と注入

元の node 実行ファイルをコピーし、そこに Blob を注入します。注入には postject ツールを使います。

# postject をインストール
npm install -g postject

# 現在の node 実行ファイルをコピー (Windows の場合は node.exe)
# ※以下は Windows (PowerShell) の例です。
# Mac/Linux の場合は `cp $(which node) app` などでコピーしてください。
Copy-Item (Get-Command node).Source app.exe

# Blob を注入 (Windows)
# --sentinel-fuse は SEA のマジックナンバーです
npx postject app.exe NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

これで app.exe (または Mac/Linux なら app) という単一の実行可能ファイルが完成しました。これを実行すると app.js の内容が動作します。

2. バイナリの配置とリネーム

Tauri の Sidecar として認識させるためには、バイナリファイル名を <command>-<target-triple><extension> の形式で配置する必要があります。

ターゲットトリプル(Target Triple)は rustc -Vv コマンドなどで確認できます(例: host: x86_64-pc-windows-msvc)。

例: Windows (x64) で app というコマンド名にする場合
1. 生成された app.exe をコピーします。
2. プロジェクトの src-tauri/binaries/ フォルダ(なければ作成)に配置します。
3. ファイル名を app-x86_64-pc-windows-msvc.exe にリネームします。

3. tauri.conf.json の設定

bundle 設定でバイナリの場所(相対パス)を指定し、externalBin に登録します。

{
  "bundle": {
    "externalBin": [
      "binaries/app"
    ]
  }
}

※ 設定には 拡張子やターゲットトリプルは含めず、コマンド名のみを記述します(binaries/app)。

1. フロントエンドから作成する (TypeScript)

Command.sidecar メソッドを使用します。引数には src-tauri/binaries/ 以下のファイル名(コマンド名)を指定します。

import { Command } from '@tauri-apps/plugin-shell';

async function runNodeSidecar() {
  // externalBin に登録したパスを指定します
  const cmd = Command.sidecar('binaries/app');
  
  const output = await cmd.execute();
  console.log(output.stdout); // "Hello from Node.js SEA Sidecar!"
}

2. バックエンドから作成する (Rust)

tauri_plugin_shell::ShellExt を使用して Sidecar コマンドを実行します。

前提: Cargo.toml

tauri-plugin-shell クレートを依存関係に追加している必要があります。

実装例

use tauri_plugin_shell::ShellExt;
use tauri::AppHandle;

#[tauri::command]
async fn run_node_sidecar(app: AppHandle) -> Result<String, String> {
    // externalBin に登録されているサイドカーを実行
    // "binaries/app" と登録してあっても、渡すのは "app" (コマンド名) のみです
    let output = app.shell()
        .sidecar("app")
        .map_err(|e| e.to_string())?
        .output()
        .await
        .map_err(|e| e.to_string())?;

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        Ok(stdout.to_string())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        Err(stderr.to_string())
    }
}

トラブルシューティング

「指定されたパスが見つかりません」というエラーが出る場合、以下の点を入念に確認してください。

1. ファイル名は正しいか: app-x86_64-pc-windows-msvc.exe のように、現在のプラットフォームのターゲットトリプルが正確に含まれている必要があります。rustc -Vv で確認した host の値と一字一句一致しているか確認してください。
2. フォルダは正しいか: src-tauri/binaries/ フォルダの中にファイルがあるか確認してください。
3. 設定は正しいか:
- TypeScript: tauri.conf.json と同じパス "binaries/app" を指定してください。
- Rust: コマンド名 "app" のみを指定してください。