• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 16323306954

16 Jul 2025 03:10PM UTC coverage: 22.222% (+0.6%) from 21.654%
16323306954

Pull #482

github

web-flow
Merge de76d2402 into 1166e2910
Pull Request #482: feat(cargo-php)!: escalate privilege and to copy extension and edit ini file

0 of 48 new or added lines in 1 file covered. (0.0%)

193 existing lines in 10 files now uncovered.

870 of 3915 relevant lines covered (22.22%)

3.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/build.rs
1
//! The build script for ext-php-rs.
2
//! This script is responsible for generating the bindings to the PHP Zend API.
3
//! It also checks the PHP version for compatibility with ext-php-rs and sets
4
//! configuration flags accordingly.
5
#![allow(clippy::inconsistent_digit_grouping)]
6
#[cfg_attr(windows, path = "windows_build.rs")]
7
#[cfg_attr(not(windows), path = "unix_build.rs")]
8
mod impl_;
9

10
use std::{
11
    env,
12
    fs::File,
13
    io::{BufWriter, Write},
14
    path::{Path, PathBuf},
15
    process::Command,
16
    str::FromStr,
17
};
18

19
use anyhow::{anyhow, bail, Context, Error, Result};
20
use bindgen::RustTarget;
21
use impl_::Provider;
22

23
/// Provides information about the PHP installation.
24
pub trait PHPProvider<'a>: Sized {
25
    /// Create a new PHP provider.
26
    #[allow(clippy::missing_errors_doc)]
27
    fn new(info: &'a PHPInfo) -> Result<Self>;
28

29
    /// Retrieve a list of absolute include paths.
30
    #[allow(clippy::missing_errors_doc)]
31
    fn get_includes(&self) -> Result<Vec<PathBuf>>;
32

33
    /// Retrieve a list of macro definitions to pass to the compiler.
34
    #[allow(clippy::missing_errors_doc)]
35
    fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>>;
36

37
    /// Writes the bindings to a file.
38
    #[allow(clippy::missing_errors_doc)]
UNCOV
39
    fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> {
×
UNCOV
40
        for line in bindings.lines() {
×
UNCOV
41
            writeln!(writer, "{line}")?;
×
42
        }
43
        Ok(())
×
44
    }
45

46
    /// Prints any extra link arguments.
47
    #[allow(clippy::missing_errors_doc)]
UNCOV
48
    fn print_extra_link_args(&self) -> Result<()> {
×
UNCOV
49
        Ok(())
×
50
    }
51
}
52

53
/// Finds the location of an executable `name`.
54
#[must_use]
55
pub fn find_executable(name: &str) -> Option<PathBuf> {
56
    const WHICH: &str = if cfg!(windows) { "where" } else { "which" };
57
    let cmd = Command::new(WHICH).arg(name).output().ok()?;
58
    if cmd.status.success() {
59
        let stdout = String::from_utf8_lossy(&cmd.stdout);
60
        stdout.trim().lines().next().map(|l| l.trim().into())
61
    } else {
62
        None
63
    }
64
}
65

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

71
/// Finds the location of the PHP executable.
72
fn find_php() -> Result<PathBuf> {
73
    // If path is given via env, it takes priority.
74
    if let Some(path) = path_from_env("PHP") {
75
        if !path.try_exists()? {
76
            // If path was explicitly given and it can't be found, this is a hard error
77
            bail!("php executable not found at {:?}", path);
78
        }
79
        return Ok(path);
80
    }
81
    find_executable("php").with_context(|| {
82
        "Could not find PHP executable. \
83
        Please ensure `php` is in your PATH or the `PHP` environment variable is set."
84
    })
85
}
86

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

90
impl PHPInfo {
91
    /// Get the PHP info.
92
    ///
93
    /// # Errors
94
    /// - `php -i` command failed to execute successfully
95
    pub fn get(php: &Path) -> Result<Self> {
96
        let cmd = Command::new(php)
97
            .arg("-i")
98
            .output()
99
            .context("Failed to call `php -i`")?;
100
        if !cmd.status.success() {
101
            bail!("Failed to call `php -i` status code {}", cmd.status);
102
        }
103
        let stdout = String::from_utf8_lossy(&cmd.stdout);
104
        Ok(Self(stdout.to_string()))
105
    }
106

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

112
        self.get_key("Architecture")
113
            .context("Could not find architecture of PHP")?
114
            .try_into()
115
    }
