1use std::borrow::Cow;
9use std::future::Future;
10use std::pin::Pin;
11
12use crate::{
13 android::{
14 device::AndroidDevice,
15 platform::AndroidPlatform,
16 toolchain::{
17 AndroidBuildTools, AndroidNdk, AndroidPlatformTools, AndroidRustTargets, AndroidSdk,
18 AndroidSdkPlatforms, Java, Kotlin,
19 },
20 },
21 apple::{
22 device::AppleSimulator,
23 toolchain::{AppleSdk, Xcode},
24 },
25 device::Device,
26 gtk4::toolchain::Gtk4Toolchain,
27 toolchain::{
28 Host, Installation, Toolchain, ToolchainError, UnfixableToolchain,
29 cmake::Cmake,
30 linux::LinuxSystemToolchain,
31 rust::RustToolchain,
32 sccache::Sccache,
33 web::{PackageManagerToolchain, Wasm32UnknownUnknownTarget, WasmPack},
34 windows_arm64_llvm::WindowsArm64LlvmToolchain,
35 },
36};
37use eyre;
38use serde::{Deserialize, Serialize};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CheckStatus {
43 Ok,
45 Missing,
47 Skipped,
49}
50
51impl CheckStatus {
52 #[must_use]
54 pub const fn as_str(&self) -> &'static str {
55 match self {
56 Self::Ok => "ok",
57 Self::Missing => "missing",
58 Self::Skipped => "skipped",
59 }
60 }
61}
62
63pub type BoxedInstallFn =
65 Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = eyre::Result<()>> + Send>> + Send>;
66
67pub struct DoctorItem {
69 pub id: &'static str,
71 pub name: &'static str,
73 pub status: CheckStatus,
75 pub message: Option<String>,
77 pub install_fn: Option<BoxedInstallFn>,
79}
80
81impl std::fmt::Debug for DoctorItem {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("DoctorItem")
84 .field("id", &self.id)
85 .field("name", &self.name)
86 .field("status", &self.status)
87 .field("message", &self.message)
88 .field("install_fn", &self.install_fn.as_ref().map(|_| "..."))
89 .finish()
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct DoctorItemRecord {
101 #[serde(rename = "type")]
103 pub ty: Cow<'static, str>,
104 pub id: Cow<'static, str>,
106 pub name: Cow<'static, str>,
108 pub status: Cow<'static, str>,
110 pub fixable: bool,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub message: Option<String>,
115}
116
117impl From<&DoctorItem> for DoctorItemRecord {
118 fn from(item: &DoctorItem) -> Self {
119 Self {
120 ty: Cow::Borrowed("doctor-item"),
121 id: Cow::Borrowed(item.id),
122 name: Cow::Borrowed(item.name),
123 status: Cow::Borrowed(item.status.as_str()),
124 fixable: item.is_fixable(),
125 message: item.message.clone(),
126 }
127 }
128}
129
130impl DoctorItem {
131 const fn ok(id: &'static str, name: &'static str) -> Self {
132 Self {
133 id,
134 name,
135 status: CheckStatus::Ok,
136 message: None,
137 install_fn: None,
138 }
139 }
140
141 fn missing(id: &'static str, name: &'static str, message: impl Into<String>) -> Self {
142 Self {
143 id,
144 name,
145 status: CheckStatus::Missing,
146 message: Some(message.into()),
147 install_fn: None,
148 }
149 }
150
151 fn fixable<I: Installation + Send + 'static>(
152 id: &'static str,
153 name: &'static str,
154 message: impl Into<String>,
155 installation: I,
156 host: &Host,
157 ) -> Self {
158 let host = host.clone();
159 Self {
160 id,
161 name,
162 status: CheckStatus::Missing,
163 message: Some(message.into()),
164 install_fn: Some(Box::new(move || {
165 Box::pin(async move { installation.install(&host).await.map_err(Into::into) })
166 })),
167 }
168 }
169
170 const fn skipped(id: &'static str, name: &'static str) -> Self {
171 Self {
172 id,
173 name,
174 status: CheckStatus::Skipped,
175 message: None,
176 install_fn: None,
177 }
178 }
179
180 fn skipped_with_message(
181 id: &'static str,
182 name: &'static str,
183 message: impl Into<String>,
184 ) -> Self {
185 Self {
186 id,
187 name,
188 status: CheckStatus::Skipped,
189 message: Some(message.into()),
190 install_fn: None,
191 }
192 }
193
194 #[must_use]
196 pub const fn is_fixable(&self) -> bool {
197 self.install_fn.is_some()
198 }
199}
200
201pub mod ids {
206 pub const XCODE: &str = "xcode";
208 pub const IOS_SDK: &str = "ios-sdk";
210 pub const IOS_SIMULATOR_SDK: &str = "ios-simulator-sdk";
212 pub const IOS_SIMULATORS: &str = "ios-simulators";
214 pub const MACOS_SDK: &str = "macos-sdk";
216 pub const RUST: &str = "rust";
218 pub const ANDROID_SDK: &str = "android-sdk";
220 pub const ANDROID_PLATFORM_TOOLS: &str = "android-platform-tools";
222 pub const ANDROID_SDK_PLATFORMS: &str = "android-sdk-platforms";
224 pub const ANDROID_BUILD_TOOLS: &str = "android-build-tools";
226 pub const ANDROID_NDK: &str = "android-ndk";
228 pub const ANDROID_RUST_TARGETS: &str = "android-rust-targets";
230 pub const ANDROID_RUN_TARGETS: &str = "android-run-targets";
232 pub const CMAKE: &str = "cmake";
234 pub const WINDOWS_ARM64_LLVM: &str = "windows-arm64-llvm";
236 pub const JAVA: &str = "java";
238 pub const KOTLIN: &str = "kotlin";
240 pub const WASM32_TARGET: &str = "wasm32-target";
242 pub const WASM_PACK: &str = "wasm-pack";
244 pub const LINUX_SYSTEM_PACKAGES: &str = "linux-system-packages";
246 pub const GTK4: &str = "gtk4";
248 pub const SCCACHE: &str = "sccache";
250 pub const WEB_PACKAGE_MANAGER: &str = "web-package-manager";
252
253 pub const ALL: &[&str] = &[
259 XCODE,
260 IOS_SDK,
261 IOS_SIMULATOR_SDK,
262 IOS_SIMULATORS,
263 MACOS_SDK,
264 RUST,
265 ANDROID_SDK,
266 ANDROID_PLATFORM_TOOLS,
267 ANDROID_SDK_PLATFORMS,
268 ANDROID_BUILD_TOOLS,
269 ANDROID_NDK,
270 ANDROID_RUST_TARGETS,
271 ANDROID_RUN_TARGETS,
272 CMAKE,
273 WINDOWS_ARM64_LLVM,
274 JAVA,
275 KOTLIN,
276 WASM32_TARGET,
277 WASM_PACK,
278 LINUX_SYSTEM_PACKAGES,
279 GTK4,
280 SCCACHE,
281 WEB_PACKAGE_MANAGER,
282 ];
283}
284
285fn unfixable_message(error: &UnfixableToolchain) -> String {
286 format!(
287 "Cannot auto-fix: {}. Next step: {}",
288 error.message(),
289 error.suggestion()
290 )
291}
292
293async fn push_toolchain_check<T>(
294 host: &Host,
295 items: &mut Vec<DoctorItem>,
296 id: &'static str,
297 name: &'static str,
298 fixable_message: &'static str,
299 toolchain: T,
300) where
301 T: Toolchain,
302 T::Installation: Send + 'static,
303{
304 match toolchain.check(host).await {
305 Ok(()) => items.push(DoctorItem::ok(id, name)),
306 Err(ToolchainError::Fixable(installation)) => {
307 items.push(DoctorItem::fixable(
308 id,
309 name,
310 fixable_message,
311 installation,
312 host,
313 ));
314 }
315 Err(ToolchainError::Unfixable(error)) => {
316 items.push(DoctorItem::missing(id, name, unfixable_message(&error)));
317 }
318 }
319}
320
321async fn push_toolchain_check_with_unfixable<T, F>(
322 host: &Host,
323 items: &mut Vec<DoctorItem>,
324 id: &'static str,
325 name: &'static str,
326 fixable_message: &'static str,
327 toolchain: T,
328 unfixable_message_fn: F,
329) where
330 T: Toolchain,
331 T::Installation: Send + 'static,
332 F: FnOnce(&UnfixableToolchain) -> String,
333{
334 match toolchain.check(host).await {
335 Ok(()) => items.push(DoctorItem::ok(id, name)),
336 Err(ToolchainError::Fixable(installation)) => {
337 items.push(DoctorItem::fixable(
338 id,
339 name,
340 fixable_message,
341 installation,
342 host,
343 ));
344 }
345 Err(ToolchainError::Unfixable(error)) => {
346 items.push(DoctorItem::missing(id, name, unfixable_message_fn(&error)));
347 }
348 }
349}
350
351async fn push_apple_checks(host: &Host, items: &mut Vec<DoctorItem>) {
352 if !cfg!(target_os = "macos") {
353 items.push(DoctorItem::skipped(ids::XCODE, "Xcode"));
354 items.push(DoctorItem::skipped(ids::IOS_SDK, "iOS SDK"));
355 items.push(DoctorItem::skipped(
356 ids::IOS_SIMULATOR_SDK,
357 "iOS Simulator SDK",
358 ));
359 items.push(DoctorItem::skipped(ids::IOS_SIMULATORS, "iOS Simulators"));
360 items.push(DoctorItem::skipped(ids::MACOS_SDK, "macOS SDK"));
361 return;
362 }
363
364 push_simple_check(items, ids::XCODE, "Xcode", Xcode.check(host).await);
365 push_simple_check(
366 items,
367 ids::IOS_SDK,
368 "iOS SDK",
369 AppleSdk::Ios.check(host).await,
370 );
371 push_simple_check(
372 items,
373 ids::IOS_SIMULATOR_SDK,
374 "iOS Simulator SDK",
375 AppleSdk::IosSimulator.check(host).await,
376 );
377 push_ios_simulator_check(host, items).await;
378 push_simple_check(
379 items,
380 ids::MACOS_SDK,
381 "macOS SDK",
382 AppleSdk::Macos.check(host).await,
383 );
384}
385
386fn push_simple_check(
387 items: &mut Vec<DoctorItem>,
388 id: &'static str,
389 name: &'static str,
390 result: Result<(), impl std::fmt::Display>,
391) {
392 match result {
393 Ok(()) => items.push(DoctorItem::ok(id, name)),
394 Err(error) => items.push(DoctorItem::missing(id, name, error.to_string())),
395 }
396}
397
398async fn push_ios_simulator_check(host: &Host, items: &mut Vec<DoctorItem>) {
399 match AppleSimulator::scan_ios(host).await {
400 Ok(simulators) if simulators.is_empty() => items.push(DoctorItem::missing(
401 ids::IOS_SIMULATORS,
402 "iOS Simulators",
403 "No iOS simulators available. Install a simulator runtime in Xcode Settings > Platforms.",
404 )),
405 Ok(_) => items.push(DoctorItem::ok(ids::IOS_SIMULATORS, "iOS Simulators")),
406 Err(error) => items.push(DoctorItem::missing(
407 ids::IOS_SIMULATORS,
408 "iOS Simulators",
409 format!("Failed to list iOS simulators: {error}"),
410 )),
411 }
412}
413
414async fn push_android_sdk_checks(host: &Host, items: &mut Vec<DoctorItem>) -> bool {
415 push_toolchain_check(
416 host,
417 items,
418 ids::ANDROID_SDK,
419 "Android SDK",
420 "Android SDK is missing (automatic install is supported on this host)",
421 AndroidSdk,
422 )
423 .await;
424
425 AndroidSdk::sdkmanager_path(host).await.is_some()
426}
427
428async fn push_android_component_checks(host: &Host, items: &mut Vec<DoctorItem>, sdk_ready: bool) {
429 if !sdk_ready {
430 push_blocked_android_component_checks(items);
431 return;
432 }
433
434 push_toolchain_check(
435 host,
436 items,
437 ids::ANDROID_PLATFORM_TOOLS,
438 "Android Platform-Tools (adb)",
439 "Required for `water run --platform android`",
440 AndroidPlatformTools,
441 )
442 .await;
443 push_toolchain_check(
444 host,
445 items,
446 ids::ANDROID_SDK_PLATFORMS,
447 "Android SDK Platforms",
448 "Required for Android build/package workflows",
449 AndroidSdkPlatforms,
450 )
451 .await;
452 push_toolchain_check(
453 host,
454 items,
455 ids::ANDROID_BUILD_TOOLS,
456 "Android SDK Build-Tools (d8)",
457 "Required for Android build/package workflows",
458 AndroidBuildTools,
459 )
460 .await;
461 push_toolchain_check(
462 host,
463 items,
464 ids::ANDROID_NDK,
465 "Android NDK",
466 "Required for Android build/package workflows",
467 AndroidNdk,
468 )
469 .await;
470 push_toolchain_check(
471 host,
472 items,
473 ids::ANDROID_RUST_TARGETS,
474 "Android Rust Targets",
475 "Required for Android Rust cross-compilation",
476 AndroidRustTargets::default(),
477 )
478 .await;
479}
480
481fn push_blocked_android_component_checks(items: &mut Vec<DoctorItem>) {
484 for (id, name) in [
485 (ids::ANDROID_PLATFORM_TOOLS, "Android Platform-Tools (adb)"),
486 (ids::ANDROID_SDK_PLATFORMS, "Android SDK Platforms"),
487 (ids::ANDROID_BUILD_TOOLS, "Android SDK Build-Tools (d8)"),
488 (ids::ANDROID_NDK, "Android NDK"),
489 (ids::ANDROID_RUST_TARGETS, "Android Rust Targets"),
490 ] {
491 items.push(DoctorItem::missing(
492 id,
493 name,
494 "Blocked: Android SDK / `sdkmanager` is not ready yet. Fix Android SDK first.",
495 ));
496 }
497}
498
499async fn push_android_run_target_check(host: &Host, items: &mut Vec<DoctorItem>) {
500 if AndroidSdk::adb_path(host).is_none() {
501 items.push(DoctorItem::missing(
502 ids::ANDROID_RUN_TARGETS,
503 "Android Run Targets",
504 "Blocked: Android Platform-Tools (`adb`) is not ready yet.",
505 ));
506 return;
507 }
508
509 match AndroidDevice::scan(host).await {
510 Ok(devices) if !devices.is_empty() => {
511 items.push(DoctorItem::ok(ids::ANDROID_RUN_TARGETS, "Android Run Targets"));
512 }
513 Ok(_) => match AndroidPlatform::list_avds(host).await {
514 Ok(avds) if !avds.is_empty() => {
515 items.push(DoctorItem::ok(ids::ANDROID_RUN_TARGETS, "Android Run Targets"));
516 }
517 Ok(_) => items.push(DoctorItem::missing(
518 ids::ANDROID_RUN_TARGETS,
519 "Android Run Targets",
520 "No connected Android devices and no emulator AVDs were found. Connect a device or create an AVD.",
521 )),
522 Err(error) => items.push(DoctorItem::missing(
523 ids::ANDROID_RUN_TARGETS,
524 "Android Run Targets",
525 format!(
526 "No connected Android devices and failed to list AVDs: {error}. Install Android emulator components or connect a device."
527 ),
528 )),
529 },
530 Err(error) => items.push(DoctorItem::missing(
531 ids::ANDROID_RUN_TARGETS,
532 "Android Run Targets",
533 format!("Failed to query Android devices via adb: {error}"),
534 )),
535 }
536}
537
538async fn push_desktop_and_web_checks(host: &Host, items: &mut Vec<DoctorItem>) {
539 push_toolchain_check(
540 host,
541 items,
542 ids::CMAKE,
543 "Host CMake",
544 "Required for native Rust dependencies in Android builds",
545 Cmake::default(),
546 )
547 .await;
548
549 if WindowsArm64LlvmToolchain::required_on_host() {
550 push_toolchain_check(
551 host,
552 items,
553 ids::WINDOWS_ARM64_LLVM,
554 "Windows ARM64 LLVM toolchain",
555 "Required by native assembly dependencies in Windows ARM64 hydrolysis builds",
556 WindowsArm64LlvmToolchain,
557 )
558 .await;
559 } else {
560 items.push(DoctorItem::skipped_with_message(
561 ids::WINDOWS_ARM64_LLVM,
562 "Windows ARM64 LLVM toolchain",
563 "Only required on Windows ARM64 hosts for native assembly dependencies.",
564 ));
565 }
566
567 push_toolchain_check(
568 host,
569 items,
570 ids::JAVA,
571 "Java",
572 "Required for Android Gradle builds",
573 Java,
574 )
575 .await;
576 push_toolchain_check(
577 host,
578 items,
579 ids::KOTLIN,
580 "Kotlin",
581 "Required for Android Kotlin helper compilation",
582 Kotlin,
583 )
584 .await;
585 push_toolchain_check_with_unfixable(
586 host,
587 items,
588 ids::WASM32_TARGET,
589 "Rust wasm32 target",
590 "wasm32-unknown-unknown target not installed",
591 Wasm32UnknownUnknownTarget,
592 ToString::to_string,
593 )
594 .await;
595 push_toolchain_check_with_unfixable(
596 host,
597 items,
598 ids::WASM_PACK,
599 "wasm-pack",
600 "wasm-pack not found (required for web packaging)",
601 WasmPack,
602 ToString::to_string,
603 )
604 .await;
605}
606
607async fn push_linux_checks(host: &Host, items: &mut Vec<DoctorItem>) {
608 if !cfg!(target_os = "linux") {
609 items.push(DoctorItem::skipped(
610 ids::LINUX_SYSTEM_PACKAGES,
611 "Linux system packages",
612 ));
613 items.push(DoctorItem::skipped(ids::GTK4, "GTK4"));
614 return;
615 }
616
617 let linux_packages_fixable = match LinuxSystemToolchain.check(host).await {
618 Ok(()) => {
619 items.push(DoctorItem::ok(
620 ids::LINUX_SYSTEM_PACKAGES,
621 "Linux system packages",
622 ));
623 false
624 }
625 Err(ToolchainError::Fixable(installation)) => {
626 let msg = format!(
627 "Missing packages for {}: {}. Install command: {}",
628 installation.package_manager_name(),
629 installation.missing_packages().join(", "),
630 installation.install_command_hint(),
631 );
632 items.push(DoctorItem::fixable(
633 ids::LINUX_SYSTEM_PACKAGES,
634 "Linux system packages",
635 msg,
636 installation,
637 host,
638 ));
639 true
640 }
641 Err(ToolchainError::Unfixable(error)) => {
642 items.push(DoctorItem::missing(
643 ids::LINUX_SYSTEM_PACKAGES,
644 "Linux system packages",
645 unfixable_message(&error),
646 ));
647 false
648 }
649 };
650
651 match Gtk4Toolchain.check(host).await {
652 Ok(()) => items.push(DoctorItem::ok(ids::GTK4, "GTK4")),
653 Err(ToolchainError::Fixable(installation)) => {
654 items.push(DoctorItem::fixable(
655 ids::GTK4,
656 "GTK4",
657 "GTK4 dependencies are missing",
658 installation,
659 host,
660 ));
661 }
662 Err(ToolchainError::Unfixable(error)) => {
663 if linux_packages_fixable {
664 items.push(DoctorItem::missing(
665 ids::GTK4,
666 "GTK4",
667 "GTK4 probe failed because required Linux packages are missing. Run `water doctor --fix` to install Linux system packages, then re-run `water doctor`.",
668 ));
669 } else {
670 items.push(DoctorItem::missing(
671 ids::GTK4,
672 "GTK4",
673 unfixable_message(&error),
674 ));
675 }
676 }
677 }
678}
679
680pub async fn doctor(host: &Host) -> Vec<DoctorItem> {
686 let mut items = Vec::new();
687 push_apple_checks(host, &mut items).await;
688 push_rust_toolchain_check(host, &mut items).await;
689 let sdk_ready = push_android_sdk_checks(host, &mut items).await;
690 push_android_component_checks(host, &mut items, sdk_ready).await;
691 push_android_run_target_check(host, &mut items).await;
692 push_desktop_and_web_checks(host, &mut items).await;
693 push_linux_checks(host, &mut items).await;
694 push_toolchain_check(
695 host,
696 &mut items,
697 ids::SCCACHE,
698 "sccache",
699 "sccache not found (recommended for faster builds)",
700 Sccache,
701 )
702 .await;
703 push_web_package_manager_check(host, &mut items).await;
704
705 items
706}
707
708async fn push_web_package_manager_check(host: &Host, items: &mut Vec<DoctorItem>) {
712 let Ok(cwd) = std::env::current_dir() else {
713 return;
714 };
715 let Ok(manifest_text) = smol::fs::read_to_string(cwd.join("Water.toml")).await else {
716 return;
717 };
718 let Ok(manifest) = toml::from_str::<crate::project::Manifest>(&manifest_text) else {
719 return;
720 };
721 let Some(web) = manifest.web else {
722 return;
723 };
724 let package_manager = web.package_manager;
725 let name: &'static str = match package_manager {
726 crate::web::PackageManager::Bun => "bun (web package manager)",
727 crate::web::PackageManager::Pnpm => "pnpm (web package manager)",
728 crate::web::PackageManager::Npm => "npm (web package manager)",
729 crate::web::PackageManager::Yarn => "yarn (web package manager)",
730 };
731 push_toolchain_check(
732 host,
733 items,
734 ids::WEB_PACKAGE_MANAGER,
735 name,
736 package_manager.install_hint(),
737 PackageManagerToolchain(package_manager),
738 )
739 .await;
740}
741
742async fn push_rust_toolchain_check(host: &Host, items: &mut Vec<DoctorItem>) {
743 match RustToolchain.check(host).await {
744 Ok(()) => items.push(DoctorItem::ok(ids::RUST, "Rust toolchain")),
745 Err(ToolchainError::Fixable(installation)) => {
746 items.push(DoctorItem::fixable(
747 ids::RUST,
748 "Rust toolchain",
749 format!(
750 "Rust toolchain is missing, outdated, or incomplete. Planned automatic fixes: {}",
751 installation.summary()
752 ),
753 installation,
754 host,
755 ));
756 }
757 Err(ToolchainError::Unfixable(error)) => items.push(DoctorItem::missing(
758 ids::RUST,
759 "Rust toolchain",
760 unfixable_message(&error),
761 )),
762 }
763}
764#[cfg(test)]
765mod tests {
766 use super::{CheckStatus, doctor, ids};
767 use crate::toolchain::testing::TestMachine;
768
769 const ANDROID_COMPONENT_IDS: &[&str] = &[
770 ids::ANDROID_PLATFORM_TOOLS,
771 ids::ANDROID_SDK_PLATFORMS,
772 ids::ANDROID_BUILD_TOOLS,
773 ids::ANDROID_NDK,
774 ids::ANDROID_RUST_TARGETS,
775 ];
776
777 fn ids_of(items: &[super::DoctorItem]) -> Vec<&'static str> {
778 items.iter().map(|item| item.id).collect()
779 }
780
781 fn item<'a>(items: &'a [super::DoctorItem], id: &str) -> &'a super::DoctorItem {
782 items
783 .iter()
784 .find(|item| item.id == id)
785 .unwrap_or_else(|| panic!("doctor report must contain `{id}`"))
786 }
787
788 #[test]
789 fn doctor_emits_every_item_in_stable_order() {
790 let machine = TestMachine::new();
791 let host = machine.host(Vec::<(String, String)>::new());
792 let items = smol::block_on(doctor(&host));
793 let expected: Vec<&'static str> = ids::ALL
796 .iter()
797 .copied()
798 .filter(|id| *id != ids::WEB_PACKAGE_MANAGER)
799 .collect();
800 assert_eq!(ids_of(&items), expected);
801 }
802
803 #[test]
804 fn doctor_blocks_android_components_when_sdk_absent() {
805 let machine = TestMachine::new();
806 let host = machine.host(Vec::<(String, String)>::new());
807 let items = smol::block_on(doctor(&host));
808
809 assert_eq!(item(&items, ids::ANDROID_SDK).status, CheckStatus::Missing);
810 for id in ANDROID_COMPONENT_IDS {
811 let component = item(&items, id);
812 assert_eq!(component.status, CheckStatus::Missing, "{id}");
813 assert!(
814 component
815 .message
816 .as_deref()
817 .is_some_and(|message| message.contains("Blocked")),
818 "{id} must carry the blocked diagnostic: {:?}",
819 component.message
820 );
821 assert!(
822 !component.is_fixable(),
823 "blocked {id} must not offer an install"
824 );
825 }
826
827 let run_targets = item(&items, ids::ANDROID_RUN_TARGETS);
828 assert_eq!(run_targets.status, CheckStatus::Missing);
829 assert!(
830 run_targets
831 .message
832 .as_deref()
833 .is_some_and(|message| message.contains("Blocked"))
834 );
835 }
836
837 #[test]
838 fn doctor_probes_android_components_when_sdk_ready() {
839 let machine = TestMachine::new();
840 let sdk = machine.install_android_sdk();
841 let host = machine.host([(
842 String::from("ANDROID_SDK_ROOT"),
843 sdk.as_os_str().to_os_string(),
844 )]);
845 let items = smol::block_on(doctor(&host));
846
847 assert_eq!(item(&items, ids::ANDROID_SDK).status, CheckStatus::Ok);
848 for id in ANDROID_COMPONENT_IDS {
849 let component = item(&items, id);
850 assert_eq!(component.status, CheckStatus::Missing, "{id}");
851 assert!(
852 !component
853 .message
854 .as_deref()
855 .is_some_and(|message| message.contains("Blocked")),
856 "{id} must be a real diagnosis, not the blocked marker: {:?}",
857 component.message
858 );
859 }
860
861 for id in [
863 ids::ANDROID_PLATFORM_TOOLS,
864 ids::ANDROID_SDK_PLATFORMS,
865 ids::ANDROID_BUILD_TOOLS,
866 ids::ANDROID_NDK,
867 ] {
868 assert!(item(&items, id).is_fixable(), "{id} must be fixable");
869 }
870 assert!(!item(&items, ids::ANDROID_RUST_TARGETS).is_fixable());
872 }
873
874 #[test]
875 fn doctor_apple_items_match_platform() {
876 let machine = TestMachine::new();
877 let host = machine.host(Vec::<(String, String)>::new());
878 let items = smol::block_on(doctor(&host));
879 for id in [
880 ids::XCODE,
881 ids::IOS_SDK,
882 ids::IOS_SIMULATOR_SDK,
883 ids::IOS_SIMULATORS,
884 ids::MACOS_SDK,
885 ] {
886 let status = item(&items, id).status;
887 if cfg!(target_os = "macos") {
888 assert_eq!(
889 status,
890 CheckStatus::Missing,
891 "{id} is probed on macOS and missing on a bare host"
892 );
893 } else {
894 assert_eq!(
895 status,
896 CheckStatus::Skipped,
897 "{id} must be skipped off macOS"
898 );
899 }
900 }
901 }
902
903 #[test]
907 #[cfg(target_os = "macos")]
908 fn doctor_ios_simulators_ok_when_simctl_reports_healthy_device() {
909 let machine = TestMachine::new();
910 machine.install("xcrun");
911 machine.dir(
914 "Library/Developer/CoreSimulator/Devices/3E8B0C4F-0000-4000-8000-000000000001/data",
915 );
916 let transcript = include_str!("testdata/simctl_devices.json")
917 .replace("/fake/", &format!("{}/", machine.root().display()));
918 machine.respond("XCRUN_SIMCTL_DEVICES", &transcript);
919 let host = machine.host(Vec::<(String, String)>::new());
920 let items = smol::block_on(doctor(&host));
921 assert_eq!(
922 item(&items, ids::IOS_SIMULATORS).status,
923 CheckStatus::Ok,
924 "a healthy simctl device must satisfy ios-simulators"
925 );
926 }
927
928 #[test]
929 fn doctor_linux_items_match_platform() {
930 let machine = TestMachine::new();
931 let host = machine.host(Vec::<(String, String)>::new());
932 let items = smol::block_on(doctor(&host));
933 for id in [ids::LINUX_SYSTEM_PACKAGES, ids::GTK4] {
934 let status = item(&items, id).status;
935 if cfg!(target_os = "linux") {
936 assert_eq!(
937 status,
938 CheckStatus::Missing,
939 "{id} is probed on Linux and missing on a bare host"
940 );
941 } else {
942 assert_eq!(
943 status,
944 CheckStatus::Skipped,
945 "{id} must be skipped off Linux"
946 );
947 }
948 }
949 }
950
951 #[test]
952 fn doctor_windows_llvm_skipped_where_not_required() {
953 let machine = TestMachine::new();
954 let host = machine.host(Vec::<(String, String)>::new());
955 let items = smol::block_on(doctor(&host));
956 let status = item(&items, ids::WINDOWS_ARM64_LLVM).status;
957 if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
958 assert_eq!(status, CheckStatus::Missing);
959 } else {
960 assert_eq!(status, CheckStatus::Skipped);
961 }
962 }
963
964 #[test]
965 fn doctor_fixable_and_manual_classification() {
966 let machine = TestMachine::new();
967 let host = machine.host(Vec::<(String, String)>::new());
968 let items = smol::block_on(doctor(&host));
969
970 let rust = item(&items, ids::RUST);
972 assert_eq!(rust.status, CheckStatus::Missing);
973 assert!(!rust.is_fixable());
974
975 let wasm_pack = item(&items, ids::WASM_PACK);
977 assert_eq!(wasm_pack.status, CheckStatus::Missing);
978 assert!(wasm_pack.is_fixable());
979
980 #[cfg(target_os = "linux")]
982 assert!(item(&items, ids::ANDROID_SDK).is_fixable());
983 }
984
985 #[test]
986 #[cfg(unix)]
987 fn doctor_reports_complete_android_chain_when_fully_staged() {
988 let machine = TestMachine::new();
989 let sdk = machine.install_android_sdk();
990 machine.install_adb();
991 machine.install_android_platform("android-37.0");
992 machine.install_android_build_tools("37.0.0");
993 machine.install_android_ndk("29.0.14206865");
994 machine.install_android_emulator();
995 machine.install("rustup");
996 machine.respond("EMULATOR_AVDS", "Medium_Phone_API_37\n");
997 machine.respond(
998 "RUSTUP_INSTALLED_TARGETS",
999 &[
1000 "aarch64-linux-android",
1001 "armv7-linux-androideabi",
1002 "i686-linux-android",
1003 "x86_64-linux-android",
1004 ]
1005 .join("\n"),
1006 );
1007 let host = machine.host([(
1008 String::from("ANDROID_SDK_ROOT"),
1009 sdk.as_os_str().to_os_string(),
1010 )]);
1011 let items = smol::block_on(doctor(&host));
1012 for id in [
1013 ids::ANDROID_SDK,
1014 ids::ANDROID_PLATFORM_TOOLS,
1015 ids::ANDROID_SDK_PLATFORMS,
1016 ids::ANDROID_BUILD_TOOLS,
1017 ids::ANDROID_NDK,
1018 ids::ANDROID_RUST_TARGETS,
1019 ids::ANDROID_RUN_TARGETS,
1020 ] {
1021 assert_eq!(
1022 item(&items, id).status,
1023 CheckStatus::Ok,
1024 "{id} must be ok on a fully staged SDK: {:?}",
1025 item(&items, id).message
1026 );
1027 }
1028 }
1029}