mcpField
E
embedding-tauri-sidecars

embedding-tauri-sidecars

github.com/dchuk開発36183 回インストール40 週間前に更新
リポジトリ

embedding-tauri-sidecars について

Tauri 開発者向けに、外部バイナリをサイドカーとしてアプリに組み込み、実行する方法を解説するガイドです。設定、プラットフォーム別の命名、Rust または JavaScript からの実行、プロセス管理を扱います。

主な機能

  1. 01バンドルするサイドカーの設定
  2. 02対象プラットフォーム別のバイナリ命名
  3. 03Rust と JavaScript からの実行
  4. 04出力ストリーミングとプロセス管理
  5. 05実行引数の制限

活用シーン

  1. 01Python CLI のアプリへの同梱
  2. 02バックグラウンド API サーバーの実行
  3. 03外部バイナリによるファイル処理
  4. 04プラットフォーム別実行ファイルの同梱

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill embedding-tauri-sidecars

インストール後、Claude Code で/embedding-tauri-sidecarsを入力して読み込まれたか確認します。

詳細

Tauri サイドカー:外部バイナリの埋め込み

このスキルでは、Tauri アプリケーションへの外部バイナリ(サイドカー)の埋め込みと実行について、設定、クロスプラットフォームでの考慮事項、Rust および JavaScript からの実行方法を説明します。

概要

サイドカーは、機能を拡張したり、ユーザーが依存関係をインストールする必要をなくしたりするために、Tauri アプリケーションに埋め込む外部バイナリです。任意のプログラミング言語で記述された実行ファイルを使用できます。

一般的なユースケース:

  • PyInstaller でパッケージ化した Python CLI アプリケーション
  • 特定のタスク向けにコンパイルした Go または Rust バイナリ
  • 実行ファイルとしてバンドルした Node.js アプリケーション
  • API サーバーまたはバックグラウンドサービス

プラグインの依存関係

サイドカーには shell プラグインが必要です。

Cargo.toml:

[dependencies]
tauri-plugin-shell = "2"

main.rs で登録:

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンドパッケージ:

npm install @tauri-apps/plugin-shell

設定

サイドカーの登録

tauri.conf.json の bundle.externalBin でサイドカーを設定します。パスは src-tauri からの相対パスです。

{
  "bundle": {
    "externalBin": [
      "binaries/my-sidecar",
      "../external/processor"
    ]
  }
}

重要: パスにはベース名を指定します。ビルド時に Tauri がターゲットトリプルのサフィックスを追加します。

クロスプラットフォームでのバイナリ命名

各サイドカーには、ターゲットトリプルのサフィックスが付いた、プラットフォーム固有のバリアントが必要です。

プラットフォームアーキテクチャ必須ファイル名
Linuxx86_64my-sidecar-x86_64-unknown-linux-gnu
LinuxARM64my-sidecar-aarch64-unknown-linux-gnu
macOSIntelmy-sidecar-x86_64-apple-darwin
macOSApple Siliconmy-sidecar-aarch64-apple-darwin
Windowsx86_64my-sidecar-x86_64-pc-windows-msvc.exe

ターゲットトリプルを確認する:

rustc --print host-tuple    # Rust 1.84.0+
rustc -Vv | grep host       # Older versions
ディレクトリ構成
src-tauri/
  binaries/
    my-sidecar-x86_64-unknown-linux-gnu
    my-sidecar-aarch64-apple-darwin
    my-sidecar-x86_64-apple-darwin
    my-sidecar-x86_64-pc-windows-msvc.exe
  tauri.conf.json
  src/main.rs

Rust からサイドカーを実行する

基本的な実行
use tauri_plugin_shell::ShellExt;

#[tauri::command]
async fn run_sidecar(app: tauri::AppHandle) -> Result<String, String> {
    let output = app
        .shell()
        .sidecar("my-sidecar")
        .map_err(|e| e.to_string())?
        .output()
        .await
        .map_err(|e| e.to_string())?;

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

注: sidecar() には、設定内のフルパスではなくファイル名のみを渡します。

引数を指定して実行
#[tauri::command]
async fn process_file(app: tauri::AppHandle, file_path: String) -> Result<String, String> {
    let output = app
        .shell()
        .sidecar("processor")
        .map_err(|e| e.to_string())?
        .args(["--input", &file_path, "--format", "json"])
        .output()
        .await
        .map_err(|e| e.to_string())?;

    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
長時間実行するプロセスの起動

継続的に実行するサイドカー(API サーバー、ウォッチャーなど)の場合:

use tauri_plugin_shell::{ShellExt, process::CommandEvent};

#[tauri::command]
async fn start_server(app: tauri::AppHandle) -> Result<u32, String> {
    let (mut rx, child) = app
        .shell()
        .sidecar("api-server")
        .map_err(|e| e.to_string())?
        .args(["--port", "8080"])
        .spawn()
        .map_err(|e| e.to_string())?;

    let pid = child.pid();

    tauri::async_runtime::spawn(async move {
        while let Some(event) = rx.recv().await {
            match event {
                CommandEvent::Stdout(line) => println!("{}", String::from_utf8_lossy(&line)),
                CommandEvent::Stderr(line) => eprintln!("{}", String::from_utf8_lossy(&line)),
                CommandEvent::Terminated(payload) => {
                    println!("Terminated: {:?}", payload.code);
                    break;
                }
                _ => {}
            }
        }
    });

    Ok(pid)
}
サイドカーのライフサイクル管理
use std::sync::Mutex;
use tauri::State;
use tauri_plugin_shell::{ShellExt, process::CommandChild};

struct SidecarState {
    child: Mutex<Option<CommandChild>>,
}

#[tauri::command]
async fn start_sidecar(app: tauri::AppHandle, state: State<'_, SidecarState>) -> Result<(), String> {
    let (_, child) = app.shell().sidecar("service").map_err(|e| e.to_string())?
        .spawn().map_err(|e| e.to_string())?;
    *state.child.lock().unwrap() = Some(child);
    Ok(())
}

#[tauri::command]
async fn stop_sidecar(state: State<'_, SidecarState>) -> Result<(), String> {
    if let Some(child) = state.child.lock().unwrap().take() {
        child.kill().map_err(|e| e.to_string())?;
    }
    Ok(())
}

JavaScript からサイドカーを実行する

権限の設定

src-tauri/capabilities/default.json でシェルの実行権限を付与します。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{ "name": "binaries/my-sidecar", "sidecar": true }]
    }
  ]
}
基本的な実行
import { Command } from '@tauri-apps/plugin-shell';

