Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ bindgen = { version = "0.72", default-features = false, features = [
] }
cc = "1.2"
skeptic = "0.13"
ext-php-rs-build = { version = "0.1.0", path = "./crates/php-build" }

[target.'cfg(windows)'.build-dependencies]
ureq = { version = "3.0", features = [
Expand All @@ -56,7 +57,7 @@ runtime = ["bindgen/runtime"]
static = ["bindgen/static"]

[workspace]
members = ["crates/macros", "crates/cli", "tests"]
members = ["crates/macros", "crates/cli", "crates/php-build", "tests"]

[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docs"]
Expand Down
15 changes: 15 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ bind! {
zend_hash_str_update,
zend_internal_arg_info,
zend_is_callable,
zend_is_callable_ex,
zend_fcall_info_cache,
_zend_fcall_info_cache,
zend_is_identical,
zend_is_iterable,
zend_known_strings,
Expand Down Expand Up @@ -266,6 +269,18 @@ bind! {
zend_std_get_properties,
zend_std_has_property,
zend_objects_new,
zend_object_make_lazy,
zend_lazy_object_init,
zend_lazy_object_mark_as_initialized,
// Note: zend_lazy_object_get_instance and zend_lazy_object_get_flags are not
// exported (no ZEND_API) in PHP, so they cannot be used on Windows.
// Use zend_lazy_object_init instead which returns the instance for proxies.
zend_class_can_be_lazy,
zend_lazy_object_flags_t,
ZEND_LAZY_OBJECT_STRATEGY_GHOST,
ZEND_LAZY_OBJECT_STRATEGY_PROXY,
ZEND_LAZY_OBJECT_INITIALIZED,
ZEND_LAZY_OBJECT_SKIP_INITIALIZATION_ON_SERIALIZE,
zend_standard_class_def,
zend_class_serialize_deny,
zend_class_unserialize_deny,
Expand Down
247 changes: 21 additions & 226 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//! This script is responsible for generating the bindings to the PHP Zend API.
//! It also checks the PHP version for compatibility with ext-php-rs and sets
//! configuration flags accordingly.
#![allow(clippy::inconsistent_digit_grouping)]
#[cfg_attr(windows, path = "windows_build.rs")]
#[cfg_attr(not(windows), path = "unix_build.rs")]
mod impl_;
Expand All @@ -11,13 +10,12 @@ use std::{
env,
fs::File,
io::{BufWriter, Write},
path::{Path, PathBuf},
process::Command,
str::FromStr,
path::PathBuf,
};

use anyhow::{Context, Error, Result, anyhow, bail};
use anyhow::{Context, Result, anyhow};
use bindgen::RustTarget;
use ext_php_rs_build::{ApiVersion, PHPInfo, find_php};
use impl_::Provider;

/// Provides information about the PHP installation.
Expand Down Expand Up @@ -50,123 +48,6 @@ pub trait PHPProvider<'a>: Sized {
}
}

/// Finds the location of an executable `name`.
#[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
}
}

/// Returns an environment variable's value as a `PathBuf`
pub fn path_from_env(key: &str) -> Option<PathBuf> {
std::env::var_os(key).map(PathBuf::from)
}

/// Finds the location of the PHP executable.
fn find_php() -> Result<PathBuf> {
// If path is given via env, it takes priority.
if let Some(path) = path_from_env("PHP") {
if !path.try_exists()? {
// If path was explicitly given and it can't be found, this is a hard error
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."
})
}

/// Output of `php -i`.
pub struct PHPInfo(String);

impl PHPInfo {
/// Get the PHP info.
///
/// # Errors
/// - `php -i` command failed to execute successfully
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()))
}

// Only present on Windows.
#[cfg(windows)]
pub fn architecture(&self) -> Result<impl_::Arch> {
use std::convert::TryInto;

self.get_key("Architecture")
.context("Could not find architecture of PHP")?
.try_into()
}

