embedding-tauri-sidecars

📁 beshkenadze/claude-code-tauri-skills 📅 14 days ago
1
总安装量
1
周安装量
#42413
全站排名
安装命令
npx skills add https://github.com/beshkenadze/claude-code-tauri-skills --skill embedding-tauri-sidecars

Agent 安装分布

cursor 1
codex 1
claude-code 1
gemini-cli 1

Skill 文档

Tauri Sidecars: Embedding External Binaries

This skill covers embedding and executing external binaries (sidecars) in Tauri applications, including configuration, cross-platform considerations, and execution from Rust and JavaScript.

Overview

Sidecars are external binaries embedded within Tauri applications to extend functionality or eliminate the need for users to install dependencies. They can be executables written in any programming language.

Common Use Cases:

  • Python CLI applications packaged with PyInstaller
  • Go or Rust compiled binaries for specific tasks
  • Node.js applications bundled as executables
  • API servers or background services

Plugin Dependency

Sidecars require the shell plugin:

Cargo.toml:

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

Register in main.rs:

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

Frontend package:

npm install @tauri-apps/plugin-shell

Configuration

Registering Sidecars

Configure sidecars in tauri.conf.json under bundle.externalBin. Paths are relative to src-tauri:

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

Important: The path is a stem. Tauri appends the target triple suffix at build time.

Cross-Platform Binary Naming

Each sidecar requires platform-specific variants with target triple suffixes:

Platform Architecture Required Filename
Linux x86_64 my-sidecar-x86_64-unknown-linux-gnu
Linux ARM64 my-sidecar-aarch64-unknown-linux-gnu
macOS Intel my-sidecar-x86_64-apple-darwin
macOS Apple Silicon my-sidecar-aarch64-apple-darwin
Windows x86_64 my-sidecar-x86_64-pc-windows-msvc.exe

Determine your target triple:

rustc --print host-tuple    # Rust 1.84.0+
rustc -Vv | grep host       # Older versions

Directory Structure

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

Executing Sidecars from Rust

Basic Execution

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())
    }
}

Note: Pass only the filename to sidecar(), not the full path from configuration.

With Arguments

#[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())
}

Spawning Long-Running Processes

For sidecars that run continuously (API servers, watchers):

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)
}

Managing Sidecar Lifecycle

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(())
}

Executing Sidecars from JavaScript

Permission Configuration

Grant shell execution permissions in 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 }]
    }
  ]
}

Basic Execution

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);
}

With Arguments

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;
}

Handling Streaming Output

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();
}

Managing Long-Running Processes

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;
  }
}

Argument Validation

Configure argument validation in capabilities:

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

Argument types:

  • Static string: Exact match required (-o, --verbose)
  • Validator object: Regex pattern for dynamic values
  • true: Allow any argument (use with caution)

Cross-Platform Considerations

Building Platform-Specific Binaries

Rust sidecars:

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

Python with PyInstaller:

pyinstaller --onefile my_script.py
mv dist/my_script dist/my_script-x86_64-unknown-linux-gnu

Platform Notes

Windows:

  • Executables must have .exe extension
  • Handle line endings in text file processing

macOS:

  • Use lipo for universal binaries (Intel + Apple Silicon)
  • Code signing may be required for distribution
  • Gatekeeper may block unsigned sidecars

Linux:

  • Mark binaries as executable (chmod +x)
  • Consider glibc version compatibility
  • Static linking reduces dependency issues

Complete Example

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");
}

Frontend (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>;
}

Best Practices

  1. Validate all sidecar paths: Never pass untrusted paths to sidecars
  2. Use argument validators: Restrict allowed arguments in capabilities
  3. Handle errors gracefully: Sidecars may fail or be missing
  4. Clean up processes: Kill spawned processes on app exit
  5. Test on all platforms: Binary naming and execution varies
  6. Consider binary size: Sidecars increase bundle size significantly