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.
This commit is contained in:
ju1ius 2022-12-11 20:08:50 +01:00 committed by GitHub
parent d52a878e7b
commit 4ca5c0d06e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 278 additions and 119 deletions

View File

@ -43,7 +43,7 @@ pub trait PHPProvider<'a>: Sized {
}
/// Finds the location of an executable `name`.
fn find_executable(name: &str) -> Option<PathBuf> {
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() {
@ -54,15 +54,25 @@ fn find_executable(name: &str) -> Option<PathBuf> {
}
}
/// 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 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() {

View File

@ -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)

View File

@ -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
<?php
var_dump(hello_world("David"));
```
Now let's build our extension and run our test script. This is done through
`cargo` like any other Rust crate. It is required that the `php-config`
executable is able to be found by the `ext-php-rs` build script.
The extension is stored inside `target/debug` (if you did a debug build,
`target/release` for release builds). The file name will be based on your crate
name, so for us it will be `libhello_world`. The extension is based on your OS -
on Linux it will be `libhello_world.so`, on macOS it will be
`libhello_world.dylib` and on Windows it will be `hello_world.dll` (no `lib`
prefix).
```sh
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
$ php -dextension=./target/debug/libhello_world.dylib test.php
string(13) "Hello, David!"
```

View File

@ -1,3 +0,0 @@
# Examples
- [Hello World](./hello_world.md)

View File

@ -0,0 +1,162 @@
# Hello World
## Project Setup
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"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "*"
[profile.release]
strip = "debuginfo"
```
### `.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}}
```
## 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
<?php
var_dump(hello_world("David"));
```
And run it:
```sh
$ php -d extension=./target/debug/libhello_world.so test.php
string(13) "Hello, David!"
```

View File

@ -0,0 +1,68 @@
# Installation
To get started using `ext-php-rs` you will need both a Rust toolchain
and a PHP development environment. We'll cover each of these below.
## Rust toolchain
First, make sure you have rust installed on your system.
If you haven't already done so you can do so by following the instructions [here](https://www.rust-lang.org/tools/install).
`ext-php-rs` runs on both the stable and nightly versions so you can choose whichever one fits you best.
## PHP development environment
In order to develop PHP extensions, you'll need the following installed on your system:
1. The PHP CLI executable itself
2. The PHP development headers
3. The `php-config` binary
While the easiest way to get started is to use the packages provided by your distribution,
we recommend building PHP from source.
**NB:** To use `ext-php-rs` you'll need at least PHP 8.0.
### Using a package manager
```sh
# Debian and derivatives
apt install php-dev
# Arch Linux
pacman -S php
# Fedora
dnf install php-devel
# Homebrew
brew install php
```
### Compiling PHP from source
Please refer to this [PHP internals book chapter](https://www.phpinternalsbook.com/php7/build_system/building_php.html)
for an in-depth guide on how to build PHP from source.
**TL;DR;** use the following commands to build a minimal development version
with debug symbols enabled.
```sh
# clone the php-src repository
git clone https://github.com/php/php-src.git
cd php-src
# by default you will be on the master branch, which is the current
# development version. You can check out a stable branch instead:
git checkout PHP-8.1
./buildconf
PREFIX="${HOME}/build/php"
.configure --prefix="${PREFIX}" \
--enable-debug \
--disable-all --disable-cgi
make -j "$(nproc)"
make install
```
The PHP CLI binary should now be located at `${PREFIX}/bin/php`
and the `php-config` binary at `${PREFIX}/bin/php-config`.
## Next steps
Now that we have our development environment in place,
let's go [build an extension](./hello_world.md) !

View File

@ -2,14 +2,14 @@ use std::{path::PathBuf, process::Command};
use anyhow::{bail, Context, Result};
use crate::{PHPInfo, PHPProvider};
use crate::{find_executable, path_from_env, PHPInfo, PHPProvider};
pub struct Provider {}
impl Provider {
/// Runs `php-config` with one argument, returning the stdout.
fn php_config(&self, arg: &str) -> Result<String> {
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<PathBuf> {
// 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 {