diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 7ea67006f..ecf2800f4 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -325,5 +325,10 @@ bind! { php_default_post_reader, php_default_treat_data, php_default_input_filter, - php_error + php_error, + php_ini_builder, + php_ini_builder_prepend, + php_ini_builder_unquoted, + php_ini_builder_quoted, + php_ini_builder_define } diff --git a/build.rs b/build.rs index 68261a9da..e144eda65 100644 --- a/build.rs +++ b/build.rs @@ -16,13 +16,10 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Error, Result}; use bindgen::RustTarget; use impl_::Provider; -const MIN_PHP_API_VER: u32 = 2020_09_30; -const MAX_PHP_API_VER: u32 = 2024_09_24; - /// Provides information about the PHP installation. pub trait PHPProvider<'a>: Sized { /// Create a new PHP provider. @@ -170,6 +167,20 @@ impl PHPInfo { } } +fn add_php_version_defines( + defines: &mut Vec<(&'static str, &'static str)>, + info: &PHPInfo, +) -> Result<()> { + let version = info.zend_version()?; + let supported_version: ApiVersion = version.try_into()?; + + for supported_api in supported_version.supported_apis() { + defines.push((supported_api.define_name(), "1")); + } + + Ok(()) +} + /// Builds the wrapper library. fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { let mut build = cc::Build::new(); @@ -217,6 +228,7 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Result Self { + ApiVersion::Php80 + } + + /// Returns the maximum API version supported by ext-php-rs. + pub const fn max() -> Self { + ApiVersion::Php84 + } + + pub fn versions() -> Vec { + vec![ + ApiVersion::Php80, + ApiVersion::Php81, + ApiVersion::Php82, + ApiVersion::Php83, + ApiVersion::Php84, + ] + } + + /// Returns the API versions that are supported by this version. + pub fn supported_apis(self) -> Vec { + ApiVersion::versions() + .into_iter() + .filter(|&v| v <= self) + .collect() + } + + pub fn cfg_name(self) -> &'static str { + match self { + ApiVersion::Php80 => "php80", + ApiVersion::Php81 => "php81", + ApiVersion::Php82 => "php82", + ApiVersion::Php83 => "php83", + ApiVersion::Php84 => "php84", + } + } + + 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", + } + } +} + +impl TryFrom for ApiVersion { + type Error = Error; + + fn try_from(version: u32) -> Result { + 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) == x => Ok(ApiVersion::Php84), + version => Err(anyhow!( + "The current version of PHP is not supported. Current PHP API version: {}, requires a version between {} and {}", + version, + ApiVersion::min() as u32, + ApiVersion::max() as u32 + )) + } + } +} + /// Checks the PHP Zend API version for compatibility with ext-php-rs, setting /// any configuration flags required. fn check_php_version(info: &PHPInfo) -> Result<()> { - const PHP_81_API_VER: u32 = 2021_09_02; - const PHP_82_API_VER: u32 = 2022_08_29; - const PHP_83_API_VER: u32 = 2023_08_31; - const PHP_84_API_VER: u32 = 2024_09_24; - 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); - } + let version: ApiVersion = version.try_into()?; // 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 @@ -275,26 +358,13 @@ fn check_php_version(info: &PHPInfo) -> Result<()> { println!( "cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php_zts, php_debug, docs)" ); - println!("cargo:rustc-cfg=php80"); - if (MIN_PHP_API_VER..PHP_81_API_VER).contains(&version) { - println!("cargo:warning=PHP version 8.0 is EOL and will no longer be supported in a future release. Please upgrade to a supported version of PHP. See https://www.php.net/supported-versions.php for information on version support timelines."); + if version == ApiVersion::Php80 { + println!("cargo:warning=PHP 8.0 is EOL and will no longer be supported in a future release. Please upgrade to a supported version of PHP. See https://www.php.net/supported-versions.php for information on version support timelines."); } - if version >= PHP_81_API_VER { - println!("cargo:rustc-cfg=php81"); - } - - if version >= PHP_82_API_VER { - println!("cargo:rustc-cfg=php82"); - } - - if version >= PHP_83_API_VER { - println!("cargo:rustc-cfg=php83"); - } - - if version >= PHP_84_API_VER { - println!("cargo:rustc-cfg=php84"); + for supported_version in version.supported_apis() { + println!("cargo:rustc-cfg={}", supported_version.cfg_name()); } Ok(()) @@ -339,7 +409,8 @@ fn main() -> Result<()> { let provider = Provider::new(&info)?; let includes = provider.get_includes()?; - let defines = provider.get_defines()?; + let mut defines = provider.get_defines()?; + add_php_version_defines(&mut defines, &info)?; check_php_version(&info)?; build_wrapper(&defines, &includes)?; diff --git a/guide/src/ini-builder.md b/guide/src/ini-builder.md new file mode 100644 index 000000000..937f03362 --- /dev/null +++ b/guide/src/ini-builder.md @@ -0,0 +1,38 @@ +# INI Builder + +When configuring a SAPI you may use `IniBuilder` to load INI settings as text. +This is useful for setting up configurations required by the SAPI capabilities. + +INI settings applied to a SAPI through `sapi.ini_entries` will be immutable, +meaning they cannot be changed at runtime. This is useful for applying settings +to match hard requirements of the way your SAPI works. + +To apply _configurable_ defaults it is recommended to use a `sapi.ini_defaults` +callback instead, which will allow settings to be changed at runtime. + +```rust,no_run,ignore +use ext_php_rs::builder::{IniBuilder, SapiBuilder}; + +# fn main() { +// Create a new IniBuilder instance. +let mut builder = IniBuilder::new(); + +// Append a single key/value pair to the INIT buffer with an unquoted value. +builder.unquoted("log_errors", "1"); + +// Append a single key/value pair to the INI buffer with a quoted value. +builder.quoted("default_mimetype", "text/html"); + +// Append INI line text as-is. A line break will be automatically appended. +builder.define("memory_limit=128MB"); + +// Prepend INI line text as-is. No line break insertion will occur. +builder.prepend("error_reporting=0\ndisplay_errors=1\n"); + +// Construct a SAPI. +let mut sapi = SapiBuilder::new("name", "pretty_name").build() + .expect("should build SAPI"); + +// Dump INI entries from the builder into the SAPI. +sapi.ini_entries = builder.finish(); +# } diff --git a/src/alloc.rs b/src/alloc.rs index 9ed94f9ea..4b27b711d 100644 --- a/src/alloc.rs +++ b/src/alloc.rs @@ -58,7 +58,7 @@ pub unsafe fn efree(ptr: *mut u8) { 0, std::ptr::null_mut(), 0, - ) + ); } else { #[allow(clippy::used_underscore_items)] _efree(ptr.cast::()); diff --git a/src/builders/ini.rs b/src/builders/ini.rs new file mode 100644 index 000000000..ca71a53b5 --- /dev/null +++ b/src/builders/ini.rs @@ -0,0 +1,293 @@ +use crate::embed::ext_php_rs_php_ini_builder_deinit; +use crate::ffi::{ + php_ini_builder, php_ini_builder_define, php_ini_builder_prepend, php_ini_builder_quoted, + php_ini_builder_unquoted, +}; +use crate::util::CStringScope; +use std::ffi::{CStr, NulError}; +use std::fmt::Display; +use std::ops::Deref; + +/// A builder for creating INI configurations. +pub type IniBuilder = php_ini_builder; + +impl Default for IniBuilder { + fn default() -> Self { + Self::new() + } +} + +impl IniBuilder { + /// Creates a new INI builder. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// ``` + #[must_use] + pub fn new() -> IniBuilder { + IniBuilder { + value: std::ptr::null_mut(), + length: 0, + } + } + + /// Appends a value to the INI builder. + /// + /// # Arguments + /// + /// * `value` - The value to append. + /// + /// # Errors + /// + /// Returns a `NulError` if the value contains a null byte. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// builder.prepend("foo=bar"); + /// ``` + pub fn prepend>(&mut self, value: V) -> Result<(), NulError> { + let value = value.as_ref(); + let raw = CStringScope::new(value)?; + + unsafe { + php_ini_builder_prepend(self, *raw, value.len()); + } + + Ok(()) + } + + /// Appends an unquoted name-value pair to the INI builder. + /// + /// # Arguments + /// + /// * `name` - The name of the pair. + /// * `value` - The value of the pair. + /// + /// # Errors + /// + /// Returns a `NulError` if the value contains a null byte. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// builder.unquoted("foo", "bar"); + /// ``` + pub fn unquoted(&mut self, name: N, value: V) -> Result<(), NulError> + where + N: AsRef, + V: AsRef, + { + let name = name.as_ref(); + let value = value.as_ref(); + + let raw_name = CStringScope::new(name)?; + let raw_value = CStringScope::new(value)?; + + unsafe { + php_ini_builder_unquoted(self, *raw_name, name.len(), *raw_value, value.len()); + } + + Ok(()) + } + + /// Appends a quoted name-value pair to the INI builder. + /// + /// # Arguments + /// + /// * `name` - The name of the pair. + /// * `value` - The value of the pair. + /// + /// # Errors + /// + /// Returns a `NulError` if the value contains a null byte. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// builder.quoted("foo", "bar"); + /// ``` + pub fn quoted(&mut self, name: N, value: V) -> Result<(), NulError> + where + N: AsRef, + V: AsRef, + { + let name = name.as_ref(); + let value = value.as_ref(); + + let raw_name = CStringScope::new(name)?; + let raw_value = CStringScope::new(value)?; + + unsafe { + php_ini_builder_quoted(self, *raw_name, name.len(), *raw_value, value.len()); + } + + Ok(()) + } + + /// Defines a value in the INI builder. + /// + /// # Arguments + /// + /// * `value` - The value to define. + /// + /// # Errors + /// + /// Returns a `NulError` if the value contains a null byte. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// builder.define("foo=bar"); + /// ``` + pub fn define>(&mut self, value: V) -> Result<(), NulError> { + let value = value.as_ref(); + let raw = CStringScope::new(value)?; + + unsafe { + php_ini_builder_define(self, *raw); + } + + Ok(()) + } + + /// Finishes building the INI configuration. + /// + /// # Examples + /// + /// ``` + /// # use ext_php_rs::builders::IniBuilder; + /// let mut builder = IniBuilder::new(); + /// let ini = builder.finish(); + /// ``` + #[must_use] + pub fn finish(&self) -> &CStr { + unsafe { + if self.value.is_null() { + return c""; + } + + self.value.add(self.length).write(0); + CStr::from_ptr(self.value) + } + } +} + +impl Display for IniBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let content: &str = self.as_ref(); + write!(f, "{content}") + } +} + +impl Deref for IniBuilder { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl From for String { + fn from(builder: IniBuilder) -> Self { + let temp: &str = builder.as_ref(); + temp.to_string() + } +} + +impl AsRef for IniBuilder { + fn as_ref(&self) -> &CStr { + self.finish() + } +} + +impl AsRef for IniBuilder { + fn as_ref(&self) -> &str { + self.finish().to_str().unwrap_or("") + } +} + +// Ensure the C buffer is properly deinitialized when the builder goes out of scope. +impl Drop for IniBuilder { + fn drop(&mut self) { + if !self.value.is_null() { + unsafe { + ext_php_rs_php_ini_builder_deinit(self); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ini_builder_prepend() { + let mut builder = IniBuilder::new(); + builder.prepend("foo=bar").expect("should prepend value"); + + let ini = builder.finish(); + assert!(!ini.is_empty()); + assert_eq!(ini, c"foo=bar"); + } + + #[test] + fn test_ini_builder_unquoted() { + let mut builder = IniBuilder::new(); + builder + .unquoted("baz", "qux") + .expect("should add unquoted value"); + + let ini = builder.finish(); + assert!(!ini.is_empty()); + assert_eq!(ini, c"baz=qux\n"); + } + + #[test] + fn test_ini_builder_quoted() { + let mut builder = IniBuilder::new(); + builder + .quoted("quux", "corge") + .expect("should add quoted value"); + + let ini = builder.finish(); + assert!(!ini.is_empty()); + assert_eq!(ini, c"quux=\"corge\"\n"); + } + + #[test] + fn test_ini_builder_define() { + let mut builder = IniBuilder::new(); + builder + .define("grault=garply") + .expect("should define value"); + + let ini = builder.finish(); + assert!(!ini.is_empty()); + assert_eq!(ini, c"grault=garply\n"); + } + + #[test] + fn test_ini_builder_null_byte_error() { + let mut builder = IniBuilder::new(); + assert!(builder.prepend("key=val\0ue").is_err()); + } + + #[test] + fn test_ini_builder_empty_values() { + let mut builder = IniBuilder::new(); + assert!(builder.unquoted("", "").is_ok()); + } +} diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 070d71731..bb984e86b 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -3,12 +3,16 @@ mod class; mod function; +#[cfg(all(php82, feature = "embed"))] +mod ini; mod module; #[cfg(feature = "embed")] mod sapi; pub use class::ClassBuilder; pub use function::FunctionBuilder; +#[cfg(all(php82, feature = "embed"))] +pub use ini::IniBuilder; pub use module::{ModuleBuilder, ModuleStartup}; #[cfg(feature = "embed")] pub use sapi::SapiBuilder; diff --git a/src/builders/sapi.rs b/src/builders/sapi.rs index 797f352ed..ae0cdfeca 100644 --- a/src/builders/sapi.rs +++ b/src/builders/sapi.rs @@ -6,8 +6,10 @@ use crate::ffi::{ use crate::types::Zval; use crate::{embed::SapiModule, error::Result}; -use std::ffi::{c_char, c_int, c_void}; -use std::{ffi::CString, ptr}; +use std::{ + ffi::{c_char, c_int, c_void, CString}, + ptr, +}; /// Builder for `SapiModule`s /// @@ -43,6 +45,7 @@ pub struct SapiBuilder { module: SapiModule, executable_location: Option, php_ini_path_override: Option, + ini_entries: Option, } impl SapiBuilder { @@ -95,6 +98,7 @@ impl SapiBuilder { }, executable_location: None, php_ini_path_override: None, + ini_entries: None, } } @@ -271,6 +275,16 @@ impl SapiBuilder { self } + /// Set the `ini_entries` for this SAPI + /// + /// # Parameters + /// + /// * `entries` - A pointer to the ini entries. + pub fn ini_entries>(mut self, entries: E) -> Self { + self.ini_entries = Some(entries.into()); + self + } + /// Sets the php ini path override for this SAPI /// /// # Parameters @@ -326,6 +340,10 @@ impl SapiBuilder { self.module.executable_location = CString::new(path)?.into_raw(); } + if let Some(entries) = self.ini_entries { + self.module.ini_entries = CString::new(entries)?.into_raw(); + } + if let Some(path) = self.php_ini_path_override { self.module.php_ini_path_override = CString::new(path)?.into_raw(); } @@ -709,6 +727,26 @@ mod test { ); } + #[cfg(php82)] + #[test] + fn test_sapi_ini_entries() { + let mut ini = crate::builders::IniBuilder::new(); + ini.define("foo=bar").expect("should define ini entry"); + ini.quoted("memory_limit", "128M") + .expect("should add quoted ini entry"); + + let sapi = SapiBuilder::new("test", "Test") + .ini_entries(ini) + .build() + .expect("should build sapi module"); + + assert!(!sapi.ini_entries.is_null()); + assert_eq!( + unsafe { CStr::from_ptr(sapi.ini_entries) }, + c"foo=bar\nmemory_limit=\"128M\"\n" + ); + } + #[test] fn test_php_ini_path_override() { let sapi = SapiBuilder::new("test", "Test") @@ -718,10 +756,8 @@ mod test { assert!(!sapi.php_ini_path_override.is_null()); assert_eq!( - unsafe { CStr::from_ptr(sapi.php_ini_path_override) } - .to_str() - .expect("should convert CStr to str"), - "/custom/path/php.ini" + unsafe { CStr::from_ptr(sapi.php_ini_path_override) }, + c"/custom/path/php.ini" ); } @@ -754,10 +790,8 @@ mod test { assert!(!sapi.executable_location.is_null()); assert_eq!( - unsafe { CStr::from_ptr(sapi.executable_location) } - .to_str() - .expect("should convert CStr to str"), - "/usr/bin/php" + unsafe { CStr::from_ptr(sapi.executable_location) }, + c"/usr/bin/php" ); } @@ -790,18 +824,8 @@ mod test { .build() .expect("should build sapi module"); - assert_eq!( - unsafe { CStr::from_ptr(sapi.name) } - .to_str() - .expect("should convert CStr to str"), - "chained" - ); - assert_eq!( - unsafe { CStr::from_ptr(sapi.pretty_name) } - .to_str() - .expect("should convert CStr to str"), - "Chained SAPI" - ); + assert_eq!(unsafe { CStr::from_ptr(sapi.name) }, c"chained"); + assert_eq!(unsafe { CStr::from_ptr(sapi.pretty_name) }, c"Chained SAPI"); assert!(sapi.startup.is_some()); assert!(sapi.shutdown.is_some()); assert!(sapi.activate.is_some()); diff --git a/src/embed/embed.c b/src/embed/embed.c index 743eee02b..ddb9009c6 100644 --- a/src/embed/embed.c +++ b/src/embed/embed.c @@ -36,3 +36,11 @@ void ext_php_rs_php_error(int type, const char *format, ...) { vprintf(format, args); va_end(args); } + +// Wrap `php_ini_builder_deinit` as it's `static inline` which gets discarded +// by cbindgen. +#ifdef EXT_PHP_RS_PHP_82 +void ext_php_rs_php_ini_builder_deinit(struct php_ini_builder *b) { + php_ini_builder_deinit(b); +} +#endif diff --git a/src/embed/embed.h b/src/embed/embed.h index 1b756cf7c..41a5e0b17 100644 --- a/src/embed/embed.h +++ b/src/embed/embed.h @@ -1,5 +1,8 @@ #include "zend.h" #include "sapi/embed/php_embed.h" +#ifdef EXT_PHP_RS_PHP_82 +#include "php_ini_builder.h" +#endif void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *), void *ctx); diff --git a/src/embed/ffi.rs b/src/embed/ffi.rs index 9878e6139..91ce3a59e 100644 --- a/src/embed/ffi.rs +++ b/src/embed/ffi.rs @@ -3,6 +3,9 @@ #![allow(clippy::all)] #![allow(warnings)] +#[cfg(php82)] +use crate::ffi::php_ini_builder; + use std::ffi::{c_char, c_int, c_void}; #[link(name = "wrapper")] @@ -20,4 +23,7 @@ extern "C" { error_msg: *const ::std::os::raw::c_char, ... ); + + #[cfg(php82)] + pub fn ext_php_rs_php_ini_builder_deinit(builder: *mut php_ini_builder); } diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 0d4e8c552..ec1c409f9 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -9,7 +9,6 @@ mod ffi; mod sapi; use crate::boxed::ZBox; -use crate::embed::ffi::ext_php_rs_embed_callback; use crate::ffi::{ _zend_file_handle__bindgen_ty_1, php_execute_script, zend_eval_string, zend_file_handle, zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS, @@ -22,7 +21,7 @@ use std::panic::{resume_unwind, RefUnwindSafe}; use std::path::Path; use std::ptr::null_mut; -pub use ffi::{ext_php_rs_php_error, ext_php_rs_sapi_startup}; +pub use ffi::*; pub use sapi::SapiModule; /// The embed module provides a way to run php code from rust diff --git a/src/lib.rs b/src/lib.rs index 15489f1fd..47e33405a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub mod rc; #[cfg(test)] pub mod test; pub mod types; +mod util; pub mod zend; /// A module typically glob-imported containing the typically required macros diff --git a/src/util/cstring_scope.rs b/src/util/cstring_scope.rs new file mode 100644 index 000000000..0c175457d --- /dev/null +++ b/src/util/cstring_scope.rs @@ -0,0 +1,29 @@ +use std::{ + ffi::{c_char, CString, NulError}, + ops::Deref, +}; + +// Helpful for CString which only needs to live until immediately after C call. +pub struct CStringScope(*mut c_char); + +impl CStringScope { + #[allow(dead_code)] + pub fn new>>(string: T) -> Result { + Ok(Self(CString::new(string)?.into_raw())) + } +} + +impl Deref for CStringScope { + type Target = *mut c_char; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for CStringScope { + fn drop(&mut self) { + // Convert back to a CString to ensure it gets dropped + drop(unsafe { CString::from_raw(self.0) }); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 000000000..76d36f21b --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,3 @@ +mod cstring_scope; + +pub use cstring_scope::CStringScope; diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index 199c9414e..647bbfc5c 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -20,6 +20,7 @@ pub mod variadic_args; mod test { use std::env; + use std::path::PathBuf; use std::process::Command; use std::sync::Once; @@ -36,6 +37,56 @@ mod test { }); } + /// Finds the location of an executable `name`. + pub fn find_executable(name: &str) -> Result { + const WHICH: &str = if cfg!(windows) { "where" } else { "which" }; + let cmd = Command::new(WHICH) + .arg(name) + .output() + .map_err(|_| format!("Failed to execute \"{WHICH} {name}\""))?; + if cmd.status.success() { + let stdout = String::from_utf8(cmd.stdout) + .map_err(|_| format!("Failed to parse output of \"{WHICH} {name}\""))?; + + stdout + .trim() + .lines() + .next() + .map(|l| l.trim().into()) + .ok_or_else(|| format!("No output from \"{WHICH} {name}\"")) + } else { + Err(format!( + "Executable \"{name}\" not found in PATH. \ + Please ensure it is installed and available in your PATH." + )) + } + } + + /// Returns an environment variable's value as a `PathBuf` + pub fn path_from_env(key: &str) -> Option { + std::env::var_os(key).map(PathBuf::from) + } + + /// Finds the location of the PHP executable. + fn find_php() -> Result { + // If path is given via env, it takes priority. + if let Some(path) = path_from_env("PHP") { + if !path + .try_exists() + .map_err(|e| format!("Could not check existence: {e}"))? + { + // If path was explicitly given and it can't be found, this is a hard error + return Err(format!("php executable not found at {path:?}")); + } + return Ok(path); + } + find_executable("php").map_err(|_| { + "Could not find PHP executable. \ + Please ensure `php` is in your PATH or the `PHP` environment variable is set." + .into() + }) + } + pub fn run_php(file: &str) -> bool { setup(); let mut path = env::current_dir().expect("Could not get cwd"); @@ -48,7 +99,7 @@ mod test { "libtests" }); path.set_extension(std::env::consts::DLL_EXTENSION); - let output = Command::new("php") + let output = Command::new(find_php().expect("Could not find PHP executable")) .arg(format!("-dextension={}", path.to_str().unwrap())) .arg("-dassert.active=1") .arg("-dassert.exception=1") diff --git a/unix_build.rs b/unix_build.rs index c41afb1dc..123eed9ba 100644 --- a/unix_build.rs +++ b/unix_build.rs @@ -4,9 +4,11 @@ use anyhow::{bail, Context, Result}; use crate::{find_executable, path_from_env, PHPInfo, PHPProvider}; -pub struct Provider {} +pub struct Provider<'a> { + info: &'a PHPInfo, +} -impl Provider { +impl Provider<'_> { /// Runs `php-config` with one argument, returning the stdout. fn php_config(arg: &str) -> Result { let cmd = Command::new(Self::find_bin()?) @@ -38,9 +40,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(info: &'a PHPInfo) -> Result { + Ok(Self { info }) } fn get_includes(&self) -> Result> { @@ -52,7 +54,11 @@ impl<'a> PHPProvider<'a> for Provider { } fn get_defines(&self) -> Result> { - Ok(vec![]) + let mut defines = vec![]; + if self.info.thread_safety()? { + defines.push(("ZTS", "1")); + } + Ok(defines) } fn print_extra_link_args(&self) -> Result<()> { diff --git a/windows_build.rs b/windows_build.rs index ce2db78d2..03b8314c8 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -58,7 +58,7 @@ impl<'a> PHPProvider<'a> for Provider<'a> { ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), ]; if self.info.thread_safety()? { - defines.push(("ZTS", "")); + defines.push(("ZTS", "1")); } Ok(defines) }