116

117
    /// Checks if thread safety is enabled.
118
    ///
119
    /// # Errors
120
    /// - `PHPInfo` does not contain thread safety information
121
    pub fn thread_safety(&self) -> Result<bool> {
122
        Ok(self
123
            .get_key("Thread Safety")
124
            .context("Could not find thread safety of PHP")?
125
            == "enabled")
126
    }
127

128
    /// Checks if PHP was built with debug.
129
    ///
130
    /// # Errors
131
    /// - `PHPInfo` does not contain debug build information
132
    pub fn debug(&self) -> Result<bool> {
133
        Ok(self
134
            .get_key("Debug Build")
135
            .context("Could not find debug build of PHP")?
136
            == "yes")
137
    }
138

139
    /// Get the php version.
140
    ///
141
    /// # Errors
142
    /// - `PHPInfo` does not contain version number
143
    pub fn version(&self) -> Result<&str> {
144
        self.get_key("PHP Version")
145
            .context("Failed to get PHP version")
146
    }
147

148
    /// Get the zend version.
149
    ///
150
    /// # Errors
151
    /// - `PHPInfo` does not contain php api version
152
    pub fn zend_version(&self) -> Result<u32> {
153
        self.get_key("PHP API")
154
            .context("Failed to get Zend version")
155
            .and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer"))
156
    }
157

158
    fn get_key(&self, key: &str) -> Option<&str> {
159
        let split = format!("{key} => ");
160
        for line in self.0.lines() {
161
            let components: Vec<_> = line.split(&split).collect();
162
            if components.len() > 1 {
163
                return Some(components[1]);
164
            }
165
        }
166
        None
167
    }
168
}
169

170
fn add_php_version_defines(
171
    defines: &mut Vec<(&'static str, &'static str)>,
172
    info: &PHPInfo,
173
) -> Result<()> {
174
    let version = info.zend_version()?;
175
    let supported_version: ApiVersion = version.try_into()?;
176

177
    for supported_api in supported_version.supported_apis() {
178
        defines.push((supported_api.define_name(), "1"));
179
    }
180

181
    Ok(())
182
}
183

184
/// Builds the wrapper library.
185
fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
186
    let mut build = cc::Build::new();
187
    for (var, val) in defines {
188
        build.define(var, *val);
189
    }
190
    build
191
        .file("src/wrapper.c")
192
        .includes(includes)
193
        .try_compile("wrapper")
194
        .context("Failed to compile ext-php-rs C interface")?;
195
    Ok(())
196
}
197

198
#[cfg(feature = "embed")]
199
/// Builds the embed library.
200
fn build_embed(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
201
    let mut build = cc::Build::new();
202
    for (var, val) in defines {
203
        build.define(var, *val);
204
    }
205
    build
206
        .file("src/embed/embed.c")
207
        .includes(includes)
208
        .try_compile("embed")
209
        .context("Failed to compile ext-php-rs C embed interface")?;
210
    Ok(())
211
}
212

213
/// Generates bindings to the Zend API.
214
fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<String> {
215
    let mut bindgen = bindgen::Builder::default();
216

217
    #[cfg(feature = "embed")]
218
    {
219
        bindgen = bindgen.header("src/embed/embed.h");
220
    }
221

222
    bindgen = bindgen
223
        .header("src/wrapper.h")
224
        .clang_args(
225
            includes
226
                .iter()
227
                .map(|inc| format!("-I{}", inc.to_string_lossy())),
228
        )
229
        .clang_args(defines.iter().map(|(var, val)| format!("-D{var}={val}")))
230
        .formatter(bindgen::Formatter::Rustfmt)
231
        .no_copy("php_ini_builder")
232
        .no_copy("_zval_struct")
233
        .no_copy("_zend_string")
234
        .no_copy("_zend_array")
235
        .no_debug("_zend_function_entry") // On Windows when the handler uses vectorcall, Debug cannot be derived so we do it in code.
236
        .layout_tests(env::var("EXT_PHP_RS_TEST").is_ok())
237
        .rust_target(RustTarget::Nightly);
238

239
    for binding in ALLOWED_BINDINGS {
240
        bindgen = bindgen
241
            .allowlist_function(binding)
242
            .allowlist_type(binding)
243
            .allowlist_var(binding);
244
    }
245

246
    let extension_allowed_bindings = env::var("EXT_PHP_RS_ALLOWED_BINDINGS").ok();
247
    if let Some(extension_allowed_bindings) = extension_allowed_bindings {
248
        for binding in extension_allowed_bindings.split(',') {
249
            bindgen = bindgen
250
                .allowlist_function(binding)
251
                .allowlist_type(binding)
252
                .allowlist_var(binding);
253
        }
254
    }
255

256
    let bindings = bindgen
257
        .generate()
258
        .map_err(|_| anyhow!("Unable to generate bindings for PHP"))?
259
        .to_string();
260

261
    Ok(bindings)
262
}
263