/// Checks if thread safety is enabled.
///
/// # Errors
/// - `PHPInfo` does not contain thread safety information
pub fn thread_safety(&self) -> Result<bool> {
Ok(self
.get_key("Thread Safety")
.context("Could not find thread safety of PHP")?
== "enabled")
}

/// Checks if PHP was built with debug.
///
/// # Errors
/// - `PHPInfo` does not contain debug build information
pub fn debug(&self) -> Result<bool> {
Ok(self
.get_key("Debug Build")
.context("Could not find debug build of PHP")?
== "yes")
}

/// Get the php version.
///
/// # Errors
/// - `PHPInfo` does not contain version number
pub fn version(&self) -> Result<&str> {
self.get_key("PHP Version")
.context("Failed to get PHP version")
}

/// Get the zend version.
///
/// # Errors
/// - `PHPInfo` does not contain php api 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"))
}

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
}
}

fn add_php_version_defines(
defines: &mut Vec<(&'static str, &'static str)>,
info: &PHPInfo,
Expand Down Expand Up @@ -278,105 +159,16 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<S
Ok(bindings)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
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 {
/// Returns the minimum API version supported by ext-php-rs.
pub fn min() -> Self {
[
ApiVersion::Php80,
#[cfg(feature = "enum")]
ApiVersion::Php81,
]
.into_iter()
.max()
.unwrap_or(Self::max())
}

/// Returns the maximum API version supported by ext-php-rs.
pub const fn max() -> Self {
ApiVersion::Php85
}

pub fn versions() -> Vec<Self> {
vec![
ApiVersion::Php80,
ApiVersion::Php81,
ApiVersion::Php82,
ApiVersion::Php83,
ApiVersion::Php84,
ApiVersion::Php85,
]
}

/// Returns the API versions that are supported by this version.
pub fn supported_apis(self) -> Vec<ApiVersion> {
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",
ApiVersion::Php85 => "php85",
}
}

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 => 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
)),
}
}
/// Returns the minimum API version supported by ext-php-rs.
fn min_api_version() -> ApiVersion {
[
ApiVersion::Php80,
#[cfg(feature = "enum")]
ApiVersion::Php81,
]
.into_iter()
.max()
.unwrap_or(ApiVersion::max())
}

/// Checks the PHP Zend API version for compatibility with ext-php-rs, setting
Expand All @@ -394,20 +186,23 @@ fn check_php_version(info: &PHPInfo) -> Result<()> {
//
// The PHP version cfg flags should also stack - if you compile on PHP 8.2 you
// should get both the `php81` and `php82` flags.
println!(
"cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php85, php_zts, php_debug, docs)"
);
ext_php_rs_build::emit_check_cfg();

if version == ApiVersion::Php80 {
println!(
"cargo:warning=PHP 8.0 is EOL and is no longer supported. Please upgrade to a supported version of PHP. See https://www.php.net/supported-versions.php for information on version support timelines."
);
}

for supported_version in version.supported_apis() {
println!("cargo:rustc-cfg={}", supported_version.cfg_name());
if version < min_api_version() {
anyhow::bail!(
"PHP version {} is below minimum supported version",
version.cfg_name()
);
}

ext_php_rs_build::emit_php_cfg_flags(version);

Ok(())
}

Expand Down
21 changes: 21 additions & 0 deletions crates/php-build/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "ext-php-rs-build"
description = "Build-time PHP detection utilities for ext-php-rs."
repository = "https://github.com/extphprs/ext-php-rs"
homepage = "https://ext-php.rs"
license = "MIT OR Apache-2.0"
keywords = ["php", "ffi", "zend", "build"]
version = "0.1.0"
authors = [
"Pierre Tondereau <pierre.tondereau@protonmail.com>",
"Xenira <xenira@php.rs>",
"David Cole <david.cole1340@gmail.com>",
]
edition = "2024"
categories = ["development-tools::build-utils"]

[dependencies]
anyhow = "1"

[lints.rust]
missing_docs = "warn"
Loading