use anyhow::Result;
use std::path::{Path, PathBuf};
const PREFERRED_NDKS: &[&str] = &[
"23.1.7779620",
"25.1.8937393",
"26.1.10909125",
"26.3.11579264",
"27.0.12077973",
"27.1.12297006",
];
pub fn android_home() -> Result<PathBuf> {
if let Some(p) = std::env::var_os("ANDROID_HOME").map(PathBuf::from) {
if p.is_dir() {
return Ok(p);
}
}
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
let cand = home.join("Library/Android/sdk");
if cand.is_dir() {
return Ok(cand);
}
}
anyhow::bail!(
"ANDROID_HOME not set and no SDK found at the default macOS \
location ($HOME/Library/Android/sdk)."
)
}
pub fn ndk_home() -> Result<PathBuf> {
if let Some(p) = std::env::var_os("ANDROID_NDK_HOME").map(PathBuf::from) {
if p.is_dir() {
return Ok(p);
}
}
let ndk_dir = android_home()?.join("ndk");
for version in PREFERRED_NDKS {
let cand = ndk_dir.join(version);
if cand.is_dir() {
return Ok(cand);
}
}
anyhow::bail!(
"no supported NDK found in {} (need one of: {})",
ndk_dir.display(),
PREFERRED_NDKS.join(", "),
)
}
pub fn host_tag() -> Result<&'static str> {
if cfg!(target_os = "macos") {
Ok("darwin-x86_64")
} else if cfg!(target_os = "linux") {
Ok("linux-x86_64")
} else if cfg!(target_os = "windows") {
Ok("windows-x86_64")
} else {
anyhow::bail!("unsupported host OS for Android cross-compilation")
}
}
pub fn clang_target_prefix(abi: &str) -> Result<&'static str> {
match abi {
"arm64-v8a" => Ok("aarch64-linux-android"),
"armeabi-v7a" => Ok("armv7a-linux-androideabi"),
"x86_64" => Ok("x86_64-linux-android"),
"x86" => Ok("i686-linux-android"),
other => anyhow::bail!("unknown Android ABI: {other}"),
}
}
pub fn android_clang_for(abi: &str, api: u32) -> Result<PathBuf> {
let bin = ndk_bin_dir()?;
let prefix = clang_target_prefix(abi)?;
let clang = bin.join(format!("{prefix}{api}-clang"));
if !clang.exists() {
anyhow::bail!(
"NDK clang not found: {} — check that the NDK is installed and \
API level {api} is supported",
clang.display(),
);
}
Ok(clang)
}
pub fn ndk_bin_dir() -> Result<PathBuf> {
let ndk = ndk_home()?;
let host = host_tag()?;
Ok(ndk.join("toolchains/llvm/prebuilt").join(host).join("bin"))
}
pub fn abi_from_jni_libs_path(p: &Path) -> Option<&'static str> {
let parent = p.parent()?.file_name()?.to_str()?;
match parent {
"arm64-v8a" => Some("arm64-v8a"),
"armeabi-v7a" => Some("armeabi-v7a"),
"x86_64" => Some("x86_64"),
"x86" => Some("x86"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_tag_returns_a_known_string_for_this_host() {
let t = host_tag().expect("host tag");
assert!(matches!(
t,
"darwin-x86_64" | "linux-x86_64" | "windows-x86_64",
));
}
#[test]
fn clang_target_prefix_maps_known_abis() {
assert_eq!(
clang_target_prefix("arm64-v8a").unwrap(),
"aarch64-linux-android"
);
assert_eq!(
clang_target_prefix("armeabi-v7a").unwrap(),
"armv7a-linux-androideabi"
);
assert_eq!(
clang_target_prefix("x86_64").unwrap(),
"x86_64-linux-android"
);
assert_eq!(clang_target_prefix("x86").unwrap(), "i686-linux-android");
}
#[test]
fn clang_target_prefix_rejects_unknown_abi() {
let err = clang_target_prefix("riscv64").unwrap_err();
assert!(format!("{err:#}").contains("unknown Android ABI"));
}
#[test]
fn abi_from_jni_libs_path_maps_known_layouts() {
assert_eq!(
abi_from_jni_libs_path(Path::new(
"/ws/examples/foo/android/app/src/main/jniLibs/arm64-v8a/libfoo.so",
)),
Some("arm64-v8a"),
);
assert_eq!(
abi_from_jni_libs_path(Path::new("/ws/jniLibs/x86_64/libfoo.so")),
Some("x86_64"),
);
}
#[test]
fn abi_from_jni_libs_path_returns_none_for_non_abi_layout() {
assert_eq!(
abi_from_jni_libs_path(Path::new("/random/path/libfoo.so")),
None,
);
assert_eq!(
abi_from_jni_libs_path(Path::new("/ws/jniLibs/unknown-abi/libfoo.so")),
None,
);
}
#[test]
fn android_clang_returns_a_path_when_ndk_is_installed() {
let Ok(ndk) = ndk_home() else { return };
let clang = android_clang_for("arm64-v8a", 21).expect("ndk clang");
assert!(clang.starts_with(&ndk));
assert!(clang.is_file(), "{clang:?} should exist");
}
}