Skip to main content

waterui_cli/
toolchain_checks.rs

1//! Shared toolchain checks for the terminal commands and the `water mcp`
2//! `preview` tool.
3
4use std::path::{Path, PathBuf};
5
6use eyre::{Result, bail};
7
8use crate::{
9    android::{
10        AndroidBuildTools, AndroidNdk, AndroidPlatformTools, AndroidRustTargets, AndroidSdk,
11        AndroidSdkPlatforms, Java, Kotlin,
12        platform::{ALL_ABIS, AndroidAbi},
13    },
14    apple::toolchain::{AppleSdk, Xcode},
15    gtk4::toolchain::Gtk4Toolchain,
16    toolchain::{
17        Host, Installation, Toolchain, ToolchainError,
18        cmake::Cmake,
19        doctor::{CheckStatus, doctor, ids},
20        web::web_toolchain,
21        windows_arm64_llvm::WindowsArm64LlvmToolchain,
22    },
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26enum AndroidCheckScope {
27    BuildOrPackage,
28    Run,
29}
30
31fn toolchain_check_message<I: Installation>(component: &str, error: &ToolchainError<I>) -> String {
32    match error {
33        ToolchainError::Fixable(_) => format!(
34            "{component} toolchain check failed: missing dependencies can be fixed automatically with `water doctor --fix`."
35        ),
36        ToolchainError::Unfixable(unfixable) => {
37            format!("{component} toolchain check failed: {unfixable}")
38        }
39    }
40}
41
42fn android_doctor_item_in_scope(id: &str, scope: AndroidCheckScope) -> bool {
43    match id {
44        ids::ANDROID_SDK
45        | ids::ANDROID_SDK_PLATFORMS
46        | ids::ANDROID_BUILD_TOOLS
47        | ids::ANDROID_NDK
48        | ids::ANDROID_RUST_TARGETS
49        | ids::CMAKE
50        | ids::JAVA
51        | ids::KOTLIN => true,
52        ids::ANDROID_PLATFORM_TOOLS => scope == AndroidCheckScope::Run,
53        _ => false,
54    }
55}
56
57fn format_path_or_missing(label: &str, path: Option<&Path>) -> String {
58    path.map_or_else(
59        || format!("- {label}: <not detected>"),
60        |path| format!("- {label}: {}", path.display()),
61    )
62}
63
64async fn android_detection_summary(host: &Host) -> String {
65    let sdk_root = AndroidSdk::detect_path(host);
66    let d8_jar = AndroidSdk::d8_jar_path(host);
67    let ndk_root = AndroidNdk::detect_path(host);
68    let java_bin: Option<PathBuf> = Java::detect_path(host).await;
69    let java_home: Option<PathBuf> = Java::detect_home(host).await;
70
71    [
72        "Detected Android/JDK configuration:".to_string(),
73        format_path_or_missing("Android SDK root", sdk_root.as_deref()),
74        format_path_or_missing("Android build-tools d8.jar", d8_jar.as_deref()),
75        format_path_or_missing("Android NDK root", ndk_root.as_deref()),
76        format_path_or_missing("Java executable", java_bin.as_deref()),
77        format_path_or_missing("JAVA_HOME", java_home.as_deref()),
78    ]
79    .join("\n")
80}
81
82fn format_doctor_missing_item(
83    name: &'static str,
84    message: Option<String>,
85    is_fixable: bool,
86) -> String {
87    let mode = if is_fixable { "fixable" } else { "manual" };
88    message.map_or_else(
89        || format!("- {name} [{mode}]"),
90        |message| format!("- {name} [{mode}]: {message}"),
91    )
92}
93
94async fn android_doctor_summary(host: &Host, scope: AndroidCheckScope) -> String {
95    let mut lines = vec!["Relevant doctor diagnostics:".to_string()];
96
97    for item in doctor(host).await {
98        if item.status != CheckStatus::Missing || !android_doctor_item_in_scope(item.id, scope) {
99            continue;
100        }
101        let is_fixable = item.is_fixable();
102        let message = item.message;
103        lines.push(format_doctor_missing_item(item.name, message, is_fixable));
104    }
105
106    if lines.len() == 1 {
107        lines.push("- No additional Android diagnostics were reported by doctor.".to_string());
108    }
109
110    lines.join("\n")
111}
112
113async fn android_failure_message<I: Installation>(
114    host: &Host,
115    component: &str,
116    error: &ToolchainError<I>,
117    scope: AndroidCheckScope,
118) -> String {
119    [
120        toolchain_check_message(component, error),
121        android_detection_summary(host).await,
122        android_doctor_summary(host, scope).await,
123        "Next steps: run `water doctor` for full diagnostics, then `water doctor --fix` to auto-install fixable dependencies.".to_string(),
124    ]
125    .join("\n")
126}
127
128/// Verify Xcode and the requested Apple SDK are installed.
129///
130/// # Errors
131/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
132pub async fn check_apple(host: &Host, sdk: AppleSdk) -> Result<()> {
133    let xcode = Xcode;
134    if let Err(e) = xcode.check(host).await {
135        bail!("{}", toolchain_check_message("Xcode", &e));
136    }
137    if let Err(e) = sdk.check(host).await {
138        bail!("{}", toolchain_check_message(&sdk.to_string(), &e));
139    }
140    Ok(())
141}
142
143/// Verify the Android toolchain covers building and packaging for all ABIs.
144///
145/// # Errors
146/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
147pub async fn check_android_build_or_package(host: &Host) -> Result<()> {
148    check_android_build_or_package_for_abis(host, ALL_ABIS).await
149}
150
151/// Verify the Android toolchain covers building and packaging for `required_abis`.
152///
153/// # Errors
154/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
155pub async fn check_android_build_or_package_for_abis(
156    host: &Host,
157    required_abis: &[AndroidAbi],
158) -> Result<()> {
159    let sdk = AndroidSdk;
160    if let Err(e) = sdk.check(host).await {
161        bail!(
162            "{}",
163            android_failure_message(host, "Android SDK", &e, AndroidCheckScope::BuildOrPackage)
164                .await
165        );
166    }
167    let platforms = AndroidSdkPlatforms;
168    if let Err(e) = platforms.check(host).await {
169        bail!(
170            "{}",
171            android_failure_message(
172                host,
173                "Android SDK Platforms",
174                &e,
175                AndroidCheckScope::BuildOrPackage
176            )
177            .await
178        );
179    }
180    let build_tools = AndroidBuildTools;
181    if let Err(e) = build_tools.check(host).await {
182        bail!(
183            "{}",
184            android_failure_message(
185                host,
186                "Android SDK Build-Tools (d8)",
187                &e,
188                AndroidCheckScope::BuildOrPackage
189            )
190            .await
191        );
192    }
193    let ndk = AndroidNdk;
194    if let Err(e) = ndk.check(host).await {
195        bail!(
196            "{}",
197            android_failure_message(host, "Android NDK", &e, AndroidCheckScope::BuildOrPackage)
198                .await
199        );
200    }
201    let cmake = Cmake::default();
202    if let Err(e) = cmake.check(host).await {
203        bail!(
204            "{}",
205            android_failure_message(host, "Host CMake", &e, AndroidCheckScope::BuildOrPackage)
206                .await
207        );
208    }
209    let java = Java;
210    if let Err(e) = java.check(host).await {
211        bail!(
212            "{}",
213            android_failure_message(host, "Java", &e, AndroidCheckScope::BuildOrPackage).await
214        );
215    }
216    let rust_targets = AndroidRustTargets::for_abis(required_abis);
217    if let Err(e) = rust_targets.check(host).await {
218        bail!(
219            "{}",
220            android_failure_message(
221                host,
222                "Android Rust Targets",
223                &e,
224                AndroidCheckScope::BuildOrPackage
225            )
226            .await
227        );
228    }
229    let kotlin = Kotlin;
230    if let Err(e) = kotlin.check(host).await {
231        bail!(
232            "{}",
233            android_failure_message(host, "Kotlin", &e, AndroidCheckScope::BuildOrPackage).await
234        );
235    }
236    Ok(())
237}
238
239/// Verify the Android toolchain covers running an app (adds `adb` to the build requirements).
240///
241/// # Errors
242/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
243pub async fn check_android_run(host: &Host) -> Result<()> {
244    check_android_build_or_package(host).await?;
245    let platform_tools = AndroidPlatformTools;
246    if let Err(e) = platform_tools.check(host).await {
247        bail!(
248            "{}",
249            android_failure_message(host, "Android Platform-Tools", &e, AndroidCheckScope::Run)
250                .await
251        );
252    }
253    Ok(())
254}
255
256/// Verify the GTK4 toolchain is installed.
257///
258/// # Errors
259/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
260pub async fn check_gtk4(host: &Host) -> Result<()> {
261    let toolchain = Gtk4Toolchain;
262    if let Err(e) = toolchain.check(host).await {
263        bail!("{}", toolchain_check_message("GTK4", &e));
264    }
265    Ok(())
266}
267
268/// Verify the `WinUI` toolchain is installed.
269///
270/// # Errors
271/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
272pub async fn check_winui(host: &Host) -> Result<()> {
273    let toolchain = crate::winui::toolchain::WinUiToolchain;
274    if let Err(e) = toolchain.check(host).await {
275        bail!("{}", toolchain_check_message("WinUI", &e));
276    }
277    Ok(())
278}
279
280/// Verify the host toolchain components Hydrolysis builds need.
281///
282/// # Errors
283/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
284pub async fn check_hydrolysis(host: &Host) -> Result<()> {
285    let llvm = WindowsArm64LlvmToolchain;
286    if let Err(e) = llvm.check(host).await {
287        bail!(
288            "{}",
289            toolchain_check_message("Windows ARM64 LLVM toolchain", &e)
290        );
291    }
292    Ok(())
293}
294
295/// Verify the web toolchain is installed.
296///
297/// # Errors
298/// Returns an error describing any missing toolchain component and the `water doctor --fix` remedy.
299pub async fn check_web(host: &Host) -> Result<()> {
300    if let Err(error) = web_toolchain().check(host).await {
301        bail!(
302            "Web toolchain check failed: {error}. Run `water doctor --fix` to install fixable components."
303        );
304    }
305    Ok(())
306}