From c513451277c01b3dd12908e1124762eca2f3dc7f Mon Sep 17 00:00:00 2001 From: Snowy Date: Thu, 29 Jan 2026 09:11:41 +0300 Subject: [PATCH] Fix dev mode to work without pre-built server binary Previously, running `bun run dev` would fail because Tauri requires the sidecar binary to exist at compile time, even in development mode. This forced developers to build the full PyInstaller binary before they could start development. This change introduces a streamlined dev workflow: 1. Add `scripts/setup-dev-sidecar.js` - Creates minimal placeholder binaries that satisfy Tauri's compile-time check. Works cross-platform (Windows PE stub, Unix shell script). 2. Update Rust code to gracefully handle dev mode - When the sidecar fails to start, it checks if a manually-started server is already running on the expected port and connects to it instead. 3. Update npm scripts - `bun run dev` now auto-runs the setup script, and `dev:server` uses the correct port (17493). 4. Update CONTRIBUTING.md with clearer dev workflow documentation. New development workflow: Terminal 1: bun run dev:server Terminal 2: bun run dev The bundled binary is only required for production builds. --- CONTRIBUTING.md | 17 ++-- package.json | 5 +- scripts/setup-dev-sidecar.js | 156 +++++++++++++++++++++++++++++++++++ tauri/src-tauri/src/main.rs | 147 ++++++++++++++++++++++++++++----- 4 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 scripts/setup-dev-sidecar.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1daede3..a7e13296 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,25 +74,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 e17f2eab..e6191689 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 c93cae79..81cae81b 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