mirror of
https://github.com/danog/ext-php-rs.git
synced 2024-11-30 04:39:04 +01:00
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:
parent
d52a878e7b
commit
4ca5c0d06e
27
build.rs
27
build.rs
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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!"
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
# Examples
|
||||
|
||||
- [Hello World](./hello_world.md)
|
162
guide/src/getting-started/hello_world.md
Normal file
162
guide/src/getting-started/hello_world.md
Normal 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!"
|
||||
```
|
68
guide/src/getting-started/installation.md
Normal file
68
guide/src/getting-started/installation.md
Normal 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) !
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user