use std::{env, path::{Path, PathBuf}};
use self::find_java::find_java_home;
mod find_android_sdk;
mod find_java;
pub const ANDROID_HOME: &str = "ANDROID_HOME";
pub const ANDROID_SDK_ROOT: &str = "ANDROID_SDK_ROOT";
pub const ANDROID_BUILD_TOOLS_VERSION: &str = "ANDROID_BUILD_TOOLS_VERSION";
pub const ANDROID_PLATFORM: &str = "ANDROID_PLATFORM";
pub const ANDROID_SDK_VERSION: &str = "ANDROID_SDK_VERSION";
pub const ANDROID_API_LEVEL: &str = "ANDROID_API_LEVEL";
pub const ANDROID_SDK_EXTENSION: &str = "ANDROID_SDK_EXTENSION";
pub const ANDROID_D8_JAR: &str = "ANDROID_D8_JAR";
pub const ANDROID_JAR: &str = "ANDROID_JAR";
pub const JAVA_HOME: &str = "JAVA_HOME";
pub const JAVA_SOURCE_VERSION: &str = "JAVA_SOURCE_VERSION";
pub const JAVA_TARGET_VERSION: &str = "JAVA_TARGET_VERSION";
pub trait PathExt {
fn path_if_exists(self) -> Option<Self> where Self: Sized;
}
impl<P: AsRef<Path>> PathExt for P {
fn path_if_exists(self) -> Option<P> {
if self.as_ref().as_os_str().is_empty() {
return None;
}
match self.as_ref().try_exists() {
Ok(true) => Some(self),
_ => None,
}
}
}
#[doc(alias("ANDROID_HOME", "ANDROID_SDK_ROOT", "home", "sdk", "root"))]
pub fn android_sdk() -> Option<PathBuf> {
env_var(ANDROID_HOME).ok()
.and_then(PathExt::path_if_exists)
.or_else(|| env_var(ANDROID_SDK_ROOT).ok()
.and_then(PathExt::path_if_exists)
)
.map(PathBuf::from)
.or_else(|| find_android_sdk::find_android_sdk().and_then(PathExt::path_if_exists))
}
pub fn android_jar(platform_string: Option<&str>) -> Option<PathBuf> {
env_var(ANDROID_JAR).ok()
.and_then(PathExt::path_if_exists)
.map(PathBuf::from)
.or_else(|| android_sdk()
.and_then(|sdk| {
let platforms = sdk.join("platforms");
platform_string.map(ToString::to_string)
.or_else(env_android_platform_api_level)
.or_else(|| {
let latest = find_latest_version(&platforms, "android.jar");
#[cfg(feature = "cargo")]
if let Some(ver) = latest.as_ref() {
println!("cargo::warning=ANDROID_PLATFORM environment variable \
is not set, using '{ver}'.");
}
latest
})
.map(|version| platforms.join(version))
})
.and_then(|path| path.join("android.jar").path_if_exists())
)
}
pub fn android_d8_jar(build_tools_version: Option<&str>) -> Option<PathBuf> {
env_var(ANDROID_D8_JAR).ok()
.and_then(PathExt::path_if_exists)
.map(PathBuf::from)
.or_else(|| android_sdk()
.and_then(|sdk| {
let build_tools = sdk.join("build-tools");
build_tools_version.map(ToString::to_string)
.or_else(|| env_var(ANDROID_BUILD_TOOLS_VERSION).ok())
.or_else(|| {
let latest = find_latest_version(&build_tools, Path::new("lib").join("d8.jar"));
#[cfg(feature = "cargo")]
if let Some(ver) = latest.as_ref() {
println!("cargo::warning=ANDROID_BUILD_TOOLS_VERSION environment variable \
is not set, using '{ver}'.");
}
latest
})
.map(|version| build_tools.join(version))
})
.and_then(|path| path.join("lib").join("d8.jar").path_if_exists())
)
}
fn env_android_platform_api_level() -> Option<String> {
let mut base = env_var(ANDROID_PLATFORM).ok()
.or_else(|| env_var(ANDROID_API_LEVEL).ok())
.or_else(|| env_var(ANDROID_SDK_VERSION).ok())?;
if base.is_empty() {
return None;
}
if !base.starts_with("android-") {
base = format!("android-{}", base);
}
if base.contains("-ext") {
return Some(base);
}
if let Ok(raw_ext) = env_var(ANDROID_SDK_EXTENSION) {
let ext_num = raw_ext
.trim_start_matches("-")
.trim_start_matches("ext");
if !ext_num.is_empty() {
base = format!("{}-ext{}", base, ext_num);
}
}
Some(base)
}
fn find_latest_version(base: impl AsRef<Path>, arg: impl AsRef<Path>) -> Option<String> {
std::fs::read_dir(base)
.ok()?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().join(arg.as_ref()).exists())
.map(|entry| entry.file_name())
.max()
.and_then(|name| name.to_os_string().into_string().ok())
}
pub fn java() -> Option<PathBuf> {
java_home().and_then(|jh| jh
.join("bin")
.join("java")
.path_if_exists()
)
}
pub fn javac() -> Option<PathBuf> {
java_home().and_then(|jh| jh
.join("bin")
.join("javac")
.path_if_exists()
)
}
pub fn java_home() -> Option<PathBuf> {
env_var(JAVA_HOME).ok()
.and_then(PathExt::path_if_exists)
.map(PathBuf::from)
.or_else(find_java_home)
}
pub fn java_source_version() -> Option<u32> {
env_var(JAVA_SOURCE_VERSION).ok()?.parse().ok()
}
pub fn java_target_version() -> Option<u32> {
env_var(JAVA_TARGET_VERSION).ok()?.parse().ok()
}
pub fn check_javac_version(java_home: impl AsRef<Path>) -> std::io::Result<u32> {
let javac = java_home.as_ref().join("bin").join("javac");
let output = std::process::Command::new(&javac)
.arg("-version")
.output()
.map_err(|e| std::io::Error::other(
format!("Failed to execute javac -version: {:?}", e)
))?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"Failed to get javac version: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let mut version_output = String::from_utf8_lossy(&output.stdout);
if version_output.is_empty() {
version_output = String::from_utf8_lossy(&output.stderr);
}
let version = parse_javac_version_output(&version_output);
if version > 0 {
Ok(version as u32)
} else {
Err(std::io::Error::other(
format!("Failed to parse javac version: '{version_output}'")
))
}
}
fn parse_javac_version_output(version_output: &str) -> i32 {
let version = version_output
.split_whitespace()
.nth(1)
.and_then(|v| v.split('-').next())
.unwrap_or_default();
let mut java_ver: i32 = version.split('.').next().unwrap_or("0").parse().unwrap_or(0);
if java_ver == 1 {
java_ver = version.split('.').nth(1).unwrap_or("0").parse().unwrap_or(0);
}
java_ver
}
#[test]
fn test_parse_javac_version() {
assert_eq!(parse_javac_version_output("javac 1.8.0_292"), 8);
assert_eq!(parse_javac_version_output("javac 17.0.13"), 17);
assert_eq!(parse_javac_version_output("javac 21.0.5"), 21);
assert_eq!(parse_javac_version_output("javac 24-ea"), 24);
assert_eq!(parse_javac_version_output("error"), 0);
assert_eq!(parse_javac_version_output("javac error"), 0);
}
fn env_var(var: &str) -> Result<String, env::VarError> {
#[cfg(feature = "cargo")]
println!("cargo:rerun-if-env-changed={}", var);
env::var(var)
}