Fix sidecar not found on Windows/macOS/Linux: switch from externalBin to resources
Tauri's externalBin only bundled the single sidecar executable, but PyInstaller's onedir output requires companion DLLs and _internal/. The binary was also renamed with a target triple suffix that resolve_sidecar_path() didn't look for, causing it to fall back to dev mode which used a compile-time CI path (CARGO_MANIFEST_DIR). - Switch from externalBin to bundle.resources to include all sidecar files - Pass Tauri resource_dir to sidecar manager for platform-aware path resolution - Remove rename_binary() since externalBin target triple naming is no longer needed - Remove broken production-to-dev fallback that could never work on user machines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,9 +41,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Place sidecar for Tauri
|
- name: Place sidecar for Tauri
|
||||||
run: |
|
run: |
|
||||||
mkdir -p src-tauri/binaries
|
cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar
|
||||||
cp -r python/dist/voice-to-notes-sidecar/* src-tauri/binaries/
|
chmod +x src-tauri/sidecar/voice-to-notes-sidecar
|
||||||
chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }}
|
|
||||||
|
|
||||||
# ── Tauri app ──
|
# ── Tauri app ──
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
@@ -66,7 +65,7 @@ jobs:
|
|||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
run: npm run tauri build
|
run: npm run tauri build
|
||||||
env:
|
env:
|
||||||
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
|
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||||
|
|
||||||
# ── Release ──
|
# ── Release ──
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
|
|||||||
@@ -41,9 +41,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Place sidecar for Tauri
|
- name: Place sidecar for Tauri
|
||||||
run: |
|
run: |
|
||||||
mkdir -p src-tauri/binaries
|
cp -r python/dist/voice-to-notes-sidecar src-tauri/sidecar
|
||||||
cp -r python/dist/voice-to-notes-sidecar/* src-tauri/binaries/
|
chmod +x src-tauri/sidecar/voice-to-notes-sidecar
|
||||||
chmod +x src-tauri/binaries/voice-to-notes-sidecar-${{ env.TARGET }}
|
|
||||||
|
|
||||||
# ── Tauri app ──
|
# ── Tauri app ──
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
@@ -65,7 +64,7 @@ jobs:
|
|||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
run: npm run tauri build
|
run: npm run tauri build
|
||||||
env:
|
env:
|
||||||
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
|
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||||
|
|
||||||
# ── Release ──
|
# ── Release ──
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ jobs:
|
|||||||
- name: Place sidecar for Tauri
|
- name: Place sidecar for Tauri
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
New-Item -ItemType Directory -Force -Path src-tauri\binaries
|
Copy-Item -Path python\dist\voice-to-notes-sidecar -Destination src-tauri\sidecar -Recurse -Force
|
||||||
Copy-Item -Path python\dist\voice-to-notes-sidecar\* -Destination src-tauri\binaries\ -Recurse -Force
|
|
||||||
|
|
||||||
# ── Tauri app ──
|
# ── Tauri app ──
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
@@ -74,7 +73,7 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
run: npm run tauri build
|
run: npm run tauri build
|
||||||
env:
|
env:
|
||||||
TAURI_CONFIG: '{"bundle":{"externalBin":["binaries/voice-to-notes-sidecar"]}}'
|
TAURI_CONFIG: '{"bundle":{"resources":["sidecar/**"]}}'
|
||||||
|
|
||||||
# ── Release ──
|
# ── Release ──
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
|
|||||||
@@ -236,10 +236,9 @@ def main() -> None:
|
|||||||
python = create_venv_and_install(cpu_only)
|
python = create_venv_and_install(cpu_only)
|
||||||
output_dir = run_pyinstaller(python)
|
output_dir = run_pyinstaller(python)
|
||||||
download_ffmpeg(output_dir)
|
download_ffmpeg(output_dir)
|
||||||
rename_binary(output_dir, target_triple)
|
|
||||||
|
|
||||||
print(f"\n[build] Done! Sidecar built at: {output_dir}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.manage(app_state)
|
.manage(app_state)
|
||||||
.setup(|app| {
|
.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
|
// Set the webview background to match the app's dark theme
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.set_background_color(Some(Color(10, 10, 35, 255)));
|
let _ = window.set_background_color(Some(Color(10, 10, 35, 255)));
|
||||||
|
|||||||
@@ -2,11 +2,22 @@ pub mod ipc;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
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();
|
||||||
|
|
||||||
|
/// 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.
|
/// Get the global sidecar manager singleton.
|
||||||
pub fn sidecar() -> &'static SidecarManager {
|
pub fn sidecar() -> &'static SidecarManager {
|
||||||
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
|
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
|
||||||
@@ -41,34 +52,56 @@ impl SidecarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the frozen sidecar binary path (production mode).
|
/// 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> {
|
fn resolve_sidecar_path() -> Result<std::path::PathBuf, String> {
|
||||||
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") {
|
let binary_name = if cfg!(target_os = "windows") {
|
||||||
"voice-to-notes-sidecar.exe"
|
"voice-to-notes-sidecar.exe"
|
||||||
} else {
|
} else {
|
||||||
"voice-to-notes-sidecar"
|
"voice-to-notes-sidecar"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tauri places externalBin next to the app binary
|
let mut candidates: Vec<std::path::PathBuf> = Vec::new();
|
||||||
let path = exe_dir.join(binary_name);
|
|
||||||
if path.exists() {
|
// Primary: Tauri resource directory (set during app setup)
|
||||||
return Ok(path);
|
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)
|
// Fallback: relative to the current executable
|
||||||
let subdir_path = exe_dir.join("voice-to-notes-sidecar").join(binary_name);
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
if subdir_path.exists() {
|
if let Some(exe_dir) = exe.parent() {
|
||||||
return Ok(subdir_path);
|
// 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!(
|
Err(format!(
|
||||||
"Sidecar binary not found. Looked for:\n {}\n {}",
|
"Sidecar binary not found. Checked:\n{}",
|
||||||
path.display(),
|
candidates
|
||||||
subdir_path.display(),
|
.iter()
|
||||||
|
.map(|p| format!(" {}", p.display()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +147,8 @@ impl SidecarManager {
|
|||||||
if Self::is_dev_mode() {
|
if Self::is_dev_mode() {
|
||||||
self.start_python_dev()
|
self.start_python_dev()
|
||||||
} else {
|
} else {
|
||||||
match Self::resolve_sidecar_path() {
|
let path = Self::resolve_sidecar_path()?;
|
||||||
Ok(path) => self.start_binary(&path),
|
self.start_binary(&path)
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"[sidecar-rs] Frozen binary not found ({e}), falling back to dev mode"
|
|
||||||
);
|
|
||||||
self.start_python_dev()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user