264
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
265
enum ApiVersion {
266
    Php80 = 2020_09_30,
267
    Php81 = 2021_09_02,
268
    Php82 = 2022_08_29,
269
    Php83 = 2023_08_31,
270
    Php84 = 2024_09_24,
271
}
272

273
impl ApiVersion {
274
    /// Returns the minimum API version supported by ext-php-rs.
275
    pub const fn min() -> Self {
276
        ApiVersion::Php80
277
    }
278

279
    /// Returns the maximum API version supported by ext-php-rs.
280
    pub const fn max() -> Self {
281
        ApiVersion::Php84
282
    }
283

284
    pub fn versions() -> Vec<Self> {
285
        vec![
286
            ApiVersion::Php80,
287
            ApiVersion::Php81,
288
            ApiVersion::Php82,
289
            ApiVersion::Php83,
290
            ApiVersion::Php84,
291
        ]
292
    }
293

294
    /// Returns the API versions that are supported by this version.
295
    pub fn supported_apis(self) -> Vec<ApiVersion> {
296
        ApiVersion::versions()
297
            .into_iter()
298
            .filter(|&v| v <= self)
299
            .collect()
300
    }
301

302
    pub fn cfg_name(self) -> &'static str {
303
        match self {
304
            ApiVersion::Php80 => "php80",
305
            ApiVersion::Php81 => "php81",
306
            ApiVersion::Php82 => "php82",
307
            ApiVersion::Php83 => "php83",
308
            ApiVersion::Php84 => "php84",
309
        }
310
    }
311

312
    pub fn define_name(self) -> &'static str {
313
        match self {
314
            ApiVersion::Php80 => "EXT_PHP_RS_PHP_80",
315
            ApiVersion::Php81 => "EXT_PHP_RS_PHP_81",
316
            ApiVersion::Php82 => "EXT_PHP_RS_PHP_82",
317
            ApiVersion::Php83 => "EXT_PHP_RS_PHP_83",
318
            ApiVersion::Php84 => "EXT_PHP_RS_PHP_84",
319
        }
320
    }
321
}
322

323
impl TryFrom<u32> for ApiVersion {
324
    type Error = Error;
325

326
    fn try_from(version: u32) -> Result<Self, Self::Error> {
327
        match version {
328
            x if ((ApiVersion::Php80 as u32)..(ApiVersion::Php81 as u32)).contains(&x) => Ok(ApiVersion::Php80),
329
            x if ((ApiVersion::Php81 as u32)..(ApiVersion::Php82 as u32)).contains(&x) => Ok(ApiVersion::Php81),
330
            x if ((ApiVersion::Php82 as u32)..(ApiVersion::Php83 as u32)).contains(&x) => Ok(ApiVersion::Php82),
331
            x if ((ApiVersion::Php83 as u32)..(ApiVersion::Php84 as u32)).contains(&x) => Ok(ApiVersion::Php83),
332
            x if (ApiVersion::Php84 as u32) == x => Ok(ApiVersion::Php84),
333
            version => Err(anyhow!(
334
              "The current version of PHP is not supported. Current PHP API version: {}, requires a version between {} and {}",
335
              version,
336
              ApiVersion::min() as u32,
337
              ApiVersion::max() as u32
338
            ))
339
        }
340
    }
341
}
342

