1use 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#[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#[must_use]
50pub fn path_from_env(key: &str) -> Option<PathBuf> {
51 std::env::var_os(key).map(PathBuf::from)
52}
53
54pub fn find_php() -> Result<PathBuf> {
60 if let Some(path) = path_from_env("PHP") {
62 if !path.try_exists()? {
63 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
74pub struct PHPInfo(String);
76
77impl PHPInfo {
78 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 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 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 pub fn version(&self) -> Result<&str> {
125 self.get_key("PHP Version")
126 .context("Failed to get PHP version")
127 }
128
129 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 #[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 #[must_use]
155 pub fn as_str(&self) -> &str {
156 &self.0
157 }
158
159 #[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#[cfg(windows)]
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum Arch {
176 X86,
178 X64,
180 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
214#[allow(clippy::inconsistent_digit_grouping)]
215pub enum ApiVersion {
216 Php80 = 2020_09_30,
218 Php81 = 2021_09_02,
220 Php82 = 2022_08_29,
222 Php83 = 2023_08_31,
224 Php84 = 2024_09_24,
226 Php85 = 2025_09_25,
228}
229
230impl ApiVersion {
231 #[must_use]
233 pub const fn max() -> Self {
234 ApiVersion::Php85
235 }
236
237 #[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 #[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 #[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 #[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
318pub 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
327pub 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
336pub fn emit_rerun_if_env_changed() {
338 println!("cargo:rerun-if-env-changed=PHP");
339 println!("cargo:rerun-if-env-changed=PATH");
340}