cargo-bubba 0.1.0

cargo subcommand for the Bubba mobile framework
//! `cargo bubba build` — compile Rust → shared library → APK.
//!
//! ## Pipeline
//! ```
//! 1. cargo build --target <abi> [--release]   → libapp.so
//! 2. aapt2 compile assets/                    → compiled resources
//! 3. aapt2 link  AndroidManifest.xml + res    → base.apk (unsigned)
//! 4. zip-add     lib/<abi>/libapp.so           → base.apk
//! 5. zipalign 4  base.apk                     → aligned.apk
//! 6. apksigner   (debug key)                  → app-debug.apk
//! ```

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;

/// Map Rust target triple → Android ABI directory name.
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…");

    // Step 1: cargo build
    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());

    // Determine profile dir
    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()
        );
    }

    // Step 2–6: APK packaging (requires Android SDK / NDK)
    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) => {
            // APK packaging failed (likely missing SDK tools in dev env)
            // This is expected until the full SDK is configured.
            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)?;
    }

    // aapt2
    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."
    )?;

    // Compile assets
    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"); }
    }

    // Link
    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"); }

    // zipalign
    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()?;

    // apksigner (debug key)
    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> {
    // Check PATH first
    if let Ok(p) = which::which(name) {
        return Some(p.to_string_lossy().into_owned());
    }

    // Check ANDROID_SDK_ROOT / ANDROID_HOME
    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
}