1use crate::error::NdkError;
2use crate::target::Target;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub 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 .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 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 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 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 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 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 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 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 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}