diff --git a/tauri-app/src-tauri/src/lib.rs b/tauri-app/src-tauri/src/lib.rs index 9fddde9..45f6c49 100644 --- a/tauri-app/src-tauri/src/lib.rs +++ b/tauri-app/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::sync::Mutex; use tauri::{AppHandle, Manager, State}; @@ -47,6 +47,56 @@ fn query_gpu() -> Result { // ─── Server lifecycle ───────────────────────────────────────────────────────── +/// Locate the `localllm` binary by searching several well-known locations: +/// 1. Workspace `target/release/` relative to the Tauri app's source tree +/// 2. Next to the running executable (for installed/bundled builds) +/// 3. System PATH +fn find_localllm_binary() -> Option { + let exe_name = if cfg!(target_os = "windows") { "localllm.exe" } else { "localllm" }; + + // 1. Workspace target/release — works when running `npm run tauri dev` from tauri-app/ + // The Tauri dev binary lives at tauri-app/src-tauri/target/debug/forgelm-chat.exe + // so we walk up to find the workspace root and look for target/release/localllm.exe + if let Ok(exe) = std::env::current_exe() { + // Walk up from the current exe looking for target/release/localllm.exe + let mut dir = exe.parent(); + while let Some(d) = dir { + let candidate = d.join("target").join("release").join(exe_name); + if candidate.exists() { + return Some(candidate); + } + // Also check sibling: if we're inside target/debug, go up two levels + let candidate2 = d.join(exe_name); + if candidate2.exists() { + return Some(candidate2); + } + dir = d.parent(); + } + } + + // 2. Relative to CWD (useful when launched from repo root) + let cwd_candidate = PathBuf::from("target").join("release").join(exe_name); + if cwd_candidate.exists() { + return Some(cwd_candidate); + } + + // 3. System PATH + if let Ok(output) = Command::new(if cfg!(target_os = "windows") { "where" } else { "which" }) + .arg("localllm") + .output() + { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout); + let first_line = path_str.lines().next().unwrap_or("").trim(); + if !first_line.is_empty() { + return Some(PathBuf::from(first_line)); + } + } + } + + None +} + #[tauri::command] fn start_server( state: State, @@ -60,15 +110,20 @@ fn start_server( return Err("Server is already running".into()); } - let binary = if cfg!(target_os = "windows") { "localllm.exe" } else { "localllm" }; + let binary_path = find_localllm_binary() + .ok_or_else(|| { + "localllm binary not found. Build it with: cargo build --release -p inference-server\n\ + Then ensure the binary is in one of: target/release/, PATH, or next to this app." + .to_string() + })?; - let child = Command::new(binary) + let child = Command::new(&binary_path) .env("LOCALLLM_MODEL", &model_path) .env("LOCALLLM_ADDR", &addr) .env("LOCALLLM_GPU_LAYERS", gpu_layers.to_string()) .env("LOCALLLM_CTX", ctx_size.to_string()) .spawn() - .map_err(|e| format!("Failed to start server: {e}"))?; + .map_err(|e| format!("Failed to start server ({}): {e}", binary_path.display()))?; let pid = child.id(); *guard = Some(child); @@ -275,6 +330,17 @@ fn load_config(app: AppHandle) -> Result { serde_json::from_str(&json).map_err(|e| format!("Parse error: {e}")) } +/// Returns the resolved path to the localllm binary (useful for debugging). +#[tauri::command] +fn find_server_binary() -> Result { + find_localllm_binary() + .map(|p| p.to_string_lossy().to_string()) + .ok_or_else(|| { + "localllm binary not found. Build with: cargo build --release -p inference-server" + .to_string() + }) +} + /// Returns the app data directory path (useful for debugging). #[tauri::command] fn get_app_data_dir(app: AppHandle) -> Result { @@ -304,6 +370,7 @@ pub fn run() { save_config, load_config, get_app_data_dir, + find_server_binary, ]) .setup(|app| { #[cfg(debug_assertions)]