Skip to main content

waterui_cli/dependencies/
brew.rs

1//! Brew toolchain manager for `WaterUI` CLI
2
3use crate::{
4    toolchain::{Host, Installation, Toolchain, ToolchainError},
5    utils::CommandError,
6};
7
8/// Homebrew toolchain manager
9#[derive(Debug, Default)]
10pub struct Brew {}
11
12impl Brew {
13    /// Install a formula via Homebrew
14    ///
15    /// # Arguments
16    /// * `name` - The name of the formula to install
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the `brew install` command fails.
21    pub async fn install(&self, host: &Host, name: &str) -> Result<(), CommandError> {
22        host.run("brew", ["install", name]).await?;
23        Ok(())
24    }
25
26    /// Install a cask via Homebrew
27    ///
28    /// # Arguments
29    /// * `cask` - The cask identifier (e.g. "android-studio")
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the `brew install --cask` command fails.
34    pub async fn install_cask(&self, host: &Host, cask: &str) -> Result<(), CommandError> {
35        host.run("brew", ["install", "--cask", cask]).await?;
36        Ok(())
37    }
38}
39
40impl Toolchain for Brew {
41    type Installation = BrewInstallation;
42    async fn check(
43        &self,
44        host: &Host,
45    ) -> Result<(), crate::toolchain::ToolchainError<Self::Installation>> {
46        if host.which("brew").await.is_ok() {
47            Ok(())
48        } else if cfg!(target_os = "macos") {
49            Err(ToolchainError::fixable(BrewInstallation))
50        } else {
51            Err(ToolchainError::unfixable(
52                "Homebrew is only supported on macOS",
53                "Why did you try to use Homebrew on a non-macOS system?",
54            ))
55        }
56    }
57}
58
59/// Installation procedure for Homebrew
60///
61/// This will run the official Homebrew installation script.
62#[derive(Debug)]
63pub struct BrewInstallation;
64
65impl Installation for BrewInstallation {
66    type Error = CommandError;
67
68    async fn install(&self, host: &Host) -> Result<(), Self::Error> {
69        host.run(
70            "sh",
71            [
72                "-c",
73                "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)",
74            ],
75        )
76        .await?;
77        Ok(())
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::Brew;
84    use crate::toolchain::testing::TestMachine;
85    use crate::toolchain::{Toolchain, ToolchainError};
86
87    #[test]
88    fn ok_when_brew_on_path() {
89        let machine = TestMachine::new();
90        machine.install("brew");
91        let host = machine.host(Vec::<(String, String)>::new());
92        smol::block_on(Brew::default().check(&host)).expect("brew on PATH must be ok");
93    }
94
95    #[test]
96    fn missing_brew_classification_is_platform_scoped() {
97        let machine = TestMachine::new();
98        let host = machine.host(Vec::<(String, String)>::new());
99        let result = smol::block_on(Brew::default().check(&host));
100        if cfg!(target_os = "macos") {
101            assert!(
102                matches!(result, Err(ToolchainError::Fixable(_))),
103                "missing brew on macOS must be fixable: {result:?}"
104            );
105        } else {
106            assert!(
107                matches!(result, Err(ToolchainError::Unfixable(_))),
108                "brew off macOS must be unfixable: {result:?}"
109            );
110        }
111    }
112
113    #[test]
114    fn install_runs_brew_install() {
115        let machine = TestMachine::new();
116        machine.install("brew");
117        let host = machine.host(Vec::<(String, String)>::new());
118        smol::block_on(Brew::default().install(&host, "cmake"))
119            .expect("brew install must succeed against the fake tool");
120        smol::block_on(Brew::default().install_cask(&host, "android-studio"))
121            .expect("brew install --cask must succeed against the fake tool");
122    }
123}