use anyhow::{Context, Result};
use colored::*;
use std::process::Command;
use std::path::Path;
use std::fs;
fn validate_project_structure() -> Result<()> {
if !Path::new("jffi.toml").exists() {
anyhow::bail!(
"{}\n\n{}",
"Error: Not in a JFFI project directory.".red().bold(),
format!(
"This command must be run from a project created with:\n {} {}\n\nOr navigate to an existing JFFI project directory.",
"jffi new".bright_cyan(),
"<project-name>".bright_yellow()
)
);
}
if !Path::new("core").exists() {
anyhow::bail!(
"{}\n\n{}",
"Error: Missing 'core' directory.".red().bold(),
"Your project structure appears incomplete. Expected directories: core/, platforms/"
);
}
Ok(())
}
fn ensure_rust_targets(targets: &[&str]) -> Result<()> {
let output = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.context("Failed to run rustup. Please install Rust with rustup.")?;
if !output.status.success() {
anyhow::bail!("Failed to check installed Rust targets via rustup");
}
let installed = String::from_utf8_lossy(&output.stdout);
let mut missing = Vec::new();
for target in targets {
if !installed.lines().any(|l| l.trim() == *target) {
missing.push(*target);
}
}
if missing.is_empty() {
return Ok(());
}
let status = Command::new("rustup")
.arg("target")
.arg("add")
.args(&missing)
.status()
.context("Failed to install required Rust targets via rustup")?;
if !status.success() {
anyhow::bail!("Failed to install Rust targets: {}", missing.join(", "));
}
Ok(())
}
pub fn build_project(platform: Option<String>, all: bool, release: bool, device: bool, deploy: bool) -> Result<()> {
validate_project_structure()?;
if all {
build_all_platforms(release)?;
} else if let Some(platform) = platform {
build_platform_with_options(&platform, release, device, deploy)?;
} else {
anyhow::bail!("Specify --platform <platform> or --all");
}
Ok(())
}
pub fn build_platform_with_options(platform: &str, release: bool, device: bool, deploy: bool) -> Result<()> {
validate_project_structure()?;
println!("{}", format!("🔨 Building for {}...", platform).bright_green().bold());
match platform {
"ios" => {
let target_type = if device { "device" } else { "simulator" };
build_ios_target(release, target_type)
},
"android" => build_android(release),
"macos" => {
let arch = if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"x86_64"
};
build_macos(arch, release)
}
"macos-arm64" => build_macos("aarch64", release),
"macos-x64" => build_macos("x86_64", release),
"windows" => {
if deploy {
build_windows_all_archs(release)
} else {
build_windows_host_arch(release)
}
},
"linux" => build_linux(release),
"web" => build_web(release),
_ => anyhow::bail!("Unknown platform: {}", platform),
}
}
fn build_all_platforms(release: bool) -> Result<()> {
println!("{}", "🔨 Building all platforms...".bright_green().bold());
println!();
let config = crate::config::load_config()?;
for platform in &config.platforms.enabled {
println!("{} Building {}...", "→".bright_blue(), platform.bright_cyan());
if let Err(e) = build_platform(platform, release) {
println!("{} Failed to build {}: {}", "✗".red(), platform, e);
} else {
println!("{} {} built successfully", "✓".green(), platform);
}
println!();
}
Ok(())
}
pub fn build_platform(platform: &str, release: bool) -> Result<()> {
validate_project_structure()?;
println!("{}", format!("🔨 Building for {}...", platform).bright_green().bold());
match platform {
"ios" => build_ios(release),
"android" => build_android(release),
"macos" => {
let arch = if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"x86_64"
};
build_macos(arch, release)
}
"macos-arm64" => build_macos("aarch64", release),
"macos-x64" => build_macos("x86_64", release),
"windows" => build_windows_all_archs(release),
"linux" => build_linux(release),
"web" => build_web(release),
_ => anyhow::bail!("Unknown platform: {}", platform),
}
}
fn build_ios(release: bool) -> Result<()> {
build_ios_xcframework(release)?;
Ok(())
}
fn build_ios_target(release: bool, _target_type: &str) -> Result<()> {
build_ios_xcframework(release)
}
fn build_ios_xcframework(release: bool) -> Result<()> {
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
let profile = if release { "release" } else { "debug" };
let targets = vec![
("aarch64-apple-ios-sim", "iOS Simulator (ARM64)"),
("x86_64-apple-ios", "iOS Simulator (x86_64)"),
("aarch64-apple-ios", "iOS Device"),
];
let target_names: Vec<&str> = targets.iter().map(|(t, _)| *t).collect();
ensure_rust_targets(&target_names)?;
let multi = MultiProgress::new();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
for (target, target_name) in &targets {
let pb = multi.add(ProgressBar::new_spinner());
pb.set_style(spinner_style.clone());
pb.set_prefix(" →");
pb.set_message(format!("Building {}", target_name));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let mut args = vec!["build"];
if release {
args.push("--release");
}
args.extend(&["--manifest-path", "core/Cargo.toml", "--target", target]);
let status = Command::new("cargo")
.env("CARGO_TARGET_DIR", "target")
.env("IPHONEOS_DEPLOYMENT_TARGET", "16.0")
.args(&args)
.status()
.context(format!("Failed to build Rust library for {}", target))?;
if !status.success() {
pb.finish_with_message(format!("{} {}", "✗".red(), target_name));
anyhow::bail!("Rust build failed for {}", target);
}
pb.finish_with_message(format!("{} {}", "✓".green(), target_name));
}
println!(" {} Generating Swift bindings...", "→".bright_blue());
let lib_dir = format!("target/aarch64-apple-ios-sim/{}", profile);
let lib_path = std::fs::read_dir(&lib_dir)
.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("core.a")
})
.map(|e| e.path())
.context("Could not find FFI library")?;
let status = Command::new("uniffi-bindgen")
.args(&[
"generate",
"--library",
lib_path.to_str().unwrap(),
"--language",
"swift",
"--out-dir",
"platforms/ios",
])
.status()
.context("Failed to generate Swift bindings")?;
if !status.success() {
anyhow::bail!("Binding generation failed");
}
patch_swift_concurrency("platforms/ios")?;
println!(" {} Creating XCFramework...", "→".bright_blue());
let lib_name = lib_path.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.strip_prefix("lib"))
.context("Could not determine library name")?;
let xcframework_path = format!("platforms/ios/{}.xcframework", lib_name);
let sim_arm64_lib = format!("target/aarch64-apple-ios-sim/{}/lib{}.a", profile, lib_name);
let sim_x86_lib = format!("target/x86_64-apple-ios/{}/lib{}.a", profile, lib_name);
let sim_universal_lib = format!("target/ios-simulator-universal/lib{}.a", lib_name);
let universal_dir = std::path::Path::new("target/ios-simulator-universal");
for attempt in 0..3 {
if universal_dir.exists() {
if let Err(e) = std::fs::remove_dir_all(universal_dir) {
if attempt < 2 {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
} else {
eprintln!(" {} Warning: Could not clean ios-simulator-universal directory: {}", "⚠".yellow(), e);
}
}
}
break;
}
std::fs::create_dir_all(universal_dir)?;
let lipo_status = Command::new("lipo")
.args(&[
"-create",
&sim_arm64_lib,
&sim_x86_lib,
"-output",
&sim_universal_lib,
])
.status()
.context("Failed to create universal simulator library")?;
if !lipo_status.success() {
anyhow::bail!("lipo failed to create universal simulator library");
}
let device_lib = format!("target/aarch64-apple-ios/{}/lib{}.a", profile, lib_name);
if std::path::Path::new(&xcframework_path).exists() {
let mut sim_dir = None;
let mut device_dir = None;
if let Ok(entries) = std::fs::read_dir(&xcframework_path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if dir_name.contains("..") || dir_name.contains('/') || dir_name.contains('\\') {
continue;
}
if dir_name.contains("simulator") {
sim_dir = Some(dir_name.to_string());
} else if dir_name.starts_with("ios-arm64") && !dir_name.contains("simulator") {
device_dir = Some(dir_name.to_string());
}
}
}
}
if let (Some(sim), Some(dev)) = (sim_dir, device_dir) {
let sim_dest = format!("{}/{}/lib{}.a", xcframework_path, sim, lib_name);
let device_dest = format!("{}/{}/lib{}.a", xcframework_path, dev, lib_name);
match (
std::fs::copy(&sim_universal_lib, &sim_dest),
std::fs::copy(&device_lib, &device_dest)
) {
(Ok(_), Ok(_)) => {
let info_plist = format!("{}/Info.plist", xcframework_path);
if Command::new("touch").arg(&info_plist).status().is_err() {
if let Ok(content) = std::fs::read(&info_plist) {
let _ = std::fs::write(&info_plist, content);
}
}
println!("{}", " ✅ iOS XCFramework updated in-place".green());
return Ok(());
}
_ => {
println!(" {} XCFramework update failed, recreating...", "⚠".yellow());
let _ = std::fs::remove_dir_all(&xcframework_path);
}
}
} else {
println!(" {} XCFramework structure incomplete, recreating...", "⚠".yellow());
let _ = std::fs::remove_dir_all(&xcframework_path);
}
}
let xcframework_status = Command::new("xcodebuild")
.args(&[
"-create-xcframework",
"-library", &sim_universal_lib,
"-library", &device_lib,
"-output", &xcframework_path,
])
.status()
.context("Failed to create XCFramework")?;
if !xcframework_status.success() {
anyhow::bail!("xcodebuild failed to create XCFramework");
}
println!("{}", " ✅ iOS XCFramework created successfully".green());
Ok(())
}
fn build_android(release: bool) -> Result<()> {
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
let architectures = vec![
("aarch64-linux-android", "arm64-v8a"),
("armv7-linux-androideabi", "armeabi-v7a"),
("x86_64-linux-android", "x86_64"),
];
let targets: Vec<&str> = architectures.iter().map(|(t, _)| *t).collect();
ensure_android_targets(&targets)?;
ensure_cargo_ndk()?;
let profile = if release { "release" } else { "debug" };
let multi = MultiProgress::new();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
for (target, abi) in &architectures {
let pb = multi.add(ProgressBar::new_spinner());
pb.set_style(spinner_style.clone());
pb.set_prefix(" →");
pb.set_message(format!("Building Android {}", abi));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let mut args = vec!["ndk", "-t", target, "-o", "platforms/android/app/src/main/jniLibs", "build"];
if release {
args.push("--release");
}
args.extend(&["--manifest-path", "core/Cargo.toml"]);
let status = Command::new("cargo")
.env("CARGO_TARGET_DIR", "target")
.args(&args)
.status()
.context(format!("Failed to build for {}", target))?;
if !status.success() {
pb.finish_with_message(format!("{} Android {}", "✗".red(), abi));
anyhow::bail!("Rust build failed for {}", target);
}
pb.finish_with_message(format!("{} Android {}", "✓".green(), abi));
}
let lib_dir = format!("target/aarch64-linux-android/{}", profile);
let lib_path = std::fs::read_dir(&lib_dir)
.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("core.so")
})
.map(|e| e.path())
.context("Could not find FFI library")?;
let status = Command::new("uniffi-bindgen")
.args(&[
"generate",
"--library",
lib_path.to_str().unwrap(),
"--language",
"kotlin",
"--out-dir",
"platforms/android/app/src/main/java",
])
.status()
.context("Failed to generate Kotlin bindings")?;
if !status.success() {
anyhow::bail!("Binding generation failed");
}
println!("{}", " ✅ Android build complete".green());
Ok(())
}
fn build_macos(_arch: &str, release: bool) -> Result<()> {
build_macos_xcframework(release)
}
fn build_macos_xcframework(release: bool) -> Result<()> {
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
let profile = if release { "release" } else { "debug" };
let targets = vec![
("aarch64-apple-darwin", "macOS Apple Silicon"),
("x86_64-apple-darwin", "macOS Intel"),
];
let target_names: Vec<&str> = targets.iter().map(|(t, _)| *t).collect();
ensure_rust_targets(&target_names)?;
let multi = MultiProgress::new();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
for (target, target_name) in &targets {
let pb = multi.add(ProgressBar::new_spinner());
pb.set_style(spinner_style.clone());
pb.set_prefix(" →");
pb.set_message(format!("Building {}", target_name));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let mut args = vec!["build"];
if release {
args.push("--release");
}
args.extend(&["--manifest-path", "core/Cargo.toml", "--target", target]);
let status = Command::new("cargo")
.env("CARGO_TARGET_DIR", "target")
.env("MACOSX_DEPLOYMENT_TARGET", "13.0")
.args(&args)
.status()
.context(format!("Failed to build Rust library for {}", target))?;
if !status.success() {
pb.finish_with_message(format!("{} {}", "✗".red(), target_name));
anyhow::bail!("Rust build failed for {}", target);
}
pb.finish_with_message(format!("{} {}", "✓".green(), target_name));
}
println!(" {} Generating Swift bindings...", "→".bright_blue());
let lib_dir = format!("target/aarch64-apple-darwin/{}", profile);
let lib_path = std::fs::read_dir(&lib_dir)
.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("core.dylib")
})
.map(|e| e.path())
.context("Could not find FFI library")?;
let status = Command::new("uniffi-bindgen")
.args(&[
"generate",
"--library",
lib_path.to_str().unwrap(),
"--language",
"swift",
"--out-dir",
"platforms/macos",
])
.status()
.context("Failed to generate Swift bindings")?;
if !status.success() {
anyhow::bail!("Binding generation failed");
}
patch_swift_concurrency("platforms/macos")?;
println!(" {} Creating XCFramework...", "→".bright_blue());
let lib_name = lib_path.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.strip_prefix("lib"))
.context("Could not determine library name")?;
let xcframework_path = format!("platforms/macos/{}.xcframework", lib_name);
let arm64_lib = format!("target/aarch64-apple-darwin/{}/lib{}.dylib", profile, lib_name);
let x86_lib = format!("target/x86_64-apple-darwin/{}/lib{}.dylib", profile, lib_name);
let universal_lib = format!("target/macos-universal/lib{}.dylib", lib_name);
std::fs::create_dir_all("target/macos-universal")?;
let lipo_status = Command::new("lipo")
.args(&[
"-create",
&arm64_lib,
&x86_lib,
"-output",
&universal_lib,
])
.status()
.context("Failed to create universal macOS library")?;
if !lipo_status.success() {
anyhow::bail!("lipo failed to create universal macOS library");
}
if std::path::Path::new(&xcframework_path).exists() {
let mut macos_dir = None;
if let Ok(entries) = std::fs::read_dir(&xcframework_path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if dir_name.contains("..") || dir_name.contains('/') || dir_name.contains('\\') {
continue;
}
if dir_name.starts_with("macos-") {
macos_dir = Some(dir_name.to_string());
break;
}
}
}
}
if let Some(dir) = macos_dir {
let dest = format!("{}/{}/lib{}.dylib", xcframework_path, dir, lib_name);
match std::fs::copy(&universal_lib, &dest) {
Ok(_) => {
let info_plist = format!("{}/Info.plist", xcframework_path);
if Command::new("touch").arg(&info_plist).status().is_err() {
if let Ok(content) = std::fs::read(&info_plist) {
let _ = std::fs::write(&info_plist, content);
}
}
println!("{}", " ✅ macOS XCFramework updated in-place".green());
return Ok(());
}
Err(_) => {
println!(" {} XCFramework update failed, recreating...", "⚠".yellow());
let _ = std::fs::remove_dir_all(&xcframework_path);
}
}
} else {
println!(" {} XCFramework structure not found, recreating...", "⚠".yellow());
let _ = std::fs::remove_dir_all(&xcframework_path);
}
}
let xcframework_status = Command::new("xcodebuild")
.args(&[
"-create-xcframework",
"-library", &universal_lib,
"-output", &xcframework_path,
])
.status()
.context("Failed to create XCFramework")?;
if !xcframework_status.success() {
anyhow::bail!("xcodebuild failed to create XCFramework");
}
println!("{}", " ✅ macOS XCFramework created successfully".green());
Ok(())
}
fn build_windows_host_arch(release: bool) -> Result<()> {
ensure_uniffi_bindgen_cs()?;
let host_arch = if let Ok(arch) = std::env::var("PROCESSOR_ARCHITECTURE") {
match arch.as_str() {
"AMD64" => "x86_64",
"x86" => "i686",
"ARM64" => "aarch64",
_ => "x86_64",
}
} else {
"x86_64"
};
let host_platform = match host_arch {
"x86_64" => "x64",
"i686" => "x86",
"aarch64" => "ARM64",
_ => "x64",
};
println!(" {} Detected host architecture: {}", "→".bright_blue(), host_platform);
let archs = [host_arch];
let platforms = [host_platform];
let profile = if release { "release" } else { "debug" };
build_windows_archs(&archs, &platforms, release, profile)?;
Ok(())
}
fn build_windows_all_archs(release: bool) -> Result<()> {
ensure_uniffi_bindgen_cs()?;
let archs = ["i686", "x86_64", "aarch64"];
let platforms = ["x86", "x64", "ARM64"];
let profile = if release { "release" } else { "debug" };
build_windows_archs(&archs, &platforms, release, profile)?;
Ok(())
}
fn build_windows_archs(archs: &[&str], platforms: &[&str], release: bool, profile: &str) -> Result<()> {
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
let multi = MultiProgress::new();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
for arch in archs.iter() {
let target = format!("{}-pc-windows-msvc", arch);
let target_installed = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains(&target))
.unwrap_or(false);
if !target_installed {
println!(" {} Installing Rust target {}...", "→".bright_blue(), target);
let status = Command::new("rustup")
.args(["target", "add", &target])
.status()
.context("Failed to install Rust target via rustup")?;
if !status.success() {
anyhow::bail!("rustup target add {} failed", target);
}
}
let pb = multi.add(ProgressBar::new_spinner());
pb.set_style(spinner_style.clone());
pb.set_prefix(" →");
pb.set_message(format!("Building Windows {}", arch));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let mut args = vec!["build"];
if release {
args.push("--release");
}
args.extend(&["--target", &target, "--manifest-path", "core/Cargo.toml"]);
let status = Command::new("cargo")
.env("CARGO_TARGET_DIR", "target")
.args(&args)
.status()
.context("Failed to build Rust library")?;
if !status.success() {
pb.finish_with_message(format!("{} Windows {}", "✗".red(), arch));
anyhow::bail!("Rust build failed for {}", arch);
}
pb.finish_with_message(format!("{} Windows {}", "✓".green(), arch));
}
let lib_name = std::env::current_dir()?
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("core")
.replace("-", "_");
let lib_path = format!("target/x86_64-pc-windows-msvc/{}/{}_core.dll", profile, lib_name);
println!(" {} Generating C# bindings with uniffi-bindgen-cs...", "→".bright_blue());
std::fs::create_dir_all("platforms/windows/Generated")?;
let status = Command::new("uniffi-bindgen-cs")
.arg("--library")
.arg(&lib_path)
.arg("--config")
.arg("core/uniffi.toml")
.arg("--out-dir")
.arg("platforms/windows/Generated")
.status()
.context("Failed to run uniffi-bindgen-cs")?;
if !status.success() {
anyhow::bail!("uniffi-bindgen-cs failed to generate C# bindings");
}
println!(" {} Copying DLL to project directory...", "→".bright_blue());
let target = "x86_64-pc-windows-msvc"; let lib_path = format!("target/{}/{}/{}_core.dll", target, profile, lib_name);
let dll_dest = format!("platforms/windows/{}_core.dll", lib_name);
if std::path::Path::new(&lib_path).exists() {
std::fs::copy(&lib_path, &dll_dest)
.with_context(|| format!("Failed to copy DLL to {}", dll_dest))?;
println!(" {} Copied DLL to platforms/windows/", "✓".green());
} else {
anyhow::bail!("Rust DLL not found at {}", lib_path);
}
println!(" {} Building C# project with MSBuild...", "→".bright_blue());
let csproj_file = std::fs::read_dir("platforms/windows")
.context("Failed to read platforms/windows directory")?
.filter_map(|e| e.ok())
.find(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
name_str.ends_with(".csproj")
})
.map(|e| e.path())
.context("Could not find .csproj file")?;
let dotnet_cmd = find_dotnet();
let msbuild_cmd = find_msbuild();
let build_cmd: &str = if dotnet_cmd.is_some() { "dotnet" } else { "msbuild" };
for platform in platforms.iter() {
println!(" {} Building for {}...", "→".bright_blue(), platform);
let mut build_args: Vec<String> = Vec::new();
if build_cmd == "dotnet" {
build_args.push("build".to_string());
build_args.push(csproj_file.to_string_lossy().into_owned());
build_args.push(format!("-p:Platform={}", platform));
if release {
build_args.extend(["-c".to_string(), "Release".to_string()]);
}
} else {
build_args.push(csproj_file.to_string_lossy().into_owned());
build_args.push(format!("/p:Platform={}", platform));
if release {
build_args.extend(["/p:Configuration=Release".to_string()]);
}
}
let mut cmd = if build_cmd == "dotnet" {
Command::new(dotnet_cmd.as_deref().unwrap_or("dotnet"))
} else {
Command::new(msbuild_cmd.as_deref().unwrap_or("msbuild"))
};
let status = cmd
.args(&build_args)
.status()
.with_context(|| {
let dotnet_hint = if dotnet_cmd.is_some() {
"dotnet was found but failed to run."
} else {
"dotnet was not found (checked PATH and `C:\\Program Files\\dotnet\\dotnet.exe`). Install .NET 8 SDK from https://dotnet.microsoft.com/download"
};
let msbuild_hint = if msbuild_cmd.is_some() {
"msbuild was found but failed to run. Note: MSBuild requires the .NET SDK to resolve Microsoft.NET.Sdk-style projects."
} else {
"msbuild was not found (checked PATH and Visual Studio via vswhere)."
};
format!(
"Failed to build Windows app. {dotnet_hint} {msbuild_hint} Install the .NET SDK (recommended) or Visual Studio Build Tools."
)
})?;
if !status.success() {
anyhow::bail!("C# build failed for platform {}", platform);
}
}
println!("{}", " ✅ Windows build complete".green());
Ok(())
}
fn find_dotnet() -> Option<String> {
if Command::new("dotnet").arg("--version").output().is_ok() {
return Some("dotnet".to_string());
}
let candidates = [
r"C:\Program Files\dotnet\dotnet.exe",
r"C:\Program Files (x86)\dotnet\dotnet.exe",
];
for c in candidates {
if std::path::Path::new(c).exists() {
return Some(c.to_string());
}
}
None
}
fn find_msbuild() -> Option<String> {
if Command::new("msbuild").arg("-version").output().is_ok() {
return Some("msbuild".to_string());
}
let vswhere = r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe";
if !std::path::Path::new(vswhere).exists() {
return None;
}
let out = Command::new(vswhere)
.args(&[
"-latest",
"-products",
"*",
"-requires",
"Microsoft.Component.MSBuild",
"-find",
r"MSBuild\**\Bin\MSBuild.exe",
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout);
let first = s.lines().find(|l| !l.trim().is_empty())?.trim().to_string();
if std::path::Path::new(&first).exists() {
Some(first)
} else {
None
}
}
fn ensure_uniffi_bindgen() -> Result<()> {
println!(" {} Checking uniffi-bindgen...", "→".bright_blue());
let check = Command::new("uniffi-bindgen")
.arg("--version")
.output();
if check.is_err() || !check.unwrap().status.success() {
println!(" Installing uniffi-bindgen...");
let status = Command::new("cargo")
.args(&["install", "uniffi", "--features", "cli", "--bin", "uniffi-bindgen", "--version", "0.31.1"])
.status()
.context("Failed to install uniffi-bindgen")?;
if !status.success() {
anyhow::bail!("Failed to install uniffi-bindgen");
}
println!(" {} uniffi-bindgen installed successfully", "✓".green());
} else {
println!(" {} uniffi-bindgen is already installed", "✓".green());
}
Ok(())
}
fn ensure_uniffi_bindgen_cs() -> Result<()> {
println!(" {} Checking uniffi-bindgen-cs...", "→".bright_blue());
let check = Command::new("uniffi-bindgen-cs")
.arg("--version")
.output();
if check.is_err() || !check.unwrap().status.success() {
let repo = std::env::var("JFFI_UNIFFI_BINDGEN_CS_GIT").unwrap_or_else(|_| {
"https://github.com/NordSecurity/uniffi-bindgen-cs".to_string()
});
let tag = std::env::var("JFFI_UNIFFI_BINDGEN_CS_TAG").unwrap_or_else(|_| "v0.10.0+v0.29.4".to_string());
let branch = std::env::var("JFFI_UNIFFI_BINDGEN_CS_BRANCH").ok();
println!(" Installing uniffi-bindgen-cs ({} @ {})...", repo, branch.as_deref().unwrap_or(&tag));
println!(" {} If you use UniFFI v0.31+, you may need a newer uniffi-bindgen-cs fork; set JFFI_UNIFFI_BINDGEN_CS_GIT/JFFI_UNIFFI_BINDGEN_CS_TAG.", "⚠".yellow());
let mut cmd = Command::new("cargo");
cmd.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true")
.args(&["install", "uniffi-bindgen-cs", "--git", &repo]);
if let Some(branch) = branch.as_deref() {
cmd.args(&["--branch", branch]);
} else {
cmd.args(&["--tag", &tag]);
}
let status = cmd
.status()
.context("Failed to install uniffi-bindgen-cs")?;
if !status.success() {
anyhow::bail!("Failed to install uniffi-bindgen-cs");
}
println!(" {} uniffi-bindgen-cs installed successfully", "✓".green());
} else {
println!(" {} uniffi-bindgen-cs is already installed", "✓".green());
}
Ok(())
}
fn build_linux(release: bool) -> Result<()> {
println!("{} Building Linux project...", "→".bright_blue());
let profile = if release { "release" } else { "debug" };
println!(" {} Checking dependencies...", "→".bright_blue());
let python_check = Command::new("python3")
.args(&["-c", "import gi; gi.require_version('Gtk', '4.0')"])
.output();
if python_check.is_err() || !python_check.unwrap().status.success() {
println!("{}", " ⚠️ Python GTK4 dependencies not found.".yellow());
println!(" Run setup.sh to install: sudo ./platforms/linux/setup.sh");
}
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new_spinner();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
pb.set_style(spinner_style);
pb.set_prefix(" →");
pb.set_message("Building Rust library");
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let mut args = vec!["build"];
if release {
args.push("--release");
}
args.extend(&["--manifest-path", "core/Cargo.toml"]);
let status = Command::new("cargo")
.args(&args)
.status()
.context("Failed to build Rust library")?;
if !status.success() {
pb.finish_with_message(format!("{} Rust library", "✗".red()));
anyhow::bail!("Rust build failed");
}
pb.finish_with_message(format!("{} Rust library", "✓".green()));
println!(" {} Generating Python bindings...", "→".bright_blue());
let lib_dir = format!("target/{}", profile);
let lib_path = std::fs::read_dir(&lib_dir)
.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("core.so")
})
.map(|e| e.path())
.context("Could not find FFI library")?;
ensure_uniffi_bindgen()?;
let status = Command::new("uniffi-bindgen")
.args(&[
"generate",
"--library",
lib_path.to_str().unwrap(),
"--language",
"python",
"--out-dir",
"platforms/linux",
])
.status()
.context("Failed to generate Python bindings")?;
if !status.success() {
anyhow::bail!("Binding generation failed");
}
println!("{}", " ✅ Linux build complete".green());
Ok(())
}
fn build_web(release: bool) -> Result<()> {
use indicatif::{ProgressBar, ProgressStyle};
ensure_wasm_target()?;
let pb = ProgressBar::new_spinner();
let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
pb.set_style(spinner_style);
pb.set_prefix(" →");
pb.set_message("Building WASM");
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let profile = if release { "release" } else { "debug" };
let mut args = vec!["build", "--target", "wasm32-unknown-unknown"];
if release {
args.push("--release");
}
args.extend(&["--manifest-path", "ffi-web/Cargo.toml"]);
let status = Command::new("cargo")
.args(&args)
.status()
.context("Failed to build Rust library for WASM")?;
if !status.success() {
pb.finish_with_message(format!("{} WASM", "✗".red()));
anyhow::bail!("Rust WASM build failed");
}
pb.finish_with_message(format!("{} WASM", "✓".green()));
println!(" {} Generating JavaScript bindings with wasm-bindgen...", "→".bright_blue());
let wasm_dir = format!("target/wasm32-unknown-unknown/{}", profile);
let wasm_file = std::fs::read_dir(&wasm_dir)
.context("Failed to read wasm target directory")?
.filter_map(|e| e.ok())
.find(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
name_str.ends_with("_ffi_web.wasm")
})
.map(|e| e.path())
.context("Could not find WASM file")?;
ensure_wasm_bindgen_cli()?;
let status = Command::new("wasm-bindgen")
.arg(wasm_file.to_str().unwrap())
.arg("--out-dir")
.arg("platforms/web/pkg")
.arg("--target")
.arg("web")
.arg("--out-name")
.arg("wasm")
.status()
.context("Failed to run wasm-bindgen")?;
if !status.success() {
anyhow::bail!("wasm-bindgen failed");
}
println!("{}", " ✅ Web build complete".green());
Ok(())
}
fn ensure_wasm_target() -> Result<()> {
println!(" {} Checking wasm32-unknown-unknown target...", "→".bright_blue());
let output = Command::new("rustup")
.args(&["target", "list", "--installed"])
.output()
.context("Failed to check installed targets")?;
let installed = String::from_utf8_lossy(&output.stdout);
if !installed.contains("wasm32-unknown-unknown") {
println!(" Installing wasm32-unknown-unknown...");
let status = Command::new("rustup")
.args(&["target", "add", "wasm32-unknown-unknown"])
.status()
.context("Failed to install wasm32-unknown-unknown target")?;
if !status.success() {
anyhow::bail!("Failed to install wasm32-unknown-unknown target");
}
}
Ok(())
}
fn ensure_wasm_bindgen_cli() -> Result<()> {
println!(" {} Checking wasm-bindgen-cli...", "→".bright_blue());
let lock_content = std::fs::read_to_string("Cargo.lock")
.context("Failed to read Cargo.lock. Run cargo build first.")?;
let mut in_wasm_bindgen = false;
let mut required_version = None;
for line in lock_content.lines() {
let trimmed = line.trim();
if trimmed == "[[package]]" {
in_wasm_bindgen = false;
} else if trimmed == r#"name = "wasm-bindgen""# {
in_wasm_bindgen = true;
} else if in_wasm_bindgen && trimmed.starts_with(r#"version = ""#) {
required_version = trimmed.split('"').nth(1).map(|s| s.to_string());
break;
}
}
let required_version = required_version
.context("Could not find wasm-bindgen version in Cargo.lock")?;
let check = Command::new("wasm-bindgen")
.arg("--version")
.output();
let needs_install = if let Ok(output) = check {
if output.status.success() {
let installed_version = String::from_utf8_lossy(&output.stdout);
let installed_version = installed_version.trim().split_whitespace().last().unwrap_or("");
installed_version != required_version
} else {
true
}
} else {
true
};
if needs_install {
println!(" Installing wasm-bindgen-cli {} (this may take a few minutes)...", required_version);
let status = Command::new("cargo")
.args(&["install", "-f", "wasm-bindgen-cli", "--version", &required_version])
.status()
.context("Failed to install wasm-bindgen-cli")?;
if !status.success() {
anyhow::bail!("Failed to install wasm-bindgen-cli");
}
println!(" {} wasm-bindgen-cli {} installed", "✓".green(), required_version);
} else {
println!(" {} wasm-bindgen-cli {} already installed", "✓".green(), required_version);
}
Ok(())
}
fn ensure_android_targets(targets: &[&str]) -> Result<()> {
println!(" {} Checking Android targets...", "→".bright_blue());
let output = Command::new("rustup")
.args(&["target", "list", "--installed"])
.output()
.context("Failed to check installed targets")?;
let installed = String::from_utf8_lossy(&output.stdout);
for target in targets {
if !installed.contains(target) {
println!(" Installing {}...", target.bright_yellow());
let status = Command::new("rustup")
.args(&["target", "add", target])
.status()
.context(format!("Failed to install target {}", target))?;
if !status.success() {
anyhow::bail!("Failed to install Android target: {}", target);
}
}
}
println!(" {} Android targets ready", "✓".green());
Ok(())
}
fn ensure_cargo_ndk() -> Result<()> {
println!(" {} Checking cargo-ndk...", "→".bright_blue());
let check = Command::new("cargo")
.args(&["ndk", "--version"])
.output();
if check.is_err() || !check.unwrap().status.success() {
println!(" Installing cargo-ndk...");
let status = Command::new("cargo")
.args(&["install", "cargo-ndk"])
.status()
.context("Failed to install cargo-ndk")?;
if !status.success() {
anyhow::bail!("Failed to install cargo-ndk");
}
println!(" {} cargo-ndk installed", "✓".green());
}
Ok(())
}
fn patch_swift_concurrency(platform_dir: &str) -> Result<()> {
let dir = Path::new(platform_dir);
if !dir.exists() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "swift" {
let filename = path.file_name().unwrap().to_string_lossy();
if filename.ends_with("_ffi.swift") || filename.ends_with("_core.swift") {
patch_swift_file(&path)?;
}
}
}
}
Ok(())
}
fn patch_swift_file(path: &Path) -> Result<()> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read Swift file: {}", path.display()))?;
let patched = content.replace(
"static let vtablePtr: UnsafePointer<",
"nonisolated(unsafe) static let vtablePtr: UnsafePointer<"
);
if patched != content {
fs::write(path, patched)
.with_context(|| format!("Failed to write patched Swift file: {}", path.display()))?;
println!(" {} Patched Swift 6 concurrency in {}", "✓".green(), path.file_name().unwrap().to_string_lossy());
}
Ok(())
}