use crate::brew::types::{BrewDiff, BrewListType};
use crate::brew::xcode::ensure_xcode_clt;
use crate::cli::atomic::should_dry_run;
use crate::config::Brew;
use crate::util::io::confirm;
use crate::{log_dry, log_info, log_warn};
use anyhow::{Result, bail};
use std::collections::HashSet;
use std::{env, path::Path};
use tokio::process::Command;
use tokio::{fs, try_join};
async fn set_homebrew_env_vars() {
let existing_path = std::env::var("PATH").unwrap_or_default();
if fs::try_exists(Path::new("/opt/homebrew/bin/brew"))
.await
.unwrap_or_default()
{
let bin = "/opt/homebrew/bin";
let sbin = "/opt/homebrew/sbin";
let mut new_path = existing_path.clone();
if !existing_path.split(':').any(|p| p == bin) {
new_path = format!("{bin}:{new_path}");
}
if !existing_path.split(':').any(|p| p == sbin) {
new_path = format!("{sbin}:{new_path}");
}
unsafe { env::set_var("PATH", &new_path) };
} else {
log_warn!("Brew binary not found in standard directories; $PATH not updated.");
}
unsafe { env::set_var("HOMEBREW_NO_AUTO_UPDATE", "1") };
unsafe { env::set_var("HOMEBREW_NO_ANALYTICS", "1") };
unsafe { env::set_var("HOMEBREW_NO_ENV_HINTS", "1") };
log_info!("Homebrew environment has been configured for this process.");
}
async fn install_homebrew() -> Result<()> {
let primary_status = Command::new("sudo")
.args([
"echo",
"Granted sudo permissions for this session (required for Homebrew non-interactive install).",
])
.status()
.await?;
if !primary_status.success() {
bail!("Authorization is needed for installing Homebrew.")
}
let install_command = "NONINTERACTIVE=1 curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | /bin/bash";
let status = Command::new("/bin/bash")
.arg("-c")
.arg(install_command)
.status()
.await?;
log_info!("Installing Homebrew...");
if !status.success() {
bail!("Homebrew install script failed: {status}");
}
Ok(())
}
pub async fn brew_is_installed() -> bool {
Command::new("brew")
.arg("--version")
.output()
.await
.map(|op| op.status.success())
.unwrap_or(false)
}
pub async fn ensure_brew() -> Result<()> {
ensure_xcode_clt().await?;
if !brew_is_installed().await {
if should_dry_run() {
log_dry!("Would install Homebrew since not found in $PATH.");
return Ok(());
}
log_warn!("Homebrew is not installed.");
if confirm("Install Homebrew now?") {
install_homebrew().await?;
set_homebrew_env_vars().await;
if !brew_is_installed().await {
bail!("Homebrew installation seems to have failed or brew is still not in $PATH.");
}
} else {
bail!("Homebrew is required for brew operations, but was not found.");
}
}
Ok(())
}
pub async fn brew_list(list_type: BrewListType) -> Result<HashSet<String>> {
let args: Vec<String> = if list_type == BrewListType::Tap {
vec![list_type.to_string()]
} else {
let lt_str = list_type.to_string();
vec![
"list".to_string(),
"--quiet".to_string(),
"--full-name".to_string(),
"-1".to_string(),
lt_str,
]
};
let output = Command::new("brew").args(&args).output().await?;
log_info!("Running {list_type} list command...");
if !output.status.success() {
log_warn!("{list_type} listing failed, will return empty.");
return Ok(HashSet::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: HashSet<String> = stdout
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
Ok(lines)
}
pub async fn diff_brew(brew_cfg: Brew) -> Result<BrewDiff> {
let no_deps = brew_cfg.no_deps.unwrap_or(false);
let config_formulae = brew_cfg.formulae.clone().unwrap_or_default();
let config_casks = brew_cfg.casks.clone().unwrap_or_default();
let config_taps = brew_cfg.taps.clone().unwrap_or_default();
let (mut installed_formulae, installed_casks, installed_taps) = try_join!(
brew_list(BrewListType::Formula),
brew_list(BrewListType::Cask),
brew_list(BrewListType::Tap)
)?;
if no_deps {
log_info!("--no-deps used, proceeding with checks...");
let installed_as_deps = brew_list(BrewListType::Dependency).await?;
installed_formulae.retain(|f| !installed_as_deps.contains(f));
}
let missing_formulae: Vec<String> = config_formulae
.iter()
.filter(|&f| !installed_formulae.contains(f))
.cloned()
.collect();
let extra_formulae: Vec<String> = installed_formulae
.iter()
.filter(|&f| !config_formulae.contains(f))
.cloned()
.collect();
let missing_casks: Vec<String> = config_casks
.iter()
.filter(|&c| !installed_casks.contains(c))
.cloned()
.collect();
let extra_casks: Vec<String> = installed_casks
.iter()
.filter(|&c| !config_casks.contains(c))
.cloned()
.collect();
let missing_taps: Vec<String> = config_taps
.iter()
.filter(|&t| !installed_taps.contains(t))
.cloned()
.collect();
let extra_taps: Vec<String> = installed_taps
.iter()
.filter(|&t| !config_taps.contains(t))
.cloned()
.collect();
Ok(BrewDiff {
missing_formulae,
extra_formulae,
missing_casks,
extra_casks,
missing_taps,
extra_taps,
})
}