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
//! Homebrew orchestration. reeve does not bundle servers or PHP — it
//! drives brew-installed binaries. This module detects the brew prefix and
//! provides thin query/install helpers.
use anyhow::{bail, Context, Result};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct Brew {
pub prefix: PathBuf,
}
impl Brew {
/// Detect the active Homebrew install. Checks the canonical Apple-silicon /
/// Intel locations by absolute path first — `which brew` is unreliable under
/// non-login shells (SSH, launchd) where `.zprofile` hasn't run — then falls
/// back to `brew --prefix` for unusual custom prefixes.
pub fn detect() -> Result<Self> {
for candidate in ["/opt/homebrew", "/usr/local"] {
let p = Path::new(candidate);
if p.join("bin/brew").exists() {
return Ok(Self {
prefix: p.to_path_buf(),
});
}
}
if let Ok(out) = Command::new("brew").arg("--prefix").output() {
if out.status.success() {
let prefix = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !prefix.is_empty() && Path::new(&prefix).join("bin/brew").exists() {
return Ok(Self {
prefix: PathBuf::from(prefix),
});
}
}
}
bail!("Homebrew not found")
}
/// Detect Homebrew, or — when running interactively — offer to install it.
/// Returns an error (with guidance) if absent and not installed.
pub fn detect_or_offer_install() -> Result<Self> {
match Self::detect() {
Ok(brew) => Ok(brew),
Err(_) => {
eprintln!("Homebrew is required but was not found.");
if confirm("Install Homebrew now (runs the official installer)?")? {
install_homebrew()?;
Self::detect().context(
"Homebrew install finished but it still can't be located. \
Open a new terminal and re-run `reeve init`.",
)
} else {
bail!("Install Homebrew from https://brew.sh, then re-run `reeve init`.")
}
}
}
}
/// The absolute path to the `brew` binary for this install. Always use this
/// rather than a bare `brew` so we work without a configured PATH.
pub fn brew_bin(&self) -> PathBuf {
self.prefix.join("bin/brew")
}
/// `<prefix>/etc/...`
pub fn etc(&self, sub: &str) -> PathBuf {
self.prefix.join("etc").join(sub)
}
/// `<prefix>/opt/<formula>` — the stable, version-pinned install path.
pub fn opt(&self, formula: &str) -> PathBuf {
self.prefix.join("opt").join(formula)
}
/// `<prefix>/bin/<name>`
pub fn bin(&self, name: &str) -> PathBuf {
self.prefix.join("bin").join(name)
}
/// Is a formula installed? Uses the `opt/<formula>` symlink, which is cheap
/// and avoids spawning `brew list`.
pub fn is_installed(&self, formula: &str) -> bool {
self.opt(formula).exists()
}
/// Run `brew install <formula>`, streaming output to the user's terminal.
pub fn install(&self, formula: &str) -> Result<()> {
let status = Command::new(self.brew_bin())
.arg("install")
.arg(formula)
.status()
.with_context(|| format!("Failed to spawn `brew install {formula}`"))?;
if !status.success() {
bail!("`brew install {formula}` failed");
}
Ok(())
}
/// Ensure a tap is present (`brew tap <tap>`).
pub fn ensure_tap(&self, tap: &str) -> Result<()> {
let status = Command::new(self.brew_bin())
.arg("tap")
.arg(tap)
.status()
.with_context(|| format!("Failed to spawn `brew tap {tap}`"))?;
if !status.success() {
bail!("`brew tap {tap}` failed");
}
Ok(())
}
/// Trust a third-party tap (`brew trust <tap>`). Newer Homebrew refuses to
/// load formulae from untrusted taps without this. Best-effort: older
/// Homebrew has no `trust` subcommand, so a failure here is not fatal.
pub fn trust_tap(&self, tap: &str) {
let _ = Command::new(self.brew_bin()).arg("trust").arg(tap).status();
}
}
/// Prompt the user for a yes/no answer on the terminal. Defaults to "no" when
/// stdin is not a TTY (e.g. scripted/CI runs), so we never block headless.
fn confirm(question: &str) -> Result<bool> {
if unsafe { libc::isatty(libc::STDIN_FILENO) } == 0 {
return Ok(false);
}
print!("{question} [y/N] ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(matches!(input.trim().to_lowercase().as_str(), "y" | "yes"))
}
/// Run the official Homebrew installer.
fn install_homebrew() -> Result<()> {
let installer = r#"/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)""#;
let status = Command::new("/bin/bash")
.arg("-c")
.arg(installer)
.status()
.context("Failed to run the Homebrew installer")?;
if !status.success() {
bail!("Homebrew installation failed");
}
Ok(())
}