use anyhow::{bail, Context, Result};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
fn abi_for_target(target: &str) -> &'static str {
match target {
"aarch64-linux-android" => "arm64-v8a",
"armv7-linux-androideabi" => "armeabi-v7a",
"x86_64-linux-android" => "x86_64",
"i686-linux-android" => "x86",
_ => "x86_64",
}
}
pub fn run(target: &str, release: bool, out_dir: &str) -> Result<()> {
println!("{} Building for `{}`…", "→".cyan(), target.bold());
let pb = spinner("Compiling Rust…");
let mut cargo_args = vec!["build", "--target", target];
if release { cargo_args.push("--release"); }
let status = Command::new("cargo")
.args(&cargo_args)
.status()
.context("Failed to invoke `cargo build`. Is Rust installed?")?;
pb.finish_and_clear();
if !status.success() {
bail!(
"`cargo build` failed.\n\n\
Tip: run `cargo bubba doctor` to verify your Android NDK setup.\n\
Tip: run `rustup target add {}` if the target is missing.",
target
);
}
println!(" {} Rust compiled OK", "✓".green());
let profile = if release { "release" } else { "debug" };
let lib_path = PathBuf::from(format!("target/{}/{}/libapp.so", target, profile));
if !lib_path.exists() {
println!(
" {} Expected {} — note: the final APK packaging requires the Android NDK.",
"ℹ".yellow(),
lib_path.display()
);
}
let pb2 = spinner("Packaging APK…");
let apk_result = package_apk(target, release, out_dir, profile);
pb2.finish_and_clear();
match apk_result {
Ok(apk_path) => {
println!(" {} APK packaged: {}", "✓".green(), apk_path.display().to_string().bold());
println!("\n{} Build complete!\n", "✓".green().bold());
println!(" APK → {}/{}", out_dir, "app-debug.apk".bold());
println!(" Install: {}\n", "adb install dist/app-debug.apk".dimmed());
}
Err(e) => {
println!(
" {} APK packaging: {}\n {}",
"⚠".yellow(),
"SDK tools not found".yellow(),
e.to_string().dimmed()
);
println!(
"\n Run {} to diagnose your environment.\n",
"cargo bubba doctor".bold()
);
}
}
Ok(())
}
fn package_apk(target: &str, _release: bool, out_dir: &str, profile: &str) -> Result<PathBuf> {
let abi = abi_for_target(target);
let out = PathBuf::from(out_dir);
std::fs::create_dir_all(&out)?;
let lib_src = PathBuf::from(format!("target/{}/{}/libapp.so", target, profile));
let lib_dst = out.join(format!("lib/{}/libapp.so", abi));
if let Some(parent) = lib_dst.parent() {
std::fs::create_dir_all(parent)?;
}
if lib_src.exists() {
std::fs::copy(&lib_src, &lib_dst)?;
}
let aapt2 = find_tool("aapt2").context(
"aapt2 not found. Set ANDROID_SDK_ROOT or install Android Build Tools.\n\
Run `cargo bubba doctor` for detailed setup instructions."
)?;
let res_out = out.join("compiled_res");
std::fs::create_dir_all(&res_out)?;
if Path::new("assets").exists() {
let status = Command::new(&aapt2)
.args(["compile", "--dir", "assets", "-o"])
.arg(&res_out)
.status()?;
if !status.success() { bail!("aapt2 compile failed"); }
}
let manifest = "android/app/src/main/AndroidManifest.xml";
if !Path::new(manifest).exists() {
bail!("AndroidManifest.xml not found at {}. Run `cargo bubba new` first.", manifest);
}
let base_apk = out.join("base.apk");
let android_jar = find_android_jar()?;
let status = Command::new(&aapt2)
.args(["link", "--proto-format",
"-o", base_apk.to_str().unwrap(),
"--manifest", manifest,
"-I", &android_jar])
.status()?;
if !status.success() { bail!("aapt2 link failed"); }
let zipalign = find_tool("zipalign").context("zipalign not found")?;
let aligned = out.join("aligned.apk");
Command::new(&zipalign).args(["4", base_apk.to_str().unwrap(), aligned.to_str().unwrap()]).status()?;
let apksigner = find_tool("apksigner").context("apksigner not found")?;
let final_apk = out.join("app-debug.apk");
Command::new(&apksigner)
.args(["sign", "--ks", "~/.android/debug.keystore",
"--ks-pass", "pass:android",
"--out", final_apk.to_str().unwrap(),
aligned.to_str().unwrap()])
.status()?;
Ok(final_apk)
}
fn find_tool(name: &str) -> Option<String> {
if let Ok(p) = which::which(name) {
return Some(p.to_string_lossy().into_owned());
}
for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
if let Ok(sdk) = std::env::var(env_var) {
for sub in &["build-tools/34.0.0", "build-tools/33.0.0", "build-tools/32.0.0"] {
let candidate = PathBuf::from(&sdk).join(sub).join(name);
if candidate.exists() {
return Some(candidate.to_string_lossy().into_owned());
}
}
}
}
None
}
fn find_android_jar() -> Result<String> {
for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
if let Ok(sdk) = std::env::var(env_var) {
for api in &["34", "33", "32", "31"] {
let jar = PathBuf::from(&sdk)
.join(format!("platforms/android-{}/android.jar", api));
if jar.exists() {
return Ok(jar.to_string_lossy().into_owned());
}
}
}
}
bail!("android.jar not found. Install an Android Platform via Android Studio SDK Manager.")
}
fn spinner(msg: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(msg.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
}