//! `cargo xtask apk` -- E5 (RUST.md): packages `app/shellApp` into a //! signed, installable APK with no Gradle in the packaging step itself. //! `cargo ndk` cross-compiles `android-shell`; `javac`/`d8` turn its two //! Java stub classes (plus the generated pinned-CA constant) into dex; //! `aapt2` compiles the manifest into `resources.arsc`; the dex and native //! libraries are merged into that base APK with `jar`; `zipalign` and //! `apksigner` finish it. See `apk.rs`'s module doc for what "no Gradle in //! the packaging step" does and does not cover -- one disclosed exception. //! //! Usage: `cargo xtask apk [--release|--debug] [--abi ABI]...` mod apk; mod keystore; mod sdk; use std::fmt; use std::process::ExitCode; /// A failure a person acts on: what went wrong, what this process actually /// saw, and the next thing to try. Matches CODE_RULES's "a failure message /// names the thing, the cause, and the fix." pub struct Fail { what: String, cause: String, fix: String, } impl Fail { pub fn new(what: &str, cause: &str, fix: &str) -> Self { Fail { what: what.to_string(), cause: cause.to_string(), fix: fix.to_string(), } } } impl fmt::Display for Fail { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}\n cause: {}\n fix: {}", self.what, self.cause, self.fix ) } } #[derive(Clone, Copy, PartialEq, Eq)] pub enum Variant { /// Signed with `~/.config/ai-app/release.jks`, the same key /// `build-apk.sh` uses for `androidApp` -- what E5's pass condition /// needs, since installing over an existing app requires a matching /// signature. Release, /// Signed with the standard Android debug keystore /// (`~/.android/debug.keystore`, well-known password, generated if /// missing the same way Gradle would), for a fast local loop that /// doesn't touch the real signing key. Debug, } fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let Some(("apk", rest)) = args.split_first().map(|(cmd, rest)| (cmd.as_str(), rest)) else { eprintln!("usage: cargo xtask apk [release|debug] [--abi ABI]..."); return ExitCode::FAILURE; }; let mut variant = Variant::Release; let mut abis: Vec = Vec::new(); let mut i = 0; while i < rest.len() { match rest[i].as_str() { // Bare "release"/"debug" is `.dev-updater.ron`'s interface // (`ByMode::One` appends the chosen mode as the build // command's last argument -- the same convention // `app/build-apk.sh`'s `${1:-release}` uses); the `--`-prefixed // spellings are for typing this by hand. "release" | "--release" => variant = Variant::Release, "debug" | "--debug" => variant = Variant::Debug, "--abi" => { i += 1; match rest.get(i) { Some(abi) => abis.push(abi.clone()), None => { eprintln!("--abi needs a value (e.g. arm64-v8a, x86_64)"); return ExitCode::FAILURE; } } } other => { eprintln!("unknown argument: {other}"); return ExitCode::FAILURE; } } i += 1; } if abis.is_empty() { // arm64-v8a for a real phone, x86_64 for this machine's emulator -- // the two ABIs every other experiment in RUST.md has actually run // on. `--abi` overrides either way. abis = vec!["arm64-v8a".to_string(), "x86_64".to_string()]; } match apk::build(variant, &abis) { Ok(path) => { println!("==> Built {}", path.display()); ExitCode::SUCCESS } Err(fail) => { eprintln!("xtask: {fail}"); ExitCode::FAILURE } } }