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