Skip to main content

waterui_cli/
toolchain.rs

1//! Toolchain management for `WaterUI` CLI
2
3use std::convert::Infallible;
4
5pub mod cargo_helpers;
6pub mod cmake;
7pub mod doctor;
8pub mod dxc;
9pub mod host;
10pub mod linux;
11pub mod managed_tool;
12pub mod meson;
13pub mod msvc;
14pub mod rust;
15pub mod sccache;
16#[cfg(test)]
17pub(crate) mod testing;
18pub mod web;
19pub mod windows_arm64_llvm;
20pub(crate) mod winget;
21
22pub use host::Host;
23/// A toolchain that cannot be fixed automatically.
24#[derive(Debug, Clone, thiserror::Error)]
25#[error("Unfixable toolchain: {message}\nSuggestion: {suggestion}")]
26pub struct UnfixableToolchain {
27    /// A message describing why the toolchain is unfixable.
28    message: String,
29    /// An suggestion for how to fix the toolchain manually.
30    suggestion: String,
31}
32
33impl UnfixableToolchain {
34    /// Create a new `UnfixableToolchain` with the given message and optional suggestion.
35    pub fn new(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
36        Self {
37            message: message.into(),
38            suggestion: suggestion.into(),
39        }
40    }
41
42    /// Get the message describing why the toolchain is unfixable.
43    #[must_use]
44    pub fn message(&self) -> &str {
45        &self.message
46    }
47
48    /// Get the optional suggestion for how to fix the toolchain manually.
49    #[must_use]
50    pub fn suggestion(&self) -> &str {
51        &self.suggestion
52    }
53}
54
55/// Trait representing an installation plan for toolchain components.
56pub trait Installation: Send + Sync {
57    /// The error type returned if installation fails.
58    type Error: Into<eyre::Report> + Send;
59    /// Execute the installation plan against `host`.
60    fn install(&self, host: &Host) -> impl Future<Output = Result<(), Self::Error>> + Send;
61
62    /// Whether running the plan modifies the machine outside `~/.water`
63    /// (system package managers, `msiexec`, the Visual Studio installer).
64    /// `water doctor --fix` requires an interactive confirmation or `--yes`
65    /// before it runs such a plan.
66    fn modifies_system(&self) -> bool {
67        false
68    }
69}
70
71/// Optional installation step.
72///
73/// This is used by composite toolchains (e.g. tuples) to represent "install if missing".
74impl<I: Installation> Installation for Option<I> {
75    type Error = eyre::Report;
76
77    async fn install(&self, host: &Host) -> Result<(), Self::Error> {
78        if let Some(install) = self {
79            install.install(host).await.map_err(Into::into)?;
80        }
81        Ok(())
82    }
83
84    fn modifies_system(&self) -> bool {
85        self.as_ref().is_some_and(Installation::modifies_system)
86    }
87}
88
89/// An error indicating the state of the toolchain.
90#[derive(Debug, Clone, thiserror::Error)]
91pub enum ToolchainError<Install: Installation> {
92    /// The toolchain cannot be fixed automatically.
93    #[error("{0}")]
94    Unfixable(#[from] UnfixableToolchain),
95    /// The toolchain is missing components that can be installed.
96    #[error(
97        "Toolchain is missing components that can be fixed automatically. Run `water doctor --fix` for details."
98    )]
99    Fixable(Install),
100}
101
102impl<I: Installation> ToolchainError<I> {
103    /// Returns `true` if the toolchain can be fixed automatically.
104    #[must_use]
105    pub const fn is_fixable(&self) -> bool {
106        matches!(self, Self::Fixable(_))
107    }
108
109    /// Create a new `ToolchainError` indicating that the toolchain can be fixed automatically.
110    #[must_use]
111    pub const fn fixable(install: I) -> Self {
112        Self::Fixable(install)
113    }
114
115    /// Create a new `ToolchainError` indicating that the toolchain cannot be fixed automatically.
116    #[must_use]
117    pub fn unfixable(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
118        Self::Unfixable(UnfixableToolchain::new(message, suggestion))
119    }
120}
121
122/// Trait for toolchain dependencies that can be checked and installed.
123///
124/// Implementors represent a specific toolchain configuration (e.g., Rust with
125/// certain targets, Android SDK with specific components).
126/// The associated `Installation` type preserves full type information through
127/// the composition, enabling zero-cost abstractions for parallel/sequential
128/// installation plans.
129pub trait Toolchain: Send + Sync {
130    /// The installation type returned by `fix()`.
131    type Installation: Installation;
132
133    /// Check if the toolchain is properly installed on `host`.
134    ///
135    /// Returns `Ok(())` if all components are available, or `Err` describing
136    /// what is missing.
137    fn check(
138        &self,
139        host: &Host,
140    ) -> impl Future<Output = Result<(), ToolchainError<Self::Installation>>> + Send;
141}
142
143impl Installation for Infallible {
144    type Error = Self;
145
146    fn install(&self, _host: &Host) -> impl Future<Output = Result<(), Self::Error>> + Send {
147        std::future::poll_fn(|_| {
148            unreachable!("an Infallible installation plan cannot be constructed")
149        })
150    }
151}
152
153macro_rules! tuples {
154    ($macro:ident) => {
155        $macro!(T0);
156        $macro!(T0, T1);
157        $macro!(T0, T1, T2);
158        $macro!(T0, T1, T2, T3);
159        $macro!(T0, T1, T2, T3, T4);
160        $macro!(T0, T1, T2, T3, T4, T5);
161        $macro!(T0, T1, T2, T3, T4, T5, T6);
162        $macro!(T0, T1, T2, T3, T4, T5, T6, T7);
163        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8);
164        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9);
165        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
166        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
167        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
168        $macro!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
169        $macro!(
170            T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14
171        );
172    };
173}
174
175macro_rules! impl_installations {
176    ($($ty:ident),*) => {
177        #[allow(unused_variables)]
178        #[allow(non_snake_case)]
179        impl<$($ty: Installation),*> Installation for ($($ty,)*) {
180            type Error = eyre::Report;
181            async fn install(&self, host: &Host) -> Result<(), Self::Error> {
182                let ($($ty,)*) = self;
183                $(
184                    $ty.install(host).await.map_err(|e| e.into())?;
185                )*
186                Ok(())
187            }
188
189            fn modifies_system(&self) -> bool {
190                let ($($ty,)*) = self;
191                $(
192                    if $ty.modifies_system() {
193                        return true;
194                    }
195                )*
196                false
197            }
198        }
199    };
200}
201
202impl Installation for () {
203    type Error = eyre::Report;
204
205    fn install(&self, _host: &Host) -> impl Future<Output = Result<(), Self::Error>> + Send {
206        std::future::ready(Ok(()))
207    }
208}
209
210tuples!(impl_installations);
211
212macro_rules! impl_toolchains {
213    ($(($idx:tt, $ty:ident)),*) => {
214        #[allow(unused_variables)]
215        #[allow(non_snake_case)]
216        impl<$($ty: Toolchain),*> Toolchain for ($($ty,)*) {
217            // Each slot is `Some(install)` if that component is missing-and-fixable.
218            // Components that are already OK produce `None` and are skipped during installation.
219            type Installation = ($(Option<$ty::Installation>,)*);
220
221            async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
222                #[allow(unused_mut)]
223                let mut any_fixable = false;
224                #[allow(unused_mut)]
225                let mut installs: Self::Installation = ($(None::<$ty::Installation>,)*);
226
227                $(
228                    match self.$idx.check(host).await {
229                        Ok(()) => {}
230                        Err(ToolchainError::Unfixable(u)) => {
231                            return Err(ToolchainError::Unfixable(u));
232                        }
233                        Err(ToolchainError::Fixable(install)) => {
234                            any_fixable = true;
235                            installs.$idx = Some(install);
236                        }
237                    }
238                )*
239
240                if any_fixable {
241                    Err(ToolchainError::Fixable(installs))
242                } else {
243                    Ok(())
244                }
245            }
246        }
247    };
248}
249
250impl Toolchain for () {
251    type Installation = ();
252
253    fn check(
254        &self,
255        _host: &Host,
256    ) -> impl Future<Output = Result<(), ToolchainError<Self::Installation>>> + Send {
257        std::future::ready(Ok(()))
258    }
259}
260
261macro_rules! tuples_idx {
262    ($macro:ident) => {
263        $macro!((0, T0));
264        $macro!((0, T0), (1, T1));
265        $macro!((0, T0), (1, T1), (2, T2));
266        $macro!((0, T0), (1, T1), (2, T2), (3, T3));
267        $macro!((0, T0), (1, T1), (2, T2), (3, T3), (4, T4));
268        $macro!((0, T0), (1, T1), (2, T2), (3, T3), (4, T4), (5, T5));
269        $macro!(
270            (0, T0),
271            (1, T1),
272            (2, T2),
273            (3, T3),
274            (4, T4),
275            (5, T5),
276            (6, T6)
277        );
278        $macro!(
279            (0, T0),
280            (1, T1),
281            (2, T2),
282            (3, T3),
283            (4, T4),
284            (5, T5),
285            (6, T6),
286            (7, T7)
287        );
288        $macro!(
289            (0, T0),
290            (1, T1),
291            (2, T2),
292            (3, T3),
293            (4, T4),
294            (5, T5),
295            (6, T6),
296            (7, T7),
297            (8, T8)
298        );
299        $macro!(
300            (0, T0),
301            (1, T1),
302            (2, T2),
303            (3, T3),
304            (4, T4),
305            (5, T5),
306            (6, T6),
307            (7, T7),
308            (8, T8),
309            (9, T9)
310        );
311        $macro!(
312            (0, T0),
313            (1, T1),
314            (2, T2),
315            (3, T3),
316            (4, T4),
317            (5, T5),
318            (6, T6),
319            (7, T7),
320            (8, T8),
321            (9, T9),
322            (10, T10)
323        );
324        $macro!(
325            (0, T0),
326            (1, T1),
327            (2, T2),
328            (3, T3),
329            (4, T4),
330            (5, T5),
331            (6, T6),
332            (7, T7),
333            (8, T8),
334            (9, T9),
335            (10, T10),
336            (11, T11)
337        );
338        $macro!(
339            (0, T0),
340            (1, T1),
341            (2, T2),
342            (3, T3),
343            (4, T4),
344            (5, T5),
345            (6, T6),
346            (7, T7),
347            (8, T8),
348            (9, T9),
349            (10, T10),
350            (11, T11),
351            (12, T12)
352        );
353        $macro!(
354            (0, T0),
355            (1, T1),
356            (2, T2),
357            (3, T3),
358            (4, T4),
359            (5, T5),
360            (6, T6),
361            (7, T7),
362            (8, T8),
363            (9, T9),
364            (10, T10),
365            (11, T11),
366            (12, T12),
367            (13, T13)
368        );
369        $macro!(
370            (0, T0),
371            (1, T1),
372            (2, T2),
373            (3, T3),
374            (4, T4),
375            (5, T5),
376            (6, T6),
377            (7, T7),
378            (8, T8),
379            (9, T9),
380            (10, T10),
381            (11, T11),
382            (12, T12),
383            (13, T13),
384            (14, T14)
385        );
386    };
387}
388
389tuples_idx!(impl_toolchains);