//! Finds the Android SDK/NDK pieces the packaging pipeline needs, the same //! way `app/android-env.sh` and `app/build-apk.sh` do: `$ANDROID_HOME`, then //! `$ANDROID_SDK_ROOT`, then `~/Android/Sdk`. Kept in one place because //! every step in `main.rs` needs at least one of these paths, and a //! mismatch between them (an `android.jar` from one SDK, `d8` from //! another) fails in ways that point at the wrong cause. use std::path::{Path, PathBuf}; use crate::Fail; /// compileSdk / targetSdk, matching `app/shellApp/build.gradle.kts`. Not /// read from that file -- if the two drift, `android.jar` or a platform /// tools directory goes missing and the error below names the exact path /// that wasn't there, which is no harder to act on than a parsed number /// would have been. pub const COMPILE_SDK: u32 = 37; pub const MIN_SDK: u32 = 24; pub struct Sdk { pub root: PathBuf, pub build_tools: PathBuf, pub android_jar: PathBuf, } impl Sdk { pub fn tool(&self, name: &str) -> PathBuf { self.build_tools.join(name) } } pub fn find() -> Result { let root = std::env::var_os("ANDROID_HOME") .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) .map(PathBuf::from) .filter(|p| p.is_dir()) .or_else(|| { let home = std::env::var_os("HOME").map(PathBuf::from)?; let candidate = home.join("Android/Sdk"); candidate.is_dir().then_some(candidate) }) .ok_or_else(|| { Fail::new( "no Android SDK found", "checked $ANDROID_HOME, $ANDROID_SDK_ROOT and ~/Android/Sdk", "set ANDROID_HOME, or run app/android-env.sh once to install one", ) })?; let build_tools = latest_build_tools(&root)?; let android_jar = root .join("platforms") .join(format!("android-{COMPILE_SDK}.0")) .join("android.jar"); let android_jar = if android_jar.is_file() { android_jar } else { // Some installs use the bare "android-37" directory name instead of // "android-37.0" -- both exist on this machine's SDK depending on // how the platform was installed, so try the other spelling before // giving up. let alt = root .join("platforms") .join(format!("android-{COMPILE_SDK}")) .join("android.jar"); if alt.is_file() { alt } else { return Err(Fail::new( &format!("no android.jar for API {COMPILE_SDK}"), &format!("checked {} and {}", android_jar.display(), alt.display()), &format!("install it: android sdk install \"platforms/android-{COMPILE_SDK}.0\""), )); } }; Ok(Sdk { root, build_tools, android_jar, }) } fn latest_build_tools(sdk_root: &Path) -> Result { let dir = sdk_root.join("build-tools"); let mut versions: Vec<(Vec, PathBuf)> = std::fs::read_dir(&dir) .map_err(|e| { Fail::new( "no build-tools directory in the Android SDK", &format!("{}: {e}", dir.display()), "install one: android sdk install \"build-tools;37.0.0\"", ) })? .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().is_dir()) .filter_map(|entry| { let name = entry.file_name(); let name = name.to_str()?; let parts: Vec = name.split('.').filter_map(|p| p.parse().ok()).collect(); (!parts.is_empty()).then_some((parts, entry.path())) }) .collect(); versions.sort(); versions.pop().map(|(_, path)| path).ok_or_else(|| { Fail::new( "no usable build-tools version found", &format!("{} has no version-numbered subdirectory", dir.display()), "install one: android sdk install \"build-tools;37.0.0\"", ) }) } /// The NDK version `cargo ndk` should find on its own by scanning /// `$ANDROID_HOME/ndk/*` -- this just checks one exists, so a missing NDK /// is reported before `cargo ndk` does it with a less specific message. pub fn require_ndk_installed(sdk_root: &Path) -> Result<(), Fail> { let ndk_dir = sdk_root.join("ndk"); let has_one = std::fs::read_dir(&ndk_dir) .map(|entries| entries.filter_map(|e| e.ok()).any(|e| e.path().is_dir())) .unwrap_or(false); if has_one { Ok(()) } else { Err(Fail::new( "no NDK installed under the Android SDK", &format!("{} has no version subdirectory", ndk_dir.display()), "install one: android sdk install \"ndk;29.0.14206865\"", )) } }