diff --git a/.gitea/workflows/build-linux.yml b/.gitea/workflows/build-linux.yml index d01de12..e0886f0 100644 --- a/.gitea/workflows/build-linux.yml +++ b/.gitea/workflows/build-linux.yml @@ -41,9 +41,8 @@ jobs: - name: Place sidecar for Tauri run: | - mkdir -p src-tauri/binaries - cp -r python/dist/voice-to-notes-sidecar/* src-tauri/binaries/ - chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }} + cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar + chmod +x src-tauri/sidecar/voice-to-notes-sidecar # ── Tauri app ── - name: Set up Node.js @@ -66,7 +65,7 @@ jobs: - name: Build Tauri app run: npm run tauri build env: - TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}' + TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}' # ── Release ── - name: Upload to release diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml index 58f558d..21a61d3 100644 --- a/.gitea/workflows/build-macos.yml +++ b/.gitea/workflows/build-macos.yml @@ -41,9 +41,8 @@ jobs: - name: Place sidecar for Tauri run: | - mkdir -p src-tauri/binaries - cp -r python/dist/voice-to-notes-sidecar/* src-tauri/binaries/ - chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }} + cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar + chmod +x src-tauri/sidecar/voice-to-notes-sidecar # ── 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":{"externalBin":["binaries/voice-to-notes-sidecar"]}}' + TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}' # ── Release ── - name: Upload to release diff --git a/.gitea/workflows/build-windows.yml b/.gitea/workflows/build-windows.yml index a07b969..2a19c3b 100644 --- a/.gitea/workflows/build-windows.yml +++ b/.gitea/workflows/build-windows.yml @@ -46,8 +46,7 @@ jobs: - name: Place sidecar for Tauri shell: powershell run: | - New-Item -ItemType Directory -Force -Path src-tauri\binaries - Copy-Item -Path python\dist\voice-to-notes-sidecar\* -Destination src-tauri\binaries\ -Recurse -Force + Copy-Item -Path python\dist\voice-to-notes-sidecar -Destination src-tauri\sidecar -Recurse -Force # ── Tauri app ── - name: Set up Node.js @@ -74,7 +73,7 @@ jobs: shell: powershell run: npm run tauri build env: - TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}' + TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}' # ── Release ── - name: Upload to release diff --git a/python/build_sidecar.py b/python/build_sidecar.py index 1b2c68d..57149b8 100644 --- a/python/build_sidecar.py +++ b/python/build_sidecar.py @@ -236,10 +236,9 @@ def main() -> None: python = create_venv_and_install(cpu_only) output_dir = run_pyinstaller(python) download_ffmpeg(output_dir) - rename_binary(output_dir, target_triple) print(f"\n[build] Done! Sidecar built at: {output_dir}") - print(f"[build] Copy contents to src-tauri/binaries/ for Tauri bundling") + print(f"[build] Copy directory to src-tauri/sidecar/ for Tauri resource bundling") if __name__ == "__main__": diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 22ce9c9..1ae9249 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -27,6 +27,11 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .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); + } + // Set the webview background to match the app's dark theme if let Some(window) = app.get_webview_window("main") { let _ = window.set_background_color(Some(Color(10, 10, 35, 255))); diff --git a/src-tauri/src/sidecar/mod.rs b/src-tauri/src/sidecar/mod.rs index 56140a4..792018f 100644 --- a/src-tauri/src/sidecar/mod.rs +++ b/src-tauri/src/sidecar/mod.rs @@ -2,11 +2,22 @@ pub mod ipc; pub mod messages; use std::io::{BufRead, BufReader, Write}; +use std::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(); + +/// Set the resource directory 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(); +} + /// Get the global sidecar manager singleton. pub fn sidecar() -> &'static SidecarManager { static INSTANCE: OnceLock = OnceLock::new(); @@ -41,34 +52,56 @@ 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 { - let exe = std::env::current_exe().map_err(|e| format!("Cannot get current exe: {e}"))?; - let exe_dir = exe - .parent() - .ok_or_else(|| "Cannot get exe parent directory".to_string())?; - let binary_name = if cfg!(target_os = "windows") { "voice-to-notes-sidecar.exe" } else { "voice-to-notes-sidecar" }; - // Tauri places externalBin next to the app binary - let path = exe_dir.join(binary_name); - if path.exists() { - return Ok(path); + let mut candidates: Vec = Vec::new(); + + // 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)); } - // Also check inside a subdirectory (onedir PyInstaller output) - let subdir_path = exe_dir.join("voice-to-notes-sidecar").join(binary_name); - if subdir_path.exists() { - return Ok(subdir_path); + // Fallback: relative to the current executable + 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), + ); + } + } + + for path in &candidates { + if path.exists() { + return Ok(path.canonicalize().unwrap_or_else(|_| path.clone())); + } } Err(format!( - "Sidecar binary not found. Looked for:\n {}\n {}", - path.display(), - subdir_path.display(), + "Sidecar binary not found. Checked:\n{}", + candidates + .iter() + .map(|p| format!(" {}", p.display())) + .collect::>() + .join("\n"), )) } @@ -114,15 +147,8 @@ impl SidecarManager { if Self::is_dev_mode() { self.start_python_dev() } else { - match Self::resolve_sidecar_path() { - Ok(path) => self.start_binary(&path), - Err(e) => { - eprintln!( - "[sidecar-rs] Frozen binary not found ({e}), falling back to dev mode" - ); - self.start_python_dev() - } - } + let path = Self::resolve_sidecar_path()?; + self.start_binary(&path) } }