use crate::error::NdkError;
use crate::target::Target;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const DEFAULT_DEV_KEYSTORE_PASSWORD: &str = "android";
fn is_tty() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}
fn infer_sdk_from_adb() -> Option<std::path::PathBuf> {
which::which(bin!("adb")).ok().and_then(|adb| {
adb.parent()
.and_then(|p| p.parent()) .map(|p| p.to_path_buf())
.filter(|p| p.join("platform-tools").exists())
})
}
fn default_sdk_locations() -> Vec<std::path::PathBuf> {
let mut candidates = Vec::new();
if let Some(home) = dirs::home_dir() {
#[cfg(target_os = "macos")]
candidates.push(home.join("Library/Android/sdk"));
#[cfg(target_os = "linux")]
candidates.push(home.join("Android/Sdk"));
#[cfg(target_os = "windows")]
{
if let Some(local) = std::env::var_os("LOCALAPPDATA") {
candidates.push(std::path::PathBuf::from(local).join("Android").join("Sdk"));
}
candidates.push(std::path::PathBuf::from(r"C:\Android\Sdk"));
}
}
candidates
}
fn validate_sdk_root(p: &std::path::Path) -> bool {
p.join("platforms").exists() && p.join("build-tools").exists()
}
fn parse_pkg_revision(s: &str) -> Option<(u32, u32, u32, bool)> {
let mut parts = s.trim().split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let rest = parts.next()?;
let (patch_str, beta) = rest
.split_once('-')
.map_or((rest, false), |(a, _)| (a, true));
let patch = patch_str.parse().ok()?;
Some((major, minor, patch, beta))
}
fn best_ndk_under(sdk: &std::path::Path) -> Option<std::path::PathBuf> {
let ndk_dir = sdk.join("ndk");
if !ndk_dir.exists() {
return None;
}
let mut best: Option<(std::path::PathBuf, (u32, u32, u32, bool))> = None;
for e in std::fs::read_dir(&ndk_dir).ok()?.flatten() {
let cand = e.path();
let props = cand.join("source.properties");
if !props.exists() {
continue;
}
if let Ok(text) = std::fs::read_to_string(&props) {
let revision_line = text
.lines()
.find(|line| line.starts_with("Pkg.Revision"))
.and_then(|line| line.split_once('='))
.map(|(_, v)| v.trim());
if let Some(v) = revision_line {
if let Some(rev) = parse_pkg_revision(v) {
let pick = match &best {
None => true,
Some((_, old)) => rev > *old,
};
if pick {
best = Some((cand.clone(), rev));
}
}
}
}
}
best.map(|(p, _)| p)
}
fn validate_ndk_root(p: &std::path::Path) -> bool {
p.join("source.properties").exists() && p.join("toolchains").join("llvm").exists()
}
fn prompt_path(label: &str) -> Option<std::path::PathBuf> {
use std::io::{self, Write};
eprint!("Enter path to {label} (or leave empty to abort): ");
io::stderr().flush().ok()?;
let mut line = String::new();
io::stdin().read_line(&mut line).ok()?;
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
Some(std::path::PathBuf::from(trimmed))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Ndk {
sdk_path: PathBuf,
user_home: PathBuf,
ndk_path: PathBuf,
build_tools_version: String,
build_tag: u32,
platforms: Vec<u32>,
}
impl Ndk {
pub fn from_env() -> Result<Self, NdkError> {
let sdk_path = {
let sdk_env = std::env::var("ANDROID_SDK_ROOT")
.ok()
.or_else(|| std::env::var("ANDROID_HOME").ok());
let mut sdk = sdk_env.map(std::path::PathBuf::from);
if sdk.is_none() {
sdk = infer_sdk_from_adb();
}
if sdk.is_none() {
for cand in default_sdk_locations() {
if validate_sdk_root(&cand) {
sdk = Some(cand);
break;
}
}
}
if sdk.is_none() && is_tty() {
eprintln!(
"Android SDK not found via ANDROID_HOME/ANDROID_SDK_ROOT, adb, or default locations."
);
if let Some(p) = prompt_path("Android SDK") {
if validate_sdk_root(&p) {
eprintln!("Using SDK at {}", p.display());
unsafe { std::env::set_var("ANDROID_HOME", &p) };
sdk = Some(p);
} else {
eprintln!(
"Given path does not look like an Android SDK (missing platforms/build-tools)."
);
}
}
}
sdk.ok_or(NdkError::SdkNotFound)?
};
if std::env::var_os("ANDROID_SDK_ROOT").is_some() {
eprintln!(
"Warning: Environment variable ANDROID_SDK_ROOT is deprecated \
(https://developer.android.com/studio/command-line/variables#envar). \
It will be used until it is unset and replaced by ANDROID_HOME."
);
}
let user_home = {
let user_home = std::env::var("ANDROID_SDK_HOME")
.map(std::path::PathBuf::from)
.map(|home| home.join(".android"))
.ok();
if user_home.is_some() {
eprintln!(
"Warning: Environment variable ANDROID_SDK_HOME is deprecated \
(https://developer.android.com/studio/command-line/variables#envar). \
It will be used until it is unset and replaced by ANDROID_USER_HOME."
);
}
user_home
.or_else(|| {
std::env::var("ANDROID_USER_HOME")
.map(std::path::PathBuf::from)
.ok()
})
.or_else(|| dirs::home_dir().map(|home| home.join(".android")))
.ok_or_else(|| NdkError::PathNotFound(std::path::PathBuf::from("$HOME")))?
};
let mut ndk_path = std::env::var("ANDROID_NDK_ROOT")
.ok()
.or_else(|| std::env::var("ANDROID_NDK_PATH").ok())
.or_else(|| std::env::var("ANDROID_NDK_HOME").ok())
.or_else(|| std::env::var("NDK_HOME").ok())
.map(std::path::PathBuf::from);
if ndk_path.is_none() {
if let Some(p) = best_ndk_under(&sdk_path) {
ndk_path = Some(p);
} else if sdk_path.join("ndk-bundle").exists() {
ndk_path = Some(sdk_path.join("ndk-bundle"));
}
}
if ndk_path.is_none() && is_tty() {
eprintln!(
"Android NDK not found via env or under {}/ndk / ndk-bundle.",
sdk_path.display()
);
if let Some(p) = prompt_path("Android NDK") {
if validate_ndk_root(&p) {
eprintln!("Using NDK at {}", p.display());
unsafe { std::env::set_var("ANDROID_NDK_ROOT", &p) };
ndk_path = Some(p);
} else {
eprintln!(
"Given path does not look like an Android NDK (missing toolchains/llvm, source.properties)."
);
}
}
}
let ndk_path = ndk_path.ok_or(NdkError::NdkNotFound)?;
let build_tools_dir = sdk_path.join("build-tools");
let build_tools_version = if let Ok(pinned) = std::env::var("ANDROID_BUILD_TOOLS") {
if build_tools_dir.join(&pinned).is_dir() {
pinned
} else {
return Err(NdkError::BuildToolsNotFound);
}
} else {
std::fs::read_dir(&build_tools_dir)
.or(Err(NdkError::PathNotFound(build_tools_dir)))?
.filter_map(|path| path.ok())
.filter(|path| path.path().is_dir())
.filter_map(|path| path.file_name().into_string().ok())
.filter(|name| name.chars().next().unwrap().is_ascii_digit())
.max()
.ok_or(NdkError::BuildToolsNotFound)?
};
let build_tag = std::fs::read_to_string(ndk_path.join("source.properties"))
.map_err(|e| NdkError::IoPathError(ndk_path.join("source.properties"), e))?;
let build_tag = build_tag
.lines()
.find_map(|line| {
let (key, value) = line.split_once('=')?;
if key.trim() == "Pkg.Revision" {
let mut parts = value.trim().split('.');
let _major = parts.next()?;
let _minor = parts.next()?;
let patch = parts.next()?;
let patch = patch.split_once('-').map_or(patch, |(patch, _beta)| patch);
patch.parse::<u32>().ok()
} else {
None
}
})
.ok_or(NdkError::InvalidSemver)?;
let ndk_platforms = std::fs::read_to_string(ndk_path.join("build/core/platforms.mk"))?;
let ndk_platforms: HashMap<_, _> = ndk_platforms
.lines()
.filter_map(|s| s.split_once(" := "))
.collect();
let min_platform_level = ndk_platforms
.get("NDK_MIN_PLATFORM_LEVEL")
.and_then(|s| s.parse().ok())
.ok_or(NdkError::InvalidSemver)?;
let max_platform_level = ndk_platforms
.get("NDK_MAX_PLATFORM_LEVEL")
.and_then(|s| s.parse().ok())
.ok_or(NdkError::InvalidSemver)?;
let platforms_dir = sdk_path.join("platforms");
let platforms: Vec<u32> = std::fs::read_dir(&platforms_dir)
.or(Err(NdkError::PathNotFound(platforms_dir)))?
.filter_map(|path| path.ok())
.filter(|path| path.path().is_dir())
.filter_map(|path| path.file_name().into_string().ok())
.filter_map(|name| {
name.strip_prefix("android-")
.and_then(|api| api.parse::<u32>().ok())
})
.filter(|level| *level >= min_platform_level)
.collect();
let excess: Vec<&u32> = platforms
.iter()
.filter(|l| **l > max_platform_level)
.collect();
if !excess.is_empty() {
eprintln!(
"warning: SDK has platforms ({}) newer than NDK max platform level ({})",
excess
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join(", "),
max_platform_level,
);
}
if platforms.is_empty() {
return Err(NdkError::NoPlatformFound);
}
Ok(Self {
sdk_path,
user_home,
ndk_path,
build_tools_version,
build_tag,
platforms,
})
}
pub fn sdk(&self) -> &Path {
&self.sdk_path
}
pub fn ndk(&self) -> &Path {
&self.ndk_path
}
pub fn build_tools_version(&self) -> &str {
&self.build_tools_version
}
pub fn build_tag(&self) -> u32 {
self.build_tag
}
pub fn platforms(&self) -> &[u32] {
&self.platforms
}
pub fn build_tool(&self, tool: &str) -> Result<Command, NdkError> {
let path = self
.sdk_path
.join("build-tools")
.join(&self.build_tools_version)
.join(tool);
if !path.exists() {
return Err(NdkError::CmdNotFound(tool.to_string()));
}
Ok(Command::new(dunce::canonicalize(path)?))
}
pub fn platform_tool_path(&self, tool: &str) -> Result<PathBuf, NdkError> {
let path = self.sdk_path.join("platform-tools").join(tool);
if !path.exists() {
return Err(NdkError::CmdNotFound(tool.to_string()));
}
Ok(dunce::canonicalize(path)?)
}
pub fn adb_path(&self) -> Result<PathBuf, NdkError> {
self.platform_tool_path(bin!("adb"))
}
pub fn platform_tool(&self, tool: &str) -> Result<Command, NdkError> {
Ok(Command::new(self.platform_tool_path(tool)?))
}
pub fn highest_supported_platform(&self) -> u32 {
self.platforms().iter().max().cloned().unwrap()
}
pub fn default_target_platform(&self) -> u32 {
self.highest_supported_platform().min(36)
}
pub fn platform_dir(&self, platform: u32) -> Result<PathBuf, NdkError> {
let dir = self
.sdk_path
.join("platforms")
.join(format!("android-{platform}"));
if !dir.exists() {
return Err(NdkError::PlatformNotFound(platform));
}
Ok(dir)
}
pub fn android_jar(&self, platform: u32) -> Result<PathBuf, NdkError> {
let android_jar = self.platform_dir(platform)?.join("android.jar");
if !android_jar.exists() {
return Err(NdkError::PathNotFound(android_jar));
}
Ok(android_jar)
}
fn host_arch() -> Result<&'static str, NdkError> {
let host_os = std::env::var("HOST").ok();
let host_contains = |s| host_os.as_ref().map(|h| h.contains(s)).unwrap_or(false);
Ok(if host_contains("linux") {
"linux"
} else if host_contains("macos") {
"darwin"
} else if host_contains("windows") {
"windows"
} else if host_contains("android") {
"android"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "android") {
"android"
} else {
return match host_os {
Some(host_os) => Err(NdkError::UnsupportedHost(host_os)),
_ => Err(NdkError::UnsupportedTarget),
};
})
}
pub fn toolchain_dir(&self) -> Result<PathBuf, NdkError> {
let arch = Self::host_arch()?;
let mut toolchain_dir = self
.ndk_path
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(format!("{arch}-x86_64"));
if !toolchain_dir.exists() {
toolchain_dir.set_file_name(arch);
}
if !toolchain_dir.exists() {
return Err(NdkError::PathNotFound(toolchain_dir));
}
Ok(toolchain_dir)
}
pub fn clang(&self) -> Result<(PathBuf, PathBuf), NdkError> {
let ext = if cfg!(target_os = "windows") {
"exe"
} else {
""
};
let bin_path = self.toolchain_dir()?.join("bin");
let clang = bin_path.join("clang").with_extension(ext);
if !clang.exists() {
return Err(NdkError::PathNotFound(clang));
}
let clang_pp = bin_path.join("clang++").with_extension(ext);
if !clang_pp.exists() {
return Err(NdkError::PathNotFound(clang_pp));
}
Ok((clang, clang_pp))
}
pub fn toolchain_bin(&self, name: &str, target: Target) -> Result<PathBuf, NdkError> {
let ext = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let toolchain_path = self.toolchain_dir()?.join("bin");
let gnu_bin = format!("{}-{}{}", target.ndk_triple(), name, ext);
let gnu_path = toolchain_path.join(&gnu_bin);
if gnu_path.exists() {
Ok(gnu_path)
} else {
let llvm_bin = format!("llvm-{name}{ext}");
let llvm_path = toolchain_path.join(&llvm_bin);
if llvm_path.exists() {
Ok(llvm_path)
} else {
Err(NdkError::ToolchainBinaryNotFound {
toolchain_path,
gnu_bin,
llvm_bin,
})
}
}
}
pub fn prebuilt_dir(&self) -> Result<PathBuf, NdkError> {
let arch = Self::host_arch()?;
let prebuilt_dir = self
.ndk_path
.join("prebuilt")
.join(format!("{arch}-x86_64"));
if !prebuilt_dir.exists() {
Err(NdkError::PathNotFound(prebuilt_dir))
} else {
Ok(prebuilt_dir)
}
}
pub fn ndk_gdb(
&self,
launch_dir: impl AsRef<Path>,
launch_activity: &str,
device_serial: Option<&str>,
) -> Result<(), NdkError> {
let abi = self.detect_abi(device_serial)?;
let jni_dir = launch_dir.as_ref().join("jni");
std::fs::create_dir_all(&jni_dir)?;
std::fs::write(
jni_dir.join("Android.mk"),
format!("APP_ABI={}\nTARGET_OUT=\n", abi.android_abi()),
)?;
let mut ndk_gdb = Command::new(self.prebuilt_dir()?.join("bin").join(cmd!("ndk-gdb")));
if let Some(device_serial) = &device_serial {
ndk_gdb.arg("-s").arg(device_serial);
}
ndk_gdb
.arg("--adb")
.arg(self.adb_path()?)
.arg("--launch")
.arg(launch_activity)
.current_dir(launch_dir)
.status()?;
Ok(())
}
pub fn android_user_home(&self) -> Result<PathBuf, NdkError> {
let android_user_home = self.user_home.clone();
std::fs::create_dir_all(&android_user_home)?;
Ok(android_user_home)
}
pub fn keytool(&self) -> Result<Command, NdkError> {
self.java_tool("keytool")
}
pub fn javac(&self) -> Result<Command, NdkError> {
self.java_tool("javac")
}
pub fn d8(&self) -> Result<Command, NdkError> {
if let Ok(jar) = std::env::var("ANDROID_D8_JAR") {
let jar_path = PathBuf::from(&jar);
if jar_path.is_file() {
let java = which::which("java").or_else(|_| -> Result<PathBuf, ()> {
let java_home = std::env::var("JAVA_HOME").map_err(|_| ())?;
let candidate = PathBuf::from(java_home).join("bin").join("java");
if cfg!(target_os = "windows") {
let candidate_exe = candidate.with_extension("exe");
if candidate_exe.is_file() {
return Ok(candidate_exe);
}
}
if candidate.is_file() {
return Ok(candidate);
}
Err(())
});
if let Ok(java) = java {
let mut cmd = Command::new(java);
cmd.arg("-cp").arg(&jar_path).arg("com.android.tools.r8.D8");
return Ok(cmd);
}
return Err(NdkError::CmdNotFound("java".to_string()));
}
}
self.build_tool(bat!("d8"))
}
fn java_tool(&self, tool: &str) -> Result<Command, NdkError> {
let tool_bin = if cfg!(target_os = "windows") {
format!("{tool}.exe")
} else {
tool.to_string()
};
if let Ok(path) = which::which(&tool_bin) {
return Ok(Command::new(path));
}
if let Ok(java_home) = std::env::var("JAVA_HOME") {
let candidate = PathBuf::from(java_home).join("bin").join(&tool_bin);
if candidate.exists() {
return Ok(Command::new(candidate));
}
}
Err(NdkError::CmdNotFound(tool.to_string()))
}
pub fn debug_key(&self) -> Result<Key, NdkError> {
let path = self.android_user_home()?.join("debug.keystore");
let password = DEFAULT_DEV_KEYSTORE_PASSWORD.to_owned();
if !path.exists() {
let mut keytool = self.keytool()?;
keytool
.arg("-genkey")
.arg("-v")
.arg("-keystore")
.arg(&path)
.arg("-storepass")
.arg(&password)
.arg("-alias")
.arg("androiddebugkey")
.arg("-keypass")
.arg(&password)
.arg("-dname")
.arg("CN=Android Debug,O=Android,C=US")
.arg("-keyalg")
.arg("RSA")
.arg("-keysize")
.arg("2048")
.arg("-validity")
.arg("10000");
if !keytool.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(keytool)));
}
}
Ok(Key { path, password })
}
pub fn sysroot_lib_dir(&self, target: Target) -> Result<PathBuf, NdkError> {
let sysroot_lib_dir = self
.toolchain_dir()?
.join("sysroot")
.join("usr")
.join("lib")
.join(target.ndk_triple());
if !sysroot_lib_dir.exists() {
return Err(NdkError::PathNotFound(sysroot_lib_dir));
}
Ok(sysroot_lib_dir)
}
pub fn sysroot_platform_lib_dir(
&self,
target: Target,
min_sdk_version: u32,
) -> Result<PathBuf, NdkError> {
let sysroot_lib_dir = self.sysroot_lib_dir(target)?;
let mut tmp_platform = min_sdk_version;
while tmp_platform > 1 {
let path = sysroot_lib_dir.join(tmp_platform.to_string());
if path.exists() {
return Ok(path);
}
tmp_platform += 1;
}
let mut tmp_platform = min_sdk_version;
while tmp_platform < 100 {
let path = sysroot_lib_dir.join(tmp_platform.to_string());
if path.exists() {
return Ok(path);
}
tmp_platform += 1;
}
Err(NdkError::PlatformNotFound(min_sdk_version))
}
pub fn detect_abi(&self, device_serial: Option<&str>) -> Result<Target, NdkError> {
let mut adb = self.adb(device_serial)?;
let stdout = adb
.arg("shell")
.arg("getprop")
.arg("ro.product.cpu.abi")
.output()?
.stdout;
let abi = std::str::from_utf8(&stdout).or(Err(NdkError::UnsupportedTarget))?;
Target::from_android_abi(abi.trim())
}
pub fn adb(&self, device_serial: Option<&str>) -> Result<Command, NdkError> {
let mut adb = Command::new(self.adb_path()?);
if let Some(device_serial) = device_serial {
adb.arg("-s").arg(device_serial);
}
Ok(adb)
}
}
pub struct Key {
pub path: PathBuf,
pub password: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn test_detect() {
let ndk = Ndk::from_env().unwrap();
assert_eq!(ndk.build_tools_version(), "29.0.2");
assert_eq!(ndk.platforms(), &[29, 28]);
}
}