php-extension-builder is a Rust CLI for building pre-packaged binary artifacts
for PIE (PHP Installer for Extensions) extensions.
It builds one target per invocation and writes requested artifacts containing the
compiled extension .so. By default it keeps the PIE-compatible .zip output.
Pass repeated --artifact flags to request additional formats such as .deb.
cargo install --path .Or run it directly from a checkout:
cargo run -- build --helpPrebuilt CLI binaries are published on GitHub Releases for:
x86_64-apple-darwinaarch64-apple-darwinx86_64-unknown-linux-gnuaarch64-unknown-linux-gnu
Release publishing is handled by GoReleaser. Pushing a tag like v0.1.0
triggers the release workflow, builds the CLI binaries with cargo zigbuild,
uploads .tar.gz archives, and publishes checksums.txt.
In GitHub Actions, prefer installing one of those release archives instead of building the CLI with Cargo in every extension workflow:
- name: Install php-extension-builder
uses: jaxxstorm/action-install-gh-release@v3.0.0
with:
repo: shyim/php-extension-builder
tag: v0.1.0
cache: enableThe action selects the matching Linux or macOS archive for the current runner
and puts php-extension-builder on PATH.
Linux builds run inside official PHP Docker images. The CLI mounts the current
directory into the container, installs build dependencies inside the ephemeral
container, then runs any --before-phpize-command hooks followed by phpize,
./configure, and make.
Build logs are streamed as commands run. The builder also prints progress
markers for dependency installation, before-phpize hooks, phpize,
./configure, make, and metadata collection.
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc glibc \
--artifact zip \
--artifact deb \
--configure-flag '--enable-example-pie-extension'For Alpine/musl builds:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc muslFor ZTS builds:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--ztsIf your extension needs extra system libraries, pass distro-specific packages.
The CLI installs --apt-package values only in Debian-based images and
--apk-package values only in Alpine-based images:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc glibc \
--apt-package libzstd-dev
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc musl \
--apk-package zstd-devThe default images are selected from the official PHP image family:
| Target | Default image pattern |
|---|---|
| glibc NTS | php:<version>-cli |
| glibc ZTS | php:<version>-zts |
| musl NTS | php:<version>-cli-alpine |
| musl ZTS | php:<version>-zts-alpine |
Use --image to override the default image when a project needs extra system
dependencies:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--image ghcr.io/acme/php-extension-builder:8.3Extensions written in Rust with ext-php-rs
are supported on Linux. The builder auto-detects them: if Cargo.toml at the
build path declares an ext-php-rs dependency, the build runs as a Rust build.
Pass --build-kind rust (or --build-kind c) to force the detection.
Rust builds run inside pre-baked images that ship a Rust toolchain plus the
clang/libclang that ext-php-rs's bindgen requires. The image tag is
auto-derived from the same --php-version/--libc/--zts inputs as C builds:
ghcr.io/shyim/php-extension-builder-rust:<php-version>-<suffix>
| Target | Default Rust image suffix |
|---|---|
| glibc NTS | cli |
| glibc ZTS | zts |
| musl NTS | cli-alpine |
| musl ZTS | zts-alpine |
Use --image to override it with your own Rust-capable image.
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc glibc \
--cargo-feature closureInside the container the builder reuses the standard phpize → configure →
make flow when the project ships PIE build files (a top-level config.m4 or a
pie/config.m4, as is conventional for ext-php-rs projects), since that
make step delegates to Cargo. When no PIE build files are present it falls
back to running cargo build --release directly. Repeat --cargo-feature to
enable Cargo features; --configure-flag is rejected for Rust builds.
The compiled .so is located by searching, in order, modules/<extension>.so
(the PIE/make output), <extension>.so, and
target/release/lib<crate>.so (the raw Cargo cdylib, including a workspace-root
target/ for workspace members). A valid composer.json is still required for
the extension name and the generated artifact metadata.
The pre-baked images are built and published by the
docker-images.yml workflow, which calls
docker/github-builder's reusable
build workflow from a PHP/variant matrix and pushes one multi-arch
(linux/amd64 + linux/arm64) tag per combination from docker/Dockerfile.
Rust extensions also build natively on macOS (see the macOS section below);
Windows is not yet supported for ext-php-rs.
Linux glibc non-ZTS builds can also emit native .deb packages:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc glibc \
--artifact debTo emit both the PIE ZIP and Debian package in one build:
php-extension-builder build \
--package-version 1.2.3 \
--php-version 8.3 \
--libc glibc \
--artifact zip \
--artifact debThe Debian package is named php<php-version>-<extension> so packages for
multiple PHP versions can be installed side by side on the same Debian or
Ubuntu system. For Debian package-name validity, uppercase extension names are
lowercased and underscores become hyphens in the package name only. It installs:
/usr/lib/php/<php-api>/<extension>.so/etc/php/<php-version>/mods-available/<extension>.ini
The package depends on phpapi-<php-api> and php-common, and provides the
generic virtual package php-<extension>. Its maintainer scripts run
phpenmod -v <php-version> <extension> on install and
phpdismod -v <php-version> <extension> on removal when those commands are
available.
Debian output is intentionally limited in this first version:
- Linux only
- glibc only
- non-ZTS only
The extension is still compiled in the official php:<version> Docker image,
matching the existing Linux backend. The Debian package metadata is generated
from php-config metadata reported by that build image.
Docker cannot produce macOS PHP extension binaries because Docker Desktop runs Linux containers. macOS packages must be built natively on a macOS host or GitHub Actions macOS runner.
php-extension-builder build \
--target-os darwin \
--package-version 1.2.3 \
--php-version 8.3 \
--php-config /opt/homebrew/bin/php-configThe macOS backend runs any --before-phpize-command hooks followed by
phpize, ./configure, and make on the host and packages the resulting
modules/<extension>.so. If --php-version is supplied, the selected
php-config must report that same PHP major/minor version. The default build is
non-ZTS; pass --zts when building against a ZTS PHP, and the CLI will fail if
the selected PHP thread-safety mode does not match. Native command logs are
streamed as each command runs.
Rust (ext-php-rs) extensions build natively on macOS too — the same
auto-detection applies. When the project ships PIE build files (a config.m4 or
pie/config.m4) the backend uses the phpize/configure/make flow;
otherwise it runs cargo build --release directly and packages
target/release/lib<crate>.dylib. Cargo must be on PATH, so install a Rust
toolchain first. On a GitHub Actions macOS runner, add a setup step before the
build:
- uses: dtolnay/rust-toolchain@stable
- name: Build Rust extension
run: |
php-extension-builder build \
--target-os darwin \
--package-version 1.2.3 \
--php-version 8.3 \
--php-config /opt/homebrew/bin/php-config \
--cargo-feature closureWindows PHP extensions are built as .dll files with PHP's Windows build
tooling, not with Docker, phpize, or the Unix ./configure flow. For Windows
artifacts, use php/php-windows-builder.
The Windows builder provides GitHub Actions for generating the correct PHP/version/architecture/thread-safety matrix and building extension DLLs:
jobs:
windows-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.extension-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- name: Get Windows extension matrix
id: extension-matrix
uses: php/php-windows-builder/extension-matrix@v1
with:
php-version-list: '8.2, 8.3, 8.4'
arch-list: 'x64'
ts-list: 'nts, ts'
build-windows:
needs: windows-matrix
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.windows-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v6
- name: Build Windows extension
uses: php/php-windows-builder/extension@v1
with:
php-version: ${{ matrix.php-version }}
arch: ${{ matrix.arch }}
ts: ${{ matrix.ts }}
# args: --enable-your-extension
# libs: zlibUse this Rust CLI for Linux and macOS packages, and use
php/php-windows-builder for Windows DLL artifacts. If you upload Windows
artifacts to GitHub releases, prefer the builder's own release action so the
Windows artifact format stays aligned with that project.
| Option | Description |
|---|---|
--package-version <version> |
Required. Used in the generated PIE package filename. |
--artifact <zip|deb> |
Artifact to generate. Can be supplied multiple times. Defaults to zip. |
--php-version <major.minor> |
Required for Linux Docker builds. Optional for macOS, where it validates the selected php-config. |
--target-os <linux|darwin> |
Build backend. Defaults to linux. |
--libc <glibc|musl|bsdlibc> |
Linux defaults to glibc; Darwin defaults to bsdlibc. |
--zts |
Request a ZTS PHP build. Linux uses a ZTS Docker image; macOS validates the selected PHP is ZTS. Without it, the selected PHP must be non-ZTS. |
--build-kind <c|rust> |
Force the build backend. Defaults to auto-detection (rust when Cargo.toml declares an ext-php-rs dependency, otherwise c). |
--cargo-feature <feature> |
Cargo feature enabled for Rust builds. Can be supplied multiple times. Rust builds only. |
--build-path <path> |
Extension source path containing config.m4, relative to the current directory. Defaults to .. |
--configure-flag <flag> |
Additional flag passed to ./configure. Can be supplied multiple times. |
--before-phpize-command <command> |
Shell command run before phpize. Can be supplied multiple times; quote commands that include arguments. |
--apt-package <package> |
Extra Debian/Ubuntu package installed with apt-get. Can be supplied multiple times. Linux Docker builds only. |
--apk-package <package> |
Extra Alpine package installed with apk. Can be supplied multiple times. Linux Docker builds only. |
--out-dir <path> |
Directory for the generated .zip. Defaults to the current directory. |
--image <image> |
Optional Docker image override for Linux builds. |
--php-config <path> |
Optional php-config path for native macOS builds. |
Generated ZIP archives keep PIE's expected naming convention:
php_<extension>-<release>_php<php-version>-<arch>-<os>-<libc><debug><zts>.zip
Examples:
php_example-1.2.3_php8.3-x86_64-linux-glibc.zip
php_example-1.2.3_php8.3-arm64-linux-musl-zts.zip
php_example-1.2.3_php8.3-arm64-darwin-bsdlibc.zip
Generated Debian packages use Debian-style package names and architecture names:
php<php-version>-<extension>_<release>_<deb-arch>.deb
Example:
php8.3-example_1.2.3_amd64.deb
This example builds Linux and macOS PIE packages with the released builder
binary, uploads every matrix artifact, and publishes a GitHub release with
checksums when the workflow runs for a tag. The Linux glibc non-ZTS rows also
emit .deb packages. Keep Windows builds in a separate job using
php/php-windows-builder as shown above.
name: Release PIE binaries
on:
push:
tags: ["*"]
workflow_dispatch:
inputs:
package_version:
description: Package version used in generated package names. Defaults to the ref name without a leading v.
required: false
type: string
permissions:
contents: read
env:
PHP_EXTENSION_BUILDER_VERSION: v0.1.0
ARTIFACT_DIR: dist
jobs:
build-linux:
runs-on: ubuntu-latest
name: "PHP ${{ matrix.php-version }} / Linux ${{ matrix.libc }} / ${{ matrix.thread-safety }}"
strategy:
fail-fast: false
matrix:
php-version: ["8.2", "8.3", "8.4", "8.5"]
libc: [glibc, musl]
thread-safety: [nts, zts]
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
- name: Resolve package version
env:
INPUT_PACKAGE_VERSION: ${{ inputs.package_version }}
run: |
if [ -n "$INPUT_PACKAGE_VERSION" ]; then
version="$INPUT_PACKAGE_VERSION"
else
version="${GITHUB_REF_NAME#v}"
fi
echo "PACKAGE_VERSION=$version" >> "$GITHUB_ENV"
- name: Install php-extension-builder
uses: jaxxstorm/action-install-gh-release@v3.0.0
with:
repo: shyim/php-extension-builder
tag: ${{ env.PHP_EXTENSION_BUILDER_VERSION }}
cache: enable
- name: Build extension
shell: bash
run: |
mkdir -p "$ARTIFACT_DIR"
zts_args=()
if [ "${{ matrix.thread-safety }}" = "zts" ]; then
zts_args+=(--zts)
fi
extra_args=(
# --before-phpize-command "composer install --no-dev"
# --configure-flag "--enable-your-extension"
# --apt-package libzstd-dev
# --apk-package zstd-dev
)
artifact_args=(--artifact zip)
if [ "${{ matrix.libc }}" = "glibc" ] && [ "${{ matrix.thread-safety }}" = "nts" ]; then
artifact_args+=(--artifact deb)
fi
php-extension-builder build \
--package-version "$PACKAGE_VERSION" \
--php-version "${{ matrix.php-version }}" \
--libc "${{ matrix.libc }}" \
--out-dir "$ARTIFACT_DIR" \
"${artifact_args[@]}" \
"${zts_args[@]}" \
"${extra_args[@]}"
- name: Upload package
uses: actions/upload-artifact@v7.0.1
with:
name: php-extension-php${{ matrix.php-version }}-linux-${{ matrix.libc }}-${{ matrix.thread-safety }}
path: |
${{ env.ARTIFACT_DIR }}/*.zip
${{ env.ARTIFACT_DIR }}/*.deb
if-no-files-found: error
retention-days: 7
build-macos:
runs-on: ${{ matrix.target.runner }}
name: "PHP ${{ matrix.php-version }} / macOS ${{ matrix.target.arch }} / nts"
strategy:
fail-fast: false
matrix:
php-version: ["8.2", "8.3", "8.4", "8.5"]
target:
- runner: macos-15-intel
arch: x86_64
- runner: macos-15
arch: arm64
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
- name: Resolve package version
env:
INPUT_PACKAGE_VERSION: ${{ inputs.package_version }}
run: |
if [ -n "$INPUT_PACKAGE_VERSION" ]; then
version="$INPUT_PACKAGE_VERSION"
else
version="${GITHUB_REF_NAME#v}"
fi
echo "PACKAGE_VERSION=$version" >> "$GITHUB_ENV"
- name: Setup PHP
uses: shivammathur/setup-php@2.37.1
with:
php-version: ${{ matrix.php-version }}
coverage: none
- name: Install php-extension-builder
uses: jaxxstorm/action-install-gh-release@v3.0.0
with:
repo: shyim/php-extension-builder
tag: ${{ env.PHP_EXTENSION_BUILDER_VERSION }}
cache: enable
- name: Build extension
shell: bash
run: |
mkdir -p "$ARTIFACT_DIR"
extra_args=(
# --before-phpize-command "composer install --no-dev"
# --configure-flag "--enable-your-extension"
)
php-extension-builder build \
--target-os darwin \
--package-version "$PACKAGE_VERSION" \
--php-version "${{ matrix.php-version }}" \
--php-config "$(command -v php-config)" \
--artifact zip \
--out-dir "$ARTIFACT_DIR" \
"${extra_args[@]}"
- name: Upload package
uses: actions/upload-artifact@v7.0.1
with:
name: php-extension-php${{ matrix.php-version }}-darwin-${{ matrix.target.arch }}-nts
path: ${{ env.ARTIFACT_DIR }}/*.zip
if-no-files-found: error
retention-days: 7
publish:
runs-on: ubuntu-latest
name: Publish GitHub release
needs:
- build-linux
- build-macos
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download packages
uses: actions/download-artifact@v8.0.1
with:
path: packages
- name: Prepare release assets
shell: bash
run: |
mkdir -p dist
find packages -type f -name '*.zip' -exec cp '{}' dist/ \;
find packages -type f -name '*.deb' -exec cp '{}' dist/ \;
mapfile -t packages < <(find dist -maxdepth 1 -type f -name '*.zip' -print | sort)
mapfile -t debs < <(find dist -maxdepth 1 -type f -name '*.deb' -print | sort)
packages+=("${debs[@]}")
if [ "${#packages[@]}" -eq 0 ]; then
echo "No packages found" >&2
exit 1
fi
sha256sum "${packages[@]}" > dist/checksums.txt
ls -lah dist
- name: Publish release
uses: softprops/action-gh-release@v3.0.0
with:
files: |
dist/*.zip
dist/*.deb
dist/checksums.txt
fail_on_unmatched_files: true
generate_release_notes: true