343
/// Checks the PHP Zend API version for compatibility with ext-php-rs, setting
344
/// any configuration flags required.
345
fn check_php_version(info: &PHPInfo) -> Result<()> {
346
    let version = info.zend_version()?;
347
    let version: ApiVersion = version.try_into()?;
348

349
    // Infra cfg flags - use these for things that change in the Zend API that don't
350
    // rely on a feature and the crate user won't care about (e.g. struct field
351
    // changes). Use a feature flag for an actual feature (e.g. enums being
352
    // introduced in PHP 8.1).
353
    //
354
    // PHP 8.0 is the baseline - no feature flags will be introduced here.
355
    //
356
    // The PHP version cfg flags should also stack - if you compile on PHP 8.2 you
357
    // should get both the `php81` and `php82` flags.
358
    println!(
359
        "cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php_zts, php_debug, docs)"
360
    );
361

362
    if version == ApiVersion::Php80 {
363
        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.");
364
    }
365

366
    for supported_version in version.supported_apis() {
367
        println!("cargo:rustc-cfg={}", supported_version.cfg_name());
368
    }
369

370
    Ok(())
371
}
372

373
fn main() -> Result<()> {
374
    let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?;
375
    let out_path = PathBuf::from(out_dir).join("bindings.rs");
376
    let manifest: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into();
377
    for path in [
378
        manifest.join("src").join("wrapper.h"),
379
        manifest.join("src").join("wrapper.c"),
380
        manifest.join("src").join("embed").join("embed.h"),
381
        manifest.join("src").join("embed").join("embed.c"),
382
        manifest.join("allowed_bindings.rs"),
383
        manifest.join("windows_build.rs"),
384
        manifest.join("unix_build.rs"),
385
    ] {
386
        println!("cargo:rerun-if-changed={}", path.to_string_lossy());
387
    }
388
    for env_var in ["PHP", "PHP_CONFIG", "PATH", "EXT_PHP_RS_ALLOWED_BINDINGS"] {
389
        println!("cargo:rerun-if-env-changed={env_var}");
390
    }
391

392
    println!("cargo:rerun-if-changed=build.rs");
393

394
    // docs.rs runners only have PHP 7.4 - use pre-generated bindings
395
    if env::var("DOCS_RS").is_ok() {
396
        println!("cargo:warning=docs.rs detected - using stub bindings");
397
        println!("cargo:rustc-cfg=php_debug");
398
        println!("cargo:rustc-cfg=php81");
399
        println!("cargo:rustc-cfg=php82");
400
        println!("cargo:rustc-cfg=php83");
401
        println!("cargo:rustc-cfg=php84");
402
        std::fs::copy("docsrs_bindings.rs", out_path)
403
            .expect("failed to copy docs.rs stub bindings to out directory");
404
        return Ok(());
405
    }
406

407
    let php = find_php()?;
408
    let info = PHPInfo::get(&php)?;
409
    let provider = Provider::new(&info)?;
410

411
    let includes = provider.get_includes()?;
412
    let mut defines = provider.get_defines()?;
413
    add_php_version_defines(&mut defines, &info)?;
414

415
    check_php_version(&info)?;
416
    build_wrapper(&defines, &includes)?;
417

418
    #[cfg(feature = "embed")]
419
    build_embed(&defines, &includes)?;
420

421
    let bindings = generate_bindings(&defines, &includes)?;
422

423
    let out_file =
424
        File::create(&out_path).context("Failed to open output bindings file for writing")?;
425
    let mut out_writer = BufWriter::new(out_file);
426
    provider.write_bindings(bindings, &mut out_writer)?;
427

428
    if info.debug()? {
429
        println!("cargo:rustc-cfg=php_debug");
430
    }
431
    if info.thread_safety()? {
432
        println!("cargo:rustc-cfg=php_zts");
433
    }
434
    provider.print_extra_link_args()?;
435

436
    // Generate guide tests
437
    let test_md = skeptic::markdown_files_of_directory("guide");
438
    #[cfg(not(feature = "closure"))]
439
    let test_md: Vec<_> = test_md
440
        .into_iter()
441
        .filter(|p| p.file_stem() != Some(std::ffi::OsStr::new("closure")))
442
        .collect();
443
    skeptic::generate_doc_tests(&test_md);
444

445
    Ok(())
446
}
447

448
// Mock macro for the `allowed_bindings.rs` script.
449
macro_rules! bind {
450
    ($($s: ident),*) => {
451
        &[$(
452
            stringify!($s),
453
        )*]
454
    }
455
}
456

457
/// Array of functions/types used in `ext-php-rs` - used to allowlist when
458
/// generating bindings, as we don't want to generate bindings for everything
459
/// (i.e. stdlib headers).
460
const ALLOWED_BINDINGS: &[&str] = include!("allowed_bindings.rs");
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc