Skip to main content

xtask/
build_android.rs

1//! `cargo xtask build-android`: build the examples' shared libraries, then the
2//! APK that packages them.
3//!
4//! Two tools, split along the language boundary. `cargo ndk` cross-compiles the
5//! `*_android` crates, which hold the exported entry points and nothing else,
6//! and drops each `.so` into the app's `jniLibs` directory, which is where
7//! Gradle looks for native libraries and packages whatever it finds; Gradle then
8//! compiles the activities and assembles the APK around them. The libraries are
9//! built first because an APK without them is an activity that cannot start.
10//!
11//! Both tools configure themselves from the environment: `cargo ndk` reads the
12//! NDK out of `ANDROID_NDK_HOME` (or the newest one under `ANDROID_HOME/ndk`)
13//! and Gradle reads the SDK out of `ANDROID_HOME` (or `android/local.properties`).
14
15use std::path::Path;
16use std::process::Command;
17
18use super::BuildAndroidArgs;
19use super::step;
20
21/// The crates that build the libraries the activities load, in the order the
22/// tab bar lists them. Each is a `cdylib` exporting `android_main`, and each
23/// activity names the one it loads in its `android.app.lib_name` manifest
24/// entry — the package is the example's name with `_android` appended, while
25/// the library keeps the example's own name.
26const LIBRARY_CRATES: &[&str] = &["unlit3d_examples_android", "unlit3dx_examples_android"];
27/// The ABI the APK carries. Kept in step with `abiFilters` in
28/// `android/app/build.gradle.kts`, which is what decides what the APK accepts.
29const ABI: &str = "arm64-v8a";
30/// The API level the library is compiled and linked against. Kept in step with
31/// `minSdk` in `android/app/build.gradle.kts`: linking against a higher one
32/// would let the library call into API the APK's own floor does not promise.
33const PLATFORM: &str = "26";
34/// The Android application's Gradle project, relative to the workspace root.
35const ANDROID_DIR: &str = "android";
36/// Where the Rust build puts the shared library, relative to the workspace
37/// root. This is Gradle's own `jniLibs` source directory, whose subdirectory
38/// names are ABI identifiers, so `cargo ndk -o` writes exactly its layout.
39const JNI_LIBS_DIR: &str = "android/app/src/main/jniLibs";
40
41/// Build the shared library and the APK that packages it.
42pub fn run(args: &BuildAndroidArgs) -> Result<(), String> {
43    library(args)?;
44    apk(args)
45}
46
47/// Cross-compile the libraries and stage them for the APK.
48///
49/// One `cargo ndk` run builds them all: it passes the packages through to
50/// `cargo build` and writes every resulting `.so` into the same `jniLibs`
51/// directory, which is what the activities load from at run time.
52fn library(args: &BuildAndroidArgs) -> Result<(), String> {
53    let profile: &[&str] = if args.release { &["--release"] } else { &[] };
54    let mut cargo = Command::new("cargo");
55    cargo
56        .args(["ndk", "-t", ABI, "-P", PLATFORM, "-o", JNI_LIBS_DIR])
57        .arg("build")
58        .args(LIBRARY_CRATES.iter().flat_map(|krate| ["-p", krate]))
59        .arg("--lib")
60        .args(profile);
61    step::run(&mut cargo, "the Android build")
62}
63
64/// Assemble the APK, which packages whatever `library` staged.
65fn apk(args: &BuildAndroidArgs) -> Result<(), String> {
66    let task = if args.release {
67        ":app:assembleRelease"
68    } else {
69        ":app:assembleDebug"
70    };
71    let mut gradle = Command::new("./gradlew");
72    gradle.current_dir(Path::new(ANDROID_DIR)).arg(task);
73    step::run(&mut gradle, "the Gradle build")?;
74
75    let apk = apk_path(args);
76    if apk.is_file() {
77        println!("built {}", apk.display());
78        Ok(())
79    } else {
80        Err(format!(
81            "the Gradle build reported success but {} is missing",
82            apk.display()
83        ))
84    }
85}
86
87/// The APK the selected build type produces.
88///
89/// Gradle puts it under `build/outputs/apk` on its own, so the path is the
90/// build type's name rather than anything this task asks for. A release APK
91/// is `-unsigned` because the project ships no signing configuration.
92fn apk_path(args: &BuildAndroidArgs) -> std::path::PathBuf {
93    let (build_type, file) = if args.release {
94        ("release", "app-release-unsigned.apk")
95    } else {
96        ("debug", "app-debug.apk")
97    };
98    Path::new(ANDROID_DIR)
99        .join("app/build/outputs/apk")
100        .join(build_type)
101        .join(file)
102}