waterui-cli 0.1.4

Cross-platform tooling for WaterUI applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
//! Toolchain diagnostics for the `water doctor` command.

use std::future::Future;
use std::pin::Pin;

use color_eyre::eyre;

use crate::{
    android::{
        device::AndroidDevice,
        platform::AndroidPlatform,
        toolchain::{
            AndroidBuildTools, AndroidNdk, AndroidPlatformTools, AndroidRustTargets, AndroidSdk,
            AndroidSdkPlatforms, Java, Kotlin,
        },
    },
    apple::{
        device::AppleSimulator,
        toolchain::{AppleSdk, Xcode},
    },
    device::Device,
    gtk4::toolchain::Gtk4Toolchain,
    toolchain::{
        Installation, Toolchain, ToolchainError, UnfixableToolchain,
        cmake::Cmake,
        linux::LinuxSystemToolchain,
        rust::RustToolchain,
        sccache::Sccache,
        web::{Wasm32UnknownUnknownTarget, WasmPack},
        windows_arm64_llvm::WindowsArm64LlvmToolchain,
    },
};

/// Status of a toolchain check.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
    /// Toolchain is available and working.
    Ok,
    /// Toolchain is missing or misconfigured.
    Missing,
    /// Toolchain check was skipped (e.g., not applicable on this platform).
    Skipped,
}

/// A boxed async function that performs an installation.
pub type BoxedInstallFn =
    Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = eyre::Result<()>> + Send>> + Send>;

/// A single item in the doctor report.
pub struct DoctorItem {
    /// Name of the toolchain or component.
    pub name: &'static str,
    /// Status of the check.
    pub status: CheckStatus,
    /// Optional message with details or suggestions.
    pub message: Option<String>,
    /// Optional installation function if the issue can be fixed automatically.
    pub install_fn: Option<BoxedInstallFn>,
}

impl std::fmt::Debug for DoctorItem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DoctorItem")
            .field("name", &self.name)
            .field("status", &self.status)
            .field("message", &self.message)
            .field("install_fn", &self.install_fn.as_ref().map(|_| "..."))
            .finish()
    }
}

impl DoctorItem {
    const fn ok(name: &'static str) -> Self {
        Self {
            name,
            status: CheckStatus::Ok,
            message: None,
            install_fn: None,
        }
    }

    fn missing(name: &'static str, message: impl Into<String>) -> Self {
        Self {
            name,
            status: CheckStatus::Missing,
            message: Some(message.into()),
            install_fn: None,
        }
    }

    fn fixable<I: Installation + Send + 'static>(
        name: &'static str,
        message: impl Into<String>,
        installation: I,
    ) -> Self {
        Self {
            name,
            status: CheckStatus::Missing,
            message: Some(message.into()),
            install_fn: Some(Box::new(move || {
                Box::pin(async move { installation.install().await.map_err(Into::into) })
            })),
        }
    }

    const fn skipped(name: &'static str) -> Self {
        Self {
            name,
            status: CheckStatus::Skipped,
            message: None,
            install_fn: None,
        }
    }

    fn skipped_with_message(name: &'static str, message: impl Into<String>) -> Self {
        Self {
            name,
            status: CheckStatus::Skipped,
            message: Some(message.into()),
            install_fn: None,
        }
    }

    /// Returns `true` if the issue can be fixed automatically.
    #[must_use]
    pub const fn is_fixable(&self) -> bool {
        self.install_fn.is_some()
    }
}

fn unfixable_message(error: &UnfixableToolchain) -> String {
    format!(
        "Cannot auto-fix: {}. Next step: {}",
        error.message(),
        error.suggestion()
    )
}

async fn push_toolchain_check<T>(
    items: &mut Vec<DoctorItem>,
    name: &'static str,
    fixable_message: &'static str,
    toolchain: T,
) where
    T: Toolchain,
    T::Installation: Send + 'static,
{
    match toolchain.check().await {
        Ok(()) => items.push(DoctorItem::ok(name)),
        Err(ToolchainError::Fixable(installation)) => {
            items.push(DoctorItem::fixable(name, fixable_message, installation));
        }
        Err(ToolchainError::Unfixable(error)) => {
            items.push(DoctorItem::missing(name, unfixable_message(&error)));
        }
    }
}

async fn push_toolchain_check_with_unfixable<T, F>(
    items: &mut Vec<DoctorItem>,
    name: &'static str,
    fixable_message: &'static str,
    toolchain: T,
    unfixable_message_fn: F,
) where
    T: Toolchain,
    T::Installation: Send + 'static,
    F: FnOnce(&UnfixableToolchain) -> String,
{
    match toolchain.check().await {
        Ok(()) => items.push(DoctorItem::ok(name)),
        Err(ToolchainError::Fixable(installation)) => {
            items.push(DoctorItem::fixable(name, fixable_message, installation));
        }
        Err(ToolchainError::Unfixable(error)) => {
            items.push(DoctorItem::missing(name, unfixable_message_fn(&error)));
        }
    }
}

async fn push_apple_checks(items: &mut Vec<DoctorItem>) {
    if !cfg!(target_os = "macos") {
        items.push(DoctorItem::skipped("Xcode"));
        items.push(DoctorItem::skipped("iOS SDK"));
        items.push(DoctorItem::skipped("iOS Simulator SDK"));
        items.push(DoctorItem::skipped("iOS Simulators"));
        items.push(DoctorItem::skipped("macOS SDK"));
        return;
    }

    push_simple_check(items, "Xcode", Xcode.check().await);
    push_simple_check(items, "iOS SDK", AppleSdk::Ios.check().await);
    push_simple_check(
        items,
        "iOS Simulator SDK",
        AppleSdk::IosSimulator.check().await,
    );
    push_ios_simulator_check(items).await;
    push_simple_check(items, "macOS SDK", AppleSdk::Macos.check().await);
}

fn push_simple_check(
    items: &mut Vec<DoctorItem>,
    name: &'static str,
    result: Result<(), impl std::fmt::Display>,
) {
    match result {
        Ok(()) => items.push(DoctorItem::ok(name)),
        Err(error) => items.push(DoctorItem::missing(name, error.to_string())),
    }
}

async fn push_ios_simulator_check(items: &mut Vec<DoctorItem>) {
    match AppleSimulator::scan_ios().await {
        Ok(simulators) if simulators.is_empty() => items.push(DoctorItem::missing(
            "iOS Simulators",
            "No iOS simulators available. Install a simulator runtime in Xcode Settings > Platforms.",
        )),
        Ok(_) => items.push(DoctorItem::ok("iOS Simulators")),
        Err(error) => items.push(DoctorItem::missing(
            "iOS Simulators",
            format!("Failed to list iOS simulators: {error}"),
        )),
    }
}

async fn push_android_sdk_checks(items: &mut Vec<DoctorItem>) -> bool {
    push_toolchain_check(
        items,
        "Android SDK",
        "Android SDK is missing (automatic install is supported on this host)",
        AndroidSdk,
    )
    .await;

    AndroidSdk::sdkmanager_path().await.is_some()
}

async fn push_android_component_checks(items: &mut Vec<DoctorItem>, sdk_ready: bool) {
    if !sdk_ready {
        push_blocked_android_component_checks(items);
        return;
    }

    push_toolchain_check(
        items,
        "Android Platform-Tools (adb)",
        "Required for `water run --platform android`",
        AndroidPlatformTools,
    )
    .await;
    push_toolchain_check(
        items,
        "Android SDK Platforms",
        "Required for Android build/package workflows",
        AndroidSdkPlatforms,
    )
    .await;
    push_toolchain_check(
        items,
        "Android SDK Build-Tools (d8)",
        "Required for Android build/package workflows",
        AndroidBuildTools,
    )
    .await;
    push_toolchain_check(
        items,
        "Android NDK",
        "Required for Android build/package workflows",
        AndroidNdk,
    )
    .await;
    push_toolchain_check(
        items,
        "Android Rust Targets",
        "Required for Android Rust cross-compilation",
        AndroidRustTargets::default(),
    )
    .await;
}

fn push_blocked_android_component_checks(items: &mut Vec<DoctorItem>) {
    for name in [
        "Android Platform-Tools (adb)",
        "Android NDK",
        "Android SDK Platforms",
        "Android SDK Build-Tools (d8)",
        "Android Rust Targets",
    ] {
        items.push(DoctorItem::missing(
            name,
            "Blocked: Android SDK / `sdkmanager` is not ready yet. Fix Android SDK first.",
        ));
    }
}

async fn push_android_run_target_check(items: &mut Vec<DoctorItem>) {
    if AndroidSdk::adb_path().is_none() {
        items.push(DoctorItem::missing(
            "Android Run Targets",
            "Blocked: Android Platform-Tools (`adb`) is not ready yet.",
        ));
        return;
    }

    match AndroidDevice::scan().await {
        Ok(devices) if !devices.is_empty() => items.push(DoctorItem::ok("Android Run Targets")),
        Ok(_) => match AndroidPlatform::list_avds().await {
            Ok(avds) if !avds.is_empty() => items.push(DoctorItem::ok("Android Run Targets")),
            Ok(_) => items.push(DoctorItem::missing(
                "Android Run Targets",
                "No connected Android devices and no emulator AVDs were found. Connect a device or create an AVD.",
            )),
            Err(error) => items.push(DoctorItem::missing(
                "Android Run Targets",
                format!(
                    "No connected Android devices and failed to list AVDs: {error}. Install Android emulator components or connect a device."
                ),
            )),
        },
        Err(error) => items.push(DoctorItem::missing(
            "Android Run Targets",
            format!("Failed to query Android devices via adb: {error}"),
        )),
    }
}

