ndk_build2/
ndk.rs

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