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 NDK setup.\n\
Tip: run `rustup target add {}` if the target is missing.\n\
Tip: ensure your Cargo.toml has [lib] with crate-type = [\"cdylib\"].",
target
);
}
println!(" {} Rust compiled OK", "✓".green());
let profile = if release { "release" } else { "debug" };
let lib_so = find_built_so(target, profile);
match &lib_so {
Some(p) => println!(" {} Library: {}", "✓".green(), p.display()),
None => println!(
" {} No .so found — ensure Cargo.toml [lib] crate-type = [\"cdylib\"]",
"⚠".yellow()
),
}
let pb2 = spinner("Packaging APK…");
let apk_result = package_apk(target, out_dir, profile, lib_so.as_deref());
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 → {}", apk_path.display().to_string().bold());
println!(" Install: {}\n", format!("adb install {}", apk_path.display()).dimmed());
}
Err(e) => {
println!(" {} APK packaging failed:\n {}", "⚠".yellow(), e.to_string().dimmed());
println!("\n Run {} to diagnose.\n", "cargo bubba doctor".bold());
}
}
Ok(())
}
fn find_built_so(target: &str, profile: &str) -> Option<PathBuf> {
let dir = PathBuf::from(format!("target/{}/{}", target, profile));
if !dir.exists() { return None; }
if let Ok(entries) = std::fs::read_dir(&dir) {
let candidates: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.is_file()
&& p.extension().map(|e| e == "so").unwrap_or(false)
&& p.file_name()
.map(|n| n.to_string_lossy().starts_with("lib"))
.unwrap_or(false)
})
.collect();
if let Some(p) = candidates.iter().find(|p| {
p.file_name().map(|n| n == "libapp.so").unwrap_or(false)
}) {
return Some(p.clone());
}
candidates.into_iter().next()
} else {
None
}
}
fn package_apk(
target: &str,
out_dir: &str,
profile: &str,
lib_so: Option<&Path>,
) -> Result<PathBuf> {
let _ = profile;
let abi = abi_for_target(target);
let out = PathBuf::from(out_dir);
std::fs::create_dir_all(&out)?;
let aapt2 = find_tool("aapt2").context(
"aapt2 not found.\n\
Add to PATH: export PATH=\"$ANDROID_SDK_ROOT/build-tools/<version>:$PATH\""
)?;
let res_out = out.join("compiled_res");
std::fs::create_dir_all(&res_out)?;
if Path::new("assets").exists() {
let s = Command::new(&aapt2)
.args(["compile", "--dir", "assets", "-o"])
.arg(&res_out)
.status()?;
if !s.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()?;
if base_apk.exists() { std::fs::remove_file(&base_apk)?; }
let s = Command::new(&aapt2)
.args([
"link",
"-o", base_apk.to_str().unwrap(),
"--manifest", manifest,
"-I", &android_jar,
])
.status()?;
if !s.success() { bail!("aapt2 link failed"); }
let dex_script = std::path::Path::new("android/build_dex.sh");
if dex_script.exists() {
let s = Command::new("bash")
.arg(dex_script)
.status()
.context("Failed to run android/build_dex.sh. Is bash and javac installed?")?;
if !s.success() { bail!("DEX compilation failed. Check javac and d8 are installed."); }
}
if let Some(so) = lib_so {
let zip_entry = format!("lib/{}/libapp.so", abi);
let tmp = out.join("tmp_lib");
let tmp_abi = tmp.join("lib").join(abi);
std::fs::create_dir_all(&tmp_abi)?;
std::fs::copy(so, tmp_abi.join("libapp.so"))?;
let base_apk_abs = base_apk.canonicalize()
.unwrap_or_else(|_| base_apk.clone());
let s = Command::new("zip")
.current_dir(&tmp)
.args([base_apk_abs.to_str().unwrap(), &zip_entry])
.status()?;
std::fs::remove_dir_all(&tmp)?;
if !s.success() {
bail!(
"Failed to pack libapp.so into APK.\n\
Make sure zip is installed: sudo apt install zip"
);
}
}
let zipalign = find_tool("zipalign")
.context("zipalign not found. Install Android Build Tools.")?;
let aligned = out.join("aligned.apk");
if aligned.exists() { std::fs::remove_file(&aligned)?; }
let s = Command::new(&zipalign)
.args(["4", base_apk.to_str().unwrap(), aligned.to_str().unwrap()])
.status()?;
if !s.success() { bail!("zipalign failed"); }
let home = std::env::var("HOME").unwrap_or_default();
let keystore = format!("{}/.android/debug.keystore", home);
if !Path::new(&keystore).exists() {
bail!(
"Debug keystore not found at {}.\n\
Generate with:\n \
keytool -genkey -v -keystore ~/.android/debug.keystore \\\n \
-alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \\\n \
-storepass android -keypass android \\\n \
-dname \"CN=Android Debug,O=Android,C=US\"",
keystore
);
}
let apksigner = find_tool("apksigner")
.context("apksigner not found. Install Android Build Tools.")?;
let final_apk = out.join("app-debug.apk");
if final_apk.exists() { std::fs::remove_file(&final_apk)?; }
let s = Command::new(&apksigner)
.args([
"sign",
"--ks", &keystore,
"--ks-pass", "pass:android",
"--out", final_apk.to_str().unwrap(),
aligned.to_str().unwrap(),
])
.status()?;
if !s.success() { bail!("apksigner failed"); }
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) {
let bt = PathBuf::from(&sdk).join("build-tools");
if let Ok(entries) = std::fs::read_dir(&bt) {
let mut versions: Vec<String> = entries
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
versions.sort_by(|a, b| b.cmp(a));
for ver in &versions {
let c = bt.join(ver).join(name);
if c.exists() { return Some(c.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) {
let platforms = PathBuf::from(&sdk).join("platforms");
if let Ok(entries) = std::fs::read_dir(&platforms) {
let mut dirs: Vec<String> = entries
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
dirs.sort_by(|a, b| b.cmp(a));
for dir in &dirs {
let jar = platforms.join(dir).join("android.jar");
if jar.exists() { return Ok(jar.to_string_lossy().into_owned()); }
}
}
}
}
bail!(
"android.jar not found.\n\
Install an Android Platform: Android Studio → SDK Manager → SDK Platforms."
)
}
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
}
pub fn compile_and_add_dex(base_apk: &std::path::Path) -> anyhow::Result<()> {
let dex_script = std::path::Path::new("android/build_dex.sh");
if !dex_script.exists() {
return Ok(());
}
let status = std::process::Command::new("bash")
.arg(dex_script)
.status()
.context("Failed to run android/build_dex.sh")?;
if !status.success() {
anyhow::bail!("DEX compilation failed. Check that javac and d8 are installed.");
}
Ok(())
}