use anyhow::Result;
use colored::Colorize;
use std::process::Command;
struct Check {
name: &'static str,
status: CheckStatus,
detail: String,
fix: Option<&'static str>,
}
enum CheckStatus {
Ok,
Warning,
Error,
}
impl CheckStatus {
fn icon(&self) -> colored::ColoredString {
match self {
CheckStatus::Ok => "✓".green().bold(),
CheckStatus::Warning => "⚠".yellow().bold(),
CheckStatus::Error => "✖".red().bold(),
}
}
}
pub fn run() -> Result<()> {
println!("{}\n", "Checking your Bubba development environment…".bold());
let checks = vec![
check_rust(),
check_cargo(),
check_rustup(),
check_android_target("aarch64-linux-android"),
check_android_target("x86_64-linux-android"),
check_android_sdk(),
check_android_ndk(),
check_aapt2(),
check_adb(),
check_java(),
];
let mut errors = 0;
let mut warnings = 0;
for check in &checks {
let status_icon = check.status.icon();
println!(" {} {}", status_icon, check.name.bold());
if !check.detail.is_empty() {
println!(" {}", check.detail.dimmed());
}
if let Some(fix) = check.fix {
println!(" {} {}", "Fix:".yellow(), fix);
}
match check.status {
CheckStatus::Error => errors += 1,
CheckStatus::Warning => warnings += 1,
CheckStatus::Ok => {}
}
}
println!();
if errors == 0 && warnings == 0 {
println!("{} Your environment is ready! Run `cargo bubba build` to compile.\n", "✓".green().bold());
} else {
if errors > 0 {
println!(
"{} {} error(s) found. Fix them before building.",
"✖".red().bold(), errors
);
}
if warnings > 0 {
println!(
"{} {} warning(s) found. Some features may not work.",
"⚠".yellow().bold(), warnings
);
}
println!();
println!(
" Full setup guide: {}\n",
"https://bubba-rs.netlify.app/docs/setup".cyan().underline()
);
}
Ok(())
}
fn check_rust() -> Check {
match Command::new("rustc").arg("--version").output() {
Ok(out) => {
let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
Check { name: "Rust compiler (rustc)", status: CheckStatus::Ok, detail: version, fix: None }
}
Err(_) => Check {
name: "Rust compiler (rustc)",
status: CheckStatus::Error,
detail: "rustc not found in PATH".to_string(),
fix: Some("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"),
},
}
}
fn check_cargo() -> Check {
match Command::new("cargo").arg("--version").output() {
Ok(out) => {
let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
Check { name: "Cargo", status: CheckStatus::Ok, detail: version, fix: None }
}
Err(_) => Check {
name: "Cargo",
status: CheckStatus::Error,
detail: "cargo not found".to_string(),
fix: Some("Install via rustup (comes with Rust)"),
},
}
}
fn check_rustup() -> Check {
match Command::new("rustup").arg("--version").output() {
Ok(out) => {
let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
Check { name: "rustup", status: CheckStatus::Ok, detail: version, fix: None }
}
Err(_) => Check {
name: "rustup",
status: CheckStatus::Warning,
detail: "rustup not found — target management may be manual".to_string(),
fix: Some("https://rustup.rs"),
},
}
}
fn check_android_target(target: &'static str) -> Check {
let name = Box::leak(format!("Rust target: {}", target).into_boxed_str());
let out = Command::new("rustup").args(["target", "list", "--installed"]).output();
match out {
Ok(out) => {
let installed = String::from_utf8_lossy(&out.stdout);
if installed.contains(target) {
Check { name, status: CheckStatus::Ok, detail: "installed".to_string(), fix: None }
} else {
let fix = Box::leak(
format!("rustup target add {}", target).into_boxed_str()
);
Check {
name,
status: CheckStatus::Error,
detail: "not installed".to_string(),
fix: Some(fix),
}
}
}
Err(_) => Check {
name,
status: CheckStatus::Warning,
detail: "Could not check rustup targets".to_string(),
fix: Some("Ensure rustup is installed"),
},
}
}
fn check_android_sdk() -> Check {
for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
if let Ok(val) = std::env::var(env_var) {
if std::path::Path::new(&val).exists() {
return Check {
name: "Android SDK",
status: CheckStatus::Ok,
detail: format!("{}={}", env_var, val),
fix: None,
};
}
}
}
Check {
name: "Android SDK",
status: CheckStatus::Error,
detail: "ANDROID_SDK_ROOT / ANDROID_HOME not set or path does not exist".to_string(),
fix: Some("Install Android Studio → SDK Manager, then set ANDROID_SDK_ROOT"),
}
}
fn check_android_ndk() -> Check {
if let Ok(val) = std::env::var("ANDROID_NDK_ROOT").or_else(|_| std::env::var("NDK_HOME")) {
if std::path::Path::new(&val).exists() {
return Check {
name: "Android NDK",
status: CheckStatus::Ok,
detail: format!("NDK found at {}", val),
fix: None,
};
}
}
for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
if let Ok(sdk) = std::env::var(env_var) {
let ndk_dir = std::path::PathBuf::from(&sdk).join("ndk");
if ndk_dir.exists() {
return Check {
name: "Android NDK",
status: CheckStatus::Ok,
detail: format!("NDK found under {}", sdk),
fix: None,
};
}
}
}
Check {
name: "Android NDK",
status: CheckStatus::Error,
detail: "Android NDK not found".to_string(),
fix: Some("Install via Android Studio → SDK Manager → NDK (Side by side), then set ANDROID_NDK_ROOT"),
}
}
fn check_tool(binary: &'static str, display_name: &'static str, fix: &'static str) -> Check {
match which::which(binary) {
Ok(path) => Check {
name: display_name,
status: CheckStatus::Ok,
detail: path.to_string_lossy().into_owned(),
fix: None,
},
Err(_) => Check {
name: display_name,
status: CheckStatus::Warning,
detail: format!("`{}` not found in PATH", binary),
fix: Some(fix),
},
}
}
fn check_aapt2() -> Check {
check_tool(
"aapt2",
"aapt2 (Android Asset Packaging Tool)",
"Install Android Build Tools via SDK Manager",
)
}
fn check_adb() -> Check {
check_tool(
"adb",
"adb (Android Debug Bridge)",
"Install Android Platform Tools via SDK Manager",
)
}
fn check_java() -> Check {
match Command::new("java").arg("-version").output() {
Ok(_) => Check {
name: "Java (required for apksigner)",
status: CheckStatus::Ok,
detail: "java found".to_string(),
fix: None,
},
Err(_) => Check {
name: "Java (required for apksigner)",
status: CheckStatus::Warning,
detail: "java not found — APK signing may fail".to_string(),
fix: Some("Install JDK 17+: https://adoptium.net"),
},
}
}