From 31712066c8dc05900831460b1da9bcf47210564d Mon Sep 17 00:00:00 2001 From: Saif Eddin Gmati <29315886+azjezz@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:09:59 +0100 Subject: [PATCH] chore: use php-discovery to find matching PHP build (#201) --- Cargo.toml | 1 + build.rs | 170 +++++++++++++++-------------------------------- unix_build.rs | 26 +++++--- windows_build.rs | 71 ++++++-------------- 4 files changed, 93 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 520a779..99b626f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ anyhow = "1" bindgen = "0.60" cc = "1.0" skeptic = "0.13" +php-discovery = "0.1.2" [target.'cfg(windows)'.build-dependencies] ureq = { version = "2.4", features = ["native-tls", "gzip"], default-features = false } diff --git a/build.rs b/build.rs index 7617096..50e363f 100644 --- a/build.rs +++ b/build.rs @@ -6,21 +6,20 @@ use std::{ env, fs::File, io::{BufWriter, Write}, - path::{Path, PathBuf}, - process::Command, - str::FromStr, + path::PathBuf, }; use anyhow::{anyhow, bail, Context, Result}; use bindgen::RustTarget; use impl_::Provider; +use php_discovery::build::Build as PhpBuild; const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20210902; pub trait PHPProvider<'a>: Sized { /// Create a new PHP provider. - fn new(info: &'a PHPInfo) -> Result; + fn new(info: &'a PhpBuild) -> Result; /// Retrieve a list of absolute include paths. fn get_includes(&self) -> Result>; @@ -33,6 +32,7 @@ pub trait PHPProvider<'a>: Sized { for line in bindings.lines() { writeln!(writer, "{}", line)?; } + Ok(()) } @@ -42,89 +42,49 @@ pub trait PHPProvider<'a>: Sized { } } -/// Finds the location of an executable `name`. -fn find_executable(name: &str) -> Option { - 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); - Some(stdout.trim().into()) - } else { - None - } -} - /// Finds the location of the PHP executable. -fn find_php() -> Result { - // If PHP path is given via env, it takes priority. - let env = std::env::var("PHP"); - if let Ok(env) = env { - return Ok(env.into()); - } - - find_executable("php").context("Could not find PHP path. 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 { - 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())) - } - - // Only present on Windows. - #[cfg(windows)] - pub fn architecture(&self) -> Result { - use std::convert::TryInto; - - self.get_key("Architecture") - .context("Could not find architecture of PHP")? - .try_into() - } - - pub fn thread_safety(&self) -> Result { - Ok(self - .get_key("Thread Safety") - .context("Could not find thread safety of PHP")? - == "enabled") - } - - pub fn debug(&self) -> Result { - 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 { - 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")) - } - - 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]); +fn find_php() -> Result { + php_discovery::discover() + .map_err(|e| anyhow!("failed to discover available PHP builds: {:?}", e)) + .and_then(|builds| { + if builds.is_empty() { + bail!("Could not find any PHP builds in the system, please ensure that PHP is installed.") } - } - None - } + + Ok(builds) + }) + .and_then(|builds| { + let mut available = Vec::new(); + let mut matching = Vec::new(); + + for build in builds { + available.push(build.php_api.to_string()); + if build.php_api >= MIN_PHP_API_VER && build.php_api <= MAX_PHP_API_VER { + matching.push(build); + } + } + + if matching.is_empty() { + bail!( + "Unable to find matching PHP binary, available PHP API version(s): '{}', requires a version between {} and {}", + available.join(", "), + MIN_PHP_API_VER, + MAX_PHP_API_VER, + ) + } + + let mut index = 0; + if let Ok(version) = env::var("RUST_PHP_VERSION") { + for (i, build) in matching.iter().enumerate() { + if build.version.to_string() == version { + index = i; + break; + } + } + } + + Ok(matching.remove(index)) + }) } /// Builds the wrapper library. @@ -178,33 +138,6 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Result<()> { - let version = info.zend_version()?; - - if !(MIN_PHP_API_VER..=MAX_PHP_API_VER).contains(&version) { - bail!("The current version of PHP is not supported. Current PHP API version: {}, requires a version between {} and {}", version, MIN_PHP_API_VER, MAX_PHP_API_VER); - } - - // Infra cfg flags - use these for things that change in the Zend API that don't - // rely on a feature and the crate user won't care about (e.g. struct field - // changes). Use a feature flag for an actual feature (e.g. enums being - // introduced in PHP 8.1). - // - // PHP 8.0 is the baseline - no feature flags will be introduced here. - // - // The PHP version cfg flags should also stack - if you compile on PHP 8.2 you - // should get both the `php81` and `php82` flags. - const PHP_81_API_VER: u32 = 20210902; - - if version >= PHP_81_API_VER { - println!("cargo:rustc-cfg=php81"); - } - - Ok(()) -} - fn main() -> Result<()> { let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?; let out_path = PathBuf::from(out_dir).join("bindings.rs"); @@ -229,14 +162,12 @@ fn main() -> Result<()> { return Ok(()); } - let php = find_php()?; - let info = PHPInfo::get(&php)?; - let provider = Provider::new(&info)?; + let php_build = find_php()?; + let provider = Provider::new(&php_build)?; let includes = provider.get_includes()?; let defines = provider.get_defines()?; - check_php_version(&info)?; build_wrapper(&defines, &includes)?; let bindings = generate_bindings(&defines, &includes)?; @@ -245,10 +176,13 @@ fn main() -> Result<()> { let mut out_writer = BufWriter::new(out_file); provider.write_bindings(bindings, &mut out_writer)?; - if info.debug()? { + if php_build.version.major == 8 && php_build.version.minor == 1 { + println!("cargo:rustc-cfg=php81"); + } + if php_build.is_debug { println!("cargo:rustc-cfg=php_debug"); } - if info.thread_safety()? { + if php_build.is_thread_safety_enabled { println!("cargo:rustc-cfg=php_zts"); } provider.print_extra_link_args()?; diff --git a/unix_build.rs b/unix_build.rs index 623d356..d1ba674 100644 --- a/unix_build.rs +++ b/unix_build.rs @@ -1,15 +1,25 @@ use std::{path::PathBuf, process::Command}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; +use php_discovery::build::Build; -use crate::{PHPInfo, PHPProvider}; +use crate::PHPProvider; -pub struct Provider {} +pub struct Provider<'a> { + build: &'a Build, +} -impl Provider { +impl<'a> Provider<'a> { /// Runs `php-config` with one argument, returning the stdout. fn php_config(&self, arg: &str) -> Result { - let cmd = Command::new("php-config") + let config = self.build.config().ok_or_else(|| { + anyhow!( + "unable to locate `php-config` binary for `{}`.", + self.build.binary.to_string_lossy() + ) + })?; + + let cmd = Command::new(config) .arg(arg) .output() .context("Failed to run `php-config`")?; @@ -22,9 +32,9 @@ impl Provider { } } -impl<'a> PHPProvider<'a> for Provider { - fn new(_: &'a PHPInfo) -> Result { - Ok(Self {}) +impl<'a> PHPProvider<'a> for Provider<'a> { + fn new(build: &'a Build) -> Result { + Ok(Self { build }) } fn get_includes(&self) -> Result> { diff --git a/windows_build.rs b/windows_build.rs index bc0e75d..f137ce7 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -1,20 +1,20 @@ +use std::io::Write; use std::{ - convert::TryFrom, - fmt::Display, - io::{Cursor, Read, Write}, + io::{Cursor, Read}, path::{Path, PathBuf}, process::Command, sync::Arc, }; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; +use php_discovery::build::Build; -use crate::{PHPInfo, PHPProvider}; +use crate::PHPProvider; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); pub struct Provider<'a> { - info: &'a PHPInfo, + build: &'a Build, devel: DevelPack, } @@ -32,18 +32,25 @@ impl<'a> Provider<'a> { } impl<'a> PHPProvider<'a> for Provider<'a> { - fn new(info: &'a PHPInfo) -> Result { - let version = info.version()?; - let is_zts = info.thread_safety()?; - let arch = info.architecture()?; - let devel = DevelPack::new(version, is_zts, arch)?; + fn new(build: &'a Build) -> Result { + // don't use `build.version.to_string()` as it includes extra part which is not + // needed. + let version = format!( + "{}.{}.{}", + build.version.major, build.version.minor, build.version.release + ); + let devel = DevelPack::new( + &version, + build.is_thread_safety_enabled, + build.architecture.to_string(), + )?; if let Ok(linker) = get_rustc_linker() { if looks_like_msvc_linker(&linker) { println!("cargo:warning=It looks like you are using a MSVC linker. You may encounter issues when attempting to load your compiled extension into PHP if your MSVC linker version is not compatible with the linker used to compile your PHP. It is recommended to use `rust-lld` as your linker."); } } - Ok(Self { info, devel }) + Ok(Self { build, devel }) } fn get_includes(&self) -> Result> { @@ -56,9 +63,9 @@ impl<'a> PHPProvider<'a> for Provider<'a> { ("PHP_WIN32", "1"), ("WINDOWS", "1"), ("WIN32", "1"), - ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), + ("ZEND_DEBUG", if self.build.is_debug { "1" } else { "0" }), ]; - if self.info.thread_safety()? { + if self.build.is_thread_safety_enabled { defines.push(("ZTS", "")); } Ok(defines) @@ -123,46 +130,12 @@ fn looks_like_msvc_linker(linker: &Path) -> bool { false } -#[derive(Debug, PartialEq, Eq)] -pub enum Arch { - X86, - X64, - AArch64, -} - -impl TryFrom<&str> for Arch { - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result { - Ok(match value { - "x86" => Self::X86, - "x64" => Self::X64, - "arm64" => Self::AArch64, - a => bail!("Unknown architecture {}", a), - }) - } -} - -impl Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Arch::X86 => "x86", - Arch::X64 => "x64", - Arch::AArch64 => "arm64", - } - ) - } -} - struct DevelPack(PathBuf); impl DevelPack { /// Downloads a new PHP development pack, unzips it in the build script /// temporary directory. - fn new(version: &str, is_zts: bool, arch: Arch) -> Result { + fn new(version: &str, is_zts: bool, arch: String) -> Result { let zip_name = format!( "php-devel-pack-{}{}-Win32-{}-{}.zip", version,