async function runSidecar(): Promise<string> {
  const command = Command.sidecar('binaries/my-sidecar');
  const output = await command.execute();
  if (output.code === 0) return output.stdout;
  throw new Error(output.stderr);
}
引数を指定して実行
async function processFile(filePath: string): Promise<string> {
  const command = Command.sidecar('binaries/processor', [
    '--input', filePath, '--format', 'json'
  ]);
  const output = await command.execute();
  return output.stdout;
}
ストリーミング出力の処理
import { Command, Child } from '@tauri-apps/plugin-shell';

async function runWithStreaming(): Promise<Child> {
  const command = Command.sidecar('binaries/long-task');

  command.on('close', (data) => console.log(`Finished: ${data.code}`));
  command.on('error', (error) => console.error(error));
  command.stdout.on('data', (line) => console.log(line));
  command.stderr.on('data', (line) => console.error(line));

  return await command.spawn();
}
長時間実行するプロセスの管理
let serverProcess: Child | null = null;

async function startServer(): Promise<number> {
  const command = Command.sidecar('binaries/api-server', ['--port', '8080']);
  command.stdout.on('data', console.log);
  serverProcess = await command.spawn();
  return serverProcess.pid;
}

async function stopServer(): Promise<void> {
  if (serverProcess) {
    await serverProcess.kill();
    serverProcess = null;
  }
}

引数の検証

capabilities で引数の検証を設定します。

{
  "identifier": "shell:allow-execute",
  "allow": [{
    "name": "binaries/my-sidecar",
    "sidecar": true,
    "args": [
      "-o",
      "--verbose",
      { "validator": "\\S+" }
    ]
  }]
}

引数の種類:

  • 静的文字列:完全一致が必要です(-o、--verbose)
  • バリデーターオブジェクト:動的な値に対する正規表現パターンです
  • true:任意の引数を許可します(慎重に使用してください)

クロスプラットフォームでの考慮事項

プラットフォーム固有のバイナリのビルド

Rust のサイドカー:

cargo build --release --target x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/release/my-tool \
   src-tauri/binaries/my-tool-x86_64-unknown-linux-gnu

PyInstaller を使用した Python:

pyinstaller --onefile my_script.py
mv dist/my_script dist/my_script-x86_64-unknown-linux-gnu
プラットフォームに関する注意事項

Windows:

  • 実行ファイルには .exe 拡張子が必要です
  • テキストファイルを処理する際は、改行コードを考慮してください

macOS:

  • ユニバーサルバイナリ(Intel + Apple Silicon)には lipo を使用します
  • 配布時にコード署名が必要になる場合があります
  • Gatekeeper が署名されていないサイドカーをブロックする場合があります

Linux:

  • バイナリに実行権限を付与します(chmod +x)
  • glibc バージョンとの互換性を考慮してください
  • 静的リンクにより依存関係の問題を軽減できます

完全な例

tauri.conf.json:

{
  "productName": "My App",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "bundle": {
    "externalBin": ["binaries/data-processor"]
  }
}

capabilities/default.json:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{
        "name": "binaries/data-processor",
        "sidecar": true,
        "args": [
          "--input", { "validator": "^[a-zA-Z0-9_\\-./]+$" },
          "--output", { "validator": "^[a-zA-Z0-9_\\-./]+$" }
        ]
      }]
    }
  ]
}

src/main.rs:

use tauri_plugin_shell::ShellExt;

#[tauri::command]
async fn process_data(app: tauri::AppHandle, input: String, output: String) -> Result<String, String> {
    let result = app.shell().sidecar("data-processor").map_err(|e| e.to_string())?
        .args(["--input", &input, "--output", &output])
        .output().await.map_err(|e| e.to_string())?;

    if result.status.success() {
        Ok(String::from_utf8_lossy(&result.stdout).to_string())
    } else {
        Err(String::from_utf8_lossy(&result.stderr).to_string())
    }
}

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![process_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンド(App.tsx):

import { invoke } from '@tauri-apps/api/core';

function App() {
  const handleProcess = async () => {
    try {
      const result = await invoke('process_data', {
        input: '/path/to/input.txt',
        output: '/path/to/output.txt'
      });
      console.log('Result:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return <button onClick={handleProcess}>Process Data</button>;
}

ベストプラクティス

  1. すべてのサイドカーパスを検証する:信頼できないパスをサイドカーに渡さないでください
  2. 引数バリデーターを使用する:capabilities で許可する引数を制限します
  3. エラーを適切に処理する:サイドカーが失敗したり、見つからなかったりする場合があります
  4. プロセスをクリーンアップする:アプリ終了時に起動したプロセスを停止します
  5. すべてのプラットフォームでテストする:バイナリの命名と実行方法はプラットフォームによって異なります
  6. バイナリサイズを考慮する:サイドカーによってバンドルサイズが大幅に増加します

コメント