关于 embedding-tauri-sidecars
面向 Tauri 开发者的指南,介绍如何将外部二进制程序作为 sidecar 嵌入应用并运行。内容涵盖配置、平台专属命名、从 Rust 或 JavaScript 执行及进程管理。
主要功能
- 01配置随应用打包的 sidecar
- 02按目标平台命名二进制文件
- 03从 Rust 和 JavaScript 运行程序
- 04流式读取输出并管理进程
- 05限制可执行参数
适用场景
- 01在应用中打包 Python CLI
- 02运行后台 API 服务
- 03使用外部二进制程序处理文件
- 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 都需要带有目标三元组后缀的平台专用版本:
| 平台 | 架构 | 所需文件名 |
|---|---|---|
| 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 |
确定你的目标三元组:
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>;
}
最佳实践
- 验证所有 Sidecar 路径:切勿向 Sidecar 传递不可信路径
- 使用参数验证器:在 capabilities 中限制允许的参数
- 妥善处理错误:Sidecar 可能运行失败或缺失
- 清理进程:应用退出时终止已启动的进程
- 在所有平台上测试:二进制文件的命名和执行方式因平台而异
- 考虑二进制文件大小:Sidecar 会显著增加打包体积