android_build/env_paths/
mod.rs

1use std::{env, path::{Path, PathBuf}};
2use self::find_java::find_java_home;
3
4mod find_android_sdk;
5mod find_java;
6
7
8pub const ANDROID_HOME:                 &str = "ANDROID_HOME";
9pub const ANDROID_SDK_ROOT:             &str = "ANDROID_SDK_ROOT";
10pub const ANDROID_BUILD_TOOLS_VERSION:  &str = "ANDROID_BUILD_TOOLS_VERSION";
11pub const ANDROID_PLATFORM:             &str = "ANDROID_PLATFORM";
12pub const ANDROID_SDK_VERSION:          &str = "ANDROID_SDK_VERSION";
13pub const ANDROID_API_LEVEL:            &str = "ANDROID_API_LEVEL";
14pub const ANDROID_SDK_EXTENSION:        &str = "ANDROID_SDK_EXTENSION";
15pub const ANDROID_D8_JAR:               &str = "ANDROID_D8_JAR";
16pub const ANDROID_JAR:                  &str = "ANDROID_JAR";
17pub const JAVA_HOME:                    &str = "JAVA_HOME";
18pub const JAVA_SOURCE_VERSION:          &str = "JAVA_SOURCE_VERSION";
19pub const JAVA_TARGET_VERSION:          &str = "JAVA_TARGET_VERSION";
20
21/// An extension trait for checking if a path exists.
22pub trait PathExt {
23    fn path_if_exists(self) -> Option<Self> where Self: Sized;
24}
25impl<P: AsRef<Path>> PathExt for P {
26    fn path_if_exists(self) -> Option<P> {
27        if self.as_ref().as_os_str().is_empty() {
28            return None;
29        }
30        match self.as_ref().try_exists() {
31            Ok(true) => Some(self),
32            _ => None,
33        }
34    }
35}
36
37
38/// Returns the path to the Android SDK directory.
39///
40/// The path is determined by an ordered set of attempts:
41/// * The `ANDROID_HOME` environment variable, if it is set and if the directory exists.
42/// * The `ANDROID_SDK_HOME` environment variable, if it is set and if the directory exists.
43/// * The default installation location for the Android SDK, if it exists.
44///   * On Windows, this is `%LOCALAPPDATA%\Android\Sdk`.
45///   * On macOS, this is `~/Library/Android/sdk`.
46///   * On Linux, this is `~/Android/Sdk`.
47#[doc(alias("ANDROID_HOME", "ANDROID_SDK_ROOT", "home", "sdk", "root"))]
48pub fn android_sdk() -> Option<PathBuf> {
49    env_var(ANDROID_HOME).ok()
50        .and_then(PathExt::path_if_exists)
51        .or_else(|| env_var(ANDROID_SDK_ROOT).ok()
52            .and_then(PathExt::path_if_exists)
53        )
54        .map(PathBuf::from)
55        .or_else(|| find_android_sdk::find_android_sdk().and_then(PathExt::path_if_exists))
56}
57
58/// Returns the path to the `android.jar` file for the given API level.
59///
60/// The path is determined by an ordered set of attempts:
61/// * The `ANDROID_JAR` environment variable, if it is set and points to a file that exists.
62/// * The argument `platform_string` is used if it is `Some`, to find the subdirectory for
63///   the specific platform version under the Android SDK `platforms` directory, in which
64///   the `android.jar` file should exist.
65/// * The value of the following environment variables are used to calculate the platform string:
66///   * `ANDROID_PLATFORM`, `ANDROID_API_LEVEL` or `ANDROID_SDK_VERSION`
67///   * `ANDROID_SDK_EXTENSION` (optional)
68/// * The highest Android platform version found in the SDK `platforms` directory is used if
69///   the platform version is not set by environment variables.
70pub fn android_jar(platform_string: Option<&str>) -> Option<PathBuf> {
71    env_var(ANDROID_JAR).ok()
72        .and_then(PathExt::path_if_exists)
73        .map(PathBuf::from)
74        .or_else(|| android_sdk()
75            .and_then(|sdk| {
76                let platforms = sdk.join("platforms");
77                platform_string.map(ToString::to_string)
78                    .or_else(env_android_platform_api_level)
79                    .or_else(|| {
80                        let latest = find_latest_version(&platforms, "android.jar");
81                        #[cfg(feature = "cargo")]
82                        if let Some(ver) = latest.as_ref() {
83                            println!("cargo::warning=ANDROID_PLATFORM environment variable \
84                                is not set, using '{ver}'.");
85                        }
86                        latest
87                    })
88                    .map(|version| platforms.join(version))
89            })
90            .and_then(|path| path.join("android.jar").path_if_exists())
91        )
92}
93
94/// Returns the path to the `d8.jar` file for the given build tools version.
95///
96/// The path is determined by an ordered set of attempts:
97/// * The `ANDROID_D8_JAR` environment variable, if it is set and points to a file that exists.
98/// * The argument `build_tools_version` is used if it is `Some`, to find the subdirectory for
99///   the specific build tools version under the Android SDK `build-tools` directory.
100/// * The `ANDROID_BUILD_TOOLS_VERSION` environment variable is used to find the subdirectory for
101///   the build tools version under the Android SDK `build-tools` directory.
102/// * The highest Android build tools version found in the SDK `build-tools` directory is used if
103///   the build tools version is not set by the environment variable.
104pub fn android_d8_jar(build_tools_version: Option<&str>) -> Option<PathBuf> {
105    env_var(ANDROID_D8_JAR).ok()
106        .and_then(PathExt::path_if_exists)
107        .map(PathBuf::from)
108        .or_else(|| android_sdk()
109            .and_then(|sdk| {
110                let build_tools = sdk.join("build-tools");
111                build_tools_version.map(ToString::to_string)
112                    .or_else(|| env_var(ANDROID_BUILD_TOOLS_VERSION).ok())
113                    .or_else(|| {
114                        let latest = find_latest_version(&build_tools, Path::new("lib").join("d8.jar"));
115                        #[cfg(feature = "cargo")]
116                        if let Some(ver) = latest.as_ref() {
117                            println!("cargo::warning=ANDROID_BUILD_TOOLS_VERSION environment variable \
118                                is not set, using '{ver}'.");
119                        }
120                        latest
121                    })
122                    .map(|version| build_tools.join(version))
123            })
124            .and_then(|path| path.join("lib").join("d8.jar").path_if_exists())
125        )
126}
127
128/// Returns the platform version string (aka API level, SDK version) being targeted for compilation.
129/// This deals with environment variables `ANDROID_PLATFORM`, `ANDROID_API_LEVEL`, and `ANDROID_SDK_VERSION`,
130/// as well as the optional `ANDROID_SDK_EXTENSION`.
131fn env_android_platform_api_level() -> Option<String> {
132    let mut base = env_var(ANDROID_PLATFORM).ok()
133        .or_else(|| env_var(ANDROID_API_LEVEL).ok())
134        .or_else(|| env_var(ANDROID_SDK_VERSION).ok())?;
135    
136    if base.is_empty() {
137        return None;
138    }
139
140    if !base.starts_with("android-") {
141        base = format!("android-{}", base);
142    }
143
144    if base.contains("-ext") {
145        return Some(base);
146    }
147
148    if let Ok(raw_ext) = env_var(ANDROID_SDK_EXTENSION) {
149        let ext_num = raw_ext
150            .trim_start_matches("-")
151            .trim_start_matches("ext");
152        if !ext_num.is_empty() {
153            base = format!("{}-ext{}", base, ext_num);
154        }
155    }
156    
157    Some(base)
158}
159
160/// Finds subdirectories in which the subpath `arg` exists, and returns the maximum
161/// item name in lexicographical order based on `Ord` impl of `std::path::Path`.
162/// NOTE: the behavior can be changed in the future.
163/// 
164/// Code inspired by <https://docs.rs/crate/i-slint-backend-android-activity/1.9.1/source/build.rs>.
165fn find_latest_version(base: impl AsRef<Path>, arg: impl AsRef<Path>) -> Option<String> {
166    std::fs::read_dir(base)
167        .ok()?
168        .filter_map(|entry| entry.ok())
169        .filter(|entry| entry.path().join(arg.as_ref()).exists())
170        .map(|entry| entry.file_name())
171        .max()
172        .and_then(|name| name.to_os_string().into_string().ok())
173}
174
175/// Returns the path to the `java` executable by looking for `$JAVA_HOME/bin/java`.
176pub fn java() -> Option<PathBuf> {
177    java_home().and_then(|jh| jh
178        .join("bin")
179        .join("java")
180        .path_if_exists()
181    )
182}
183
184/// Returns the path to the `javac` compiler by looking for `$JAVA_HOME/bin/javac`.
185pub fn javac() -> Option<PathBuf> {
186    java_home().and_then(|jh| jh
187        .join("bin")
188        .join("javac")
189        .path_if_exists()
190    )
191}
192
193/// Returns the `JAVA_HOME` path by attempting to discover it.
194/// 
195/// First, if the `$JAVA_HOME` environment variable is set and points to a directory that exists,
196/// that path is returned.
197/// Otherwise, a series of common installation locations is used,
198/// based on the current platform (macOS, Linux, Windows).
199pub fn java_home() -> Option<PathBuf> {
200    env_var(JAVA_HOME).ok()
201        .and_then(PathExt::path_if_exists)
202        .map(PathBuf::from)
203        .or_else(find_java_home)
204}
205
206/// Returns the source version for compilation from environment variable `JAVA_SOURCE_VERSION`.
207pub fn java_source_version() -> Option<u32> {
208    env_var(JAVA_SOURCE_VERSION).ok()?.parse().ok()
209}
210
211/// Returns the target version for compilation from environment variable `JAVA_TARGET_VERSION`.
212pub fn java_target_version() -> Option<u32> {
213    env_var(JAVA_TARGET_VERSION).ok()?.parse().ok()
214}
215
216/// Returns the major version number of the `javac` compiler.
217pub fn check_javac_version(java_home: impl AsRef<Path>) -> std::io::Result<u32> {
218    let javac = java_home.as_ref().join("bin").join("javac");
219    let output = std::process::Command::new(&javac)
220        .arg("-version")
221        .output()
222        .map_err(|e| std::io::Error::other(
223            format!("Failed to execute javac -version: {:?}", e)
224        ))?;
225    if !output.status.success() {
226        return Err(std::io::Error::other(format!(
227            "Failed to get javac version: {}",
228            String::from_utf8_lossy(&output.stderr)
229        )));
230    }
231    let mut version_output = String::from_utf8_lossy(&output.stdout);
232    if version_output.is_empty() {
233        // old versions of java use stderr
234        version_output = String::from_utf8_lossy(&output.stderr);
235    }
236    let version = parse_javac_version_output(&version_output);
237    if version > 0 {
238        Ok(version as u32)
239    } else {
240        Err(std::io::Error::other(
241            format!("Failed to parse javac version: '{version_output}'")
242        ))
243    }
244}
245
246/// Copied from <https://docs.rs/crate/i-slint-backend-android-activity/1.9.1/source/build.rs>.
247fn parse_javac_version_output(version_output: &str) -> i32 {
248    let version = version_output
249        .split_whitespace()
250        .nth(1)
251        .and_then(|v| v.split('-').next())
252        .unwrap_or_default();
253    let mut java_ver: i32 = version.split('.').next().unwrap_or("0").parse().unwrap_or(0);
254    if java_ver == 1 {
255        // Before java 9, the version was something like javac 1.8
256        java_ver = version.split('.').nth(1).unwrap_or("0").parse().unwrap_or(0);
257    }
258    java_ver
259}
260
261#[test]
262fn test_parse_javac_version() {
263    assert_eq!(parse_javac_version_output("javac 1.8.0_292"), 8);
264    assert_eq!(parse_javac_version_output("javac 17.0.13"), 17);
265    assert_eq!(parse_javac_version_output("javac 21.0.5"), 21);
266    assert_eq!(parse_javac_version_output("javac 24-ea"), 24);
267    assert_eq!(parse_javac_version_output("error"), 0);
268    assert_eq!(parse_javac_version_output("javac error"), 0);
269}
270
271/// Rerun the build script if the variable is changed. Do not use it for variables set by Cargo.
272fn env_var(var: &str) -> Result<String, env::VarError> {
273    #[cfg(feature = "cargo")]
274    println!("cargo:rerun-if-env-changed={}", var);
275    env::var(var)
276}