use anyhow::{Context, Result};
use colored::*;
use std::process::Command;
pub fn run_project(platform: &str, device: bool) -> Result<()> {
let target_desc = if device && platform == "ios" { "iOS Device" } else { platform };
println!("{}", format!("🚀 Running on {}...", target_desc).bright_green().bold());
println!();
crate::commands::build::build_project(Some(platform.to_string()), false, false, device)?;
println!();
println!("{}", format!("▶️ Launching {}...", target_desc).bright_cyan().bold());
run_platform_with_options(platform, device)
}
pub fn run_platform_with_options(platform: &str, device: bool) -> Result<()> {
match platform {
"ios" => {
if device {
run_ios_device()
} else {
run_ios()
}
},
"android" => run_android(),
"macos" | "macos-arm64" | "macos-x64" => run_macos(),
"windows" | "windows-x64" | "windows-x86" => run_windows(),
"linux" => run_linux(),
"web" => run_web(),
_ => anyhow::bail!("Unknown platform: {}", platform),
}
}
fn run_ios_device() -> Result<()> {
println!(" {} Finding Xcode project...", "→".bright_blue());
let ios_dir = std::path::Path::new("platforms/ios");
let xcodeproj = std::fs::read_dir(ios_dir)?
.filter_map(|e| e.ok())
.find(|e| {
e.path()
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "xcodeproj")
.unwrap_or(false)
})
.map(|e| e.path())
.context("Could not find .xcodeproj file")?;
println!(" {} Building and deploying to device...", "→".bright_blue());
println!();
println!("{}", " Note: Make sure your device is connected and trusted.".yellow());
println!("{}", " You may need to configure code signing in Xcode first.".yellow());
println!();
let status = Command::new("xcodebuild")
.args(&[
"-project",
xcodeproj.to_str().unwrap(),
"-scheme",
xcodeproj.file_stem().unwrap().to_str().unwrap(),
"-destination",
"generic/platform=iOS",
"build",
])
.status()
.context("Failed to build with xcodebuild")?;
if !status.success() {
anyhow::bail!("Build failed. Make sure code signing is configured in Xcode.");
}
println!();
println!("{}", " ✅ Build complete!".green());
println!();
println!("{}", " To deploy to your device:".bright_cyan());
println!(" 1. Open Xcode");
println!(" 2. Select your connected device");
println!(" 3. Press Cmd+R to run");
println!();
println!("{}", " Or use Xcode directly for automatic deployment.".bright_cyan());
Ok(())
}
fn run_ios() -> Result<()> {
println!(" {} Finding Xcode project...", "→".bright_blue());
let ios_dir = std::path::Path::new("platforms/ios");
let xcodeproj = std::fs::read_dir(ios_dir)?
.filter_map(|e| e.ok())
.find(|e| {
e.path()
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "xcodeproj")
.unwrap_or(false)
})
.map(|e| e.path())
.context("Could not find .xcodeproj file")?;
println!(" {} Finding available simulator...", "→".bright_blue());
let simulator_name = get_available_iphone_simulator()?;
println!(" {} Using simulator: {}", "→".bright_blue(), simulator_name);
println!(" {} Building and launching in simulator...", "→".bright_blue());
let destination = format!("platform=iOS Simulator,name={}", simulator_name);
let status = Command::new("xcodebuild")
.args(&[
"-project",
xcodeproj.to_str().unwrap(),
"-scheme",
xcodeproj.file_stem().unwrap().to_str().unwrap(),
"-destination",
&destination,
"build",
])
.status()
.context("Failed to build with xcodebuild")?;
if !status.success() {
anyhow::bail!("Build failed");
}
let app_name = xcodeproj.file_stem().unwrap().to_str().unwrap();
let module_name = app_name.replace("-", "_");
println!(" {} Launching app in simulator...", "→".bright_blue());
let app_name = xcodeproj.file_stem().unwrap().to_str().unwrap();
let home = std::env::var("HOME").unwrap();
let derived_data = format!("{}/Library/Developer/Xcode/DerivedData", home);
let _app_bundle = format!(
"{}/Build/Products/Debug-iphonesimulator/{}.app",
derived_data, app_name
);
let app_path = std::fs::read_dir(&derived_data)
.context("Could not read DerivedData directory")?
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(app_name)
})
.find_map(|project_dir| {
let app_path = project_dir
.path()
.join("Build/Products/Debug-iphonesimulator")
.join(format!("{}.app", app_name));
if app_path.exists() {
Some(app_path)
} else {
None
}
})
.context("Could not find built .app bundle. Try running 'jffi build --platform ios' first.")?;
println!(" {} Booting simulator...", "→".bright_blue());
Command::new("xcrun")
.args(&["simctl", "boot", &simulator_name])
.output()
.ok();
Command::new("open")
.args(&["-a", "Simulator"])
.status()
.ok();
std::thread::sleep(std::time::Duration::from_secs(3));
println!(" {} Installing app...", "→".bright_blue());
let install_status = Command::new("xcrun")
.args(&[
"simctl",
"install",
"booted",
app_path.to_str().unwrap(),
])
.status()
.context("Failed to install app")?;
if !install_status.success() {
anyhow::bail!("Failed to install app in simulator");
}
Command::new("open")
.args(&["-a", "Simulator"])
.status()
.ok();
let bundle_id = format!("com.example.{}", app_name.replace("-", ""));
println!(" {} Launching app...", "→".bright_blue());
let launch_status = Command::new("xcrun")
.args(&["simctl", "launch", "booted", &bundle_id])
.status()
.context("Failed to launch app")?;
if !launch_status.success() {
anyhow::bail!("Failed to launch app in simulator");
}
println!();
println!("{}", " ✅ App launched in simulator!".green());
Ok(())
}
fn find_android_tool(tool_name: &str) -> Option<String> {
if Command::new(tool_name).arg("--version").output().is_ok() {
return Some(tool_name.to_string());
}
let home = std::env::var("HOME").unwrap_or_default();
let tool_subdir = if tool_name == "emulator" { "emulator" } else { "platform-tools" };
let possible_paths = vec![
format!("{}/Library/Android/sdk/{}/{}", home, tool_subdir, tool_name),
format!("{}/Android/Sdk/{}/{}", home, tool_subdir, tool_name),
format!("{}/.android/sdk/{}/{}", home, tool_subdir, tool_name),
];
possible_paths.into_iter()
.find(|path| std::path::Path::new(path).exists())
}
fn run_android() -> Result<()> {
println!(" {} Preparing Android emulator...", "→".bright_blue());
let emulator_cmd = find_android_tool("emulator")
.context("Android SDK emulator not found. Please install Android Studio and ensure the SDK is set up.")?;
let adb_cmd = find_android_tool("adb")
.context("adb not found. Please install Android SDK platform-tools.")?;
let devices_output = Command::new(&adb_cmd)
.arg("devices")
.output()
.context("Failed to check for running devices")?;
let devices_str = String::from_utf8_lossy(&devices_output.stdout);
let running_emulator = devices_str.lines()
.skip(1) .find(|line| line.contains("emulator") && line.contains("device"));
if running_emulator.is_some() {
println!(" {} Using already-running emulator", "→".bright_blue());
} else {
let emulator_output = Command::new(&emulator_cmd)
.arg("-list-avds")
.output()
.context("Failed to list Android Virtual Devices")?;
let avds = String::from_utf8_lossy(&emulator_output.stdout);
let avd_list: Vec<&str> = avds.lines().filter(|l| !l.is_empty()).collect();
if avd_list.is_empty() {
anyhow::bail!(
"No Android Virtual Devices (AVDs) found.\n\
Create one using: Android Studio > Tools > Device Manager > Create Device"
);
}
let avd_name = avd_list[0];
println!(" {} Available AVDs: {}", "→".bright_blue(), avd_list.join(", "));
println!(" {} Starting emulator: {}...", "→".bright_blue(), avd_name.bright_cyan());
Command::new(&emulator_cmd)
.arg("-avd")
.arg(avd_name)
.arg("-no-snapshot-load")
.spawn()
.context("Failed to start emulator")?;
println!(" {} Waiting for emulator to boot...", "→".bright_blue());
std::thread::sleep(std::time::Duration::from_secs(5));
}
for i in 1..=30 {
let status = Command::new(&adb_cmd)
.args(&["shell", "getprop", "sys.boot_completed"])
.output();
if let Ok(output) = status {
if String::from_utf8_lossy(&output.stdout).trim() == "1" {
break;
}
}
if i % 5 == 0 {
println!(" {} Still waiting... ({}/30s)", "→".bright_blue(), i);
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
println!(" {} Building APK with Gradle...", "→".bright_blue());
let android_dir = std::path::Path::new("platforms/android");
let build_result = Command::new("bash")
.arg("-c")
.arg(format!(
"cd {} && export ANDROID_HOME=~/Library/Android/sdk && ./gradlew assembleDebug",
android_dir.display()
))
.status()
.context("Failed to run Gradle build")?;
if !build_result.success() {
anyhow::bail!("Gradle build failed. Check the error messages above.");
}
println!(" {} Installing APK to emulator...", "→".bright_blue());
let apk_path = android_dir.join("app/build/outputs/apk/debug/app-debug.apk");
if !apk_path.exists() {
anyhow::bail!("APK not found at: {}", apk_path.display());
}
let install_status = Command::new(&adb_cmd)
.args(&["install", "-r", apk_path.to_str().unwrap()])
.status()
.context("Failed to install APK")?;
if !install_status.success() {
anyhow::bail!("Failed to install APK on emulator");
}
println!(" {} Launching app...", "→".bright_blue());
let build_gradle = std::fs::read_to_string("platforms/android/app/build.gradle.kts")
.context("Failed to read build.gradle.kts")?;
let package_name = build_gradle
.lines()
.find(|line| line.contains("applicationId"))
.and_then(|line| line.split('"').nth(1))
.context("Could not find applicationId in build.gradle.kts")?;
let activity = format!("{}.MainActivity", package_name);
Command::new(&adb_cmd)
.args(&[
"shell",
"am",
"start",
"-n",
&format!("{}/{}", package_name, activity),
])
.status()
.context("Failed to launch app")?;
println!();
println!("{}", " ✅ App launched on emulator!".green());
println!();
Ok(())
}
fn run_macos() -> Result<()> {
println!(" {} Finding Xcode project...", "→".bright_blue());
let macos_dir = std::path::Path::new("platforms/macos");
let xcodeproj = std::fs::read_dir(macos_dir)?
.filter_map(|e| e.ok())
.find(|e| {
e.path()
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "xcodeproj")
.unwrap_or(false)
})
.map(|e| e.path())
.context("Could not find .xcodeproj file")?;
println!(" {} Building and launching macOS app...", "→".bright_blue());
let status = Command::new("xcodebuild")
.args(&[
"-project",
xcodeproj.to_str().unwrap(),
"-scheme",
xcodeproj.file_stem().unwrap().to_str().unwrap(),
"build",
])
.status()
.context("Failed to build with xcodebuild")?;
if !status.success() {
anyhow::bail!("Build failed");
}
println!(" {} Launching app...", "→".bright_blue());
let app_name = xcodeproj.file_stem().unwrap().to_str().unwrap();
let home = std::env::var("HOME").unwrap();
let derived_data = format!("{}/Library/Developer/Xcode/DerivedData", home);
let app_path = std::fs::read_dir(&derived_data)
.context("Could not read DerivedData directory")?
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(app_name)
})
.find_map(|project_dir| {
let app_path = project_dir
.path()
.join("Build/Products/Debug")
.join(format!("{}.app", app_name));
if app_path.exists() {
Some(app_path)
} else {
None
}
})
.context("Could not find built .app bundle. Try running 'jffi build --platform macos' first.")?;
Command::new("open")
.arg(app_path)
.status()
.context("Failed to launch app")?;
println!();
println!("{}", " ✅ macOS app launched!".green());
Ok(())
}
fn run_windows() -> Result<()> {
println!(" {} Building Windows app...", "→".bright_blue());
crate::commands::build::build_project(Some("windows".to_string()), false, false, false)?;
println!(" {} Launching Windows app...", "→".bright_blue());
let project_name = std::fs::read_dir("platforms/windows")
.ok()
.and_then(|entries| {
entries
.filter_map(|e| e.ok())
.find(|e| {
let name = e.file_name();
name.to_string_lossy().ends_with(".csproj")
})
.and_then(|e| e.path().file_stem().map(|s| s.to_string_lossy().to_string()))
})
.context("Could not find .csproj file to determine project name")?;
let exe_name = format!("{}.exe", project_name);
fn find_exe_recursive(dir: &std::path::Path, target_name: &str) -> Option<std::path::PathBuf> {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file() {
if let Some(name) = path.file_name() {
if name.to_string_lossy() == target_name {
return Some(path);
}
}
} else if path.is_dir() {
if let Some(exe) = find_exe_recursive(&path, target_name) {
return Some(exe);
}
}
}
}
None
}
let exe_path = find_exe_recursive(std::path::Path::new("platforms/windows/bin"), &exe_name)
.context(format!("Could not find {} in platforms/windows/bin", exe_name))?;
println!(" {} Found executable: {}", "→".bright_blue(), exe_path.display());
let exe_dir = exe_path.parent().context("Could not get executable directory")?;
let dll_files: Vec<_> = std::fs::read_dir(exe_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("dll"))
.collect();
println!(" {} DLLs in output directory:", "→".bright_blue());
for dll in &dll_files {
println!(" - {}", dll.file_name().to_string_lossy());
}
let output = Command::new(&exe_path)
.output()
.context("Failed to launch Windows app")?;
if !output.status.success() {
eprintln!(" {} stdout: {}", "→".bright_red(), String::from_utf8_lossy(&output.stdout));
eprintln!(" {} stderr: {}", "→".bright_red(), String::from_utf8_lossy(&output.stderr));
anyhow::bail!("App exited with code: {:?}", output.status.code());
}
println!("{}", " ✅ App launched!".green());
Ok(())
}
fn run_linux() -> Result<()> {
println!(" {} Checking Linux dependencies...", "→".bright_blue());
let needs_setup = !Command::new("gcc").arg("--version").output()?.status.success()
|| !Command::new("python3").arg("--version").output()?.status.success()
|| !Command::new("pkg-config").args(&["--exists", "gtk4"]).output()?.status.success();
if needs_setup {
println!(" {} Missing dependencies. Running setup script...", "→".bright_blue());
let status = Command::new("bash")
.arg("platforms/linux/setup.sh")
.status()
.context("Failed to run setup script")?;
if !status.success() {
anyhow::bail!("Dependency installation failed. Run: cd platforms/linux && ./setup.sh");
}
}
println!(" {} Building Rust FFI library...", "→".bright_blue());
crate::commands::build::build_project(Some("linux".to_string()), false, false, false)?;
let lib_path = std::fs::read_dir("target/debug")
.context("Failed to read target directory")?
.filter_map(|e| e.ok())
.find(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
name_str.starts_with("lib") && name_str.ends_with("ffi.so")
})
.map(|e| e.path())
.context("Could not find FFI library")?;
let lib_filename = lib_path.file_name()
.context("Could not get library filename")?;
let dest_path = format!("platforms/linux/{}", lib_filename.to_string_lossy());
std::fs::copy(&lib_path, &dest_path)
.context("Failed to copy library to platforms/linux")?;
println!(" {} Launching app...", "→".bright_blue());
let status = Command::new("python3")
.arg("main.py")
.current_dir("platforms/linux")
.status()
.context("Failed to launch app")?;
if !status.success() {
anyhow::bail!("App failed to run");
}
println!("{}", " ✅ App launched!".green());
Ok(())
}
fn run_web() -> Result<()> {
println!(" {} Building Rust FFI library...", "→".bright_blue());
crate::commands::build::build_project(Some("web".to_string()), false, false, false)?;
let npm_check = Command::new("npm")
.arg("--version")
.output();
if npm_check.is_err() || !npm_check.unwrap().status.success() {
anyhow::bail!("npm is not installed. Please install Node.js and npm first.");
}
let node_modules = std::path::Path::new("platforms/web/node_modules");
if !node_modules.exists() {
println!(" {} Installing npm dependencies...", "→".bright_blue());
let status = Command::new("npm")
.arg("install")
.current_dir("platforms/web")
.status()
.context("Failed to install npm dependencies")?;
if !status.success() {
anyhow::bail!("npm install failed");
}
}
println!(" {} Starting Vite dev server...", "→".bright_blue());
println!(" {} Server will open at http://localhost:3000", "→".bright_blue());
let status = Command::new("npm")
.arg("run")
.arg("dev")
.current_dir("platforms/web")
.status()
.context("Failed to start Vite dev server")?;
if !status.success() {
anyhow::bail!("Vite dev server failed");
}
println!("{}", " ✅ Web server stopped".green());
Ok(())
}
fn get_available_iphone_simulator() -> Result<String> {
let output = Command::new("xcrun")
.args(&["simctl", "list", "devices", "available", "iPhone"])
.output()
.context("Failed to list simulators")?;
if !output.status.success() {
anyhow::bail!("Failed to get simulator list");
}
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("iPhone") && (line.contains("(Shutdown)") || line.contains("(Booted)")) {
if let Some(name_part) = line.trim().split(" (").next() {
return Ok(name_part.to_string());
}
}
}
for line in output_str.lines() {
if line.contains("iPhone") {
if let Some(name_part) = line.trim().split(" (").next() {
return Ok(name_part.to_string());
}
}
}
anyhow::bail!("No iPhone simulator found. Please create one in Xcode.")
}