diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7f5027f..69a83802 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,25 +78,32 @@ Thank you for your interest in contributing to Voicebox! This document provides This creates the SQLite database at `data/voicebox.db`. 5. **Start development servers** - - **Terminal 1: Backend server** + + Development requires two terminals: one for the Python backend, one for the Tauri app. + + **Terminal 1: Backend server** (start this first) ```bash cd backend source venv/bin/activate # Activate venv if not already active bun run dev:server - # Or manually: uvicorn main:app --reload --port 8000 + # Or manually: uvicorn main:app --reload --port 17493 ``` - Backend will be available at `http://localhost:8000` - + Backend will be available at `http://localhost:17493` + **Terminal 2: Desktop app** ```bash bun run dev ``` This will: + - Create a placeholder sidecar binary (for Tauri compilation) - Start Vite dev server on port 5173 - Launch Tauri window pointing to localhost:5173 + - Connect to the Python server you started in Terminal 1 - Enable hot reload + > **Note:** In dev mode, the app connects to your manually-started Python server. + > The bundled server binary is only used in production builds. + **Optional: Web app** ```bash bun run dev:web diff --git a/package.json b/package.json index facf64eb..801eef6f 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "landing" ], "scripts": { - "dev": "cd tauri && bun run tauri dev", + "dev": "bun run setup:dev && cd tauri && bun run tauri dev", "dev:web": "cd web && bun run dev", "dev:landing": "cd landing && bun run dev", - "dev:server": "uvicorn backend.main:app --reload --port 8000", + "dev:server": "uvicorn backend.main:app --reload --port 17493", + "setup:dev": "bun run scripts/setup-dev-sidecar.js", "build": "cd tauri && bun run tauri build", "build:web": "cd web && bun run build", "build:landing": "cd landing && bun run build", diff --git a/scripts/setup-dev-sidecar.js b/scripts/setup-dev-sidecar.js new file mode 100644 index 00000000..6d5d5524 --- /dev/null +++ b/scripts/setup-dev-sidecar.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +/** + * Creates placeholder sidecar binaries for development mode. + * + * In dev mode, Tauri requires the sidecar binary files to exist at compile time, + * even though developers typically run the Python server manually. + * + * This script creates minimal placeholder binaries that allow Tauri to compile. + * The actual server should be started separately with `bun run dev:server`. + */ + +import { existsSync, mkdirSync, writeFileSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const BINARIES_DIR = join(__dirname, '..', 'tauri', 'src-tauri', 'binaries'); + +// Minimum size to consider a binary "real" (placeholder is ~256 bytes, real is MBs) +const MIN_REAL_BINARY_SIZE = 10000; + +// Get the current platform's target triple +function getTargetTriple() { + try { + const triple = execSync('rustc --print host-tuple', { encoding: 'utf-8' }).trim(); + return triple; + } catch { + // Fallback detection + const platform = process.platform; + const arch = process.arch; + + if (platform === 'win32') { + return arch === 'x64' ? 'x86_64-pc-windows-msvc' : 'i686-pc-windows-msvc'; + } else if (platform === 'darwin') { + return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin'; + } else if (platform === 'linux') { + return arch === 'x64' ? 'x86_64-unknown-linux-gnu' : 'aarch64-unknown-linux-gnu'; + } + + throw new Error(`Unsupported platform: ${platform}/${arch}`); + } +} + +// Create a minimal executable for the platform +function createPlaceholderBinary(targetTriple) { + const isWindows = targetTriple.includes('windows'); + const binaryName = `voicebox-server-${targetTriple}${isWindows ? '.exe' : ''}`; + const binaryPath = join(BINARIES_DIR, binaryName); + + // Check if real binary already exists (larger than our placeholder) + if (existsSync(binaryPath)) { + try { + const stats = statSync(binaryPath); + if (stats.size > MIN_REAL_BINARY_SIZE) { + console.log(`Real binary already exists: ${binaryName} (${(stats.size / 1024 / 1024).toFixed(1)} MB)`); + return; + } + } catch { + // File exists but can't stat - try to replace it + } + } + + // Ensure binaries directory exists + if (!existsSync(BINARIES_DIR)) { + mkdirSync(BINARIES_DIR, { recursive: true }); + } + + if (isWindows) { + // Create a minimal valid Windows PE executable that exits with code 1 + // This is the smallest valid PE that Windows will accept + const minimalPE = Buffer.from([ + // DOS Header + 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + // DOS Stub + 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, 0x54, 0x68, + 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F, + 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, + 0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // PE Signature + 0x50, 0x45, 0x00, 0x00, + // COFF Header (x64) + 0x64, 0x86, // Machine: AMD64 + 0x01, 0x00, // NumberOfSections: 1 + 0x00, 0x00, 0x00, 0x00, // TimeDateStamp + 0x00, 0x00, 0x00, 0x00, // PointerToSymbolTable + 0x00, 0x00, 0x00, 0x00, // NumberOfSymbols + 0xF0, 0x00, // SizeOfOptionalHeader + 0x22, 0x00, // Characteristics: EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE + // Optional Header (PE32+) + 0x0B, 0x02, // Magic: PE32+ + 0x00, 0x00, // Linker version + 0x00, 0x00, 0x00, 0x00, // SizeOfCode + 0x00, 0x00, 0x00, 0x00, // SizeOfInitializedData + 0x00, 0x00, 0x00, 0x00, // SizeOfUninitializedData + 0x00, 0x10, 0x00, 0x00, // AddressOfEntryPoint + 0x00, 0x00, 0x00, 0x00, // BaseOfCode + 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, // ImageBase + 0x00, 0x10, 0x00, 0x00, // SectionAlignment + 0x00, 0x02, 0x00, 0x00, // FileAlignment + 0x06, 0x00, 0x00, 0x00, // OS version + 0x00, 0x00, 0x00, 0x00, // Image version + 0x06, 0x00, 0x00, 0x00, // Subsystem version + 0x00, 0x00, 0x00, 0x00, // Win32VersionValue + 0x00, 0x20, 0x00, 0x00, // SizeOfImage + 0x00, 0x02, 0x00, 0x00, // SizeOfHeaders + 0x00, 0x00, 0x00, 0x00, // CheckSum + 0x03, 0x00, // Subsystem: CONSOLE + 0x60, 0x01, // DllCharacteristics + // Stack/Heap sizes (8 bytes each for PE32+) + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // LoaderFlags + 0x10, 0x00, 0x00, 0x00, // NumberOfRvaAndSizes + ]); + + // Pad to 512 bytes minimum for valid PE + const paddedPE = Buffer.alloc(512); + minimalPE.copy(paddedPE); + writeFileSync(binaryPath, paddedPE); + } else { + // Create a minimal shell script for Unix-like systems + const script = `#!/bin/sh +echo "[voicebox-server] Dev mode placeholder - start the real server with: bun run dev:server" +exit 1 +`; + writeFileSync(binaryPath, script, { mode: 0o755 }); + } + + console.log(`Created dev placeholder: ${binaryName}`); +} + +function main() { + console.log('Setting up development sidecar...'); + console.log(''); + + const targetTriple = getTargetTriple(); + console.log(`Platform: ${targetTriple}`); + + createPlaceholderBinary(targetTriple); + + console.log(''); + console.log('Sidecar setup complete.'); + console.log('For development, start the Python server in a separate terminal:'); + console.log(' bun run dev:server'); + console.log(''); +} + +main(); diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 0b4de0ac..36184a5d 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -178,14 +178,41 @@ async fn start_server( println!("Data directory: {:?}", data_dir); println!("Remote mode: {}", remote.unwrap_or(false)); - let mut sidecar = app - .shell() - .sidecar("voicebox-server") - .map_err(|e| { + let sidecar_result = app.shell().sidecar("voicebox-server"); + + let mut sidecar = match sidecar_result { + Ok(s) => s, + Err(e) => { eprintln!("Failed to get sidecar: {}", e); - eprintln!("This usually means the binary is not bundled correctly or doesn't have execute permissions"); - format!("Failed to get sidecar: {}", e) - })?; + + // In dev mode, check if the server is already running (started manually) + #[cfg(debug_assertions)] + { + eprintln!("Dev mode: Checking if server is already running on port {}...", SERVER_PORT); + + // Try to connect to the server port + use std::net::TcpStream; + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + println!("Found server already running on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + + eprintln!(""); + eprintln!("================================================================="); + eprintln!("DEV MODE: No server found on port {}", SERVER_PORT); + eprintln!(""); + eprintln!("Start the Python server in a separate terminal:"); + eprintln!(" bun run dev:server"); + eprintln!("================================================================="); + eprintln!(""); + } + + return Err(format!("Failed to start server. In dev mode, run 'bun run dev:server' in a separate terminal.")); + } + }; println!("Sidecar command created successfully"); @@ -204,17 +231,47 @@ async fn start_server( } println!("Spawning server process..."); - let (mut rx, child) = sidecar - .spawn() - .map_err(|e| { + let spawn_result = sidecar.spawn(); + + let (mut rx, child) = match spawn_result { + Ok(result) => result, + Err(e) => { eprintln!("Failed to spawn server process: {}", e); - eprintln!("This could be due to:"); - eprintln!(" - Missing or corrupted binary"); - eprintln!(" - Missing execute permissions"); - eprintln!(" - Code signing issues on macOS"); - eprintln!(" - Missing dependencies"); - format!("Failed to spawn: {}", e) - })?; + + // In dev mode, check if a manually-started server is available + #[cfg(debug_assertions)] + { + use std::net::TcpStream; + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + println!("Found manually-started server on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + + eprintln!(""); + eprintln!("================================================================="); + eprintln!("DEV MODE: Server binary failed to start"); + eprintln!(""); + eprintln!("Start the Python server in a separate terminal:"); + eprintln!(" bun run dev:server"); + eprintln!("================================================================="); + eprintln!(""); + return Err("Dev mode: Start server manually with 'bun run dev:server'".to_string()); + } + + #[cfg(not(debug_assertions))] + { + eprintln!("This could be due to:"); + eprintln!(" - Missing or corrupted binary"); + eprintln!(" - Missing execute permissions"); + eprintln!(" - Code signing issues on macOS"); + eprintln!(" - Missing dependencies"); + return Err(format!("Failed to spawn: {}", e)); + } + } + }; println!("Server process spawned, waiting for ready signal..."); println!("================================================================="); @@ -239,6 +296,22 @@ async fn start_server( eprintln!(" {}", line); } } + + // In dev mode, check if a manual server came up during the wait + #[cfg(debug_assertions)] + { + use std::net::TcpStream; + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + // Kill the placeholder process + let _ = state.child.lock().unwrap().take(); + println!("Found manually-started server on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + } + return Err("Server startup timeout - check Console.app for detailed logs".to_string()); } @@ -273,10 +346,42 @@ async fn start_server( } } Ok(None) => { - eprintln!("Server process ended unexpectedly during startup!"); - eprintln!("The server binary may have crashed or exited with an error."); - eprintln!("Check Console.app logs for more details (search for 'voicebox')"); - return Err("Server process ended unexpectedly".to_string()); + // In dev mode, this is expected when using the placeholder binary + #[cfg(debug_assertions)] + { + use std::net::TcpStream; + eprintln!("Server process ended (dev mode placeholder detected)"); + + // Check if a manually-started server is available + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + // Clean up state + let _ = state.child.lock().unwrap().take(); + let _ = state.server_pid.lock().unwrap().take(); + println!("Found manually-started server on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + + eprintln!(""); + eprintln!("================================================================="); + eprintln!("DEV MODE: No bundled server binary available"); + eprintln!(""); + eprintln!("Start the Python server in a separate terminal:"); + eprintln!(" bun run dev:server"); + eprintln!("================================================================="); + eprintln!(""); + return Err("Dev mode: Start server manually with 'bun run dev:server'".to_string()); + } + + #[cfg(not(debug_assertions))] + { + eprintln!("Server process ended unexpectedly during startup!"); + eprintln!("The server binary may have crashed or exited with an error."); + eprintln!("Check Console.app logs for more details (search for 'voicebox')"); + return Err("Server process ended unexpectedly".to_string()); + } } Err(_) => { // Timeout on this recv, continue loop