ndk_build/
ndk.rs

1use crate::error::NdkError;
2use crate::target::Target;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7/// The default password used when creating the default `debug.keystore` via
8/// [`Ndk::debug_key`]
9pub const DEFAULT_DEV_KEYSTORE_PASSWORD: &str = "android";
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Ndk {
13    sdk_path: PathBuf,
14    user_home: PathBuf,
15    ndk_path: PathBuf,
16    build_tools_version: String,
17    build_tag: u32,
18    platforms: Vec<u32>,
19}
20
21impl Ndk {
22    pub fn from_env() -> Result<Self, NdkError> {
23        let sdk_path = {
24            let sdk_path = std::env::var("ANDROID_SDK_ROOT").ok();
25            if sdk_path.is_some() {
26                eprintln!(
27                    "Warning: Environment variable ANDROID_SDK_ROOT is deprecated \
28                    (https://developer.android.com/studio/command-line/variables#envar). \
29                    It will be used until it is unset and replaced by ANDROID_HOME."
30                );
31            }
32
33            PathBuf::from(
34                sdk_path
35                    .or_else(|| std::env::var("ANDROID_HOME").ok())
36                    .ok_or(NdkError::SdkNotFound)?,
37            )
38        };
39
40        let user_home = {
41            let user_home = std::env::var("ANDROID_SDK_HOME")
42                .map(PathBuf::from)
43                // Unlike ANDROID_USER_HOME, ANDROID_SDK_HOME points to the _parent_ directory of .android:
44                // https://developer.android.com/studio/command-line/variables#envar
45                .map(|home| home.join(".android"))
46                .ok();
47
48            if user_home.is_some() {
49                eprintln!(
50                    "Warning: Environment variable ANDROID_SDK_HOME is deprecated \
51                    (https://developer.android.com/studio/command-line/variables#envar). \
52                    It will be used until it is unset and replaced by ANDROID_USER_HOME."
53                );
54            }
55
56            // Default to $HOME/.android
57            user_home
58                .or_else(|| std::env::var("ANDROID_USER_HOME").map(PathBuf::from).ok())
59                .or_else(|| dirs::home_dir().map(|home| home.join(".android")))
60                .ok_or_else(|| NdkError::PathNotFound(PathBuf::from("$HOME")))?
61        };
62
63        let ndk_path = {
64            let ndk_path = std::env::var("ANDROID_NDK_ROOT")
65                .ok()
66                .or_else(|| std::env::var("ANDROID_NDK_PATH").ok())
67                .or_else(|| std::env::var("ANDROID_NDK_HOME").ok())
68                .or_else(|| std::env::var("NDK_HOME").ok());
69
70            // default ndk installation path
71            if ndk_path.is_none() && sdk_path.join("ndk-bundle").exists() {
72                sdk_path.join("ndk-bundle")
73            } else {
74                PathBuf::from(ndk_path.ok_or(NdkError::NdkNotFound)?)
75            }
76        };
77
78        let build_tools_dir = sdk_path.join("build-tools");
79        let build_tools_version = std::fs::read_dir(&build_tools_dir)
80            .or(Err(NdkError::PathNotFound(build_tools_dir)))?
81            .filter_map(|path| path.ok())
82            .filter(|path| path.path().is_dir())
83            .filter_map(|path| path.file_name().into_string().ok())
84            .filter(|name| name.chars().next().unwrap().is_ascii_digit())
85            .max()
86            .ok_or(NdkError::BuildToolsNotFound)?;
87
88        let build_tag = std::fs::read_to_string(ndk_path.join("source.properties"))
89            .expect("Failed to read source.properties");
90
91        let build_tag = build_tag
92            .split('\n')
93            .find_map(|line| {
94                let (key, value) = line
95                    .split_once('=')
96                    .expect("Failed to parse `key = value` from source.properties");
97                if key.trim() == "Pkg.Revision" {
98                    // AOSP writes a constantly-incrementing build version to the patch field.
99                    // This number is incrementing across NDK releases.
100                    let mut parts = value.trim().split('.');
101                    let _major = parts.next().unwrap();
102                    let _minor = parts.next().unwrap();
103                    let patch = parts.next().unwrap();
104                    // Can have an optional `XXX-beta1`
105                    let patch = patch.split_once('-').map_or(patch, |(patch, _beta)| patch);
106                    Some(patch.parse().expect("Failed to parse patch field"))
107                } else {
108                    None
109                }
110            })
111            .expect("No `Pkg.Revision` in source.properties");
112
113        let ndk_platforms = std::fs::read_to_string(ndk_path.join("build/core/platforms.mk"))?;
114        let ndk_platforms = ndk_platforms
115            .split('\n')
116            .map(|s| s.split_once(" := ").unwrap())
117            .collect::<HashMap<_, _>>();
118
119        let min_platform_level = ndk_platforms["NDK_MIN_PLATFORM_LEVEL"]
120            .parse::<u32>()
121            .unwrap();
122        let max_platform_level = ndk_platforms["NDK_MAX_PLATFORM_LEVEL"]
123            .parse::<u32>()
124            .unwrap();
125
126        let platforms_dir = sdk_path.join("platforms");
127        let platforms: Vec<u32> = std::fs::read_dir(&platforms_dir)
128            .or(Err(NdkError::PathNotFound(platforms_dir)))?
129            .filter_map(|path| path.ok())
130            .filter(|path| path.path().is_dir())
131            .filter_map(|path| path.file_name().into_string().ok())
132            .filter_map(|name| {
133                name.strip_prefix("android-")
134                    .and_then(|api| api.parse::<u32>().ok())
135            })
136            .filter(|level| (min_platform_level..=max_platform_level).contains(level))
137            .collect();
138
139        if platforms.is_empty() {
140            return Err(NdkError::NoPlatformFound);
141        }
142
143        Ok(Self {
144            sdk_path,
145            user_home,
146            ndk_path,
147            build_tools_version,
148            build_tag,
149            platforms,
150        })
151    }
152
153    pub fn sdk(&self) -> &Path {
154        &self.sdk_path
155    }
156
157    pub fn ndk(&self) -> &Path {
158        &self.ndk_path
159    }
160
161    pub fn build_tools_version(&self) -> &str {
162        &self.build_tools_version
163    }
164
165    pub fn build_tag(&self) -> u32 {
166        self.build_tag
167    }
168
169    pub fn platforms(&self) -> &[u32] {
170        &self.platforms
171    }
172
173    pub fn build_tool(&self, tool: &str) -> Result<Command, NdkError> {
174        let path = self
175            .sdk_path
176            .join("build-tools")
177            .join(&self.build_tools_version)
178            .join(tool);
179        if !path.exists() {
180            return Err(NdkError::CmdNotFound(tool.to_string()));
181        }
182        Ok(Command::new(dunce::canonicalize(path)?))
183    }
184
185    pub fn platform_tool_path(&self, tool: &str) -> Result<PathBuf, NdkError> {
186        let path = self.sdk_path.join("platform-tools").join(tool);
187        if !path.exists() {
188            return Err(NdkError::CmdNotFound(tool.to_string()));
189        }
190        Ok(dunce::canonicalize(path)?)
191    }
192
193    pub fn adb_path(&self) -> Result<PathBuf, NdkError> {
194        self.platform_tool_path(bin!("adb"))
195    }
196
197    pub fn platform_tool(&self, tool: &str) -> Result<Command, NdkError> {
198        Ok(Command::new(self.platform_tool_path(tool)?))
199    }
200
201    pub fn highest_supported_platform(&self) -> u32 {
202        self.platforms().iter().max().cloned().unwrap()
203    }
204
205    /// Returns platform `30` as currently [required by Google Play], or lower
206    /// when the detected SDK does not support it yet.
207    ///
208    /// [required by Google Play]: https://developer.android.com/distribute/best-practices/develop/target-sdk
209    pub fn default_target_platform(&self) -> u32 {
210        self.highest_supported_platform().min(30)
211    }
212
213    pub fn platform_dir(&self, platform: u32) -> Result<PathBuf, NdkError> {
214        let dir = self
215            .sdk_path
216            .join("platforms")
217            .join(format!("android-{}", platform));
218        if !dir.exists() {
219            return Err(NdkError::PlatformNotFound(platform));
220        }
221        Ok(dir)
222    }
223
224    pub fn android_jar(&self, platform: u32) -> Result<PathBuf, NdkError> {
225        let android_jar = self.platform_dir(platform)?.join("android.jar");
226        if !android_jar.exists() {
227            return Err(NdkError::PathNotFound(android_jar));
228        }
229        Ok(android_jar)
230    }
231
232    fn host_arch() -> Result<&'static str, NdkError> {
233        let host_os = std::env::var("HOST").ok();
234        let host_contains = |s| host_os.as_ref().map(|h| h.contains(s)).unwrap_or(false);
235
236        Ok(if host_contains("linux") {
237            "linux"
238        } else if host_contains("macos") {
239            "darwin"
240        } else if host_contains("windows") {
241            "windows"
242        } else if host_contains("android") {
243            "android"
244        } else if cfg!(target_os = "linux") {
245            "linux"
246        } else if cfg!(target_os = "macos") {
247            "darwin"
248        } else if cfg!(target_os = "windows") {
249            "windows"
250        } else if cfg!(target_os = "android") {
251            "android"
252        } else {
253            return match host_os {
254                Some(host_os) => Err(NdkError::UnsupportedHost(host_os)),
255                _ => Err(NdkError::UnsupportedTarget),
256            };
257        })
258    }
259
260    pub fn toolchain_dir(&self) -> Result<PathBuf, NdkError> {
261        let arch = Self::host_arch()?;
262        let mut toolchain_dir = self
263            .ndk_path
264            .join("toolchains")
265            .join("llvm")
266            .join("prebuilt")
267            .join(format!("{}-x86_64", arch));
268        if !toolchain_dir.exists() {
269            toolchain_dir.set_file_name(arch);
270        }
271        if !toolchain_dir.exists() {
272            return Err(NdkError::PathNotFound(toolchain_dir));
273        }
274        Ok(toolchain_dir)
275    }
276
277    pub fn clang(&self) -> Result<(PathBuf, PathBuf), NdkError> {
278        let ext = if cfg!(target_os = "windows") {
279            "exe"
280        } else {
281            ""
282        };
283
284        let bin_path = self.toolchain_dir()?.join("bin");
285
286        let clang = bin_path.join("clang").with_extension(ext);
287        if !clang.exists() {
288            return Err(NdkError::PathNotFound(clang));
289        }
290
291        let clang_pp = bin_path.join("clang++").with_extension(ext);
292        if !clang_pp.exists() {
293            return Err(NdkError::PathNotFound(clang_pp));
294        }
295
296        Ok((clang, clang_pp))
297    }
298
299    pub fn toolchain_bin(&self, name: &str, target: Target) -> Result<PathBuf, NdkError> {
300        let ext = if cfg!(target_os = "windows") {
301            ".exe"
302        } else {
303            ""
304        };
305
306        let toolchain_path = self.toolchain_dir()?.join("bin");
307
308        // Since r21 (https://github.com/android/ndk/wiki/Changelog-r21) LLVM binutils are included _for testing_;
309        // Since r22 (https://github.com/android/ndk/wiki/Changelog-r22) GNU binutils are deprecated in favour of LLVM's;
310        // Since r23 (https://github.com/android/ndk/wiki/Changelog-r23) GNU binutils have been removed.
311        // To maintain stability with the current ndk-build crate release, prefer GNU binutils for
312        // as long as it is provided by the NDK instead of trying to use llvm-* from r21 onwards.
313        let gnu_bin = format!("{}-{}{}", target.ndk_triple(), name, ext);
314        let gnu_path = toolchain_path.join(&gnu_bin);
315        if gnu_path.exists() {
316            Ok(gnu_path)
317        } else {
318            let llvm_bin = format!("llvm-{}{}", name, ext);
319            let llvm_path = toolchain_path.join(&llvm_bin);
320            if llvm_path.exists() {
321                Ok(llvm_path)
322            } else {
323                Err(NdkError::ToolchainBinaryNotFound {
324                    toolchain_path,
325                    gnu_bin,
326                    llvm_bin,
327                })
328            }
329        }
330    }
331
332    pub fn prebuilt_dir(&self) -> Result<PathBuf, NdkError> {
333        let arch = Self::host_arch()?;
334        let prebuilt_dir = self
335            .ndk_path
336            .join("prebuilt")
337            .join(format!("{}-x86_64", arch));
338        if !prebuilt_dir.exists() {
339            Err(NdkError::PathNotFound(prebuilt_dir))
340        } else {
341            Ok(prebuilt_dir)
342        }
343    }
344
345    pub fn ndk_gdb(
346        &self,
347        launch_dir: impl AsRef<Path>,
348        launch_activity: &str,
349        device_serial: Option<&str>,
350    ) -> Result<(), NdkError> {
351        let abi = self.detect_abi(device_serial)?;
352        let jni_dir = launch_dir.as_ref().join("jni");
353        std::fs::create_dir_all(&jni_dir)?;
354        std::fs::write(
355            jni_dir.join("Android.mk"),
356            format!("APP_ABI={}\nTARGET_OUT=\n", abi.android_abi()),
357        )?;
358        let mut ndk_gdb = Command::new(self.prebuilt_dir()?.join("bin").join(cmd!("ndk-gdb")));
359
360        if let Some(device_serial) = &device_serial {
361            ndk_gdb.arg("-s").arg(device_serial);
362        }
363
364        ndk_gdb
365            .arg("--adb")
366            .arg(self.adb_path()?)
367            .arg("--launch")
368            .arg(launch_activity)
369            .current_dir(launch_dir)
370            .status()?;
371        Ok(())
372    }
373
374    pub fn android_user_home(&self) -> Result<PathBuf, NdkError> {
375        let android_user_home = self.user_home.clone();
376        std::fs::create_dir_all(&android_user_home)?;
377        Ok(android_user_home)
378    }
379
380    pub fn keytool(&self) -> Result<Command, NdkError> {
381        if let Ok(keytool) = which::which(bin!("keytool")) {
382            return Ok(Command::new(keytool));
383        }
384        if let Ok(java) = std::env::var("JAVA_HOME") {
385            let keytool = PathBuf::from(java).join("bin").join(bin!("keytool"));
386            if keytool.exists() {
387                return Ok(Command::new(keytool));
388            }
389        }
390        Err(NdkError::CmdNotFound("keytool".to_string()))
391    }
392
393    pub fn debug_key(&self) -> Result<Key, NdkError> {
394        let path = self.android_user_home()?.join("debug.keystore");
395        let password = DEFAULT_DEV_KEYSTORE_PASSWORD.to_owned();
396
397        if !path.exists() {
398            let mut keytool = self.keytool()?;
399            keytool
400                .arg("-genkey")
401                .arg("-v")
402                .arg("-keystore")
403                .arg(&path)
404                .arg("-storepass")
405                .arg(&password)
406                .arg("-alias")
407                .arg("androiddebugkey")
408                .arg("-keypass")
409                .arg(&password)
410                .arg("-dname")
411                .arg("CN=Android Debug,O=Android,C=US")
412                .arg("-keyalg")
413                .arg("RSA")
414                .arg("-keysize")
415                .arg("2048")
416                .arg("-validity")
417                .arg("10000");
418            if !keytool.status()?.success() {
419                return Err(NdkError::CmdFailed(keytool));
420            }
421        }
422        Ok(Key { path, password })
423    }
424
425    pub fn sysroot_lib_dir(&self, target: Target) -> Result<PathBuf, NdkError> {
426        let sysroot_lib_dir = self
427            .toolchain_dir()?
428            .join("sysroot")
429            .join("usr")
430            .join("lib")
431            .join(target.ndk_triple());
432        if !sysroot_lib_dir.exists() {
433            return Err(NdkError::PathNotFound(sysroot_lib_dir));
434        }
435        Ok(sysroot_lib_dir)
436    }
437
438    pub fn sysroot_platform_lib_dir(
439        &self,
440        target: Target,
441        min_sdk_version: u32,
442    ) -> Result<PathBuf, NdkError> {
443        let sysroot_lib_dir = self.sysroot_lib_dir(target)?;
444
445        // Look for a platform <= min_sdk_version
446        let mut tmp_platform = min_sdk_version;
447        while tmp_platform > 1 {
448            let path = sysroot_lib_dir.join(tmp_platform.to_string());
449            if path.exists() {
450                return Ok(path);
451            }
452            tmp_platform += 1;
453        }
454
455        // Look for the minimum API level supported by the NDK
456        let mut tmp_platform = min_sdk_version;
457        while tmp_platform < 100 {
458            let path = sysroot_lib_dir.join(tmp_platform.to_string());
459            if path.exists() {
460                return Ok(path);
461            }
462            tmp_platform += 1;
463        }
464
465        Err(NdkError::PlatformNotFound(min_sdk_version))
466    }
467
468    pub fn detect_abi(&self, device_serial: Option<&str>) -> Result<Target, NdkError> {
469        let mut adb = self.adb(device_serial)?;
470
471        let stdout = adb
472            .arg("shell")
473            .arg("getprop")
474            .arg("ro.product.cpu.abi")
475            .output()?
476            .stdout;
477        let abi = std::str::from_utf8(&stdout).or(Err(NdkError::UnsupportedTarget))?;
478        Target::from_android_abi(abi.trim())
479    }
480
481    pub fn adb(&self, device_serial: Option<&str>) -> Result<Command, NdkError> {
482        let mut adb = Command::new(self.adb_path()?);
483
484        if let Some(device_serial) = device_serial {
485            adb.arg("-s").arg(device_serial);
486        }
487
488        Ok(adb)
489    }
490}
491
492pub struct Key {
493    pub path: PathBuf,
494    pub password: String,
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    #[ignore]
503    fn test_detect() {
504        let ndk = Ndk::from_env().unwrap();
505        assert_eq!(ndk.build_tools_version(), "29.0.2");
506        assert_eq!(ndk.platforms(), &[29, 28]);
507    }
508}