Skip to main content

krypt_pkg/
brew.rs

1//! `brew` package manager implementation (macOS Homebrew).
2
3use crate::manager::{PackageError, PackageManager, RunOutcome, Runner};
4
5/// Package manager implementation for macOS (Homebrew).
6pub struct Brew;
7
8impl PackageManager for Brew {
9    fn name(&self) -> &'static str {
10        "brew"
11    }
12
13    fn is_available(&self) -> bool {
14        which::which("brew").is_ok()
15    }
16
17    /// `brew info --json=v2` resolves formulae and casks; one Homebrew has
18    /// disabled (e.g. a cask that fails Gatekeeper) still resolves but cannot
19    /// be installed, so it counts as missing. `brew info` does not tap a tap
20    /// that is not installed yet, so for a tap-qualified `user/repo/name` the
21    /// tap is added first — as `brew install` would do — and a tap that cannot
22    /// be added means the package is missing.
23    fn exists(&self, runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError> {
24        if let Some((tap, _)) = pkg.rsplit_once('/').filter(|(tap, _)| tap.contains('/')) {
25            let RunOutcome { status, .. } = runner.run("brew", &["tap", tap])?;
26            if status != 0 {
27                return Ok(false);
28            }
29        }
30        let RunOutcome { status, stdout, .. } = runner.run("brew", &["info", "--json=v2", pkg])?;
31        if status != 0 {
32            return Ok(false);
33        }
34        let info: serde_json::Value = serde_json::from_str(&stdout)
35            .map_err(|e| PackageError::Io(std::io::Error::other(format!("brew info JSON: {e}"))))?;
36        Ok(["formulae", "casks"]
37            .iter()
38            .filter_map(|kind| info[kind].as_array())
39            .flatten()
40            .any(|entry| entry["disabled"] != true))
41    }
42
43    /// `brew list --versions` covers casks as well as formulae; limiting it to
44    /// `--formula` reported every installed cask (e.g. `alacritty`) missing.
45    fn is_installed(&self, runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError> {
46        let RunOutcome { status, stdout, .. } = runner.run("brew", &["list", "--versions", pkg])?;
47        Ok(status == 0 && !stdout.trim().is_empty())
48    }
49
50    fn install(&self, runner: &dyn Runner, packages: &[String]) -> Result<(), PackageError> {
51        let mut args = vec!["install"];
52        let pkg_refs: Vec<&str> = packages.iter().map(String::as_str).collect();
53        args.extend_from_slice(&pkg_refs);
54        let RunOutcome { status, stderr, .. } = runner.run("brew", &args)?;
55        if status != 0 {
56            return Err(PackageError::ExitFailure { status, stderr });
57        }
58        Ok(())
59    }
60}