1use std::{
2 cmp::Ordering,
3 env,
4 ffi::{OsStr, OsString},
5 io,
6 path::{Path, PathBuf},
7 process::Output,
8};
9
10use url::Url;
11use walkdir::WalkDir;
12use waterui_assets_core::{AssetError, download_remote_bytes, write_bytes_atomically};
13
14use crate::{
15 android::{
16 ndk_version,
17 platform::{ALL_ABIS, AndroidAbi},
18 },
19 brew::Brew,
20 build_info,
21 toolchain::{
22 Host, Installation, Toolchain, ToolchainError,
23 linux::{
24 LinuxPackageManagerError, has_supported_package_manager, install_java_jdk,
25 install_named_packages,
26 },
27 winget::{WingetInstallError, ensure_package_installed},
28 },
29 utils::{CommandError, command},
30 water_dir::{HomeDirError, water_home_dir_in},
31};
32
33#[derive(Debug, thiserror::Error)]
35pub enum AndroidToolchainError {
36 #[error("Android SDK command-line tools (`sdkmanager`) not found")]
38 SdkManagerNotFound,
39 #[error("Android SDK root could not be determined from environment or sdkmanager path")]
41 SdkRootUndetermined,
42 #[error("Android SDK root cannot be determined on this host")]
44 SdkRootUnavailable,
45 #[error("Java runtime not found while invoking sdkmanager")]
47 JavaNotFound,
48 #[error(transparent)]
50 HomeDir(#[from] HomeDirError),
51 #[error("Failed to query Android SDK repository metadata: {0}")]
53 RepositoryQuery(#[source] zenwave::Error),
54 #[error("Failed to query Android SDK repository metadata: HTTP {0}")]
56 RepositoryStatus(zenwave::StatusCode),
57 #[error("Failed to read Android SDK repository metadata: {0}")]
59 RepositoryBody(#[from] zenwave::BodyError),
60 #[error("Could not locate Android command-line tools archive")]
62 CmdlineToolsArchiveNotFound,
63 #[error("Failed to download {url}: {source}")]
65 Download {
66 url: String,
68 #[source]
70 source: AssetError,
71 },
72 #[error("Failed to write downloaded archive to {}: {source}", path.display())]
74 ArchiveWrite {
75 path: PathBuf,
77 #[source]
79 source: AssetError,
80 },
81 #[error("Invalid Android command-line tools archive layout (missing bin directory)")]
83 CmdlineToolsMissingBinDir,
84 #[error("Invalid Android command-line tools archive layout (missing cmdline-tools root)")]
86 CmdlineToolsMissingRoot,
87 #[error("Android command-line tools archive does not contain sdkmanager")]
89 CmdlineToolsMissingSdkManager,
90 #[error("Android command-line tools were extracted but sdkmanager is still missing")]
92 CmdlineToolsStillMissingSdkManager,
93 #[error("Invalid Kotlin compiler archive layout (missing bin directory)")]
95 KotlinMissingBinDir,
96 #[error("Invalid Kotlin compiler archive layout (missing compiler root)")]
98 KotlinMissingRoot,
99 #[error("Kotlin compiler archive does not contain {0}")]
101 KotlinMissingCompiler(String),
102 #[error("Managed Kotlin install path has no parent")]
104 KotlinInstallPathNoParent,
105 #[error("Kotlin compiler `{version}` was extracted but `{executable}` is still missing")]
107 KotlinCompilerStillMissing {
108 version: String,
110 executable: &'static str,
112 },
113 #[error(
115 "Installed Kotlin compiler version `{installed}` does not satisfy required version `{required}`"
116 )]
117 KotlinVersionMismatch {
118 installed: String,
120 required: String,
122 },
123 #[error(
125 "Failed to parse Kotlin compiler version from `{}` output: {output}",
126 path.display()
127 )]
128 KotlinVersionParse {
129 path: PathBuf,
131 output: String,
133 },
134 #[error("Failed to parse proxy URL `{url}` for sdkmanager: {source}")]
136 ProxyParse {
137 url: String,
139 #[source]
141 source: url::ParseError,
142 },
143 #[error("Proxy URL `{0}` is missing a host")]
145 ProxyMissingHost(String),
146 #[error("Proxy URL `{0}` is missing a port")]
148 ProxyMissingPort(String),
149 #[error("Unsupported proxy scheme `{0}` for sdkmanager")]
151 ProxyUnsupportedScheme(String),
152 #[error("Failed to construct PATH with required entry '{}': {source}", entry.display())]
154 PathJoin {
155 entry: PathBuf,
157 #[source]
159 source: env::JoinPathsError,
160 },
161 #[error("Failed to accept Android SDK licenses. {0}")]
163 LicenseAcceptance(String),
164 #[error("Failed to install package `{package_id}` via sdkmanager. {output}")]
166 PackageInstall {
167 package_id: String,
169 output: String,
171 },
172 #[error("Failed to list Android SDK packages via sdkmanager. {0}")]
174 PackageList(String),
175 #[error("Invalid Android NDK package id `{0}`")]
177 InvalidNdkPackageId(String),
178 #[error("Required Android NDK package `{package_id}` is not available via `sdkmanager --list`")]
180 NdkPackageUnavailable {
181 package_id: String,
183 },
184 #[error("No installable Android platform package found via `sdkmanager --list`")]
186 NoPlatformPackage,
187 #[error("No installable Android build-tools package found via `sdkmanager --list`")]
189 NoBuildToolsPackage,
190 #[error(transparent)]
192 Command(#[from] CommandError),
193 #[error(transparent)]
195 Io(#[from] io::Error),
196 #[error(transparent)]
198 Zip(#[from] zip::result::ZipError),
199 #[error(transparent)]
201 WalkDir(#[from] walkdir::Error),
202}
203
204#[derive(Debug, Clone, Default)]
206pub struct AndroidSdk;
207
208#[derive(Debug, Clone, Default)]
210pub struct AndroidPlatformTools;
211
212#[derive(Debug, Clone, Default)]
214pub struct AndroidSdkPlatforms;
215
216#[derive(Debug, Clone, Default)]
218pub struct AndroidBuildTools;
219
220#[derive(Debug, Clone)]
222pub struct AndroidRustTargets {
223 required_targets: Vec<String>,
224}
225
226impl AndroidRustTargets {
227 #[must_use]
233 pub fn for_abis(abis: &[AndroidAbi]) -> Self {
234 assert!(
235 !abis.is_empty(),
236 "AndroidRustTargets::for_abis requires at least one ABI"
237 );
238 Self {
239 required_targets: required_android_rust_targets_for_abis(abis),
240 }
241 }
242}
243
244impl Default for AndroidRustTargets {
245 fn default() -> Self {
246 Self::for_abis(ALL_ABIS)
247 }
248}
249
250#[derive(Debug, Clone, Default)]
252pub struct AndroidNdk;
253
254#[derive(Debug, Clone, Default)]
256pub struct Java;
257
258#[derive(Debug, Clone, Default)]
260pub struct Kotlin;
261
262const ANDROID_LINUX_X86_64_HOST_TOOLS_COMPAT_PACKAGES: &[&str] =
263 &["libc6:amd64", "libstdc++6:amd64", "zlib1g:amd64"];
264
265const fn is_linux_arm_host() -> bool {
266 cfg!(target_os = "linux")
267 && (cfg!(target_arch = "aarch64")
268 || cfg!(target_arch = "arm")
269 || cfg!(target_arch = "arm64ec"))
270}
271
272fn needs_linux_x86_64_host_tools_compat(detail: &str) -> bool {
273 is_linux_arm_host() && detail.contains("ld-linux-x86-64.so.2")
274}
275
276async fn install_android_linux_x86_64_host_tools_compat(
277 host: &Host,
278) -> Result<(), LinuxPackageManagerError> {
279 install_named_packages(host, ANDROID_LINUX_X86_64_HOST_TOOLS_COMPAT_PACKAGES).await
280}
281
282#[must_use]
284pub const fn android_cmdline_tools_suggestion() -> &'static str {
285 "Install Android SDK command-line tools and ensure `sdkmanager` is available in PATH."
286}
287
288#[must_use]
290pub const fn android_sdk_path_suggestion() -> &'static str {
291 if cfg!(target_os = "windows") {
292 "Expected default SDK path is `%LOCALAPPDATA%\\Android\\Sdk`. Set `ANDROID_SDK_ROOT` to that path if needed."
293 } else if cfg!(target_os = "macos") {
294 "Expected default SDK path is `$HOME/Library/Android/sdk`. Set `ANDROID_SDK_ROOT` to that path if needed."
295 } else if cfg!(target_os = "linux") {
296 "Expected default SDK path is `$HOME/Android/Sdk`. Set `ANDROID_SDK_ROOT` to that path if needed."
297 } else {
298 "Set `ANDROID_SDK_ROOT` to your Android SDK path."
299 }
300}
301
302#[must_use]
304pub const fn android_platform_tools_suggestion() -> &'static str {
305 "Install Android Platform-Tools with `sdkmanager --install \"platform-tools\"` (or Android Studio SDK Manager), then ensure `ANDROID_SDK_ROOT` points to that SDK."
306}
307
308#[must_use]
310pub const fn android_ndk_install_suggestion() -> &'static str {
311 "Install Android NDK with `sdkmanager --install \"ndk;<version>\"` (or Android Studio SDK Manager), then set `ANDROID_NDK_ROOT` if using a custom location."
312}
313
314#[must_use]
316pub const fn android_platforms_install_suggestion() -> &'static str {
317 "Install Android SDK platform packages with `sdkmanager --install \"platforms;android-<api>\"` (or Android Studio SDK Manager)."
318}
319
320#[must_use]
322pub const fn android_build_tools_install_suggestion() -> &'static str {
323 "Install Android SDK Build-Tools with `sdkmanager --install \"build-tools;<version>\"` (or Android Studio SDK Manager)."
324}
325
326const fn sdkmanager_search_names() -> &'static [&'static str] {
327 if cfg!(target_os = "windows") {
328 &["sdkmanager.bat", "sdkmanager.exe", "sdkmanager"]
329 } else {
330 &["sdkmanager"]
331 }
332}
333
334const fn sdkmanager_binary_name() -> &'static str {
335 if cfg!(target_os = "windows") {
336 "sdkmanager.bat"
337 } else {
338 "sdkmanager"
339 }
340}
341
342const fn cmdline_tools_host_tag() -> Option<&'static str> {
343 if cfg!(target_os = "windows") {
344 Some("win")
345 } else if cfg!(target_os = "macos") {
346 Some("mac")
347 } else if cfg!(target_os = "linux") {
348 Some("linux")
349 } else {
350 None
351 }
352}
353
354fn default_android_sdk_path(host: &Host) -> Option<PathBuf> {
355 if cfg!(target_os = "windows") {
356 let localappdata = host.env_string("LOCALAPPDATA")?;
357 return Some(PathBuf::from(localappdata).join("Android/Sdk"));
358 }
359
360 let home = host.home_dir()?;
361 if cfg!(target_os = "macos") {
362 return Some(home.join("Library/Android/sdk"));
363 }
364
365 if cfg!(target_os = "linux") {
366 return Some(home.join("Android/Sdk"));
367 }
368
369 None
370}
371
372fn configured_android_sdk_path(host: &Host) -> Option<PathBuf> {
373 for key in ["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
374 if let Some(raw) = host.env_string(key) {
375 return Some(PathBuf::from(raw));
376 }
377 }
378 default_android_sdk_path(host)
379}
380
381fn sdkmanager_candidates_under_sdk_root(sdk_root: &Path) -> Vec<PathBuf> {
382 if cfg!(target_os = "windows") {
383 vec![
384 sdk_root.join("cmdline-tools/latest/bin/sdkmanager.bat"),
385 sdk_root.join("cmdline-tools/bin/sdkmanager.bat"),
386 sdk_root.join("tools/bin/sdkmanager.bat"),
387 ]
388 } else {
389 vec![
390 sdk_root.join("cmdline-tools/latest/bin/sdkmanager"),
391 sdk_root.join("cmdline-tools/bin/sdkmanager"),
392 sdk_root.join("tools/bin/sdkmanager"),
393 ]
394 }
395}
396
397fn parse_latest_cmdline_tools_archive(repository_xml: &str) -> Option<String> {
398 let host_tag = cmdline_tools_host_tag()?;
399 let prefix = format!("commandlinetools-{host_tag}-");
400 let suffix = "_latest.zip";
401
402 let mut cursor = 0usize;
403 let mut best: Option<(u64, String)> = None;
404
405 while let Some(offset) = repository_xml[cursor..].find(&prefix) {
406 let start = cursor + offset + prefix.len();
407 let remainder = &repository_xml[start..];
408 let Some(suffix_offset) = remainder.find(suffix) else {
409 cursor = start;
410 continue;
411 };
412
413 let build_id = &remainder[..suffix_offset];
414 let filename = format!("{prefix}{build_id}{suffix}");
415 cursor = start + suffix_offset + suffix.len();
416
417 if build_id.is_empty() || !build_id.chars().all(|ch| ch.is_ascii_digit()) {
418 continue;
419 }
420
421 let Ok(build_id) = build_id.parse::<u64>() else {
422 continue;
423 };
424
425 match best {
426 Some((current, _)) if build_id <= current => {}
427 _ => best = Some((build_id, filename)),
428 }
429 }
430
431 best.map(|(_, filename)| filename)
432}
433
434async fn latest_cmdline_tools_archive_url() -> Result<String, AndroidToolchainError> {
435 use zenwave::{Client, Method};
436
437 const REPOSITORY_URL: &str = "https://dl.google.com/android/repository/repository2-3.xml";
438 const REPOSITORY_PREFIX: &str = "https://dl.google.com/android/repository/";
439
440 let mut client = zenwave::client();
441 let response = client
442 .method(Method::GET, REPOSITORY_URL)
443 .map_err(AndroidToolchainError::RepositoryQuery)?
444 .await
445 .map_err(AndroidToolchainError::RepositoryQuery)?;
446 if !response.status().is_success() {
447 return Err(AndroidToolchainError::RepositoryStatus(response.status()));
448 }
449
450 let bytes = response.into_body().into_bytes().await?;
451 let repository_xml = String::from_utf8_lossy(&bytes).into_owned();
452 let archive_name = parse_latest_cmdline_tools_archive(&repository_xml)
453 .ok_or(AndroidToolchainError::CmdlineToolsArchiveNotFound)?;
454 Ok(format!("{REPOSITORY_PREFIX}{archive_name}"))
455}
456
457async fn download_file_with_redirect(
458 url: &str,
459 destination: &Path,
460) -> Result<(), AndroidToolchainError> {
461 let bytes =
462 download_remote_bytes(url)
463 .await
464 .map_err(|source| AndroidToolchainError::Download {
465 url: url.to_owned(),
466 source,
467 })?;
468 write_bytes_atomically(destination, &bytes)
469 .await
470 .map_err(|source| AndroidToolchainError::ArchiveWrite {
471 path: destination.to_path_buf(),
472 source,
473 })?;
474 Ok(())
475}
476
477fn find_cmdline_tools_dir(root: &Path) -> Result<PathBuf, AndroidToolchainError> {
478 let sdkmanager_name = sdkmanager_binary_name();
479
480 for entry in WalkDir::new(root) {
481 let entry = entry?;
482 if !entry.file_type().is_file() {
483 continue;
484 }
485
486 let path = entry.path();
487 let is_sdkmanager = path
488 .file_name()
489 .and_then(|name| name.to_str())
490 .is_some_and(|name| name.eq_ignore_ascii_case(sdkmanager_name));
491 if !is_sdkmanager {
492 continue;
493 }
494
495 let bin_dir = path
496 .parent()
497 .ok_or(AndroidToolchainError::CmdlineToolsMissingBinDir)?;
498 let cmdline_tools_dir = bin_dir
499 .parent()
500 .ok_or(AndroidToolchainError::CmdlineToolsMissingRoot)?;
501 return Ok(cmdline_tools_dir.to_path_buf());
502 }
503
504 Err(AndroidToolchainError::CmdlineToolsMissingSdkManager)
505}
506
507async fn ensure_cmdline_tools_available(sdk_root: &Path) -> Result<(), AndroidToolchainError> {
508 let latest_dir = sdk_root.join("cmdline-tools/latest");
509 let sdkmanager = latest_dir.join("bin").join(sdkmanager_binary_name());
510 if sdkmanager.exists() {
511 return Ok(());
512 }
513
514 let cmdline_tools_root = sdk_root.join("cmdline-tools");
515 let temp_dir = {
516 let cmdline_tools_root = cmdline_tools_root.clone();
517 smol::unblock(move || -> io::Result<_> {
518 std::fs::create_dir_all(&cmdline_tools_root)?;
519 tempfile::Builder::new()
520 .prefix(".water-cmdline-tools-")
521 .tempdir_in(&cmdline_tools_root)
522 })
523 .await?
524 };
525 let extract_dir = temp_dir.path().join("extract");
526 let archive_path = temp_dir.path().join("commandline-tools.zip");
527
528 {
529 let extract_dir = extract_dir.clone();
530 smol::unblock(move || std::fs::create_dir_all(&extract_dir)).await?;
531 }
532
533 let archive_url = latest_cmdline_tools_archive_url().await?;
534 download_file_with_redirect(&archive_url, &archive_path).await?;
535
536 {
537 let archive_path = archive_path.clone();
538 let extract_dir = extract_dir.clone();
539 smol::unblock(move || -> Result<(), AndroidToolchainError> {
540 let archive_file = std::fs::File::open(&archive_path)?;
541 let mut archive = zip::ZipArchive::new(archive_file)?;
542 archive.extract(&extract_dir)?;
543 Ok(())
544 })
545 .await?;
546 }
547
548 let extracted_cmdline_dir = {
549 let extract_dir = extract_dir.clone();
550 smol::unblock(move || find_cmdline_tools_dir(&extract_dir)).await?
551 };
552
553 if latest_dir.exists() {
554 let latest_dir = latest_dir.clone();
555 smol::unblock(move || std::fs::remove_dir_all(latest_dir)).await?;
556 }
557
558 {
559 let extracted_cmdline_dir = extracted_cmdline_dir.clone();
560 let latest_dir = latest_dir.clone();
561 smol::unblock(move || std::fs::rename(extracted_cmdline_dir, latest_dir)).await?;
562 }
563
564 if sdkmanager.exists() {
565 Ok(())
566 } else {
567 Err(AndroidToolchainError::CmdlineToolsStillMissingSdkManager)
568 }
569}
570
571fn looks_like_android_sdk_root(path: &Path) -> bool {
572 path.join("cmdline-tools").exists()
573 || path.join("platform-tools").exists()
574 || path.join("platforms").exists()
575 || path.join("ndk").exists()
576}
577
578fn find_android_jar_in_sdk(sdk_root: &Path) -> Option<PathBuf> {
579 let platforms_dir = sdk_root.join("platforms");
580 if !platforms_dir.exists() {
581 return None;
582 }
583
584 let mut platforms = std::fs::read_dir(&platforms_dir)
585 .ok()?
586 .filter_map(std::result::Result::ok)
587 .map(|entry| entry.path())
588 .filter(|path| path.is_dir())
589 .collect::<Vec<_>>();
590 platforms.sort_by(|left, right| {
591 let left_api = left
592 .file_name()
593 .and_then(|name| name.to_str())
594 .and_then(|name| name.strip_prefix("android-"))
595 .and_then(parse_android_version_pair)
596 .unwrap_or((0, 0));
597 let right_api = right
598 .file_name()
599 .and_then(|name| name.to_str())
600 .and_then(|name| name.strip_prefix("android-"))
601 .and_then(parse_android_version_pair)
602 .unwrap_or((0, 0));
603 right_api.cmp(&left_api)
604 });
605
606 for platform in platforms {
607 let android_jar = platform.join("android.jar");
608 if android_jar.exists() {
609 return Some(android_jar);
610 }
611 }
612 None
613}
614
615fn derive_sdk_root_from_sdkmanager_path(path: &Path) -> Option<PathBuf> {
616 let bin_dir = path.parent()?;
617 if !bin_dir
618 .file_name()?
619 .to_string_lossy()
620 .eq_ignore_ascii_case("bin")
621 {
622 return None;
623 }
624
625 let parent = bin_dir.parent()?;
626 if parent
627 .file_name()?
628 .to_string_lossy()
629 .eq_ignore_ascii_case("tools")
630 || parent
631 .file_name()?
632 .to_string_lossy()
633 .eq_ignore_ascii_case("cmdline-tools")
634 {
635 return Some(parent.parent()?.to_path_buf());
636 }
637
638 let maybe_cmdline_tools = parent.parent()?;
639 if maybe_cmdline_tools
640 .file_name()?
641 .to_string_lossy()
642 .eq_ignore_ascii_case("cmdline-tools")
643 {
644 return Some(maybe_cmdline_tools.parent()?.to_path_buf());
645 }
646
647 None
648}
649
650fn find_sdkmanager_on_host_path(host: &Host) -> Option<PathBuf> {
651 let path_env = host.env("PATH")?;
652 for path_dir in env::split_paths(path_env) {
653 for candidate_name in sdkmanager_search_names() {
654 let candidate = path_dir.join(candidate_name);
655 if candidate.exists() {
656 return Some(candidate);
657 }
658 }
659 }
660 None
661}
662
663fn parse_sdkmanager_package_id(line: &str) -> Option<&str> {
664 let trimmed = line.trim();
665 if trimmed.is_empty() {
666 return None;
667 }
668 let (first_column, _) = trimmed.split_once('|')?;
669 let package_id = first_column.trim();
670 if package_id.is_empty() || package_id == "Path" || package_id.starts_with('-') {
671 return None;
672 }
673 Some(package_id)
674}
675
676fn parse_android_version_pair(value: &str) -> Option<(u32, u32)> {
684 let mut segments = value.split('.');
685 let major = segments.next()?.parse().ok()?;
686 let minor = match segments.next() {
687 Some(segment) => segment.parse().ok()?,
688 None => 0,
689 };
690 if segments.next().is_some() {
691 return None;
692 }
693 Some((major, minor))
694}
695
696fn parse_android_platform_api_level(package_id: &str) -> Option<(u32, u32)> {
697 parse_android_version_pair(package_id.strip_prefix("platforms;android-")?)
698}
699
700fn parse_android_build_tools_version(package_id: &str) -> Option<&str> {
701 package_id.strip_prefix("build-tools;")
702}
703
704fn parse_numeric_prefix(segment: &str) -> u64 {
705 let digits: String = segment
706 .chars()
707 .take_while(char::is_ascii_digit)
708 .collect::<String>();
709 digits.parse().unwrap_or(0)
710}
711
712fn compare_version_segments(left: &[u64], right: &[u64]) -> Ordering {
713 let max_len = left.len().max(right.len());
714 for idx in 0..max_len {
715 let l = left.get(idx).copied().unwrap_or(0);
716 let r = right.get(idx).copied().unwrap_or(0);
717 match l.cmp(&r) {
718 Ordering::Equal => {}
719 ordering => return ordering,
720 }
721 }
722 Ordering::Equal
723}
724
725fn compare_sdk_package_ids(left: &str, right: &str) -> Ordering {
726 let left_version = left
727 .split_once(';')
728 .map_or("", |(_, version)| version)
729 .split('.')
730 .map(parse_numeric_prefix)
731 .collect::<Vec<_>>();
732 let right_version = right
733 .split_once(';')
734 .map_or("", |(_, version)| version)
735 .split('.')
736 .map(parse_numeric_prefix)
737 .collect::<Vec<_>>();
738
739 match compare_version_segments(&left_version, &right_version) {
740 Ordering::Equal => left.cmp(right),
741 ordering => ordering,
742 }
743}
744
745fn find_d8_jar_in_sdk(sdk_root: &Path) -> Option<PathBuf> {
746 let build_tools_dir = sdk_root.join("build-tools");
747 if !build_tools_dir.exists() {
748 return None;
749 }
750
751 let mut build_tools_versions = std::fs::read_dir(&build_tools_dir)
752 .ok()?
753 .filter_map(std::result::Result::ok)
754 .map(|entry| entry.path())
755 .filter(|path| path.is_dir())
756 .filter_map(|path| {
757 let version = path.file_name()?.to_str()?;
758 Some((format!("build-tools;{version}"), path))
759 })
760 .collect::<Vec<_>>();
761 build_tools_versions.sort_by(|(left, _), (right, _)| compare_sdk_package_ids(left, right));
762
763 while let Some((_, version_dir)) = build_tools_versions.pop() {
764 let d8_jar = version_dir.join("lib/d8.jar");
765 if d8_jar.exists() {
766 return Some(d8_jar);
767 }
768 }
769
770 None
771}
772
773async fn resolve_sdkmanager_and_root(
774 host: &Host,
775) -> Result<(PathBuf, PathBuf), AndroidToolchainError> {
776 let sdkmanager_path = AndroidSdk::sdkmanager_path(host)
777 .await
778 .ok_or(AndroidToolchainError::SdkManagerNotFound)?;
779 let sdk_root = AndroidSdk::detect_path(host)
780 .or_else(|| derive_sdk_root_from_sdkmanager_path(&sdkmanager_path))
781 .ok_or(AndroidToolchainError::SdkRootUndetermined)?;
782 Ok((sdkmanager_path, sdk_root))
783}
784
785fn prepend_path_entry(
786 entry: &Path,
787 existing: Option<OsString>,
788) -> Result<OsString, AndroidToolchainError> {
789 let mut entries = vec![entry.to_path_buf()];
790 if let Some(existing) = existing {
791 entries.extend(env::split_paths(&existing));
792 }
793 env::join_paths(entries).map_err(|source| AndroidToolchainError::PathJoin {
794 entry: entry.to_path_buf(),
795 source,
796 })
797}
798
799fn sdkmanager_combined_output(output: &Output) -> String {
800 let stdout = String::from_utf8_lossy(&output.stdout);
801 let stderr = String::from_utf8_lossy(&output.stderr);
802 format!("stdout: {} stderr: {}", stdout.trim(), stderr.trim())
803}
804
805fn sdkmanager_confirmation_input() -> String {
806 "y\n".repeat(128)
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq)]
810enum SdkManagerProxyType {
811 Http,
812 Socks,
813}
814
815impl SdkManagerProxyType {
816 const fn as_flag(self) -> &'static str {
817 match self {
818 Self::Http => "http",
819 Self::Socks => "socks",
820 }
821 }
822}
823
824#[derive(Debug, Clone, PartialEq, Eq)]
825struct SdkManagerProxyConfig {
826 proxy_type: SdkManagerProxyType,
827 host: String,
828 port: u16,
829}
830
831fn proxy_env_value(host: &Host) -> Option<String> {
832 [
833 "HTTPS_PROXY",
834 "https_proxy",
835 "ALL_PROXY",
836 "all_proxy",
837 "HTTP_PROXY",
838 "http_proxy",
839 ]
840 .into_iter()
841 .find_map(|key| {
842 host.env_string(key)
843 .filter(|value| !value.trim().is_empty())
844 })
845}
846
847fn parse_sdkmanager_proxy_config(
848 proxy: &str,
849) -> Result<SdkManagerProxyConfig, AndroidToolchainError> {
850 let trimmed = proxy.trim();
851 let normalized = if trimmed.contains("://") {
852 trimmed.to_string()
853 } else {
854 format!("http://{trimmed}")
855 };
856 let url = Url::parse(&normalized).map_err(|source| AndroidToolchainError::ProxyParse {
857 url: trimmed.to_owned(),
858 source,
859 })?;
860 let host = url
861 .host_str()
862 .ok_or_else(|| AndroidToolchainError::ProxyMissingHost(trimmed.to_owned()))?
863 .to_string();
864 let port = url
865 .port_or_known_default()
866 .ok_or_else(|| AndroidToolchainError::ProxyMissingPort(trimmed.to_owned()))?;
867 let proxy_type = match url.scheme() {
868 "http" | "https" => SdkManagerProxyType::Http,
869 "socks" | "socks5" | "socks5h" => SdkManagerProxyType::Socks,
870 scheme => {
871 return Err(AndroidToolchainError::ProxyUnsupportedScheme(
872 scheme.to_owned(),
873 ));
874 }
875 };
876 Ok(SdkManagerProxyConfig {
877 proxy_type,
878 host,
879 port,
880 })
881}
882
883fn sdkmanager_proxy_args(host: &Host) -> Result<Vec<OsString>, AndroidToolchainError> {
884 let Some(proxy) = proxy_env_value(host) else {
885 return Ok(Vec::new());
886 };
887 let proxy = parse_sdkmanager_proxy_config(&proxy)?;
888 Ok(vec![
889 OsString::from(format!("--proxy={}", proxy.proxy_type.as_flag())),
890 OsString::from(format!("--proxy_host={}", proxy.host)),
891 OsString::from(format!("--proxy_port={}", proxy.port)),
892 ])
893}
894
895pub(super) fn java_proxy_properties_from_env(
896 host: &Host,
897) -> Result<Vec<String>, AndroidToolchainError> {
898 let Some(proxy) = proxy_env_value(host) else {
899 return Ok(Vec::new());
900 };
901 let proxy = parse_sdkmanager_proxy_config(&proxy)?;
902 Ok(match proxy.proxy_type {
903 SdkManagerProxyType::Http => vec![
904 format!("-Dhttp.proxyHost={}", proxy.host),
905 format!("-Dhttp.proxyPort={}", proxy.port),
906 format!("-Dhttps.proxyHost={}", proxy.host),
907 format!("-Dhttps.proxyPort={}", proxy.port),
908 ],
909 SdkManagerProxyType::Socks => vec![
910 format!("-DsocksProxyHost={}", proxy.host),
911 format!("-DsocksProxyPort={}", proxy.port),
912 ],
913 })
914}
915
916fn sdkmanager_requires_license_acceptance(output: &Output) -> bool {
917 let lower = sdkmanager_combined_output(output).to_ascii_lowercase();
918 lower.contains("license is not accepted")
919 || lower.contains("licenses or those of the packages they depend on were not accepted")
920 || lower.contains("accept? (y/n):")
921}
922
923async fn run_sdkmanager_output_with_java(
924 host: &Host,
925 args: Vec<OsString>,
926 stdin_payload: Option<&str>,
927) -> Result<Output, AndroidToolchainError> {
928 let (sdkmanager_path, sdk_root) = resolve_sdkmanager_and_root(host).await?;
929 let java_home = Java::detect_home(host)
930 .await
931 .ok_or(AndroidToolchainError::JavaNotFound)?;
932 let java_bin = java_home.join("bin");
933 let path_env = prepend_path_entry(&java_bin, host.env("PATH").map(OsStr::to_os_string))?;
934
935 let mut sdk_root_arg = OsString::from("--sdk_root=");
936 sdk_root_arg.push(&sdk_root);
937 let mut full_args = vec![sdk_root_arg];
938 full_args.extend(sdkmanager_proxy_args(host)?);
939 full_args.extend(args);
940
941 let mut cmd = host.command(&sdkmanager_path);
942 cmd.args(full_args)
943 .env("ANDROID_SDK_ROOT", &sdk_root)
944 .env("ANDROID_HOME", &sdk_root)
945 .env("JAVA_HOME", &java_home)
946 .env("PATH", path_env)
947 .env_remove("HTTP_PROXY")
948 .env_remove("http_proxy")
949 .env_remove("HTTPS_PROXY")
950 .env_remove("https_proxy")
951 .env_remove("ALL_PROXY")
952 .env_remove("all_proxy");
953
954 if let Some(stdin_payload) = stdin_payload {
955 use smol::io::AsyncWriteExt;
956 use std::process::Stdio;
957
958 cmd.stdin(Stdio::piped());
959 let mut child = command(&mut cmd).spawn()?;
960 if let Some(mut stdin) = child.stdin.take() {
961 stdin.write_all(stdin_payload.as_bytes()).await?;
962 stdin.flush().await?;
963 }
964 child.output().await.map_err(AndroidToolchainError::from)
965 } else {
966 command(&mut cmd)
967 .output()
968 .await
969 .map_err(AndroidToolchainError::from)
970 }
971}
972
973async fn accept_sdkmanager_licenses(host: &Host) -> Result<(), AndroidToolchainError> {
974 let license_input = sdkmanager_confirmation_input();
975 let output = run_sdkmanager_output_with_java(
976 host,
977 vec![OsString::from("--licenses")],
978 Some(&license_input),
979 )
980 .await?;
981 if output.status.success() {
982 return Ok(());
983 }
984 Err(AndroidToolchainError::LicenseAcceptance(
985 sdkmanager_combined_output(&output),
986 ))
987}
988
989async fn install_android_sdk_package(
990 host: &Host,
991 package_id: &str,
992) -> Result<(), AndroidToolchainError> {
993 let install_args = vec![OsString::from("--install"), OsString::from(package_id)];
994 let confirmation_input = sdkmanager_confirmation_input();
995 let mut output =
996 run_sdkmanager_output_with_java(host, install_args.clone(), Some(&confirmation_input))
997 .await?;
998 if sdkmanager_requires_license_acceptance(&output) {
999 accept_sdkmanager_licenses(host).await?;
1000 output =
1001 run_sdkmanager_output_with_java(host, install_args, Some(&confirmation_input)).await?;
1002 }
1003 if output.status.success() {
1004 return Ok(());
1005 }
1006
1007 Err(AndroidToolchainError::PackageInstall {
1008 package_id: package_id.to_owned(),
1009 output: sdkmanager_combined_output(&output),
1010 })
1011}
1012
1013async fn list_sdk_package_ids(host: &Host) -> Result<Vec<String>, AndroidToolchainError> {
1014 let output =
1015 run_sdkmanager_output_with_java(host, vec![OsString::from("--list")], None).await?;
1016 if !output.status.success() {
1017 return Err(AndroidToolchainError::PackageList(
1018 sdkmanager_combined_output(&output),
1019 ));
1020 }
1021
1022 let stdout = String::from_utf8_lossy(&output.stdout);
1023 Ok(stdout
1024 .lines()
1025 .filter_map(parse_sdkmanager_package_id)
1026 .map(ToOwned::to_owned)
1027 .collect::<Vec<_>>())
1028}
1029
1030fn select_installed_ndk_path(ndk_dir: &Path, required_version: Option<&str>) -> Option<PathBuf> {
1031 if !ndk_dir.exists() {
1032 return None;
1033 }
1034
1035 if let Some(required_version) = required_version {
1036 let required_path = ndk_dir.join(required_version);
1037 if required_path.is_dir() {
1038 return Some(required_path);
1039 }
1040 }
1041
1042 let mut versions: Vec<PathBuf> = std::fs::read_dir(ndk_dir)
1043 .ok()?
1044 .filter_map(std::result::Result::ok)
1045 .map(|entry| entry.path())
1046 .filter(|path| path.is_dir())
1047 .collect();
1048 versions.sort();
1049 versions.pop()
1050}
1051
1052fn ndk_version_from_package_id(package_id: &str) -> Result<&str, AndroidToolchainError> {
1053 package_id
1054 .strip_prefix("ndk;")
1055 .ok_or_else(|| AndroidToolchainError::InvalidNdkPackageId(package_id.to_owned()))
1056}
1057
1058fn ndk_path_for_package_id(
1059 sdk_root: &Path,
1060 package_id: &str,
1061) -> Result<PathBuf, AndroidToolchainError> {
1062 Ok(sdk_root
1063 .join("ndk")
1064 .join(ndk_version_from_package_id(package_id)?))
1065}
1066
1067fn ndk_layout_is_complete(ndk_path: &Path) -> bool {
1068 ndk_path.join("toolchains/llvm/prebuilt").exists()
1069}
1070
1071async fn remove_directory_if_exists(path: &Path) -> io::Result<()> {
1072 let path = path.to_path_buf();
1073 smol::unblock(move || {
1074 if path.exists() {
1075 remove_dir_all::remove_dir_all(&path)?;
1076 }
1077 Ok(())
1078 })
1079 .await
1080}
1081
1082const fn kotlinc_binary_name() -> &'static str {
1083 if cfg!(target_os = "windows") {
1084 "kotlinc.bat"
1085 } else {
1086 "kotlinc"
1087 }
1088}
1089
1090fn kotlin_executable_from_home(home: &Path) -> Option<PathBuf> {
1091 let executable = home.join("bin").join(kotlinc_binary_name());
1092 executable.exists().then_some(executable)
1093}
1094
1095fn managed_kotlin_home(host: &Host, version: &str) -> Result<PathBuf, AndroidToolchainError> {
1096 Ok(water_home_dir_in(host)?
1097 .join("toolchains/kotlin")
1098 .join(version))
1099}
1100
1101fn kotlin_compiler_release_url(version: &str) -> String {
1102 format!(
1103 "https://github.com/JetBrains/kotlin/releases/download/v{version}/kotlin-compiler-{version}.zip"
1104 )
1105}
1106
1107fn find_kotlin_home_dir(root: &Path) -> Result<PathBuf, AndroidToolchainError> {
1108 let executable_name = kotlinc_binary_name();
1109 for entry in WalkDir::new(root) {
1110 let entry = entry?;
1111 if !entry.file_type().is_file() {
1112 continue;
1113 }
1114 let path = entry.path();
1115 let is_kotlinc = path
1116 .file_name()
1117 .and_then(|name| name.to_str())
1118 .is_some_and(|name| name.eq_ignore_ascii_case(executable_name));
1119 if !is_kotlinc {
1120 continue;
1121 }
1122
1123 let bin_dir = path
1124 .parent()
1125 .ok_or(AndroidToolchainError::KotlinMissingBinDir)?;
1126 let kotlin_home = bin_dir
1127 .parent()
1128 .ok_or(AndroidToolchainError::KotlinMissingRoot)?;
1129 return Ok(kotlin_home.to_path_buf());
1130 }
1131
1132 Err(AndroidToolchainError::KotlinMissingCompiler(
1133 executable_name.to_owned(),
1134 ))
1135}
1136
1137fn parse_kotlinc_version_output(output: &str) -> Option<String> {
1138 output.lines().find_map(|line| {
1139 let mut tokens = line
1140 .split_whitespace()
1141 .map(|token| token.trim_matches(|ch: char| ch == ':' || ch == '(' || ch == ')'));
1142 while let Some(token) = tokens.next() {
1143 if token.starts_with("kotlinc") {
1144 return tokens
1145 .find(|candidate| {
1146 candidate
1147 .chars()
1148 .next()
1149 .is_some_and(|ch| ch.is_ascii_digit())
1150 })
1151 .map(ToOwned::to_owned);
1152 }
1153 }
1154 None
1155 })
1156}
1157
1158fn kotlin_version_is_compatible(installed: &str, required: &str) -> bool {
1159 let installed_segments = installed
1160 .split('.')
1161 .map(parse_numeric_prefix)
1162 .collect::<Vec<_>>();
1163 let required_segments = required
1164 .split('.')
1165 .map(parse_numeric_prefix)
1166 .collect::<Vec<_>>();
1167 compare_version_segments(&installed_segments, &required_segments) != Ordering::Less
1168}
1169
1170async fn kotlin_compiler_version(
1171 host: &Host,
1172 kotlinc_path: &Path,
1173) -> Result<String, AndroidToolchainError> {
1174 let output = host.output(kotlinc_path, ["-version"]).await?;
1175 let combined = format!(
1176 "{} {}",
1177 String::from_utf8_lossy(&output.stdout),
1178 String::from_utf8_lossy(&output.stderr)
1179 );
1180 parse_kotlinc_version_output(&combined).ok_or_else(|| {
1181 AndroidToolchainError::KotlinVersionParse {
1182 path: kotlinc_path.to_path_buf(),
1183 output: combined.trim().to_owned(),
1184 }
1185 })
1186}
1187
1188async fn install_managed_kotlin_compiler(
1189 host: &Host,
1190 version: &str,
1191) -> Result<PathBuf, AndroidToolchainError> {
1192 let install_home = managed_kotlin_home(host, version)?;
1193 if let Some(kotlinc_path) = kotlin_executable_from_home(&install_home)
1194 && let Ok(installed_version) = kotlin_compiler_version(host, &kotlinc_path).await
1195 && kotlin_version_is_compatible(&installed_version, version)
1196 {
1197 return Ok(kotlinc_path);
1198 }
1199
1200 let install_parent = install_home
1201 .parent()
1202 .ok_or(AndroidToolchainError::KotlinInstallPathNoParent)?
1203 .to_path_buf();
1204 {
1205 let install_parent = install_parent.clone();
1206 smol::unblock(move || std::fs::create_dir_all(&install_parent)).await?;
1207 }
1208
1209 let temp_dir = {
1210 let install_parent = install_parent.clone();
1211 smol::unblock(move || {
1212 tempfile::Builder::new()
1213 .prefix(".water-kotlin-")
1214 .tempdir_in(&install_parent)
1215 })
1216 .await?
1217 };
1218 let extract_dir = temp_dir.path().join("extract");
1219 let archive_path = temp_dir
1220 .path()
1221 .join(format!("kotlin-compiler-{version}.zip"));
1222 {
1223 let extract_dir = extract_dir.clone();
1224 smol::unblock(move || std::fs::create_dir_all(&extract_dir)).await?;
1225 }
1226
1227 download_file_with_redirect(&kotlin_compiler_release_url(version), &archive_path).await?;
1228 {
1229 let archive_path = archive_path.clone();
1230 let extract_dir = extract_dir.clone();
1231 smol::unblock(move || -> Result<(), AndroidToolchainError> {
1232 let archive_file = std::fs::File::open(&archive_path)?;
1233 let mut archive = zip::ZipArchive::new(archive_file)?;
1234 archive.extract(&extract_dir)?;
1235 Ok(())
1236 })
1237 .await?;
1238 }
1239
1240 let extracted_home = {
1241 let extract_dir = extract_dir.clone();
1242 smol::unblock(move || find_kotlin_home_dir(&extract_dir)).await?
1243 };
1244 remove_directory_if_exists(&install_home).await?;
1245 {
1246 let extracted_home = extracted_home.clone();
1247 let install_home = install_home.clone();
1248 smol::unblock(move || std::fs::rename(extracted_home, install_home)).await?;
1249 }
1250
1251 let kotlinc_path = kotlin_executable_from_home(&install_home).ok_or(
1252 AndroidToolchainError::KotlinCompilerStillMissing {
1253 version: version.to_owned(),
1254 executable: kotlinc_binary_name(),
1255 },
1256 )?;
1257 let installed_version = kotlin_compiler_version(host, &kotlinc_path).await?;
1258 if kotlin_version_is_compatible(&installed_version, version) {
1259 Ok(kotlinc_path)
1260 } else {
1261 Err(AndroidToolchainError::KotlinVersionMismatch {
1262 installed: installed_version,
1263 required: version.to_owned(),
1264 })
1265 }
1266}
1267
1268const fn required_kotlin_version() -> &'static str {
1269 build_info::ANDROID_KOTLIN_VERSION
1270}
1271
1272async fn required_ndk_package_id(host: &Host) -> Result<String, AndroidToolchainError> {
1273 let package_id = format!("ndk;{}", ndk_version::ANDROID_NDK_VERSION);
1276 let available_packages = list_sdk_package_ids(host).await?;
1277 if available_packages
1278 .iter()
1279 .any(|candidate| candidate == &package_id)
1280 {
1281 Ok(package_id)
1282 } else {
1283 Err(AndroidToolchainError::NdkPackageUnavailable { package_id })
1284 }
1285}
1286
1287async fn latest_android_platform_package_id(host: &Host) -> Result<String, AndroidToolchainError> {
1288 list_sdk_package_ids(host)
1289 .await?
1290 .into_iter()
1291 .filter_map(|package_id| {
1292 parse_android_platform_api_level(&package_id).map(|api_level| (api_level, package_id))
1293 })
1294 .max_by_key(|(api_level, _)| *api_level)
1295 .map(|(_, package_id)| package_id)
1296 .ok_or(AndroidToolchainError::NoPlatformPackage)
1297}
1298
1299async fn latest_android_build_tools_package_id(
1300 host: &Host,
1301) -> Result<String, AndroidToolchainError> {
1302 let mut build_tools_packages = list_sdk_package_ids(host)
1303 .await?
1304 .into_iter()
1305 .filter(|package_id| parse_android_build_tools_version(package_id).is_some())
1306 .collect::<Vec<_>>();
1307 build_tools_packages.sort_by(|left, right| compare_sdk_package_ids(left, right));
1308 build_tools_packages.dedup();
1309
1310 build_tools_packages
1311 .pop()
1312 .ok_or(AndroidToolchainError::NoBuildToolsPackage)
1313}
1314
1315const fn rust_target_for_android_abi(abi: AndroidAbi) -> &'static str {
1316 match abi {
1317 AndroidAbi::Arm64V8a => "aarch64-linux-android",
1318 AndroidAbi::X86_64 => "x86_64-linux-android",
1319 AndroidAbi::ArmeabiV7a => "armv7-linux-androideabi",
1320 AndroidAbi::X86 => "i686-linux-android",
1321 }
1322}
1323
1324fn required_android_rust_targets_for_abis(abis: &[AndroidAbi]) -> Vec<String> {
1325 let mut targets = abis
1326 .iter()
1327 .map(|abi| rust_target_for_android_abi(*abi).to_owned())
1328 .collect::<Vec<_>>();
1329 targets.sort_unstable();
1330 targets.dedup();
1331 targets
1332}
1333
1334async fn installed_rustup_targets(host: &Host) -> Result<Vec<String>, CommandError> {
1335 let installed = host
1336 .run("rustup", ["target", "list", "--installed"])
1337 .await?;
1338 Ok(installed
1339 .lines()
1340 .map(str::trim)
1341 .filter(|line| !line.is_empty())
1342 .map(ToOwned::to_owned)
1343 .collect())
1344}
1345
1346fn missing_android_rust_targets(
1347 installed_targets: &[String],
1348 required_targets: &[String],
1349) -> Vec<String> {
1350 required_targets
1351 .iter()
1352 .filter(|target| {
1353 !installed_targets
1354 .iter()
1355 .any(|installed| installed == *target)
1356 })
1357 .cloned()
1358 .collect()
1359}
1360
1361impl AndroidSdk {
1362 #[must_use]
1364 pub fn detect_path(host: &Host) -> Option<PathBuf> {
1365 if let Some(configured) = configured_android_sdk_path(host)
1366 && configured.exists()
1367 && looks_like_android_sdk_root(&configured)
1368 {
1369 return Some(configured);
1370 }
1371
1372 if let Some(sdkmanager_path) = find_sdkmanager_on_host_path(host)
1373 && let Some(sdk_root) = derive_sdk_root_from_sdkmanager_path(&sdkmanager_path)
1374 && sdk_root.exists()
1375 && looks_like_android_sdk_root(&sdk_root)
1376 {
1377 return Some(sdk_root);
1378 }
1379
1380 None
1381 }
1382
1383 #[must_use]
1385 pub fn android_jar_path(host: &Host) -> Option<PathBuf> {
1386 let sdk_root = Self::detect_path(host)?;
1387 find_android_jar_in_sdk(&sdk_root)
1388 }
1389
1390 #[must_use]
1392 pub fn d8_jar_path(host: &Host) -> Option<PathBuf> {
1393 let sdk_root = Self::detect_path(host)?;
1394 find_d8_jar_in_sdk(&sdk_root)
1395 }
1396
1397 pub async fn sdkmanager_path(host: &Host) -> Option<PathBuf> {
1399 if let Some(sdk_root) = Self::detect_path(host) {
1400 for candidate in sdkmanager_candidates_under_sdk_root(&sdk_root) {
1401 if candidate.exists() {
1402 return Some(candidate);
1403 }
1404 }
1405 }
1406
1407 for name in sdkmanager_search_names() {
1408 if let Ok(path) = host.which(name).await {
1409 return Some(path);
1410 }
1411 }
1412
1413 find_sdkmanager_on_host_path(host)
1414 }
1415
1416 #[must_use]
1418 pub fn adb_path(host: &Host) -> Option<PathBuf> {
1419 let sdk_path = Self::detect_path(host)?;
1420 let adb = sdk_path
1421 .join("platform-tools")
1422 .join(if cfg!(target_os = "windows") {
1423 "adb.exe"
1424 } else {
1425 "adb"
1426 });
1427 if adb.exists() { Some(adb) } else { None }
1428 }
1429
1430 #[must_use]
1432 pub fn emulator_path(host: &Host) -> Option<PathBuf> {
1433 let sdk_path = Self::detect_path(host)?;
1434 let emulator = sdk_path
1435 .join("emulator")
1436 .join(if cfg!(target_os = "windows") {
1437 "emulator.exe"
1438 } else {
1439 "emulator"
1440 });
1441 if emulator.exists() {
1442 Some(emulator)
1443 } else {
1444 None
1445 }
1446 }
1447}
1448
1449#[derive(Debug, Clone, Default)]
1451pub struct AndroidSdkInstallation;
1452
1453#[derive(Debug, thiserror::Error)]
1455pub enum FailToInstallAndroidSdk {
1456 #[error("Homebrew not found. Install Homebrew first, then retry `water doctor --fix`.")]
1457 BrewNotFound,
1458 #[error(
1459 "winget is required for automatic Android Studio installation on Windows. Install App Installer and retry."
1460 )]
1461 WingetNotFound,
1462 #[error("Failed to install Android Studio via winget: {0}")]
1463 WingetInstallFailed(String),
1464 #[error("Failed to install Android SDK prerequisites: {0}")]
1465 InstallFailed(#[from] AndroidToolchainError),
1466 #[error(
1467 "Android SDK setup completed, but SDK root is still not detectable. Install Android command-line tools and set `ANDROID_SDK_ROOT`."
1468 )]
1469 PostInstallSetupRequired,
1470 #[error(
1471 "Automatic Android SDK command-line tools installation is unsupported on this host. Set up Android SDK manually and set `ANDROID_SDK_ROOT`."
1472 )]
1473 UnsupportedPlatform,
1474}
1475
1476impl Toolchain for AndroidSdk {
1477 type Installation = AndroidSdkInstallation;
1478
1479 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
1480 if Self::detect_path(host).is_some() {
1481 if Self::sdkmanager_path(host).await.is_some() {
1482 Ok(())
1483 } else {
1484 Err(ToolchainError::fixable(AndroidSdkInstallation))
1485 }
1486 } else if cfg!(target_os = "windows") {
1487 if host.which("winget").await.is_ok() {
1488 Err(ToolchainError::fixable(AndroidSdkInstallation))
1489 } else {
1490 Err(ToolchainError::unfixable(
1491 "Android SDK not found and winget is unavailable",
1492 format!(
1493 "Install Microsoft App Installer to provide winget, then retry `water doctor --fix`. {} {}",
1494 android_cmdline_tools_suggestion(),
1495 android_sdk_path_suggestion()
1496 ),
1497 ))
1498 }
1499 } else if cfg!(target_os = "macos") {
1500 if host.which("brew").await.is_ok() {
1501 Err(ToolchainError::fixable(AndroidSdkInstallation))
1502 } else {
1503 Err(ToolchainError::unfixable(
1504 "Android SDK not found and Homebrew is unavailable",
1505 format!(
1506 "Install Homebrew to enable automatic fixes, or install Android SDK manually. {} {}",
1507 android_cmdline_tools_suggestion(),
1508 android_sdk_path_suggestion()
1509 ),
1510 ))
1511 }
1512 } else if cfg!(target_os = "linux") {
1513 if configured_android_sdk_path(host).is_some() {
1514 Err(ToolchainError::fixable(AndroidSdkInstallation))
1515 } else {
1516 Err(ToolchainError::unfixable(
1517 "Android SDK root cannot be determined",
1518 format!(
1519 "Set `ANDROID_SDK_ROOT` to your Android SDK path, then retry `water doctor --fix`. {}",
1520 android_cmdline_tools_suggestion()
1521 ),
1522 ))
1523 }
1524 } else {
1525 Err(ToolchainError::unfixable(
1526 "Android SDK not found",
1527 format!(
1528 "{} {}",
1529 android_cmdline_tools_suggestion(),
1530 android_sdk_path_suggestion()
1531 ),
1532 ))
1533 }
1534 }
1535}
1536
1537impl Installation for AndroidSdkInstallation {
1538 type Error = FailToInstallAndroidSdk;
1539
1540 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
1541 if cfg!(target_os = "windows") {
1542 ensure_package_installed(host, "Google.AndroidStudio")
1543 .await
1544 .map_err(map_winget_error_for_android_sdk)?;
1545 } else if cfg!(target_os = "macos") {
1546 let brew = Brew::default();
1547 brew.check(host)
1548 .await
1549 .map_err(|_| FailToInstallAndroidSdk::BrewNotFound)?;
1550 brew.install_cask(host, "android-studio")
1551 .await
1552 .map_err(|source| {
1553 FailToInstallAndroidSdk::InstallFailed(AndroidToolchainError::from(source))
1554 })?;
1555 } else if cfg!(target_os = "linux") {
1556 } else {
1558 return Err(FailToInstallAndroidSdk::UnsupportedPlatform);
1559 }
1560
1561 let sdk_root = configured_android_sdk_path(host)
1562 .ok_or(AndroidToolchainError::SdkRootUnavailable)
1563 .map_err(FailToInstallAndroidSdk::InstallFailed)?;
1564 {
1565 let sdk_root = sdk_root.clone();
1566 smol::unblock(move || std::fs::create_dir_all(&sdk_root))
1567 .await
1568 .map_err(AndroidToolchainError::from)
1569 .map_err(FailToInstallAndroidSdk::InstallFailed)?;
1570 }
1571 ensure_cmdline_tools_available(&sdk_root)
1572 .await
1573 .map_err(FailToInstallAndroidSdk::InstallFailed)?;
1574
1575 if AndroidSdk::sdkmanager_path(host).await.is_some() {
1576 Ok(())
1577 } else {
1578 Err(FailToInstallAndroidSdk::PostInstallSetupRequired)
1579 }
1580 }
1581}
1582
1583fn map_winget_error_for_android_sdk(error: WingetInstallError) -> FailToInstallAndroidSdk {
1584 match error {
1585 WingetInstallError::WingetNotFound => FailToInstallAndroidSdk::WingetNotFound,
1586 WingetInstallError::CommandFailed(err) => {
1587 FailToInstallAndroidSdk::WingetInstallFailed(err.to_string())
1588 }
1589 WingetInstallError::NotInstalled { package_id } => {
1590 FailToInstallAndroidSdk::WingetInstallFailed(format!(
1591 "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
1592 ))
1593 }
1594 }
1595}
1596
1597#[derive(Debug, Clone, Copy, Default)]
1599pub enum AndroidPlatformToolsInstallation {
1600 #[default]
1602 SdkPackage,
1603 LinuxX86_64HostToolsCompat,
1605}
1606
1607#[derive(Debug, thiserror::Error)]
1609pub enum FailToInstallAndroidPlatformTools {
1610 #[error("Android SDK command-line tools (`sdkmanager`) not found.")]
1611 SdkManagerNotFound,
1612 #[error("Failed to install Android Platform-Tools via sdkmanager: {0}")]
1613 InstallFailed(#[from] AndroidToolchainError),
1614 #[error("Failed to install Android x86_64 host-tools compatibility packages: {0}")]
1615 HostToolsCompatFailed(#[from] LinuxPackageManagerError),
1616 #[error("{0}")]
1618 VerificationFailed(#[from] ToolchainError<AndroidPlatformToolsInstallation>),
1619 #[error("Android Platform-Tools (`adb`) is still missing after installation.")]
1620 StillMissing,
1621}
1622
1623impl Toolchain for AndroidPlatformTools {
1624 type Installation = AndroidPlatformToolsInstallation;
1625
1626 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
1627 if let Some(adb_path) = AndroidSdk::adb_path(host) {
1628 return verify_android_platform_tools_executable(host, &adb_path).await;
1629 }
1630
1631 if AndroidSdk::sdkmanager_path(host).await.is_some() {
1632 Err(ToolchainError::fixable(
1633 AndroidPlatformToolsInstallation::SdkPackage,
1634 ))
1635 } else {
1636 Err(ToolchainError::unfixable(
1637 "Android Platform-Tools (`adb`) not found",
1638 format!(
1639 "{} {}",
1640 android_platform_tools_suggestion(),
1641 android_cmdline_tools_suggestion()
1642 ),
1643 ))
1644 }
1645 }
1646}
1647
1648impl Installation for AndroidPlatformToolsInstallation {
1649 type Error = FailToInstallAndroidPlatformTools;
1650
1651 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
1652 if matches!(self, Self::LinuxX86_64HostToolsCompat) {
1653 return install_android_linux_x86_64_host_tools_compat(host)
1654 .await
1655 .map_err(FailToInstallAndroidPlatformTools::HostToolsCompatFailed);
1656 }
1657
1658 if AndroidSdk::sdkmanager_path(host).await.is_none() {
1659 return Err(FailToInstallAndroidPlatformTools::SdkManagerNotFound);
1660 }
1661
1662 install_android_sdk_package(host, "platform-tools")
1663 .await
1664 .map_err(FailToInstallAndroidPlatformTools::InstallFailed)?;
1665
1666 verify_android_platform_tools_after_install(host).await
1667 }
1668}
1669
1670async fn verify_android_platform_tools_after_install(
1671 host: &Host,
1672) -> Result<(), FailToInstallAndroidPlatformTools> {
1673 let adb_path =
1674 AndroidSdk::adb_path(host).ok_or(FailToInstallAndroidPlatformTools::StillMissing)?;
1675 match verify_android_platform_tools_executable(host, &adb_path).await {
1676 Ok(()) => Ok(()),
1677 Err(ToolchainError::Fixable(
1678 AndroidPlatformToolsInstallation::LinuxX86_64HostToolsCompat,
1679 )) => {
1680 install_android_linux_x86_64_host_tools_compat(host)
1681 .await
1682 .map_err(FailToInstallAndroidPlatformTools::HostToolsCompatFailed)?;
1683 verify_android_platform_tools_executable(host, &adb_path)
1684 .await
1685 .map_err(FailToInstallAndroidPlatformTools::VerificationFailed)
1686 }
1687 Err(error) => Err(FailToInstallAndroidPlatformTools::VerificationFailed(error)),
1688 }
1689}
1690
1691#[derive(Debug, Clone, Default)]
1693pub struct AndroidSdkPlatformsInstallation;
1694
1695#[derive(Debug, thiserror::Error)]
1697pub enum FailToInstallAndroidSdkPlatforms {
1698 #[error("Android SDK command-line tools (`sdkmanager`) not found.")]
1699 SdkManagerNotFound,
1700 #[error("Failed to install Android SDK platform package via sdkmanager: {0}")]
1701 InstallFailed(#[from] AndroidToolchainError),
1702 #[error("Android SDK platforms are still missing after installation.")]
1703 StillMissing,
1704}
1705
1706impl Toolchain for AndroidSdkPlatforms {
1707 type Installation = AndroidSdkPlatformsInstallation;
1708
1709 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
1710 if AndroidSdk::android_jar_path(host).is_some() {
1711 return Ok(());
1712 }
1713
1714 if AndroidSdk::sdkmanager_path(host).await.is_some() {
1715 Err(ToolchainError::fixable(AndroidSdkPlatformsInstallation))
1716 } else {
1717 Err(ToolchainError::unfixable(
1718 "Android SDK platforms are missing",
1719 format!(
1720 "{} {}",
1721 android_platforms_install_suggestion(),
1722 android_cmdline_tools_suggestion()
1723 ),
1724 ))
1725 }
1726 }
1727}
1728
1729impl Installation for AndroidSdkPlatformsInstallation {
1730 type Error = FailToInstallAndroidSdkPlatforms;
1731
1732 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
1733 if AndroidSdk::sdkmanager_path(host).await.is_none() {
1734 return Err(FailToInstallAndroidSdkPlatforms::SdkManagerNotFound);
1735 }
1736
1737 let platform_package = latest_android_platform_package_id(host)
1738 .await
1739 .map_err(FailToInstallAndroidSdkPlatforms::InstallFailed)?;
1740 install_android_sdk_package(host, &platform_package)
1741 .await
1742 .map_err(FailToInstallAndroidSdkPlatforms::InstallFailed)?;
1743
1744 if AndroidSdk::android_jar_path(host).is_some() {
1745 Ok(())
1746 } else {
1747 Err(FailToInstallAndroidSdkPlatforms::StillMissing)
1748 }
1749 }
1750}
1751
1752#[derive(Debug, Clone, Default)]
1754pub struct AndroidBuildToolsInstallation;
1755
1756#[derive(Debug, thiserror::Error)]
1758pub enum FailToInstallAndroidBuildTools {
1759 #[error("Android SDK command-line tools (`sdkmanager`) not found.")]
1760 SdkManagerNotFound,
1761 #[error("Failed to install Android SDK build-tools package via sdkmanager: {0}")]
1762 InstallFailed(#[from] AndroidToolchainError),
1763 #[error("Android SDK build-tools are still missing after installation.")]
1764 StillMissing,
1765}
1766
1767impl Toolchain for AndroidBuildTools {
1768 type Installation = AndroidBuildToolsInstallation;
1769
1770 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
1771 if AndroidSdk::d8_jar_path(host).is_some() {
1772 return Ok(());
1773 }
1774
1775 if AndroidSdk::sdkmanager_path(host).await.is_some() {
1776 Err(ToolchainError::fixable(AndroidBuildToolsInstallation))
1777 } else {
1778 Err(ToolchainError::unfixable(
1779 "Android SDK build-tools are missing",
1780 format!(
1781 "{} {}",
1782 android_build_tools_install_suggestion(),
1783 android_cmdline_tools_suggestion()
1784 ),
1785 ))
1786 }
1787 }
1788}
1789
1790impl Installation for AndroidBuildToolsInstallation {
1791 type Error = FailToInstallAndroidBuildTools;
1792
1793 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
1794 if AndroidSdk::sdkmanager_path(host).await.is_none() {
1795 return Err(FailToInstallAndroidBuildTools::SdkManagerNotFound);
1796 }
1797
1798 let build_tools_package = latest_android_build_tools_package_id(host)
1799 .await
1800 .map_err(FailToInstallAndroidBuildTools::InstallFailed)?;
1801 install_android_sdk_package(host, &build_tools_package)
1802 .await
1803 .map_err(FailToInstallAndroidBuildTools::InstallFailed)?;
1804
1805 if AndroidSdk::d8_jar_path(host).is_some() {
1806 Ok(())
1807 } else {
1808 Err(FailToInstallAndroidBuildTools::StillMissing)
1809 }
1810 }
1811}
1812
1813#[derive(Debug, Clone)]
1815pub struct AndroidRustTargetsInstallation {
1816 missing_targets: Vec<String>,
1817}
1818
1819impl AndroidRustTargetsInstallation {
1820 fn new(missing_targets: Vec<String>) -> Self {
1821 assert!(
1822 !missing_targets.is_empty(),
1823 "AndroidRustTargetsInstallation requires at least one missing target"
1824 );
1825 Self { missing_targets }
1826 }
1827}
1828
1829#[derive(Debug, thiserror::Error)]
1831pub enum FailToInstallAndroidRustTargets {
1832 #[error("rustup is required to install Android Rust targets but was not found in PATH.")]
1833 RustupNotFound,
1834 #[error("Failed to install Rust Android target `{target}`: {source}")]
1835 AddTarget {
1836 target: String,
1838 source: CommandError,
1840 },
1841 #[error("Failed to list installed Rust targets after installation: {0}")]
1842 QueryTargets(CommandError),
1843 #[error("Android Rust targets are still missing after installation: {missing_targets}")]
1844 StillMissing {
1845 missing_targets: String,
1847 },
1848}
1849
1850impl Toolchain for AndroidRustTargets {
1851 type Installation = AndroidRustTargetsInstallation;
1852
1853 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
1854 if host.which("rustup").await.is_err() {
1855 return Err(ToolchainError::unfixable(
1856 "rustup is not available, so Android Rust targets cannot be managed automatically",
1857 "Install rustup from https://rustup.rs, then run `water doctor --fix`.",
1858 ));
1859 }
1860
1861 let installed_targets = installed_rustup_targets(host).await.map_err(|error| {
1862 ToolchainError::unfixable(
1863 format!("Failed to query installed Rust targets: {error}"),
1864 "Run `rustup target list --installed`; if it fails, repair rustup with `rustup self update` or reinstall rustup.",
1865 )
1866 })?;
1867
1868 let missing_targets =
1869 missing_android_rust_targets(&installed_targets, &self.required_targets);
1870 if missing_targets.is_empty() {
1871 Ok(())
1872 } else {
1873 Err(ToolchainError::fixable(
1874 AndroidRustTargetsInstallation::new(missing_targets),
1875 ))
1876 }
1877 }
1878}
1879
1880impl Installation for AndroidRustTargetsInstallation {
1881 type Error = FailToInstallAndroidRustTargets;
1882
1883 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
1884 if host.which("rustup").await.is_err() {
1885 return Err(FailToInstallAndroidRustTargets::RustupNotFound);
1886 }
1887
1888 for target in &self.missing_targets {
1889 host.run("rustup", ["target", "add", target.as_str()])
1890 .await
1891 .map_err(|source| FailToInstallAndroidRustTargets::AddTarget {
1892 target: target.clone(),
1893 source,
1894 })?;
1895 }
1896
1897 let installed_targets = installed_rustup_targets(host)
1898 .await
1899 .map_err(FailToInstallAndroidRustTargets::QueryTargets)?;
1900 let still_missing = missing_android_rust_targets(&installed_targets, &self.missing_targets);
1901 if still_missing.is_empty() {
1902 Ok(())
1903 } else {
1904 Err(FailToInstallAndroidRustTargets::StillMissing {
1905 missing_targets: still_missing.join(", "),
1906 })
1907 }
1908 }
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913 use super::*;
1914
1915 #[test]
1916 fn requested_android_rust_targets_are_deduplicated() {
1917 let required = required_android_rust_targets_for_abis(&[
1918 AndroidAbi::Arm64V8a,
1919 AndroidAbi::Arm64V8a,
1920 AndroidAbi::X86_64,
1921 ]);
1922 assert_eq!(
1923 required,
1924 vec![
1925 "aarch64-linux-android".to_string(),
1926 "x86_64-linux-android".to_string()
1927 ]
1928 );
1929 }
1930
1931 #[test]
1932 fn missing_android_targets_only_consider_requested_abis() {
1933 let required = required_android_rust_targets_for_abis(&[AndroidAbi::Arm64V8a]);
1934 let installed = vec![
1935 "aarch64-linux-android".to_string(),
1936 "armv7-linux-androideabi".to_string(),
1937 "x86_64-linux-android".to_string(),
1938 ];
1939 assert_eq!(
1940 missing_android_rust_targets(&installed, &required),
1941 [] as [String; 0]
1942 );
1943 }
1944
1945 #[test]
1946 fn missing_android_targets_report_only_requested_missing_entries() {
1947 let required =
1948 required_android_rust_targets_for_abis(&[AndroidAbi::Arm64V8a, AndroidAbi::X86]);
1949 let installed = vec!["aarch64-linux-android".to_string()];
1950 assert_eq!(
1951 missing_android_rust_targets(&installed, &required),
1952 vec!["i686-linux-android".to_string()]
1953 );
1954 }
1955
1956 #[test]
1957 fn select_installed_ndk_path_prefers_required_version_over_latest_directory() {
1958 let tempdir = tempfile::tempdir().unwrap();
1959 let ndk_dir = tempdir.path().join("ndk");
1960 std::fs::create_dir_all(ndk_dir.join("29.0.14206865")).unwrap();
1961 std::fs::create_dir_all(ndk_dir.join("30.0.14904198")).unwrap();
1962
1963 assert_eq!(
1964 select_installed_ndk_path(&ndk_dir, Some("29.0.14206865")),
1965 Some(ndk_dir.join("29.0.14206865"))
1966 );
1967 }
1968
1969 #[test]
1970 fn ndk_path_for_package_id_rejects_non_ndk_package_ids() {
1971 let error =
1972 ndk_path_for_package_id(Path::new("/tmp/android-sdk"), "platform-tools").unwrap_err();
1973 assert!(
1974 error
1975 .to_string()
1976 .contains("Invalid Android NDK package id `platform-tools`")
1977 );
1978 }
1979
1980 #[test]
1981 fn parse_kotlinc_version_output_extracts_version_token() {
1982 assert_eq!(
1983 parse_kotlinc_version_output("info: kotlinc-jvm 1.3-SNAPSHOT (JRE 21.0.10+7)"),
1984 Some("1.3-SNAPSHOT".to_string())
1985 );
1986 }
1987
1988 #[test]
1989 fn parse_kotlinc_version_output_ignores_jdk_warning_prefix() {
1990 let output = "OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.\ninfo: kotlinc-jvm 1.3-SNAPSHOT (JRE 21.0.10+7)";
1991 assert_eq!(
1992 parse_kotlinc_version_output(output),
1993 Some("1.3-SNAPSHOT".to_string())
1994 );
1995 }
1996
1997 #[test]
1998 fn kotlin_version_compatibility_uses_backend_minimum() {
1999 assert!(kotlin_version_is_compatible("2.0.21", "2.0.21"));
2000 assert!(kotlin_version_is_compatible("2.1.0", "2.0.21"));
2001 assert!(!kotlin_version_is_compatible("1.9.24", "2.0.21"));
2002 }
2003
2004 #[test]
2005 fn parse_sdkmanager_proxy_config_maps_http_proxy() {
2006 assert_eq!(
2007 parse_sdkmanager_proxy_config("http://host.docker.internal:7891").unwrap(),
2008 SdkManagerProxyConfig {
2009 proxy_type: SdkManagerProxyType::Http,
2010 host: "host.docker.internal".to_string(),
2011 port: 7891,
2012 }
2013 );
2014 }
2015
2016 #[test]
2017 fn parse_sdkmanager_proxy_config_maps_socks5h_proxy() {
2018 assert_eq!(
2019 parse_sdkmanager_proxy_config("socks5h://host.docker.internal:7890").unwrap(),
2020 SdkManagerProxyConfig {
2021 proxy_type: SdkManagerProxyType::Socks,
2022 host: "host.docker.internal".to_string(),
2023 port: 7890,
2024 }
2025 );
2026 }
2027}
2028
2029fn windows_jdk_candidates_from_root(root: &Path) -> Vec<PathBuf> {
2030 let Ok(entries) = std::fs::read_dir(root) else {
2031 return Vec::new();
2032 };
2033
2034 let mut candidates = entries
2035 .filter_map(std::result::Result::ok)
2036 .map(|entry| entry.path())
2037 .filter(|path| path.is_dir())
2038 .filter_map(|path| {
2039 let name = path.file_name()?.to_string_lossy().to_ascii_lowercase();
2040 if !name.starts_with("jdk") {
2041 return None;
2042 }
2043 let java_path = path.join("bin/java.exe");
2044 if java_path.exists() {
2045 Some(java_path)
2046 } else {
2047 None
2048 }
2049 })
2050 .collect::<Vec<_>>();
2051 candidates.sort();
2052 candidates
2053}
2054
2055fn detect_windows_jdk_java_path(host: &Host) -> Option<PathBuf> {
2056 let program_files = host.env_string("ProgramFiles")?;
2057 let roots = [
2058 PathBuf::from(&program_files).join("Microsoft"),
2059 PathBuf::from(&program_files).join("Eclipse Adoptium"),
2060 PathBuf::from(&program_files).join("Java"),
2061 ];
2062
2063 let mut matches = roots
2064 .iter()
2065 .flat_map(|root| windows_jdk_candidates_from_root(root))
2066 .collect::<Vec<_>>();
2067 matches.sort();
2068 matches.pop()
2069}
2070
2071async fn verify_android_platform_tools_executable(
2072 host: &Host,
2073 adb_path: &Path,
2074) -> Result<(), ToolchainError<AndroidPlatformToolsInstallation>> {
2075 let output = host.output(adb_path, ["version"]).await.map_err(|error| {
2076 ToolchainError::unfixable(
2077 format!(
2078 "Android Platform-Tools (`adb`) exists but failed to spawn on this host: {error}"
2079 ),
2080 format!(
2081 "Ensure the Android Platform-Tools binary at `{}` can start on this host, then retry `water doctor`.",
2082 adb_path.display()
2083 ),
2084 )
2085 })?;
2086
2087 if output.status.success() {
2088 return Ok(());
2089 }
2090
2091 let stderr = String::from_utf8_lossy(&output.stderr);
2092 let stdout = String::from_utf8_lossy(&output.stdout);
2093 let detail = if !stderr.trim().is_empty() {
2094 stderr.trim().to_owned()
2095 } else if !stdout.trim().is_empty() {
2096 stdout.trim().to_owned()
2097 } else {
2098 format!("exit status {}", output.status)
2099 };
2100 if needs_linux_x86_64_host_tools_compat(&detail) {
2101 return Err(ToolchainError::fixable(
2102 AndroidPlatformToolsInstallation::LinuxX86_64HostToolsCompat,
2103 ));
2104 }
2105
2106 let suggestion = if detail.contains("ld-linux-x86-64.so.2") {
2107 format!(
2108 "Install x86_64 userspace compatibility libraries for this Linux host, then retry `water doctor --fix`. Required packages on Debian/Ubuntu: {}.",
2109 ANDROID_LINUX_X86_64_HOST_TOOLS_COMPAT_PACKAGES.join(" ")
2110 )
2111 } else {
2112 format!(
2113 "Ensure the Android Platform-Tools binary at `{}` can execute on this host, then retry `water doctor`.",
2114 adb_path.display()
2115 )
2116 };
2117 Err(ToolchainError::unfixable(
2118 format!(
2119 "Android Platform-Tools (`adb`) exists but failed to execute on this host: {detail}"
2120 ),
2121 suggestion,
2122 ))
2123}
2124
2125fn ndk_host_clang_path(ndk_path: &Path) -> Option<PathBuf> {
2132 let wrapper_suffix = if cfg!(target_os = "windows") {
2133 "-clang.cmd"
2134 } else {
2135 "-clang"
2136 };
2137 let clang_wrapper = |bin_dir: &Path| {
2138 std::fs::read_dir(bin_dir)
2139 .ok()?
2140 .filter_map(Result::ok)
2141 .filter_map(|entry| {
2142 let api_level = entry
2143 .file_name()
2144 .to_str()?
2145 .strip_prefix("aarch64-linux-android")?
2146 .strip_suffix(wrapper_suffix)?
2147 .parse::<u32>()
2148 .ok()?;
2149 Some((api_level, entry.path()))
2150 })
2151 .min_by_key(|(api_level, _)| *api_level)
2152 .map(|(_, path)| path)
2153 };
2154
2155 let prebuilt_dir = ndk_path.join("toolchains/llvm/prebuilt");
2156 let entries = std::fs::read_dir(&prebuilt_dir).ok()?;
2157 let mut candidates = entries
2158 .filter_map(Result::ok)
2159 .map(|entry| entry.path())
2160 .filter(|path| path.is_dir())
2161 .collect::<Vec<_>>();
2162 candidates.sort();
2163
2164 candidates
2165 .iter()
2166 .find_map(|candidate| clang_wrapper(&candidate.join("bin")))
2167}
2168
2169async fn verify_ndk_host_toolchain_executable(
2170 host: &Host,
2171 ndk_path: &Path,
2172) -> Result<(), ToolchainError<AndroidNdkInstallation>> {
2173 let clang_path = ndk_host_clang_path(ndk_path).ok_or_else(|| {
2174 ToolchainError::unfixable(
2175 "Android NDK toolchain is incomplete (no `aarch64-linux-android*-clang` wrapper was found under toolchains/llvm/prebuilt).",
2176 android_ndk_install_suggestion(),
2177 )
2178 })?;
2179
2180 let probe_file = smol::unblock(|| -> std::io::Result<tempfile::NamedTempFile> {
2183 use std::io::Write as _;
2184 let mut file = tempfile::Builder::new()
2185 .prefix("waterui-android-ndk-probe-")
2186 .suffix(".c")
2187 .tempfile()?;
2188 file.write_all(b"int main(void) { return 0; }\n")?;
2189 file.flush()?;
2190 Ok(file)
2191 })
2192 .await
2193 .map_err(|error| {
2194 ToolchainError::unfixable(
2195 format!("Failed to create the Android NDK probe source: {error}"),
2196 "Ensure the temporary directory is writable, then retry `water doctor`.",
2197 )
2198 })?;
2199 let probe_source = probe_file.path().to_path_buf();
2200
2201 let probe_output = if cfg!(target_os = "windows") {
2202 PathBuf::from("NUL")
2203 } else {
2204 PathBuf::from("/dev/null")
2205 };
2206 let result = host
2207 .output(
2208 &clang_path,
2209 [
2210 OsString::from("-x"),
2211 OsString::from("c"),
2212 OsString::from("-c"),
2213 probe_source.into_os_string(),
2214 OsString::from("-o"),
2215 probe_output.into_os_string(),
2216 ],
2217 )
2218 .await;
2219 let output = result.map_err(|error| {
2220 ToolchainError::unfixable(
2221 format!(
2222 "Android NDK toolchain exists but failed to spawn on this host: {error}"
2223 ),
2224 format!(
2225 "Ensure the Android NDK toolchain binary `{}` can start on this host, then retry packaging.",
2226 clang_path.display()
2227 ),
2228 )
2229 })?;
2230
2231 if output.status.success() {
2232 return Ok(());
2233 }
2234
2235 let stderr = String::from_utf8_lossy(&output.stderr);
2236 let stdout = String::from_utf8_lossy(&output.stdout);
2237 let detail = if !stderr.trim().is_empty() {
2238 stderr.trim().to_owned()
2239 } else if !stdout.trim().is_empty() {
2240 stdout.trim().to_owned()
2241 } else {
2242 format!("exit status {}", output.status)
2243 };
2244 if needs_linux_x86_64_host_tools_compat(&detail) {
2245 return Err(ToolchainError::fixable(
2246 AndroidNdkInstallation::LinuxX86_64HostToolsCompat,
2247 ));
2248 }
2249
2250 let suggestion = if detail.contains("ld-linux-x86-64.so.2") {
2251 format!(
2252 "Install x86_64 userspace compatibility libraries for this Linux host, then retry `water doctor --fix`. Required packages on Debian/Ubuntu: {}.",
2253 ANDROID_LINUX_X86_64_HOST_TOOLS_COMPAT_PACKAGES.join(" ")
2254 )
2255 } else {
2256 format!(
2257 "Ensure the Android NDK toolchain binaries under `{}` can execute on this host, then retry packaging.",
2258 clang_path.display()
2259 )
2260 };
2261 Err(ToolchainError::unfixable(
2262 format!("Android NDK toolchain exists but failed to execute on this host: {detail}"),
2263 suggestion,
2264 ))
2265}
2266
2267impl Java {
2268 pub async fn detect_path(host: &Host) -> Option<PathBuf> {
2275 if cfg!(target_os = "macos") {
2276 const ANDROID_STUDIO_JBRS: &[&str] = &[
2277 "Android Studio.app/Contents/jbr/Contents/Home/bin/java",
2278 "Android Studio Preview.app/Contents/jbr/Contents/Home/bin/java",
2279 ];
2280 for app_dir in host.app_dirs() {
2281 for relative in ANDROID_STUDIO_JBRS {
2282 let java_path = app_dir.join(relative);
2283 if java_path.exists() {
2284 return Some(java_path);
2285 }
2286 }
2287 }
2288 }
2289
2290 if cfg!(target_os = "linux")
2291 && let Some(home) = host.home_dir()
2292 {
2293 let paths = [
2294 home.join(".local/share/JetBrains/Toolbox/apps/android-studio/jbr/bin/java"),
2295 home.join("android-studio/jbr/bin/java"),
2296 ];
2297 for java_path in paths {
2298 if java_path.exists() {
2299 return Some(java_path);
2300 }
2301 }
2302 }
2303
2304 if cfg!(target_os = "windows") {
2305 if let Some(program_files) = host.env_string("ProgramFiles") {
2306 let java_path =
2307 PathBuf::from(&program_files).join("Android/Android Studio/jbr/bin/java.exe");
2308 if java_path.exists() {
2309 return Some(java_path);
2310 }
2311 }
2312
2313 if let Some(java_path) = detect_windows_jdk_java_path(host) {
2314 return Some(java_path);
2315 }
2316 }
2317
2318 if let Some(home) = host.env_string("JAVA_HOME") {
2319 let java_path = PathBuf::from(home)
2320 .join("bin")
2321 .join(if cfg!(target_os = "windows") {
2322 "java.exe"
2323 } else {
2324 "java"
2325 });
2326 if java_path.exists() {
2327 return Some(java_path);
2328 }
2329 }
2330
2331 host.which("java").await.ok()
2332 }
2333
2334 pub async fn detect_home(host: &Host) -> Option<PathBuf> {
2336 let java_path = Self::detect_path(host).await?;
2337 java_path.parent()?.parent().map(PathBuf::from)
2338 }
2339}
2340
2341#[derive(Debug, Clone, Default)]
2343pub struct JavaInstallation;
2344
2345#[derive(Debug, thiserror::Error)]
2347pub enum FailToInstallJava {
2348 #[error("Homebrew not found. Install Homebrew first, then retry `water doctor --fix`.")]
2349 BrewNotFound,
2350 #[error(
2351 "winget is required for automatic Java installation on Windows. Install App Installer and retry."
2352 )]
2353 WingetNotFound,
2354 #[error("Failed to install Java via winget: {0}")]
2355 WingetInstallFailed(String),
2356 #[error(
2357 "No supported Linux package manager found (apt-get, dnf, pacman, zypper, apk). Install Java manually."
2358 )]
2359 UnsupportedPackageManager,
2360 #[error("Failed to install Java: {0}")]
2361 InstallFailed(#[from] CommandError),
2362 #[error(
2363 "Automatic Java installation is not supported on this host. Install a JDK manually and set `JAVA_HOME`."
2364 )]
2365 UnsupportedPlatform,
2366}
2367
2368impl Toolchain for Java {
2369 type Installation = JavaInstallation;
2370
2371 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
2372 if Self::detect_path(host).await.is_some() {
2373 Ok(())
2374 } else if cfg!(target_os = "windows") {
2375 if host.which("winget").await.is_ok() {
2376 Err(ToolchainError::fixable(JavaInstallation))
2377 } else {
2378 Err(ToolchainError::unfixable(
2379 "Java runtime not found and winget is unavailable",
2380 "Install Microsoft App Installer to provide winget, or install a JDK manually and set `JAVA_HOME`.",
2381 ))
2382 }
2383 } else if cfg!(target_os = "macos") {
2384 if host.which("brew").await.is_ok() {
2385 Err(ToolchainError::fixable(JavaInstallation))
2386 } else {
2387 Err(ToolchainError::unfixable(
2388 "Java runtime not found and Homebrew is unavailable",
2389 "Install Homebrew to enable automatic fixes, or install a JDK manually and set `JAVA_HOME`.",
2390 ))
2391 }
2392 } else if cfg!(target_os = "linux") {
2393 if has_supported_package_manager(host).await {
2394 Err(ToolchainError::fixable(JavaInstallation))
2395 } else {
2396 Err(ToolchainError::unfixable(
2397 "Java runtime not found and no supported package manager was detected",
2398 "Install a JDK manually and set `JAVA_HOME`, then retry.",
2399 ))
2400 }
2401 } else {
2402 Err(ToolchainError::unfixable(
2403 "Java runtime not found",
2404 "Install a JDK manually and set `JAVA_HOME`, then retry.",
2405 ))
2406 }
2407 }
2408}
2409
2410impl Installation for JavaInstallation {
2411 type Error = FailToInstallJava;
2412
2413 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
2414 if cfg!(target_os = "windows") {
2415 ensure_package_installed(host, "Microsoft.OpenJDK.21")
2416 .await
2417 .map_err(map_winget_error_for_java)
2418 } else if cfg!(target_os = "macos") {
2419 let brew = Brew::default();
2420 brew.check(host)
2421 .await
2422 .map_err(|_| FailToInstallJava::BrewNotFound)?;
2423 brew.install_cask(host, "temurin")
2424 .await
2425 .map_err(FailToInstallJava::InstallFailed)
2426 } else if cfg!(target_os = "linux") {
2427 install_java_jdk(host)
2428 .await
2429 .map_err(map_linux_error_for_java)
2430 } else {
2431 Err(FailToInstallJava::UnsupportedPlatform)
2432 }
2433 }
2434}
2435
2436fn map_linux_error_for_java(error: LinuxPackageManagerError) -> FailToInstallJava {
2437 match error {
2438 LinuxPackageManagerError::UnsupportedPackageManager => {
2439 FailToInstallJava::UnsupportedPackageManager
2440 }
2441 LinuxPackageManagerError::Command(source) => FailToInstallJava::InstallFailed(source),
2442 }
2443}
2444
2445fn map_winget_error_for_java(error: WingetInstallError) -> FailToInstallJava {
2446 match error {
2447 WingetInstallError::WingetNotFound => FailToInstallJava::WingetNotFound,
2448 WingetInstallError::CommandFailed(err) => {
2449 FailToInstallJava::WingetInstallFailed(err.to_string())
2450 }
2451 WingetInstallError::NotInstalled { package_id } => {
2452 FailToInstallJava::WingetInstallFailed(format!(
2453 "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
2454 ))
2455 }
2456 }
2457}
2458
2459impl Kotlin {
2460 pub async fn detect_path(host: &Host) -> Option<PathBuf> {
2462 let required_version = required_kotlin_version();
2463 let mut candidates = Vec::new();
2464
2465 if let Some(home) = host.env_string("KOTLIN_HOME")
2466 && let Some(kotlinc_path) = kotlin_executable_from_home(&PathBuf::from(&home))
2467 {
2468 candidates.push(kotlinc_path);
2469 }
2470
2471 if cfg!(target_os = "macos") {
2472 const ANDROID_STUDIO_KOTLINS: &[&str] = &[
2473 "Android Studio.app/Contents/plugins/Kotlin/kotlinc/bin/kotlinc",
2474 "Android Studio Preview.app/Contents/plugins/Kotlin/kotlinc/bin/kotlinc",
2475 ];
2476 for app_dir in host.app_dirs() {
2477 for relative in ANDROID_STUDIO_KOTLINS {
2478 let kotlinc_path = app_dir.join(relative);
2479 if kotlinc_path.exists() {
2480 candidates.push(kotlinc_path);
2481 }
2482 }
2483 }
2484 }
2485
2486 if cfg!(target_os = "linux")
2487 && let Some(home) = host.home_dir()
2488 {
2489 let paths = [
2490 home.join(
2491 ".local/share/JetBrains/Toolbox/apps/android-studio/plugins/Kotlin/kotlinc/bin/kotlinc",
2492 ),
2493 home.join("android-studio/plugins/Kotlin/kotlinc/bin/kotlinc"),
2494 ];
2495 for kotlinc_path in paths {
2496 if kotlinc_path.exists() {
2497 candidates.push(kotlinc_path);
2498 }
2499 }
2500 }
2501
2502 if cfg!(target_os = "windows")
2503 && let Some(program_files) = host.env_string("ProgramFiles")
2504 {
2505 let kotlinc_path = PathBuf::from(&program_files)
2506 .join("Android/Android Studio/plugins/Kotlin/kotlinc/bin/kotlinc.bat");
2507 if kotlinc_path.exists() {
2508 candidates.push(kotlinc_path);
2509 }
2510 }
2511
2512 if let Ok(managed_home) = managed_kotlin_home(host, required_version)
2513 && let Some(kotlinc_path) = kotlin_executable_from_home(&managed_home)
2514 {
2515 candidates.push(kotlinc_path);
2516 }
2517
2518 if let Ok(path) = host.which("kotlinc").await {
2519 candidates.push(path);
2520 }
2521
2522 candidates.dedup();
2523 for candidate in candidates {
2524 let Ok(installed_version) = kotlin_compiler_version(host, &candidate).await else {
2525 continue;
2526 };
2527 if kotlin_version_is_compatible(&installed_version, required_version) {
2528 return Some(candidate);
2529 }
2530 }
2531
2532 None
2533 }
2534}
2535
2536#[derive(Debug)]
2538pub struct KotlinInstallation;
2539
2540#[derive(Debug, thiserror::Error)]
2542pub enum FailToInstallKotlin {
2543 #[error("Failed to install Kotlin compiler: {0}")]
2544 InstallFailed(#[from] AndroidToolchainError),
2545 #[error("Kotlin compiler is still missing after installation.")]
2546 StillMissing,
2547}
2548
2549impl Toolchain for Kotlin {
2550 type Installation = KotlinInstallation;
2551
2552 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
2553 let kotlinc_path = Self::detect_path(host)
2554 .await
2555 .ok_or_else(|| ToolchainError::fixable(KotlinInstallation))?;
2556 #[cfg(unix)]
2561 {
2562 Self::reject_non_executable(kotlinc_path).await
2563 }
2564 #[cfg(not(unix))]
2565 {
2566 drop(kotlinc_path);
2567 Ok(())
2568 }
2569 }
2570}
2571
2572impl Kotlin {
2573 #[cfg(unix)]
2580 async fn reject_non_executable(
2581 kotlinc_path: PathBuf,
2582 ) -> Result<(), ToolchainError<KotlinInstallation>> {
2583 use std::os::unix::fs::PermissionsExt as _;
2584
2585 let Ok(metadata) = smol::unblock({
2586 let kotlinc_path = kotlinc_path.clone();
2587 move || std::fs::metadata(&kotlinc_path)
2588 })
2589 .await
2590 else {
2591 return Ok(());
2592 };
2593 if metadata.permissions().mode() & 0o111 == 0 {
2594 return Err(ToolchainError::unfixable(
2595 "Kotlin compiler (kotlinc) is not executable",
2596 format!(
2597 "The kotlinc script at '{}' does not have execute permission. Fix it with: sudo chmod +x '{}'",
2598 kotlinc_path.display(),
2599 kotlinc_path.display()
2600 ),
2601 ));
2602 }
2603 Ok(())
2604 }
2605}
2606
2607impl Installation for KotlinInstallation {
2608 type Error = FailToInstallKotlin;
2609
2610 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
2611 let required_version = required_kotlin_version();
2612 install_managed_kotlin_compiler(host, required_version)
2613 .await
2614 .map_err(FailToInstallKotlin::InstallFailed)?;
2615 if Kotlin::detect_path(host).await.is_some() {
2616 Ok(())
2617 } else {
2618 Err(FailToInstallKotlin::StillMissing)
2619 }
2620 }
2621}
2622
2623impl AndroidNdk {
2624 #[must_use]
2626 pub fn detect_path(host: &Host) -> Option<PathBuf> {
2627 if let Some(ndk_root) = host.env_string("ANDROID_NDK_ROOT") {
2628 let ndk_path = PathBuf::from(ndk_root);
2629 if ndk_path.exists() {
2630 return Some(ndk_path);
2631 }
2632 }
2633
2634 if let Some(ndk_home) = host.env_string("ANDROID_NDK_HOME") {
2635 let ndk_path = PathBuf::from(ndk_home);
2636 if ndk_path.exists() {
2637 return Some(ndk_path);
2638 }
2639 }
2640
2641 let sdk_path = AndroidSdk::detect_path(host)?;
2642 let ndk_dir = sdk_path.join("ndk");
2643 select_installed_ndk_path(&ndk_dir, Some(ndk_version::ANDROID_NDK_VERSION))
2644 }
2645}
2646
2647#[derive(Debug, Clone, Copy, Default)]
2649pub enum AndroidNdkInstallation {
2650 #[default]
2652 SdkPackage,
2653 LinuxX86_64HostToolsCompat,
2655}
2656
2657#[derive(Debug, thiserror::Error)]
2659pub enum FailToInstallAndroidNdk {
2660 #[error("Android SDK command-line tools (`sdkmanager`) not found.")]
2661 SdkManagerNotFound,
2662 #[error("Failed to install Android NDK via sdkmanager: {0}")]
2663 InstallFailed(#[from] AndroidToolchainError),
2664 #[error("Failed to install Android x86_64 host-tools compatibility packages: {0}")]
2665 HostToolsCompatFailed(#[from] LinuxPackageManagerError),
2666 #[error("{0}")]
2668 VerificationFailed(#[from] ToolchainError<AndroidNdkInstallation>),
2669 #[error("Android NDK is still missing after installation.")]
2670 StillMissing,
2671 #[error("Android NDK is installed but incomplete (`toolchains/llvm/prebuilt` is missing).")]
2672 Incomplete,
2673}
2674
2675impl Toolchain for AndroidNdk {
2676 type Installation = AndroidNdkInstallation;
2677
2678 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
2679 if let Some(ndk_path) = Self::detect_path(host) {
2680 let llvm_dir = ndk_path.join("toolchains/llvm/prebuilt");
2681 if llvm_dir.exists() {
2682 return verify_ndk_host_toolchain_executable(host, &ndk_path).await;
2683 }
2684
2685 if AndroidSdk::sdkmanager_path(host).await.is_some() {
2686 return Err(ToolchainError::fixable(AndroidNdkInstallation::SdkPackage));
2687 }
2688
2689 return Err(ToolchainError::unfixable(
2690 "Android NDK is installed but incomplete",
2691 android_ndk_install_suggestion(),
2692 ));
2693 }
2694
2695 if AndroidSdk::sdkmanager_path(host).await.is_some() {
2696 Err(ToolchainError::fixable(AndroidNdkInstallation::SdkPackage))
2697 } else {
2698 Err(ToolchainError::unfixable(
2699 "Android NDK not found",
2700 format!(
2701 "{} {}",
2702 android_ndk_install_suggestion(),
2703 android_cmdline_tools_suggestion()
2704 ),
2705 ))
2706 }
2707 }
2708}
2709
2710impl Installation for AndroidNdkInstallation {
2711 type Error = FailToInstallAndroidNdk;
2712
2713 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
2714 if matches!(self, Self::LinuxX86_64HostToolsCompat) {
2715 return install_android_linux_x86_64_host_tools_compat(host)
2716 .await
2717 .map_err(FailToInstallAndroidNdk::HostToolsCompatFailed);
2718 }
2719
2720 if AndroidSdk::sdkmanager_path(host).await.is_none() {
2721 return Err(FailToInstallAndroidNdk::SdkManagerNotFound);
2722 }
2723
2724 let (_, sdk_root) = resolve_sdkmanager_and_root(host)
2725 .await
2726 .map_err(FailToInstallAndroidNdk::InstallFailed)?;
2727 let ndk_package = required_ndk_package_id(host)
2728 .await
2729 .map_err(FailToInstallAndroidNdk::InstallFailed)?;
2730 let ndk_path = ndk_path_for_package_id(&sdk_root, &ndk_package)
2731 .map_err(FailToInstallAndroidNdk::InstallFailed)?;
2732 if ndk_path.exists() && !ndk_layout_is_complete(&ndk_path) {
2733 remove_directory_if_exists(&ndk_path)
2734 .await
2735 .map_err(AndroidToolchainError::from)
2736 .map_err(FailToInstallAndroidNdk::InstallFailed)?;
2737 }
2738 install_android_sdk_package(host, &ndk_package)
2739 .await
2740 .map_err(FailToInstallAndroidNdk::InstallFailed)?;
2741
2742 if !ndk_path.exists() {
2743 return Err(FailToInstallAndroidNdk::StillMissing);
2744 }
2745
2746 if !ndk_layout_is_complete(&ndk_path) {
2747 return Err(FailToInstallAndroidNdk::Incomplete);
2748 }
2749
2750 verify_android_ndk_after_install(host, &ndk_path).await
2751 }
2752}
2753
2754async fn verify_android_ndk_after_install(
2755 host: &Host,
2756 ndk_path: &Path,
2757) -> Result<(), FailToInstallAndroidNdk> {
2758 match verify_ndk_host_toolchain_executable(host, ndk_path).await {
2759 Ok(()) => Ok(()),
2760 Err(ToolchainError::Fixable(AndroidNdkInstallation::LinuxX86_64HostToolsCompat)) => {
2761 install_android_linux_x86_64_host_tools_compat(host)
2762 .await
2763 .map_err(FailToInstallAndroidNdk::HostToolsCompatFailed)?;
2764 verify_ndk_host_toolchain_executable(host, ndk_path)
2765 .await
2766 .map_err(FailToInstallAndroidNdk::VerificationFailed)
2767 }
2768 Err(error) => Err(FailToInstallAndroidNdk::VerificationFailed(error)),
2769 }
2770}
2771
2772#[cfg(test)]
2773mod host_tests {
2774 use std::path::Path;
2775
2776 use super::{
2777 AndroidBuildTools, AndroidNdk, AndroidPlatformTools, AndroidRustTargets, AndroidSdk,
2778 AndroidSdkPlatforms, Java, Kotlin, latest_android_platform_package_id,
2779 parse_android_platform_api_level, parse_android_version_pair, required_kotlin_version,
2780 };
2781 use crate::toolchain::testing::TestMachine;
2782 use crate::toolchain::{Host, Toolchain, ToolchainError};
2783
2784 fn sdk_host(machine: &TestMachine, sdk: &Path) -> Host {
2786 machine.host([(
2787 String::from("ANDROID_SDK_ROOT"),
2788 sdk.as_os_str().to_os_string(),
2789 )])
2790 }
2791
2792 fn sdk_machine() -> (TestMachine, Host) {
2795 let machine = TestMachine::new();
2796 let sdk = machine.install_android_sdk();
2797 let host = sdk_host(&machine, &sdk);
2798 (machine, host)
2799 }
2800
2801 #[test]
2804 fn platform_api_level_parser_accepts_minor_versioned_packages() {
2805 assert_eq!(
2806 parse_android_platform_api_level("platforms;android-37.0"),
2807 Some((37, 0)),
2808 "`platforms;android-37.0` must parse to API 37.0 (#633)"
2809 );
2810 assert_eq!(
2811 parse_android_platform_api_level("platforms;android-36"),
2812 Some((36, 0))
2813 );
2814 assert_eq!(
2815 parse_android_platform_api_level("platforms;android-Tiramisu"),
2816 None
2817 );
2818 assert_eq!(parse_android_platform_api_level("build-tools;37.0.0"), None);
2819 }
2820
2821 #[test]
2822 fn android_version_pair_orders_minor_within_major() {
2823 assert_eq!(parse_android_version_pair("37.0"), Some((37, 0)));
2824 assert_eq!(parse_android_version_pair("37.1"), Some((37, 1)));
2825 assert_eq!(parse_android_version_pair("36"), Some((36, 0)));
2826 assert_eq!(parse_android_version_pair("android-37"), None);
2827 assert_eq!(parse_android_version_pair(""), None);
2828 assert_eq!(parse_android_version_pair("36.1.2"), None);
2829 assert!(parse_android_version_pair("36") < parse_android_version_pair("36.1"));
2831 assert!(parse_android_version_pair("36.1") < parse_android_version_pair("37.0"));
2832 assert!(parse_android_version_pair("37.0") < parse_android_version_pair("37.1"));
2833 }
2834
2835 #[test]
2836 fn sdkmanager_list_prefers_latest_platform_including_minor_versions() {
2837 let (machine, host) = sdk_machine();
2838 machine.install("java");
2839 machine.respond(
2840 "SDKMANAGER_LIST",
2841 include_str!("testdata/sdkmanager_list.txt"),
2842 );
2843 let package = smol::block_on(latest_android_platform_package_id(&host))
2844 .expect("sdkmanager --list transcript must yield a platform package");
2845 assert_eq!(
2846 package, "platforms;android-37.1",
2847 "android-37.1 outranks android-37.0 and android-36.1 (#633)"
2848 );
2849 }
2850
2851 #[test]
2852 fn android_jar_prefers_minor_versioned_platform_dir() {
2853 let (machine, host) = sdk_machine();
2857 machine.install_android_platform("android-36");
2858 machine.install_android_platform("android-36.1");
2859 machine.install_android_platform("android-37.0");
2860 machine.install_android_platform("android-37.1");
2861 let jar = AndroidSdk::android_jar_path(&host).expect("a staged platform jar");
2862 assert_eq!(
2863 jar.parent().and_then(|dir| dir.file_name()),
2864 Some(std::ffi::OsStr::new("android-37.1")),
2865 "the highest (major, minor) platform dir wins: {jar:?}"
2866 );
2867
2868 let (machine36, host36) = sdk_machine();
2869 machine36.install_android_platform("android-36");
2870 machine36.install_android_platform("android-36.1");
2871 let jar = AndroidSdk::android_jar_path(&host36).expect("a staged platform jar");
2872 assert_eq!(
2873 jar.parent().and_then(|dir| dir.file_name()),
2874 Some(std::ffi::OsStr::new("android-36.1")),
2875 "android-36.1 outranks android-36: {jar:?}"
2876 );
2877 }
2878
2879 #[test]
2882 fn sdk_detect_path_reads_declared_env() {
2883 let machine = TestMachine::new();
2884 let sdk = machine.install_android_sdk();
2885 let host = sdk_host(&machine, &sdk);
2886 assert_eq!(
2887 AndroidSdk::detect_path(&host).as_deref(),
2888 Some(sdk.as_path())
2889 );
2890 }
2891
2892 #[test]
2893 fn sdk_check_ok_when_sdkmanager_present() {
2894 let (_machine, host) = sdk_machine();
2895 smol::block_on(AndroidSdk.check(&host)).expect("a staged SDK with sdkmanager must be ok");
2896 }
2897
2898 #[test]
2899 fn sdk_check_fixable_when_sdkmanager_absent() {
2900 let machine = TestMachine::new();
2901 let sdk = machine.dir("sdk/platform-tools");
2904 let host = sdk_host(&machine, sdk.parent().expect("sdk root"));
2905 let result = smol::block_on(AndroidSdk.check(&host));
2906 assert!(
2907 matches!(result, Err(ToolchainError::Fixable(_))),
2908 "an SDK root without sdkmanager must be fixable: {result:?}"
2909 );
2910 }
2911
2912 #[test]
2913 fn sdk_missing_classification_matches_platform_installer() {
2914 let machine = TestMachine::new();
2915 let host = machine.host(Vec::<(String, String)>::new());
2916 let result = smol::block_on(AndroidSdk.check(&host));
2917 #[cfg(target_os = "linux")]
2918 {
2919 assert!(
2922 matches!(result, Err(ToolchainError::Fixable(_))),
2923 "missing SDK on Linux must plan a cmdline-tools install: {result:?}"
2924 );
2925 }
2926 #[cfg(any(target_os = "macos", target_os = "windows"))]
2927 {
2928 assert!(
2929 matches!(result, Err(ToolchainError::Unfixable(_))),
2930 "missing SDK without brew/winget must be unfixable: {result:?}"
2931 );
2932 #[cfg(target_os = "macos")]
2933 machine.install("brew");
2934 #[cfg(target_os = "windows")]
2935 machine.install("winget");
2936 let host = machine.host(Vec::<(String, String)>::new());
2937 let result = smol::block_on(AndroidSdk.check(&host));
2938 assert!(
2939 matches!(result, Err(ToolchainError::Fixable(_))),
2940 "missing SDK with a platform installer must be fixable: {result:?}"
2941 );
2942 }
2943 }
2944
2945 #[test]
2948 fn platform_tools_fixable_when_sdkmanager_can_install_it() {
2949 let (_machine, host) = sdk_machine();
2950 let result = smol::block_on(AndroidPlatformTools.check(&host));
2951 assert!(
2952 matches!(result, Err(ToolchainError::Fixable(_))),
2953 "missing adb with sdkmanager must be fixable: {result:?}"
2954 );
2955 }
2956
2957 #[test]
2958 fn platform_tools_unfixable_without_sdk() {
2959 let machine = TestMachine::new();
2960 let host = machine.host(Vec::<(String, String)>::new());
2961 let result = smol::block_on(AndroidPlatformTools.check(&host));
2962 assert!(
2963 matches!(result, Err(ToolchainError::Unfixable(_))),
2964 "no SDK and no sdkmanager must be unfixable: {result:?}"
2965 );
2966 }
2967
2968 #[test]
2969 #[cfg(unix)]
2970 fn platform_tools_ok_when_adb_runs() {
2971 let (machine, host) = sdk_machine();
2972 machine.install_adb();
2973 smol::block_on(AndroidPlatformTools.check(&host))
2974 .expect("a runnable adb must satisfy platform-tools");
2975 }
2976
2977 #[test]
2978 #[cfg(windows)]
2979 fn platform_tools_unfixable_when_adb_cannot_spawn() {
2980 let (machine, host) = sdk_machine();
2983 machine.install_adb();
2984 let result = smol::block_on(AndroidPlatformTools.check(&host));
2985 assert!(
2986 matches!(result, Err(ToolchainError::Unfixable(_))),
2987 "a non-spawning adb must be unfixable: {result:?}"
2988 );
2989 }
2990
2991 #[test]
2994 fn sdk_platforms_ok_with_android_jar() {
2995 let (machine, host) = sdk_machine();
2996 machine.install_android_platform("android-36");
2997 smol::block_on(AndroidSdkPlatforms.check(&host))
2998 .expect("an installed android.jar must satisfy the check");
2999 }
3000
3001 #[test]
3002 fn sdk_platforms_ok_with_minor_versioned_platform_dir() {
3003 let (machine, host) = sdk_machine();
3005 machine.install_android_platform("android-37.0");
3006 smol::block_on(AndroidSdkPlatforms.check(&host))
3007 .expect("android-37.0 platform dir must satisfy the check (#633)");
3008 }
3009
3010 #[test]
3011 fn sdk_platforms_fixable_when_sdkmanager_can_install() {
3012 let (_machine, host) = sdk_machine();
3013 let result = smol::block_on(AndroidSdkPlatforms.check(&host));
3014 assert!(
3015 matches!(result, Err(ToolchainError::Fixable(_))),
3016 "missing platforms with sdkmanager must be fixable: {result:?}"
3017 );
3018 }
3019
3020 #[test]
3021 fn sdk_platforms_unfixable_without_sdk() {
3022 let machine = TestMachine::new();
3023 let host = machine.host(Vec::<(String, String)>::new());
3024 let result = smol::block_on(AndroidSdkPlatforms.check(&host));
3025 assert!(
3026 matches!(result, Err(ToolchainError::Unfixable(_))),
3027 "no SDK and no sdkmanager must be unfixable: {result:?}"
3028 );
3029 }
3030
3031 #[test]
3034 fn build_tools_ok_with_d8_jar() {
3035 let (machine, host) = sdk_machine();
3036 machine.install_android_build_tools("36.0.0");
3037 smol::block_on(AndroidBuildTools.check(&host))
3038 .expect("an installed d8.jar must satisfy the check");
3039 }
3040
3041 #[test]
3042 fn build_tools_fixable_when_sdkmanager_can_install() {
3043 let (_machine, host) = sdk_machine();
3044 let result = smol::block_on(AndroidBuildTools.check(&host));
3045 assert!(
3046 matches!(result, Err(ToolchainError::Fixable(_))),
3047 "missing build-tools with sdkmanager must be fixable: {result:?}"
3048 );
3049 }
3050
3051 #[test]
3054 fn rust_targets_unfixable_without_rustup() {
3055 let machine = TestMachine::new();
3056 let host = machine.host(Vec::<(String, String)>::new());
3057 let result = smol::block_on(AndroidRustTargets::default().check(&host));
3058 assert!(
3059 matches!(result, Err(ToolchainError::Unfixable(_))),
3060 "no rustup must be unfixable: {result:?}"
3061 );
3062 }
3063
3064 #[test]
3065 fn rust_targets_ok_when_all_installed() {
3066 let machine = TestMachine::new();
3067 machine.install("rustup");
3068 machine.respond(
3070 "RUSTUP_INSTALLED_TARGETS",
3071 &[
3072 "aarch64-linux-android",
3073 "armv7-linux-androideabi",
3074 "i686-linux-android",
3075 "x86_64-linux-android",
3076 ]
3077 .join("\n"),
3078 );
3079 let host = machine.host(Vec::<(String, String)>::new());
3080 smol::block_on(AndroidRustTargets::default().check(&host))
3081 .expect("all four Android targets installed must be ok");
3082 }
3083
3084 #[test]
3085 fn rust_targets_fixable_lists_missing_targets() {
3086 let machine = TestMachine::new();
3087 machine.install("rustup");
3088 let host = machine.host([(
3089 String::from("WATERUI_FAKE_RUSTUP_INSTALLED_TARGETS"),
3090 String::from("aarch64-linux-android"),
3091 )]);
3092 let result = smol::block_on(AndroidRustTargets::default().check(&host));
3093 assert!(
3094 matches!(result, Err(ToolchainError::Fixable(_))),
3095 "missing Android targets must be fixable: {result:?}"
3096 );
3097 }
3098
3099 #[test]
3102 fn java_ok_when_on_path() {
3103 let machine = TestMachine::new();
3104 machine.install("java");
3105 let host = machine.host(Vec::<(String, String)>::new());
3106 smol::block_on(Java.check(&host)).expect("java on PATH must be ok");
3107 }
3108
3109 #[test]
3110 fn java_missing_is_unfixable_without_installer() {
3111 let machine = TestMachine::new();
3112 let host = machine.host(Vec::<(String, String)>::new());
3113 let result = smol::block_on(Java.check(&host));
3114 assert!(
3115 matches!(result, Err(ToolchainError::Unfixable(_))),
3116 "no java and no installer must be unfixable: {result:?}"
3117 );
3118 }
3119
3120 #[test]
3121 fn java_missing_is_fixable_with_installer() {
3122 let machine = TestMachine::new();
3123 #[cfg(target_os = "macos")]
3124 machine.install("brew");
3125 #[cfg(target_os = "windows")]
3126 machine.install("winget");
3127 #[cfg(target_os = "linux")]
3128 machine.install("apt-get");
3129 let host = machine.host(Vec::<(String, String)>::new());
3130 let result = smol::block_on(Java.check(&host));
3131 assert!(
3132 matches!(result, Err(ToolchainError::Fixable(_))),
3133 "no java with a platform installer must be fixable: {result:?}"
3134 );
3135 }
3136
3137 #[test]
3140 fn kotlin_ok_when_compatible_kotlinc_on_path() {
3141 let machine = TestMachine::new();
3142 machine.install("kotlinc");
3143 let host = machine.host([(
3144 String::from("WATERUI_FAKE_KOTLINC_VERSION"),
3145 required_kotlin_version().to_string(),
3146 )]);
3147 smol::block_on(Kotlin.check(&host)).expect("a compatible kotlinc on PATH must be ok");
3148 }
3149
3150 #[test]
3151 fn kotlin_fixable_when_absent() {
3152 let machine = TestMachine::new();
3153 let host = machine.host(Vec::<(String, String)>::new());
3154 let result = smol::block_on(Kotlin.check(&host));
3155 assert!(
3156 matches!(result, Err(ToolchainError::Fixable(_))),
3157 "no kotlinc must be fixable via managed install: {result:?}"
3158 );
3159 }
3160
3161 #[test]
3162 fn kotlin_fixable_when_too_old() {
3163 let machine = TestMachine::new();
3164 machine.install("kotlinc");
3165 let host = machine.host([(
3166 String::from("WATERUI_FAKE_KOTLINC_VERSION"),
3167 String::from("1.0.0"),
3168 )]);
3169 let result = smol::block_on(Kotlin.check(&host));
3170 assert!(
3171 matches!(result, Err(ToolchainError::Fixable(_))),
3172 "an incompatible kotlinc must trigger the managed install fix: {result:?}"
3173 );
3174 }
3175
3176 #[test]
3179 fn ndk_fixable_when_sdkmanager_can_install() {
3180 let (_machine, host) = sdk_machine();
3181 let result = smol::block_on(AndroidNdk.check(&host));
3182 assert!(
3183 matches!(result, Err(ToolchainError::Fixable(_))),
3184 "missing NDK with sdkmanager must be fixable: {result:?}"
3185 );
3186 }
3187
3188 #[test]
3189 fn ndk_unfixable_without_sdk() {
3190 let machine = TestMachine::new();
3191 let host = machine.host(Vec::<(String, String)>::new());
3192 let result = smol::block_on(AndroidNdk.check(&host));
3193 assert!(
3194 matches!(result, Err(ToolchainError::Unfixable(_))),
3195 "no SDK and no sdkmanager must be unfixable: {result:?}"
3196 );
3197 }
3198
3199 #[test]
3200 #[cfg(unix)]
3201 fn ndk_ok_when_host_toolchain_runs() {
3202 let (machine, host) = sdk_machine();
3203 machine.install_android_ndk("29.0.14206865");
3204 smol::block_on(AndroidNdk.check(&host))
3205 .expect("a staged NDK whose clang runs must satisfy the check");
3206 }
3207}