use std::{
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
#[cfg(windows)]
use std::fmt::Display;
use anyhow::{Context, Error, Result, bail};
#[must_use]
pub fn find_executable(name: &str) -> Option<PathBuf> {
const WHICH: &str = if cfg!(windows) { "where" } else { "which" };
let cmd = Command::new(WHICH).arg(name).output().ok()?;
if cmd.status.success() {
let stdout = String::from_utf8_lossy(&cmd.stdout);
stdout.trim().lines().next().map(|l| l.trim().into())
} else {
None
}
}
#[must_use]
pub fn path_from_env(key: &str) -> Option<PathBuf> {
std::env::var_os(key).map(PathBuf::from)
}
pub fn find_php() -> Result<PathBuf> {
if let Some(path) = path_from_env("PHP") {
if !path.try_exists()? {
bail!("php executable not found at {}", path.display());
}
return Ok(path);
}
find_executable("php").with_context(|| {
"Could not find PHP executable. \
Please ensure `php` is in your PATH or the `PHP` environment variable is set."
})
}
pub struct PHPInfo(String);
impl PHPInfo {
pub fn get(php: &Path) -> Result<Self> {
let cmd = Command::new(php)
.arg("-i")
.output()
.context("Failed to call `php -i`")?;
if !cmd.status.success() {
bail!("Failed to call `php -i` status code {}", cmd.status);
}
let stdout = String::from_utf8_lossy(&cmd.stdout);
Ok(Self(stdout.to_string()))
}
pub fn thread_safety(&self) -> Result<bool> {
Ok(self
.get_key("Thread Safety")
.context("Could not find thread safety of PHP")?
== "enabled")
}
pub fn debug(&self) -> Result<bool> {
Ok(self
.get_key("Debug Build")
.context("Could not find debug build of PHP")?
== "yes")
}
pub fn version(&self) -> Result<&str> {
self.get_key("PHP Version")
.context("Failed to get PHP version")
}
pub fn zend_version(&self) -> Result<u32> {
self.get_key("PHP API")
.context("Failed to get Zend version")
.and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer"))
}
#[must_use]
pub fn get_key(&self, key: &str) -> Option<&str> {
let split = format!("{key} => ");
for line in self.0.lines() {
let components: Vec<_> = line.split(&split).collect();
if components.len() > 1 {
return Some(components[1]);
}
}
None
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[cfg(windows)]
pub fn architecture(&self) -> Result<Arch> {
self.get_key("Architecture")
.context("Could not find architecture of PHP")?
.try_into()
}
}
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Arch {
X86,
X64,
AArch64,
}
#[cfg(windows)]
impl TryFrom<&str> for Arch {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"x86" => Ok(Self::X86),
"x64" => Ok(Self::X64),
"arm64" => Ok(Self::AArch64),
arch => bail!("Unknown architecture: {}", arch),
}
}
}
#[cfg(windows)]
impl Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Arch::X86 => write!(f, "x86"),
Arch::X64 => write!(f, "x64"),
Arch::AArch64 => write!(f, "arm64"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[allow(clippy::inconsistent_digit_grouping)]
pub enum ApiVersion {
Php80 = 2020_09_30,
Php81 = 2021_09_02,
Php82 = 2022_08_29,
Php83 = 2023_08_31,
Php84 = 2024_09_24,
Php85 = 2025_09_25,
}
impl ApiVersion {
#[must_use]
pub const fn max() -> Self {
ApiVersion::Php85
}
#[must_use]
pub fn versions() -> Vec<Self> {
vec![
ApiVersion::Php80,
ApiVersion::Php81,
ApiVersion::Php82,
ApiVersion::Php83,
ApiVersion::Php84,
ApiVersion::Php85,
]
}
#[must_use]
pub fn supported_apis(self) -> Vec<ApiVersion> {
ApiVersion::versions()
.into_iter()
.filter(|&v| v <= self)
.collect()
}
#[must_use]
pub fn cfg_name(self) -> &'static str {
match self {
ApiVersion::Php80 => "php80",
ApiVersion::Php81 => "php81",
ApiVersion::Php82 => "php82",
ApiVersion::Php83 => "php83",
ApiVersion::Php84 => "php84",
ApiVersion::Php85 => "php85",
}
}
#[must_use]
pub fn define_name(self) -> &'static str {
match self {
ApiVersion::Php80 => "EXT_PHP_RS_PHP_80",
ApiVersion::Php81 => "EXT_PHP_RS_PHP_81",
ApiVersion::Php82 => "EXT_PHP_RS_PHP_82",
ApiVersion::Php83 => "EXT_PHP_RS_PHP_83",
ApiVersion::Php84 => "EXT_PHP_RS_PHP_84",
ApiVersion::Php85 => "EXT_PHP_RS_PHP_85",
}
}
}
impl TryFrom<u32> for ApiVersion {
type Error = Error;
fn try_from(version: u32) -> Result<Self, Self::Error> {
match version {
x if ((ApiVersion::Php80 as u32)..(ApiVersion::Php81 as u32)).contains(&x) => {
Ok(ApiVersion::Php80)
}
x if ((ApiVersion::Php81 as u32)..(ApiVersion::Php82 as u32)).contains(&x) => {
Ok(ApiVersion::Php81)
}
x if ((ApiVersion::Php82 as u32)..(ApiVersion::Php83 as u32)).contains(&x) => {
Ok(ApiVersion::Php82)
}
x if ((ApiVersion::Php83 as u32)..(ApiVersion::Php84 as u32)).contains(&x) => {
Ok(ApiVersion::Php83)
}
x if ((ApiVersion::Php84 as u32)..(ApiVersion::Php85 as u32)).contains(&x) => {
Ok(ApiVersion::Php84)
}
x if (ApiVersion::Php85 as u32) == x => Ok(ApiVersion::Php85),
version => bail!(
"The current version of PHP is not supported. Current PHP API version: {}, requires a version up to {}",
version,
ApiVersion::max() as u32
),
}
}
}
pub fn emit_php_cfg_flags(version: ApiVersion) {
for supported_version in version.supported_apis() {
println!("cargo:rustc-cfg={}", supported_version.cfg_name());
}
}
pub fn emit_check_cfg() {
println!(
"cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php85, php_zts, php_debug, docs)"
);
}
pub fn emit_rerun_if_env_changed() {
println!("cargo:rerun-if-env-changed=PHP");
println!("cargo:rerun-if-env-changed=PATH");
}