Fix Tauri build stack overflow: zip sidecar and extract on first launch
Tauri's build script overflows the stack when processing resource globs
matching thousands of files from PyInstaller's ML output (torch, pyannote).
Instead of bundling the sidecar directory directly:
- CI zips the sidecar output into a single sidecar.zip
- Tauri bundles just the one zip file (no recursion)
- On first launch, Rust extracts the zip to the app data directory
- Versioned extraction dir (sidecar-{version}) ensures updates re-extract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,10 +39,9 @@ jobs:
|
||||
working-directory: python
|
||||
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
|
||||
|
||||
- name: Place sidecar for Tauri
|
||||
- name: Package sidecar for Tauri
|
||||
run: |
|
||||
cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar
|
||||
chmod +x src-tauri/sidecar/voice-to-notes-sidecar
|
||||
cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
|
||||
|
||||
# ── Tauri app ──
|
||||
- name: Set up Node.js
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
- name: Build Tauri app
|
||||
run: npm run tauri build
|
||||
env:
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar.zip"]}}'
|
||||
|
||||
# ── Release ──
|
||||
- name: Upload to release
|
||||
|
||||
@@ -39,10 +39,9 @@ jobs:
|
||||
working-directory: python
|
||||
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
|
||||
|
||||
- name: Place sidecar for Tauri
|
||||
- name: Package sidecar for Tauri
|
||||
run: |
|
||||
cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar
|
||||
chmod +x src-tauri/sidecar/voice-to-notes-sidecar
|
||||
cd python/dist/voice-to-notes-sidecar && zip -r ../../../src-tauri/sidecar.zip .
|
||||
|
||||
# ── Tauri app ──
|
||||
- name: Set up Node.js
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
- name: Build Tauri app
|
||||
run: npm run tauri build
|
||||
env:
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar.zip"]}}'
|
||||
|
||||
# ── Release ──
|
||||
- name: Upload to release
|
||||
|
||||
@@ -43,10 +43,10 @@ jobs:
|
||||
working-directory: python
|
||||
run: uv run --python ${{ env.PYTHON_VERSION }} python build_sidecar.py --cpu-only
|
||||
|
||||
- name: Place sidecar for Tauri
|
||||
- name: Package sidecar for Tauri
|
||||
shell: powershell
|
||||
run: |
|
||||
Copy-Item -Path python\dist\voice-to-notes-sidecar -Destination src-tauri\sidecar -Recurse -Force
|
||||
Compress-Archive -Path python\dist\voice-to-notes-sidecar\* -DestinationPath src-tauri\sidecar.zip
|
||||
|
||||
# ── Tauri app ──
|
||||
- name: Set up Node.js
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
shell: powershell
|
||||
run: npm run tauri build
|
||||
env:
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||
TAURI_CONFIG: '{"bundle":{"resources":["sidecar.zip"]}}'
|
||||
|
||||
# ── Release ──
|
||||
- name: Upload to release
|
||||
|
||||
@@ -20,6 +20,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
thiserror = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tauri-plugin-dialog = "2.6.0"
|
||||
|
||||
@@ -28,8 +28,11 @@ pub fn run() {
|
||||
.manage(app_state)
|
||||
.setup(|app| {
|
||||
// Tell the sidecar manager where Tauri placed bundled resources
|
||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||
sidecar::init_resource_dir(resource_dir);
|
||||
// and where to extract the sidecar archive
|
||||
if let (Ok(resource_dir), Ok(data_dir)) =
|
||||
(app.path().resource_dir(), app.path().app_local_data_dir())
|
||||
{
|
||||
sidecar::init_dirs(resource_dir, data_dir);
|
||||
}
|
||||
|
||||
// Set the webview background to match the app's dark theme
|
||||
|
||||
@@ -2,20 +2,22 @@ pub mod ipc;
|
||||
pub mod messages;
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::sidecar::messages::IPCMessage;
|
||||
|
||||
/// Resource directory set by the Tauri app during setup.
|
||||
/// Used to locate the bundled sidecar binary and its companion files.
|
||||
static RESOURCE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
/// App data directory for extracting the sidecar archive.
|
||||
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// Set the resource directory for sidecar resolution.
|
||||
/// Initialize directories for sidecar resolution.
|
||||
/// Must be called from the Tauri setup before any sidecar operations.
|
||||
pub fn init_resource_dir(dir: PathBuf) {
|
||||
RESOURCE_DIR.set(dir).ok();
|
||||
pub fn init_dirs(resource_dir: PathBuf, data_dir: PathBuf) {
|
||||
RESOURCE_DIR.set(resource_dir).ok();
|
||||
DATA_DIR.set(data_dir).ok();
|
||||
}
|
||||
|
||||
/// Get the global sidecar manager singleton.
|
||||
@@ -53,50 +55,74 @@ impl SidecarManager {
|
||||
|
||||
/// Resolve the frozen sidecar binary path (production mode).
|
||||
///
|
||||
/// Searches for the PyInstaller-built sidecar in the Tauri resource directory
|
||||
/// (set via `init_resource_dir`) and falls back to paths relative to the
|
||||
/// current executable.
|
||||
fn resolve_sidecar_path() -> Result<std::path::PathBuf, String> {
|
||||
/// First checks if the sidecar is already extracted to the app data directory.
|
||||
/// If not, looks for `sidecar.zip` in the Tauri resource directory and extracts it.
|
||||
fn resolve_sidecar_path() -> Result<PathBuf, String> {
|
||||
let binary_name = if cfg!(target_os = "windows") {
|
||||
"voice-to-notes-sidecar.exe"
|
||||
} else {
|
||||
"voice-to-notes-sidecar"
|
||||
};
|
||||
|
||||
let mut candidates: Vec<std::path::PathBuf> = Vec::new();
|
||||
// Versioned extraction directory prevents stale sidecar after app updates
|
||||
let extract_dir = DATA_DIR
|
||||
.get()
|
||||
.ok_or("App data directory not initialized")?
|
||||
.join(format!("sidecar-{}", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
// Primary: Tauri resource directory (set during app setup)
|
||||
if let Some(resource_dir) = RESOURCE_DIR.get() {
|
||||
// Resources are placed under sidecar/ subdirectory
|
||||
candidates.push(resource_dir.join("sidecar").join(binary_name));
|
||||
// Also check flat layout in resource dir
|
||||
candidates.push(resource_dir.join(binary_name));
|
||||
let binary_path = extract_dir.join(binary_name);
|
||||
|
||||
// Already extracted — use it directly
|
||||
if binary_path.exists() {
|
||||
return Ok(binary_path);
|
||||
}
|
||||
|
||||
// Fallback: relative to the current executable
|
||||
// Find sidecar.zip in resource dir or next to exe
|
||||
let zip_path = Self::find_sidecar_zip()?;
|
||||
Self::extract_zip(&zip_path, &extract_dir)?;
|
||||
|
||||
if !binary_path.exists() {
|
||||
return Err(format!(
|
||||
"Sidecar binary not found after extraction at {}",
|
||||
binary_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Make executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(meta) = std::fs::metadata(&binary_path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = std::fs::set_permissions(&binary_path, perms);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
||||
/// Locate the bundled sidecar.zip archive.
|
||||
fn find_sidecar_zip() -> Result<PathBuf, String> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
|
||||
if let Some(resource_dir) = RESOURCE_DIR.get() {
|
||||
candidates.push(resource_dir.join("sidecar.zip"));
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(exe_dir) = exe.parent() {
|
||||
// sidecar/ subdirectory next to exe (Windows MSI, Linux AppImage)
|
||||
candidates.push(exe_dir.join("sidecar").join(binary_name));
|
||||
// Flat layout next to exe
|
||||
candidates.push(exe_dir.join(binary_name));
|
||||
// PyInstaller onedir subdirectory
|
||||
candidates.push(
|
||||
exe_dir
|
||||
.join("voice-to-notes-sidecar")
|
||||
.join(binary_name),
|
||||
);
|
||||
candidates.push(exe_dir.join("sidecar.zip"));
|
||||
}
|
||||
}
|
||||
|
||||
for path in &candidates {
|
||||
if path.exists() {
|
||||
return Ok(path.canonicalize().unwrap_or_else(|_| path.clone()));
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Sidecar binary not found. Checked:\n{}",
|
||||
"Sidecar archive not found. Checked:\n{}",
|
||||
candidates
|
||||
.iter()
|
||||
.map(|p| format!(" {}", p.display()))
|
||||
@@ -105,6 +131,54 @@ impl SidecarManager {
|
||||
))
|
||||
}
|
||||
|
||||
/// Extract a zip archive to the given directory.
|
||||
fn extract_zip(zip_path: &Path, dest: &Path) -> Result<(), String> {
|
||||
eprintln!(
|
||||
"[sidecar-rs] Extracting sidecar from {} to {}",
|
||||
zip_path.display(),
|
||||
dest.display()
|
||||
);
|
||||
|
||||
// Clean destination so we don't mix old and new files
|
||||
if dest.exists() {
|
||||
std::fs::remove_dir_all(dest)
|
||||
.map_err(|e| format!("Failed to clean extraction dir: {e}"))?;
|
||||
}
|
||||
std::fs::create_dir_all(dest)
|
||||
.map_err(|e| format!("Failed to create extraction dir: {e}"))?;
|
||||
|
||||
let file =
|
||||
std::fs::File::open(zip_path).map_err(|e| format!("Cannot open sidecar zip: {e}"))?;
|
||||
let mut archive =
|
||||
zip::ZipArchive::new(file).map_err(|e| format!("Invalid sidecar zip: {e}"))?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut entry = archive
|
||||
.by_index(i)
|
||||
.map_err(|e| format!("Zip entry error: {e}"))?;
|
||||
|
||||
let name = entry.name().to_string();
|
||||
let outpath = dest.join(&name);
|
||||
|
||||
if entry.is_dir() {
|
||||
std::fs::create_dir_all(&outpath)
|
||||
.map_err(|e| format!("Cannot create dir {}: {e}", outpath.display()))?;
|
||||
} else {
|
||||
if let Some(parent) = outpath.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Cannot create dir {}: {e}", parent.display()))?;
|
||||
}
|
||||
let mut outfile = std::fs::File::create(&outpath)
|
||||
.map_err(|e| format!("Cannot create {}: {e}", outpath.display()))?;
|
||||
std::io::copy(&mut entry, &mut outfile)
|
||||
.map_err(|e| format!("Write error for {}: {e}", name))?;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("[sidecar-rs] Sidecar extracted successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find a working Python command for the current platform.
|
||||
fn find_python_command() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
|
||||
Reference in New Issue
Block a user