From 4ca5c0d06e503ec8d1c3e2764494c97e8e9a91fe Mon Sep 17 00:00:00 2001 From: ju1ius Date: Sun, 11 Dec 2022 20:08:50 +0100 Subject: [PATCH] honour PHP_CONFIG & rebuild automatically when env vars change (#210) Closes https://github.com/davidcole1340/ext-php-rs/issues/208 Closes https://github.com/davidcole1340/ext-php-rs/issues/209 ## Summary of the changes ### Build scripts * the `unix_build.rs` script now honors the `PHP_CONFIG` environment variable, like `cargo php install` * use `cargo:rerun-if-env-changed` for the `PHP`, `PHP_CONFIG` and `PATH` environment variables, to avoid needless recompilation of the whole dependency tree. ### Documentation While trying to document the aforementioned changes, I realized that there was no chapter about installing and setting up a PHP environment to develop PHP extensions. So, I refactored the first chapters of the book into a `Getting Started` section, including instructions on how to quickly set up a PHP environment. --- build.rs | 27 +++- guide/src/SUMMARY.md | 14 +- guide/src/examples/hello_world.md | 103 ------------ guide/src/examples/index.md | 3 - guide/src/{ => getting-started}/cargo-php.md | 0 guide/src/getting-started/hello_world.md | 162 +++++++++++++++++++ guide/src/getting-started/installation.md | 68 ++++++++ unix_build.rs | 20 ++- 8 files changed, 278 insertions(+), 119 deletions(-) delete mode 100644 guide/src/examples/hello_world.md delete mode 100644 guide/src/examples/index.md rename guide/src/{ => getting-started}/cargo-php.md (100%) create mode 100644 guide/src/getting-started/hello_world.md create mode 100644 guide/src/getting-started/installation.md diff --git a/build.rs b/build.rs index 7617096..8a06ba2 100644 --- a/build.rs +++ b/build.rs @@ -43,7 +43,7 @@ pub trait PHPProvider<'a>: Sized { } /// Finds the location of an executable `name`. -fn find_executable(name: &str) -> Option { +pub 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() { @@ -54,15 +54,25 @@ fn find_executable(name: &str) -> Option { } } +/// 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 PHP path is given via env, it takes priority. - let env = std::env::var("PHP"); - if let Ok(env) = env { - return Ok(env.into()); + // 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); + } + return Ok(path); } - - find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") + find_executable("php").with_context(|| { + "Could not find PHP executable. \ + Please ensure `php` is in your PATH or the `PHP` environment variable is set." + }) } pub struct PHPInfo(String); @@ -218,6 +228,9 @@ fn main() -> Result<()> { ] { println!("cargo:rerun-if-changed={}", path.to_string_lossy()); } + for env_var in ["PHP", "PHP_CONFIG", "PATH"] { + println!("cargo:rerun-if-env-changed={}", env_var); + } // docs.rs runners only have PHP 7.4 - use pre-generated bindings if env::var("DOCS_RS").is_ok() { diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index c050b60..f725964 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -1,9 +1,15 @@ # Summary -- [Introduction](./introduction.md) -- [`cargo php`](./cargo-php.md) -- [Examples](./examples/index.md) - - [Hello World](./examples/hello_world.md) +[Introduction](./introduction.md) + +# Getting Started + +- [Installation](./getting-started/installation.md) +- [Hello World](./getting-started/hello_world.md) +- [`cargo php`](./getting-started/cargo-php.md) + +# Reference Guide + - [Types](./types/index.md) - [Primitive Numbers](./types/numbers.md) - [`String`](./types/string.md) diff --git a/guide/src/examples/hello_world.md b/guide/src/examples/hello_world.md deleted file mode 100644 index b650bc4..0000000 --- a/guide/src/examples/hello_world.md +++ /dev/null @@ -1,103 +0,0 @@ -# Hello World - -Let's create a basic PHP extension. We will start by creating a new Rust library -crate: - -```sh -$ cargo new hello_world --lib -$ cd hello_world -``` - -### `Cargo.toml` - -Let's set up our crate by adding `ext-php-rs` as a dependency and setting the -crate type to `cdylib`. Update the `Cargo.toml` to look something like so: - -```toml -[package] -name = "hello_world" -version = "0.1.0" -edition = "2018" - -[dependencies] -ext-php-rs = "*" - -[lib] -crate-type = ["cdylib"] -``` - -### `.cargo/config.toml` - -When compiling for Linux and macOS, we do not link directly to PHP, rather PHP -will dynamically load the library. We need to tell the linker it's ok to have -undefined symbols (as they will be resolved when loaded by PHP). - -On Windows, we also need to switch to using the `rust-lld` linker. - -> Microsoft Visual C++'s `link.exe` is supported, however you may run into -> issues if your linker is not compatible with the linker used to compile PHP. - -We do this by creating a Cargo config file in `.cargo/config.toml` with the -following contents: - -```toml -{{#include ../../../.cargo/config.toml}} -``` - -### `src/lib.rs` - -Let's actually write the extension code now. We start by importing the -`ext-php-rs` prelude, which contains most of the imports required to make a -basic extension. We will then write our basic `hello_world` function, which will -take a string argument for the callers name, and we will return another string. -Finally, we write a `get_module` function which is used by PHP to find out about -your module. The `#[php_module]` attribute automatically registers your new -function so we don't need to do anything except return the `ModuleBuilder` that -we were given. - -We also need to enable the `abi_vectorcall` feature when compiling for Windows. -This is a nightly-only feature so it is recommended to use the `#[cfg_attr]` -macro to not enable the feature on other operating systems. - -```rust,ignore -#![cfg_attr(windows, feature(abi_vectorcall))] -use ext_php_rs::prelude::*; - -#[php_function] -pub fn hello_world(name: &str) -> String { - format!("Hello, {}!", name) -} - -#[php_module] -pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module -} -``` - -### `test.php` - -Let's make a test script. - -```php - Microsoft Visual C++'s `link.exe` is supported, however you may run into +> issues if your linker is not compatible with the linker used to compile PHP. + +We do this by creating a Cargo config file in `.cargo/config.toml` with the +following contents: + +```toml +{{#include ../../../.cargo/config.toml}} +``` + +## Writing our extension + +### `src/lib.rs` + +Let's actually write the extension code now. We start by importing the +`ext-php-rs` prelude, which contains most of the imports required to make a +basic extension. We will then write our basic `hello_world` function, which will +take a string argument for the callers name, and we will return another string. +Finally, we write a `get_module` function which is used by PHP to find out about +your module. The `#[php_module]` attribute automatically registers your new +function so we don't need to do anything except return the `ModuleBuilder` that +we were given. + +We also need to enable the `abi_vectorcall` feature when compiling for Windows. +This is a nightly-only feature so it is recommended to use the `#[cfg_attr]` +macro to not enable the feature on other operating systems. + +```rust,ignore +#![cfg_attr(windows, feature(abi_vectorcall))] +use ext_php_rs::prelude::*; + +#[php_function] +pub fn hello_world(name: &str) -> String { + format!("Hello, {}!", name) +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} +``` + +## Building the extension + +Now let's build our extension. +This is done through `cargo` like any other Rust crate. + +If you installed php using a package manager in the previous chapter +(or if the `php` and `php-config` binaries are already in your `$PATH`), +then you can just run + +```sh +cargo build +``` + +If you have multiple PHP versions in your PATH, or your installation +resides in a custom location, you can use the following environment variables: + +```sh +# explicitly specifies the path to the PHP executable: +export PHP=/path/to/php +# explicitly specifies the path to the php-config executable: +export PHP_CONFIG=/path/to/php-config +``` + +As an alternative, if you compiled PHP from source and installed it under +it's own prefix (`configure --prefix=/my/prefix`), you can just put +this prefix in front of your PATH: + +```sh +export PATH="/my/prefix:${PATH}" +``` + +Once you've setup these variables, you can just run + +```sh +cargo build +``` + +Cargo will track changes to these environment variables and rebuild the library accordingly. + +## Testing our extension + +The extension we just built is stored inside the cargo target directory: +`target/debug` if you did a debug build, `target/release` for release builds. + +The extension file name is OS-dependent. The naming works as follows: + +- let `S` be the empty string +- append to `S` the value of [std::env::consts::DLL_PREFIX](https://doc.rust-lang.org/std/env/consts/constant.DLL_PREFIX.html) + (empty on windows, `lib` on unixes) +- append to `S` the lower-snake-case version of your crate name +- append to `S` the value of [std::env::consts::DLL_SUFFIX](https://doc.rust-lang.org/std/env/consts/constant.DLL_SUFFIX.html) + (`.dll` on windows, `.dylib` on macOS, `.so` on other unixes). +- set the filename to the value of `S` + +Which in our case would give us: + +- linux: `libhello_world.so` +- macOS: `libhello_world.dylib` +- windows: `hello_world.dll` + +Now we need a way to tell the PHP CLI binary to load our extension. +There are [several ways to do that](https://www.phpinternalsbook.com/php7/build_system/building_extensions.html#loading-shared-extensions). +For now we'll simply pass the `-d extension=/path/to/extension` option to the PHP CLI binary. + +Let's make a test script: + +### `test.php` + +```php + Result { - let cmd = Command::new("php-config") + let cmd = Command::new(self.find_bin()?) .arg(arg) .output() .context("Failed to run `php-config`")?; @@ -20,6 +20,22 @@ impl Provider { } Ok(stdout.to_string()) } + + fn find_bin(&self) -> Result { + // If path is given via env, it takes priority. + if let Some(path) = path_from_env("PHP_CONFIG") { + if !path.try_exists()? { + // If path was explicitly given and it can't be found, this is a hard error + bail!("php-config executable not found at {:?}", path); + } + return Ok(path); + } + find_executable("php-config").with_context(|| { + "Could not find `php-config` executable. \ + Please ensure `php-config` is in your PATH or the \ + `PHP_CONFIG` environment variable is set." + }) + } } impl<'a> PHPProvider<'a> for Provider {