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