mcpField
E
embedding-tauri-sidecars

embedding-tauri-sidecars

github.com/dchuk开发36183 次安装40 周前更新
代码仓库

关于 embedding-tauri-sidecars

面向 Tauri 开发者的指南,介绍如何将外部二进制程序作为 sidecar 嵌入应用并运行。内容涵盖配置、平台专属命名、从 Rust 或 JavaScript 执行及进程管理。

主要功能

  1. 01配置随应用打包的 sidecar
  2. 02按目标平台命名二进制文件
  3. 03从 Rust 和 JavaScript 运行程序
  4. 04流式读取输出并管理进程
  5. 05限制可执行参数

适用场景

  1. 01在应用中打包 Python 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 Sidecar:嵌入外部二进制文件

本技能介绍如何在 Tauri 应用中嵌入并执行外部二进制文件(Sidecar),包括配置、跨平台注意事项,以及如何从 Rust 和 JavaScript 中执行。

概述

Sidecar 是嵌入在 Tauri 应用中的外部二进制文件,可用于扩展功能或免去用户安装依赖项的需要。它们可以使用任何编程语言编写。

常见用例:

  • 使用 PyInstaller 打包的 Python CLI 应用
  • 用于特定任务的 Go 或 Rust 编译二进制文件
  • 打包为可执行文件的 Node.js 应用
  • API 服务或后台服务

插件依赖

Sidecar 需要 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

配置

注册 Sidecar

在 tauri.conf.json 的 bundle.externalBin 下配置 Sidecar。路径相对于 src-tauri:

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

**重要:**此处的路径是主文件名。Tauri 会在构建时附加目标三元组后缀。

跨平台二进制文件命名

每个 Sidecar 都需要带有目标三元组后缀的平台专用版本:

平台架构所需文件名
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 执行 Sidecar

基本执行
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())
}
启动长时间运行的进程

对于持续运行的 Sidecar(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)
}
管理 Sidecar 生命周期
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 执行 Sidecar

权限配置

在 src-tauri/capabilities/default.json 中授予 shell 执行权限:

{
  "$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 Sidecar:

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:

  • 使用 lipo 构建通用二进制文件(Intel + Apple Silicon)
  • 分发时可能需要代码签名
  • Gatekeeper 可能会拦截未签名的 Sidecar

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. 验证所有 Sidecar 路径:切勿向 Sidecar 传递不可信路径
  2. 使用参数验证器:在 capabilities 中限制允许的参数
  3. 妥善处理错误:Sidecar 可能运行失败或缺失
  4. 清理进程:应用退出时终止已启动的进程
  5. 在所有平台上测试:二进制文件的命名和执行方式因平台而异
  6. 考虑二进制文件大小:Sidecar 会显著增加打包体积

评论

支持 Markdown