Skip to main content

ext_php_rs_build/
lib.rs

1//! Build-time PHP detection utilities for ext-php-rs.
2//!
3//! This crate provides utilities for detecting PHP installations and version
4//! information at build time. It is used by ext-php-rs's build script and can
5//! be used by other crates that need to detect PHP at compile time.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use ext_php_rs_build::{find_php, PHPInfo, ApiVersion};
11//!
12//! fn main() -> anyhow::Result<()> {
13//!     let php = find_php()?;
14//!     let info = PHPInfo::get(&php)?;
15//!     let version: ApiVersion = info.zend_version()?.try_into()?;
16//!
17//!     for api in version.supported_apis() {
18//!         println!("cargo:rustc-cfg={}", api.cfg_name());
19//!     }
20//!     Ok(())
21//! }
22//! ```
23
24use std::{
25    path::{Path, PathBuf},
26    process::Command,
27    str::FromStr,
28};
29
30#[cfg(windows)]
31use std::fmt::Display;
32
33use anyhow::{Context, Error, Result, bail};
34
35/// Finds the location of an executable `name`.
36#[must_use]
37pub fn find_executable(name: &str) -> Option<PathBuf> {
38    const WHICH: &str = if cfg!(windows) { "where" } else { "which" };
39    let cmd = Command::new(WHICH).arg(name).output().ok()?;
40    if cmd.status.success() {
41        let stdout = String::from_utf8_lossy(&cmd.stdout);
42        stdout.trim().lines().next().map(|l| l.trim().into())
43    } else {
44        None
45    }
46}
47
48/// Returns an environment variable's value as a `PathBuf`.
49#[must_use]
50pub fn path_from_env(key: &str) -> Option<PathBuf> {
51    std::env::var_os(key).map(PathBuf::from)
52}
53
54/// Finds the location of the PHP executable.
55///
56/// # Errors
57///
58/// Returns an error if PHP cannot be found.
59pub fn find_php() -> Result<PathBuf> {
60    // If path is given via env, it takes priority.
61    if let Some(path) = path_from_env("PHP") {
62        if !path.try_exists()? {
63            // If path was explicitly given and it can't be found, this is a hard error
64            bail!("php executable not found at {}", path.display());
65        }
66        return Ok(path);
67    }
68    find_executable("php").with_context(|| {
69        "Could not find PHP executable. \
70        Please ensure `php` is in your PATH or the `PHP` environment variable is set."
71    })
72}
73
74/// Output of `php -i`.
75pub struct PHPInfo(String);
76
77impl PHPInfo {
78    /// Get the PHP info by running `php -i`.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if `php -i` command failed to execute successfully.
83    pub fn get(php: &Path) -> Result<Self> {
84        let cmd = Command::new(php)
85            .arg("-i")
86            .output()
87            .context("Failed to call `php -i`")?;
88        if !cmd.status.success() {
89            bail!("Failed to call `php -i` status code {}", cmd.status);
90        }
91        let stdout = String::from_utf8_lossy(&cmd.stdout);
92        Ok(Self(stdout.to_string()))
93    }
94
95    /// Checks if thread safety is enabled.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if `PHPInfo` does not contain thread safety information.
100    pub fn thread_safety(&self) -> Result<bool> {
101        Ok(self
102            .get_key("Thread Safety")
103            .context("Could not find thread safety of PHP")?
104            == "enabled")
105    }
106
107    /// Checks if PHP was built with debug.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if `PHPInfo` does not contain debug build information.
112    pub fn debug(&self) -> Result<bool> {
113        Ok(self
114            .get_key("Debug Build")
115            .context("Could not find debug build of PHP")?
116            == "yes")
117    }
118
119    /// Get the PHP version string.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if `PHPInfo` does not contain version number.
124    pub fn version(&self) -> Result<&str> {
125        self.get_key("PHP Version")
126            .context("Failed to get PHP version")
127    }
128
129    /// Get the Zend API version number.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if `PHPInfo` does not contain PHP API version.
134    pub fn zend_version(&self) -> Result<u32> {
135        self.get_key("PHP API")
136            .context("Failed to get Zend version")
137            .and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer"))
138    }
139
140    /// Get a key from the PHP info output.
141    #[must_use]
142    pub fn get_key(&self, key: &str) -> Option<&str> {
143        let split = format!("{key} => ");
144        for line in self.0.lines() {
145            let components: Vec<_> = line.split(&split).collect();
146            if components.len() > 1 {
147                return Some(components[1]);
148            }
149        }
150        None
151    }
152
153    /// Returns the raw PHP info output.
154    #[must_use]
155    pub fn as_str(&self) -> &str {
156        &self.0
157    }
158
159    /// Returns the PHP architecture (Windows only).
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if `PHPInfo` does not contain architecture information.
164    #[cfg(windows)]
165    pub fn architecture(&self) -> Result<Arch> {
166        self.get_key("Architecture")
167            .context("Could not find architecture of PHP")?
168            .try_into()
169    }
170}
171
172/// PHP architecture (Windows only).
173#[cfg(windows)]
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum Arch {
176    /// 32-bit x86
177    X86,
178    /// 64-bit x86_64
179    X64,
180    /// 64-bit ARM
181    AArch64,
182}
183
184#[cfg(windows)]
185impl TryFrom<&str> for Arch {
186    type Error = Error;
187
188    fn try_from(value: &str) -> Result<Self, Self::Error> {
189        match value {
190            "x86" => Ok(Self::X86),
191            "x64" => Ok(Self::X64),
192            "arm64" => Ok(Self::AArch64),
193            arch => bail!("Unknown architecture: {}", arch),
194        }
195    }
196}
197
198#[cfg(windows)]
199impl Display for Arch {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        match self {
202            Arch::X86 => write!(f, "x86"),
203            Arch::X64 => write!(f, "x64"),
204            Arch::AArch64 => write!(f, "arm64"),
205        }
206    }
207}
208
209/// PHP API version enum.
210///
211/// This enum represents the supported PHP API versions and provides utilities
212/// for version detection and comparison.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
214#[allow(clippy::inconsistent_digit_grouping)]
215pub enum ApiVersion {
216    /// PHP 8.0
217    Php80 = 2020_09_30,
218    /// PHP 8.1
219    Php81 = 2021_09_02,
220    /// PHP 8.2
221    Php82 = 2022_08_29,
222    /// PHP 8.3
223    Php83 = 2023_08_31,
224    /// PHP 8.4
225    Php84 = 2024_09_24,
226    /// PHP 8.5
227    Php85 = 2025_09_25,
228}
229
230impl ApiVersion {
231    /// Returns the maximum API version supported.
232    #[must_use]
233    pub const fn max() -> Self {
234        ApiVersion::Php85
235    }
236
237    /// Returns all known API versions.
238    #[must_use]
239    pub fn versions() -> Vec<Self> {
240        vec![
241            ApiVersion::Php80,
242            ApiVersion::Php81,
243            ApiVersion::Php82,
244            ApiVersion::Php83,
245            ApiVersion::Php84,
246            ApiVersion::Php85,
247        ]
248    }
249
250    /// Returns the API versions that are supported by this version.
251    ///
252    /// For example, PHP 8.3 supports APIs from 8.0, 8.1, 8.2, and 8.3.
253    #[must_use]
254    pub fn supported_apis(self) -> Vec<ApiVersion> {
255        ApiVersion::versions()
256            .into_iter()
257            .filter(|&v| v <= self)
258            .collect()
259    }
260
261    /// Returns the cfg flag name for this version (e.g., "php84").
262    #[must_use]
263    pub fn cfg_name(self) -> &'static str {
264        match self {
265            ApiVersion::Php80 => "php80",
266            ApiVersion::Php81 => "php81",
267            ApiVersion::Php82 => "php82",
268            ApiVersion::Php83 => "php83",
269            ApiVersion::Php84 => "php84",
270            ApiVersion::Php85 => "php85",
271        }
272    }
273
274    /// Returns the C preprocessor define name for this version.
275    #[must_use]
276    pub fn define_name(self) -> &'static str {
277        match self {
278            ApiVersion::Php80 => "EXT_PHP_RS_PHP_80",
279            ApiVersion::Php81 => "EXT_PHP_RS_PHP_81",
280            ApiVersion::Php82 => "EXT_PHP_RS_PHP_82",
281            ApiVersion::Php83 => "EXT_PHP_RS_PHP_83",
282            ApiVersion::Php84 => "EXT_PHP_RS_PHP_84",
283            ApiVersion::Php85 => "EXT_PHP_RS_PHP_85",
284        }
285    }
286}
287
288impl TryFrom<u32> for ApiVersion {
289    type Error = Error;
290
291    fn try_from(version: u32) -> Result<Self, Self::Error> {
292        match version {
293            x if ((ApiVersion::Php80 as u32)..(ApiVersion::Php81 as u32)).contains(&x) => {
294                Ok(ApiVersion::Php80)
295            }
296            x if ((ApiVersion::Php81 as u32)..(ApiVersion::Php82 as u32)).contains(&x) => {
297                Ok(ApiVersion::Php81)
298            }
299            x if ((ApiVersion::Php82 as u32)..(ApiVersion::Php83 as u32)).contains(&x) => {
300                Ok(ApiVersion::Php82)
301            }
302            x if ((ApiVersion::Php83 as u32)..(ApiVersion::Php84 as u32)).contains(&x) => {
303                Ok(ApiVersion::Php83)
304            }
305            x if ((ApiVersion::Php84 as u32)..(ApiVersion::Php85 as u32)).contains(&x) => {
306                Ok(ApiVersion::Php84)
307            }
308            x if (ApiVersion::Php85 as u32) == x => Ok(ApiVersion::Php85),
309            version => bail!(
310                "The current version of PHP is not supported. Current PHP API version: {}, requires a version up to {}",
311                version,
312                ApiVersion::max() as u32
313            ),
314        }
315    }
316}
317
318/// Emits cargo cfg flags for the detected PHP version.
319///
320/// This function prints `cargo:rustc-cfg=phpXX` for all supported API versions.
321pub fn emit_php_cfg_flags(version: ApiVersion) {
322    for supported_version in version.supported_apis() {
323        println!("cargo:rustc-cfg={}", supported_version.cfg_name());
324    }
325}
326
327/// Emits the cargo check-cfg directive for all PHP version flags.
328///
329/// Call this in your build script to avoid unknown cfg warnings.
330pub fn emit_check_cfg() {
331    println!(
332        "cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php85, php_zts, php_debug, docs)"
333    );
334}
335
336/// Emits cargo rerun-if-env-changed for PHP-related environment variables.
337pub fn emit_rerun_if_env_changed() {
338    println!("cargo:rerun-if-env-changed=PHP");
339    println!("cargo:rerun-if-env-changed=PATH");
340}