use crate::brew::Brew;
use crate::php::formula;
use anyhow::{bail, Context, Result};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
fn php_bin(brew: &Brew, version: &str) -> PathBuf {
brew.opt(&formula(version)).join("bin/php")
}
fn pecl_bin(brew: &Brew, version: &str) -> PathBuf {
brew.opt(&formula(version)).join("bin/pecl")
}
pub fn list(brew: &Brew, version: &str) -> Result<Vec<String>> {
let php = php_bin(brew, version);
if !php.exists() {
bail!("PHP {version} is not installed");
}
let out = Command::new(&php)
.arg("-m")
.output()
.with_context(|| format!("Failed to run `php -m` for {version}"))?;
if !out.status.success() {
bail!("`php -m` failed for {version}");
}
let mods = String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('['))
.map(|l| l.to_string())
.collect();
Ok(mods)
}
pub fn is_loaded(brew: &Brew, version: &str, name: &str) -> Result<bool> {
Ok(list(brew, version)?
.iter()
.any(|m| m.eq_ignore_ascii_case(name)))
}
pub fn add(brew: &Brew, version: &str, name: &str) -> Result<()> {
let pecl = pecl_bin(brew, version);
if !pecl.exists() {
bail!(
"pecl not found for PHP {version} (is {} installed?)",
formula(version)
);
}
if !brew.is_installed("autoconf") {
println!("Installing autoconf (needed to build PHP extensions)…");
brew.install("autoconf")?;
}
println!("Building {name} for PHP {version} via pecl…");
let brew_bin = brew.prefix.join("bin");
let path = format!(
"{}:{}",
brew_bin.display(),
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin:/usr/sbin:/sbin".into())
);
let mut child = Command::new(&pecl)
.args(["install", "-f", name])
.env("PATH", path)
.env("PHP_AUTOCONF", brew_bin.join("autoconf"))
.env("PHP_AUTOHEADER", brew_bin.join("autoheader"))
.stdin(Stdio::piped())
.spawn()
.with_context(|| format!("Failed to spawn pecl install {name}"))?;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
let _ = stdin.write_all(b"\n\n\n\n\n\n\n\n");
}
let status = child.wait().context("pecl install failed to run")?;
if !status.success() {
bail!("pecl could not build '{name}' for PHP {version}");
}
Ok(())
}
pub fn remove(brew: &Brew, version: &str, name: &str) -> Result<()> {
let pecl = pecl_bin(brew, version);
if !pecl.exists() {
bail!("pecl not found for PHP {version}");
}
let out = Command::new(&pecl)
.args(["uninstall", name])
.output()
.with_context(|| format!("Failed to run pecl uninstall {name}"))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.contains("not installed") && !stderr.trim().is_empty() {
bail!("pecl uninstall failed: {}", stderr.trim());
}
}
strip_extension_lines(brew, version, name)?;
Ok(())
}
fn ini_files(brew: &Brew, version: &str) -> Vec<PathBuf> {
let base = brew.etc("php").join(version);
let mut files = vec![base.join("php.ini")];
if let Ok(rd) = fs::read_dir(base.join("conf.d")) {
for e in rd.flatten() {
if e.path().extension().is_some_and(|x| x == "ini") {
files.push(e.path());
}
}
}
files
}
fn is_extension_line(line: &str, name: &str) -> bool {
let t = line.trim();
if t.starts_with(';') {
return false;
}
let lower = t.to_lowercase();
(lower.starts_with("extension") || lower.starts_with("zend_extension"))
&& lower.contains(&name.to_lowercase())
}
fn strip_extension_lines(brew: &Brew, version: &str, name: &str) -> Result<()> {
for f in ini_files(brew, version) {
let Ok(content) = fs::read_to_string(&f) else {
continue;
};
let kept: Vec<&str> = content
.lines()
.filter(|l| !is_extension_line(l, name))
.collect();
if kept.len() != content.lines().count() {
fs::write(&f, format!("{}\n", kept.join("\n")))
.with_context(|| format!("Failed to update {}", f.display()))?;
}
}
Ok(())
}