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