pub mod extensions;
use crate::brew::Brew;
use crate::daemon::{self, ServiceSpec};
use crate::paths;
use crate::state::PhpVersion;
use anyhow::{bail, Context, Result};
use std::path::PathBuf;
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
pub fn formula(version: &str) -> String {
format!("php@{version}")
}
fn compact(version: &str) -> String {
version.chars().filter(|c| c.is_ascii_digit()).collect()
}
pub fn fpm_socket(version: &str) -> Result<PathBuf> {
Ok(paths::run_dir()?.join(format!("php{}.sock", compact(version))))
}
pub fn service_id(version: &str) -> String {
format!("php-{}", compact(version))
}
fn fpm_binary(brew: &Brew, version: &str) -> PathBuf {
brew.opt(&formula(version)).join("sbin/php-fpm")
}
fn ini_dir(brew: &Brew, version: &str) -> PathBuf {
brew.etc("php").join(version)
}
fn current_user() -> String {
std::env::var("USER")
.ok()
.filter(|u| !u.is_empty())
.unwrap_or_else(|| {
Command::new("id")
.arg("-un")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "_www".to_string())
})
}
pub fn render_fpm_conf(version: &str) -> Result<PathBuf> {
paths::ensure_dirs()?;
let c = compact(version);
let socket = fpm_socket(version)?;
let user = current_user();
let run = paths::run_dir()?;
let logs = paths::logs_dir()?;
let conf = format!(
r#"; Generated by reeve — do not edit by hand.
; PHP {version} FPM master.
[global]
pid = {run}/php{c}-fpm.pid
error_log = {logs}/php{c}-fpm.log
daemonize = no
[www]
user = {user}
group = staff
listen = {socket}
listen.owner = {user}
listen.group = staff
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
catch_workers_output = yes
clear_env = no
php_admin_value[error_log] = {logs}/php{c}-php.log
php_admin_flag[log_errors] = on
"#,
version = version,
c = c,
run = run.display(),
logs = logs.display(),
user = user,
socket = socket.display(),
);
let path = paths::generated_dir()?
.join("fpm")
.join(format!("php{c}.conf"));
std::fs::write(&path, conf)
.with_context(|| format!("Failed to write FPM config {}", path.display()))?;
Ok(path)
}
pub fn ensure_fpm_running(brew: &Brew, version: &str) -> Result<()> {
let conf = render_fpm_conf(version)?;
let bin = fpm_binary(brew, version);
if !bin.exists() {
bail!(
"php-fpm binary not found at {} — is {} installed?",
bin.display(),
formula(version)
);
}
let spec = ServiceSpec {
service: service_id(version),
program: bin,
args: vec![
"--nodaemonize".into(),
"--fpm-config".into(),
conf.display().to_string(),
"-c".into(),
ini_dir(brew, version).display().to_string(),
],
log: paths::logs_dir()?.join(format!("php{}-launchd.log", compact(version))),
keep_alive: true,
run_at_load: true,
};
daemon::install(&spec)?;
daemon::restart(&service_id(version))?;
let socket = fpm_socket(version)?;
let deadline = Instant::now() + Duration::from_secs(8);
while Instant::now() < deadline {
if daemon::socket_alive(&socket) {
return Ok(());
}
thread::sleep(Duration::from_millis(150));
}
bail!(
"FPM master for PHP {version} started but socket {} never appeared. \
Check {}.",
socket.display(),
paths::logs_dir()?
.join(format!("php{}-launchd.log", compact(version)))
.display()
)
}
pub fn is_installed(brew: &Brew, version: &str) -> bool {
brew.is_installed(&formula(version))
}
pub fn install(brew: &Brew, version: &str) -> Result<PhpVersion> {
brew.ensure_tap("shivammathur/php")?;
brew.trust_tap("shivammathur/php");
if is_installed(brew, version) {
println!("• {} already installed — adopting it", formula(version));
} else {
println!(
"Installing {} (this can take a few minutes)…",
formula(version)
);
brew.install(&formula(version))?;
}
ensure_fpm_running(brew, version)?;
Ok(PhpVersion {
version: version.to_string(),
fpm_socket: fpm_socket(version)?.display().to_string(),
extensions: Vec::new(),
})
}
pub fn discover(brew: &Brew) -> Vec<String> {
let opt = brew.prefix.join("opt");
let mut found = Vec::new();
if let Ok(entries) = std::fs::read_dir(&opt) {
for e in entries.flatten() {
if let Some(name) = e.file_name().to_str() {
if let Some(ver) = name.strip_prefix("php@") {
found.push(ver.to_string());
}
}
}
}
found.sort();
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn naming_conventions() {
assert_eq!(formula("8.3"), "php@8.3");
assert_eq!(compact("8.3"), "83");
assert_eq!(service_id("8.4"), "php-84");
assert!(fpm_socket("8.3").unwrap().ends_with("php83.sock"));
}
}