Skip to content

shyim/php-extension-builder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Generate Pre-Packaged Binaries for PHP Extensions

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.

Install

cargo install --path .

Or run it directly from a checkout:

cargo run -- build --help

Prebuilt CLI binaries are published on GitHub Releases for:

  • x86_64-apple-darwin
  • aarch64-apple-darwin
  • x86_64-unknown-linux-gnu
  • aarch64-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: enable

The action selects the matching Linux or macOS archive for the current runner and puts php-extension-builder on PATH.

Linux Builds

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 musl

For ZTS builds:

php-extension-builder build \
  --package-version 1.2.3 \
  --php-version 8.3 \
  --zts

If 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-dev

The 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.3

Rust (ext-php-rs) Extensions

Extensions 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 closure

Inside the container the builder reuses the standard phpizeconfiguremake 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.

Debian Packages

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 deb

To 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 deb

The 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.

macOS Builds

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-config

The 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 closure

Windows Builds

Windows 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: zlib

Use 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.

Options

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.

Package Names

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

GitHub Actions Example

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

About

Rust CLI for building distributable PHP extension binaries across Linux and macOS

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors