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
12pub 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 pub fn from_env() -> Result<Self, NdkError> {
29 let user_home = {
30 let user_home = var("ANDROID_SDK_HOME")
31 .map(PathBuf::from)
32 .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 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 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 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 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 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 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 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 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 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 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 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}