embedding-tauri-sidecars

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Tauri Sidecars: Embedding External Binaries

Tauri Sidecars:嵌入外部二进制文件

This skill covers embedding and executing external binaries (sidecars) in Tauri applications, including configuration, cross-platform considerations, and execution from Rust and JavaScript.
本技能涵盖了如何在Tauri应用中嵌入和执行外部二进制文件(sidecars),包括配置、跨平台注意事项,以及通过Rust和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
Sidecars是嵌入在Tauri应用中的外部二进制文件,用于扩展功能或免除用户安装依赖的需求。它们可以是用任何编程语言编写的可执行文件。
常见使用场景:
  • 使用PyInstaller打包的Python CLI应用
  • 用于特定任务的Go或Rust编译二进制文件
  • 打包为可执行文件的Node.js应用
  • API服务器或后台服务

Plugin Dependency

插件依赖

Sidecars require the shell plugin:
Cargo.toml:
toml
[dependencies]
tauri-plugin-shell = "2"
Register in main.rs:
rust
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
Frontend package:
bash
npm install @tauri-apps/plugin-shell
Sidecars需要shell插件:
Cargo.toml:
toml
[dependencies]
tauri-plugin-shell = "2"
在main.rs中注册:
rust
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
前端包安装:
bash
npm install @tauri-apps/plugin-shell

Configuration

配置

Registering Sidecars

注册Sidecars

Configure sidecars in
tauri.conf.json
under
bundle.externalBin
. Paths are relative to
src-tauri
:
json
{
  "bundle": {
    "externalBin": [
      "binaries/my-sidecar",
      "../external/processor"
    ]
  }
}
Important: The path is a stem. Tauri appends the target triple suffix at build time.
tauri.conf.json
bundle.externalBin
下配置sidecars。路径相对于
src-tauri
json
{
  "bundle": {
    "externalBin": [
      "binaries/my-sidecar",
      "../external/processor"
    ]
  }
}
重要提示: 路径是主干名称。Tauri会在构建时自动添加目标三元组后缀。

Cross-Platform Binary Naming

跨平台二进制文件命名

Each sidecar requires platform-specific variants with target triple suffixes:
PlatformArchitectureRequired Filename
Linuxx86_64
my-sidecar-x86_64-unknown-linux-gnu
LinuxARM64
my-sidecar-aarch64-unknown-linux-gnu
macOSIntel
my-sidecar-x86_64-apple-darwin
macOSApple Silicon
my-sidecar-aarch64-apple-darwin
Windowsx86_64
my-sidecar-x86_64-pc-windows-msvc.exe
Determine your target triple:
bash
rustc --print host-tuple    # Rust 1.84.0+
rustc -Vv | grep host       # Older versions
每个sidecar需要带有目标三元组后缀的平台特定变体:
平台架构所需文件名
Linuxx86_64
my-sidecar-x86_64-unknown-linux-gnu
LinuxARM64
my-sidecar-aarch64-unknown-linux-gnu
macOSIntel
my-sidecar-x86_64-apple-darwin
macOSApple Silicon
my-sidecar-aarch64-apple-darwin
Windowsx86_64
my-sidecar-x86_64-pc-windows-msvc.exe
确定你的目标三元组:
bash
rustc --print host-tuple    # Rust 1.84.0+
rustc -Vv | grep host       # 旧版本

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

从Rust执行Sidecars

Basic Execution

基础执行

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())
    }
}
Note: Pass only the filename to
sidecar()
, not the full path from configuration.
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()
传递的仅为文件名,而非配置中的完整路径。

With Arguments

带参数执行

rust
#[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())
}
rust
#[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):
rust
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)
}
对于持续运行的sidecar(如API服务器、监听器):
rust
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

管理Sidecar生命周期

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

从JavaScript执行Sidecars

Permission Configuration

权限配置

Grant shell execution permissions in
src-tauri/capabilities/default.json
:
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 }]
    }
  ]
}
src-tauri/capabilities/default.json
中授予shell执行权限:
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

基础执行

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

带参数执行

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

处理流式输出

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

管理长期运行的进程

typescript
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;
  }
}
typescript
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:
json
{
  "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)
在capabilities中配置参数验证:
json
{
  "identifier": "shell:allow-execute",
  "allow": [{
    "name": "binaries/my-sidecar",
    "sidecar": true,
    "args": [
      "-o",
      "--verbose",
      { "validator": "\\S+" }
    ]
  }]
}
参数类型:
  • 静态字符串:需要完全匹配(
    -o
    ,
    --verbose
  • 验证器对象:动态值的正则表达式模式
  • true
    :允许任何参数(谨慎使用)

Cross-Platform Considerations

跨平台注意事项

Building Platform-Specific Binaries

构建平台特定的二进制文件

Rust sidecars:
bash
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:
bash
pyinstaller --onefile my_script.py
mv dist/my_script dist/my_script-x86_64-unknown-linux-gnu
Rust sidecars:
bash
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:
bash
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
Windows:
  • 可执行文件必须带有
    .exe
    扩展名
  • 处理文本文件时注意换行符
macOS:
  • 使用
    lipo
    创建通用二进制文件(Intel + Apple Silicon)
  • 分发时可能需要代码签名
  • Gatekeeper可能会阻止未签名的sidecars
Linux:
  • 将二进制文件标记为可执行(
    chmod +x
  • 考虑glibc版本兼容性
  • 静态链接可减少依赖问题

Complete Example

完整示例

tauri.conf.json:
json
{
  "productName": "My App",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "bundle": {
    "externalBin": ["binaries/data-processor"]
  }
}
capabilities/default.json:
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:
rust
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):
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>;
}
tauri.conf.json:
json
{
  "productName": "My App",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "bundle": {
    "externalBin": ["binaries/data-processor"]
  }
}
capabilities/default.json:
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:
rust
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):
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
  1. 验证所有sidecar路径:切勿将不可信路径传递给sidecars
  2. 使用参数验证器:在capabilities中限制允许的参数
  3. 优雅处理错误:sidecars可能会失败或丢失
  4. 清理进程:在应用退出时终止启动的进程
  5. 在所有平台测试:二进制文件命名和执行方式因平台而异
  6. 考虑二进制文件大小:sidecars会显著增加包体积