use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use super::Toolchain;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AndroidAbi {
Arm64V8a,
ArmeabiV7a,
X86_64,
}
impl AndroidAbi {
pub fn abi_string(&self) -> &str {
match self {
AndroidAbi::Arm64V8a => "arm64-v8a",
AndroidAbi::ArmeabiV7a => "armeabi-v7a",
AndroidAbi::X86_64 => "x86_64",
}
}
pub fn llvm_triple(&self) -> &str {
match self {
AndroidAbi::Arm64V8a => "aarch64-linux-android",
AndroidAbi::ArmeabiV7a => "armv7a-linux-androideabi",
AndroidAbi::X86_64 => "x86_64-linux-android",
}
}
pub fn stl_lib_dir(&self) -> &str {
match self {
AndroidAbi::Arm64V8a => "aarch64-linux-android",
AndroidAbi::ArmeabiV7a => "arm-linux-androideabi", AndroidAbi::X86_64 => "x86_64-linux-android",
}
}
pub fn all() -> Vec<AndroidAbi> {
vec![
AndroidAbi::Arm64V8a,
AndroidAbi::ArmeabiV7a,
AndroidAbi::X86_64,
]
}
pub fn from_str(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"arm64-v8a" | "v8" | "a64" | "arm64" | "armv8" | "aarch64" => {
Some(AndroidAbi::Arm64V8a)
}
"armeabi-v7a" | "v7" | "a32" | "arm32" | "armv7" | "aarch32" => {
Some(AndroidAbi::ArmeabiV7a)
}
"x86_64" | "x64" => Some(AndroidAbi::X86_64),
_ => None,
}
}
}
impl std::fmt::Display for AndroidAbi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.abi_string())
}
}
pub const MIN_API_LEVEL_64BIT: u32 = 21;
pub const DEFAULT_API_LEVEL: u32 = 24;
pub struct AndroidNdkToolchain {
ndk_path: PathBuf,
version: String,
revision: (u32, u32, u32),
}
impl AndroidNdkToolchain {
pub fn detect() -> Result<Self> {
let ndk_path = Self::find_ndk_path()?;
let (version, revision) = Self::parse_ndk_version(&ndk_path)?;
Ok(Self {
ndk_path,
version,
revision,
})
}
fn find_ndk_path() -> Result<PathBuf> {
if let Ok(path) = std::env::var("ANDROID_NDK_HOME") {
let path = PathBuf::from(path);
if Self::is_valid_ndk(&path) {
return Ok(path);
}
}
if let Ok(path) = std::env::var("ANDROID_NDK_ROOT") {
let path = PathBuf::from(path);
if Self::is_valid_ndk(&path) {
return Ok(path);
}
}
if let Ok(path) = std::env::var("ANDROID_NDK") {
let path = PathBuf::from(path);
if Self::is_valid_ndk(&path) {
return Ok(path);
}
}
if let Ok(android_home) = std::env::var("ANDROID_HOME") {
let path = PathBuf::from(android_home).join("ndk-bundle");
if Self::is_valid_ndk(&path) {
return Ok(path);
}
}
if let Ok(sdk_root) = std::env::var("ANDROID_SDK_ROOT") {
if let Some(ndk) = Self::find_ndk_in_sdk(&PathBuf::from(sdk_root)) {
return Ok(ndk);
}
}
if let Ok(android_home) = std::env::var("ANDROID_HOME") {
if let Some(ndk) = Self::find_ndk_in_sdk(&PathBuf::from(android_home)) {
return Ok(ndk);
}
}
let common_paths = ["/opt/android-ndk", "/usr/local/android-ndk"];
for path in common_paths {
let path = PathBuf::from(path);
if Self::is_valid_ndk(&path) {
return Ok(path);
}
}
bail!(
"Android NDK not found. Set one of these environment variables:\n\
- ANDROID_NDK_HOME (recommended)\n\
- ANDROID_NDK_ROOT\n\
- ANDROID_NDK\n\
Or install NDK via Android Studio SDK Manager."
)
}
fn find_ndk_in_sdk(sdk_path: &PathBuf) -> Option<PathBuf> {
let ndk_dir = sdk_path.join("ndk");
if !ndk_dir.exists() {
return None;
}
let mut versions: Vec<(PathBuf, (u32, u32, u32))> = Vec::new();
if let Ok(entries) = fs::read_dir(&ndk_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(ver) = Self::parse_version_from_dirname(&path) {
if Self::is_valid_ndk(&path) {
versions.push((path, ver));
}
}
}
}
}
versions.sort_by(|a, b| b.1.cmp(&a.1));
versions.first().map(|(p, _)| p.clone())
}
fn parse_version_from_dirname(path: &PathBuf) -> Option<(u32, u32, u32)> {
let name = path.file_name()?.to_str()?;
let parts: Vec<&str> = name.split('.').collect();
if parts.len() >= 2 {
let major = parts[0].parse().ok()?;
let minor = parts[1].parse().ok()?;
let patch = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
return Some((major, minor, patch));
}
None
}
fn is_valid_ndk(path: &PathBuf) -> bool {
if !path.exists() || !path.is_dir() {
return false;
}
let source_props = path.join("source.properties");
if !source_props.exists() {
return false;
}
let toolchain = path.join("build/cmake/android.toolchain.cmake");
toolchain.exists()
}
fn parse_ndk_version(ndk_path: &PathBuf) -> Result<(String, (u32, u32, u32))> {
let props_path = ndk_path.join("source.properties");
let content = fs::read_to_string(&props_path)
.with_context(|| format!("Failed to read {}", props_path.display()))?;
let props = Self::parse_properties(&content);
let version = props
.get("Pkg.Revision")
.cloned()
.unwrap_or_else(|| "unknown".to_string());
let revision = Self::parse_revision(&version);
Ok((version, revision))
}
fn parse_properties(content: &str) -> HashMap<String, String> {
let mut props = HashMap::new();
for line in content.lines() {
if let Some((key, value)) = line.split_once('=') {
props.insert(key.trim().to_string(), value.trim().to_string());
}
}
props
}
fn parse_revision(version: &str) -> (u32, u32, u32) {
let parts: Vec<&str> = version.split('.').collect();
let major = parts.first().and_then(|p| p.parse().ok()).unwrap_or(0);
let minor = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
(major, minor, patch)
}
pub fn cmake_toolchain_file(&self) -> PathBuf {
self.ndk_path.join("build/cmake/android.toolchain.cmake")
}
pub fn version(&self) -> &str {
&self.version
}
pub fn major_version(&self) -> u32 {
self.revision.0
}
pub fn cmake_variables_for_abi(
&self,
abi: AndroidAbi,
api_level: u32,
) -> Vec<(String, String)> {
let host_tag = Self::host_tag();
let toolchain_prefix = self
.ndk_path
.join("toolchains/llvm/prebuilt")
.join(host_tag);
let clang = toolchain_prefix.join("bin/clang");
let clangxx = toolchain_prefix.join("bin/clang++");
let mut vars = vec![
(
"ANDROID_NDK".to_string(),
self.ndk_path.display().to_string(),
),
(
"CMAKE_TOOLCHAIN_FILE".to_string(),
self.cmake_toolchain_file().display().to_string(),
),
("ANDROID_ABI".to_string(), abi.abi_string().to_string()),
(
"ANDROID_PLATFORM".to_string(),
format!("android-{}", api_level),
),
("ANDROID_STL".to_string(), "c++_shared".to_string()),
("CMAKE_C_COMPILER".to_string(), clang.display().to_string()),
(
"CMAKE_CXX_COMPILER".to_string(),
clangxx.display().to_string(),
),
];
if self.major_version() >= 23 {
vars.push((
"ANDROID_USE_LEGACY_TOOLCHAIN_FILE".to_string(),
"OFF".to_string(),
));
}
vars
}
pub fn toolchain_bin(&self, tool: &str) -> PathBuf {
let host_tag = Self::host_tag();
self.ndk_path
.join("toolchains/llvm/prebuilt")
.join(host_tag)
.join("bin")
.join(tool)
}
fn host_tag() -> &'static str {
#[cfg(target_os = "linux")]
return "linux-x86_64";
#[cfg(target_os = "macos")]
return "darwin-x86_64";
#[cfg(target_os = "windows")]
return "windows-x86_64";
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
return "linux-x86_64";
}
pub fn min_api_level(abi: AndroidAbi) -> u32 {
match abi {
AndroidAbi::Arm64V8a | AndroidAbi::X86_64 => MIN_API_LEVEL_64BIT,
AndroidAbi::ArmeabiV7a => 16,
}
}
pub fn validate_api_level(abi: AndroidAbi, api_level: u32) -> Result<()> {
let min = Self::min_api_level(abi);
if api_level < min {
bail!(
"API level {} is too low for {}: minimum is {}",
api_level,
abi,
min
);
}
Ok(())
}
pub fn llvm_strip_path(&self) -> PathBuf {
self.toolchain_bin("llvm-strip")
}
pub fn stl_library_path(&self, abi: AndroidAbi) -> PathBuf {
let host_tag = Self::host_tag();
self.ndk_path
.join("toolchains/llvm/prebuilt")
.join(host_tag)
.join("sysroot/usr/lib")
.join(abi.stl_lib_dir()) .join("libc++_shared.so")
}
pub fn strip_library(&self, library_path: &PathBuf, verbose: bool) -> Result<()> {
let strip_path = self.llvm_strip_path();
if !strip_path.exists() {
bail!("llvm-strip not found at: {}", strip_path.display());
}
if verbose {
eprintln!(" Stripping {}...", library_path.display());
}
let status = std::process::Command::new(&strip_path)
.arg("--strip-unneeded")
.arg(library_path)
.status()
.with_context(|| format!("Failed to run llvm-strip on {}", library_path.display()))?;
if !status.success() {
bail!("llvm-strip failed for {}", library_path.display());
}
Ok(())
}
pub fn copy_stl_library(&self, abi: AndroidAbi, dest_dir: &PathBuf) -> Result<PathBuf> {
let stl_path = self.stl_library_path(abi);
if !stl_path.exists() {
bail!(
"STL library not found at: {}\nMake sure your NDK installation is complete.",
stl_path.display()
);
}
std::fs::create_dir_all(dest_dir)?;
let dest_path = dest_dir.join("libc++_shared.so");
std::fs::copy(&stl_path, &dest_path).with_context(|| {
format!(
"Failed to copy {} to {}",
stl_path.display(),
dest_path.display()
)
})?;
Ok(dest_path)
}
}
impl Toolchain for AndroidNdkToolchain {
fn name(&self) -> &str {
"android-ndk"
}
fn is_available(&self) -> bool {
self.ndk_path.exists() && self.cmake_toolchain_file().exists()
}
fn path(&self) -> Option<PathBuf> {
Some(self.ndk_path.clone())
}
fn cmake_variables(&self) -> Vec<(String, String)> {
self.cmake_variables_for_abi(AndroidAbi::Arm64V8a, DEFAULT_API_LEVEL)
}
fn validate(&self) -> Result<()> {
if !self.ndk_path.exists() {
bail!(
"Android NDK path does not exist: {}",
self.ndk_path.display()
);
}
let toolchain = self.cmake_toolchain_file();
if !toolchain.exists() {
bail!("CMake toolchain file not found: {}", toolchain.display());
}
let clang = self.toolchain_bin("clang");
if !clang.exists() {
bail!("NDK clang not found: {}", clang.display());
}
if self.major_version() < 21 {
eprintln!(
"Warning: NDK version {} is old. Consider upgrading to NDK 25 or later.",
self.version
);
}
Ok(())
}
}
pub fn is_android_ndk_available() -> bool {
AndroidNdkToolchain::detect().is_ok()
}