From 462a4b80f6aee8f5ca1e98dac0ed20eceb113fe4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:12:22 -0700 Subject: [PATCH] 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 --- .gitea/workflows/build-linux.yml | 7 +- .gitea/workflows/build-macos.yml | 7 +- .gitea/workflows/build-windows.yml | 6 +- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 7 +- src-tauri/src/sidecar/mod.rs | 132 ++++++++++++++++++++++------- 6 files changed, 118 insertions(+), 42 deletions(-) diff --git a/.gitea/workflows/build-linux.yml b/.gitea/workflows/build-linux.yml index e0886f0..d80225f 100644 --- a/.gitea/workflows/build-linux.yml +++ b/.gitea/workflows/build-linux.yml @@ -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 diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 21a61d3..40ea942 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -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 diff --git a/.gitea/workflows/build-windows.yml b/.gitea/workflows/build-windows.yml index 2a19c3b..608470a 100644 --- a/.gitea/workflows/build-windows.yml +++ b/.gitea/workflows/build-windows.yml @@ -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 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 174dde3..ef15d3f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1ae9249..1b8de1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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 diff --git a/src-tauri/src/sidecar/mod.rs b/src-tauri/src/sidecar/mod.rs index 792018f..bd7ba28 100644 --- a/src-tauri/src/sidecar/mod.rs +++ b/src-tauri/src/sidecar/mod.rs @@ -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 = OnceLock::new(); +/// App data directory for extracting the sidecar archive. +static DATA_DIR: OnceLock = 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 { + /// 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 { let binary_name = if cfg!(target_os = "windows") { "voice-to-notes-sidecar.exe" } else { "voice-to-notes-sidecar" }; - let mut candidates: Vec = 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 { + let mut candidates: Vec = 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") {