async fn push_desktop_and_web_checks(items: &mut Vec<DoctorItem>) {
    push_toolchain_check(
        items,
        "Host CMake",
        "Required for native Rust dependencies in Android builds",
        Cmake::default(),
    )
    .await;

    if WindowsArm64LlvmToolchain::required_on_host() {
        push_toolchain_check(
            items,
            "Windows ARM64 LLVM toolchain",
            "Required by native assembly dependencies in Windows ARM64 hydrolysis builds",
            WindowsArm64LlvmToolchain,
        )
        .await;
    } else {
        items.push(DoctorItem::skipped_with_message(
            "Windows ARM64 LLVM toolchain",
            "Only required on Windows ARM64 hosts for native assembly dependencies.",
        ));
    }

    push_toolchain_check(items, "Java", "Required for Android Gradle builds", Java).await;
    push_toolchain_check(
        items,
        "Kotlin",
        "Required for Android Kotlin helper compilation",
        Kotlin,
    )
    .await;
    push_toolchain_check_with_unfixable(
        items,
        "Rust wasm32 target",
        "wasm32-unknown-unknown target not installed",
        Wasm32UnknownUnknownTarget,
        ToString::to_string,
    )
    .await;
    push_toolchain_check_with_unfixable(
        items,
        "wasm-pack",
        "wasm-pack not found (required for web packaging)",
        WasmPack,
        ToString::to_string,
    )
    .await;
}

async fn push_linux_checks(items: &mut Vec<DoctorItem>) {
    if !cfg!(target_os = "linux") {
        items.push(DoctorItem::skipped("Linux system packages"));
        items.push(DoctorItem::skipped("GTK4"));
        return;
    }

    let linux_packages_fixable = match LinuxSystemToolchain.check().await {
        Ok(()) => {
            items.push(DoctorItem::ok("Linux system packages"));
            false
        }
        Err(ToolchainError::Fixable(installation)) => {
            let msg = format!(
                "Missing packages for {}: {}. Install command: {}",
                installation.package_manager_name(),
                installation.missing_packages().join(", "),
                installation.install_command_hint(),
            );
            items.push(DoctorItem::fixable(
                "Linux system packages",
                msg,
                installation,
            ));
            true
        }
        Err(ToolchainError::Unfixable(error)) => {
            items.push(DoctorItem::missing(
                "Linux system packages",
                unfixable_message(&error),
            ));
            false
        }
    };

    match Gtk4Toolchain.check().await {
        Ok(()) => items.push(DoctorItem::ok("GTK4")),
        Err(ToolchainError::Fixable(installation)) => {
            items.push(DoctorItem::fixable(
                "GTK4",
                "GTK4 dependencies are missing",
                installation,
            ));
        }
        Err(ToolchainError::Unfixable(error)) => {
            if linux_packages_fixable {
                items.push(DoctorItem::missing(
                    "GTK4",
                    "GTK4 probe failed because required Linux packages are missing. Run `water doctor --fix` to install Linux system packages, then re-run `water doctor`.",
                ));
            } else {
                items.push(DoctorItem::missing("GTK4", unfixable_message(&error)));
            }
        }
    }
}

/// Run diagnostics on all toolchains and return a report.
pub async fn doctor() -> Vec<DoctorItem> {
    let mut items = Vec::new();
    push_apple_checks(&mut items).await;
    push_rust_toolchain_check(&mut items).await;
    let sdk_ready = push_android_sdk_checks(&mut items).await;
    push_android_component_checks(&mut items, sdk_ready).await;
    push_android_run_target_check(&mut items).await;
    push_desktop_and_web_checks(&mut items).await;
    push_linux_checks(&mut items).await;
    push_toolchain_check(
        &mut items,
        "sccache",
        "sccache not found (recommended for faster builds)",
        Sccache,
    )
    .await;

    items
}

async fn push_rust_toolchain_check(items: &mut Vec<DoctorItem>) {
    match RustToolchain.check().await {
        Ok(()) => items.push(DoctorItem::ok("Rust toolchain")),
        Err(ToolchainError::Fixable(installation)) => {
            items.push(DoctorItem::fixable(
                "Rust toolchain",
                format!(
                    "Rust toolchain is missing, outdated, or incomplete. Planned automatic fixes: {}",
                    installation.summary()
                ),
                installation,
            ));
        }
        Err(ToolchainError::Unfixable(error)) => items.push(DoctorItem::missing(
            "Rust toolchain",
            unfixable_message(&error),
        )),
    }
}