Skip to content
ephpmPublic

About

Prebuilt static PHP embed SDKs (libphp.a / php8embed.lib + headers) for ePHPm, per OS/arch/PHP version

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

141 Commits

Folders and files

Repository files navigation

php-sdk

Pre-built PHP embed SAPI static libraries (libphp.a / php8embed.lib) plus headers, for the OS/arch/PHP-version combinations ePHPm supports.

Built with static-php-cli — specifically the luthermonson/static-php-cli fork, pinned by SPC_VERSION in .github/workflows/build.yml. Every SDK is a spc build --build-embed output: a static libphp.a, never a shared libphp.so.

What this builds

Platform Tarball suffix Output
Linux x86_64 (glibc, floor 2.28) -linux-x86_64-gnu lib/libphp.a + headers, ZTS
Linux aarch64 (glibc, floor 2.28) -linux-aarch64-gnu lib/libphp.a + headers, ZTS
Linux x86_64 (musl, fully static) -linux-x86_64 lib/libphp.a + headers, ZTS
Linux aarch64 (musl, fully static) -linux-aarch64 lib/libphp.a + headers, ZTS
macOS aarch64 -macos-aarch64 lib/libphp.a + headers, ZTS
Windows x86_64 -windows-x86_64 lib/php8embed.lib + dep .libs + headers, ZTS
Linux x86_64 (glibc, NTS) -linux-x86_64-gnu-nts lib/libphp.a + headers, non-thread-safe
Linux aarch64 (glibc, NTS) -linux-aarch64-gnu-nts lib/libphp.a + headers, non-thread-safe
Linux x86_64 (glibc, NTS, shared embed) -linux-x86_64-gnu-nts-shared lib/libphp.so (dependencies linked in) + headers, non-thread-safe
Linux aarch64 (glibc, NTS, shared embed) -linux-aarch64-gnu-nts-shared lib/libphp.so (dependencies linked in) + headers, non-thread-safe
Windows x86_64 (experimental, clang-cl + TAILCALL VM) -windows-x86_64-clang lib/php8embed.lib + dep .libs + headers, ZTS
Windows x86_64 (shared engine, official build) -windows-x86_64-shared bin/php8ts.dll + lib/php8ts.lib + lib/php8embed.lib + headers + official bin/ext/php_*.dll, ZTS

The glibc floor is 2.28 (built on AlmaLinux 8 with gcc-toolset-13), so a consumer binary linked against these runs on RHEL/Alma 8, Ubuntu 20.04+, Debian 10+, Amazon Linux 2023 and Fedora 40+.

ZTS and NTS

ZTS is the default and is unsuffixed. ePHPm is a threaded server and links the ZTS SDK; renaming those assets breaks it. The -gnu-nts variants exist for consumers that cannot use a thread-safe build.

Detect which one you have from include/php/main/php_config.h: thread-safe builds define ZTS 1, non-thread-safe builds do not define ZTS at all.

NTS tarballs:

  • are Linux-glibc only (x86_64 and aarch64),
  • are not part of a platform=all build and do not gate release completeness — a failed NTS build never blocks a release,
  • carry ffi in addition to the shared extension set (see below),
  • are kept current only for the minors listed under minors_nts in versions.json.

The -gnu-nts-shared variants are the same NTS builds packaged as a shared embed: lib/libphp.so with the dependency libraries linked in, for consumers that must dlopen() or link -lphp against a shared libphp (e.g. PAM). The static libphp.a variants stay the default for everything else.

Experimental: Windows clang-cl / TAILCALL VM lane

-windows-x86_64-clang is an additive, explicitly-dispatched lane (platform=windows-x86_64-clang) that builds PHP itself with clang-cl and hand-defines HAVE_PRESERVE_NONE, so PHP 8.5+ selects the ZEND_VM_KIND_TAILCALL interpreter instead of the slow ZEND_VM_KIND_CALL every MSVC build gets. Dependencies stay MSVC-built; clang-cl is MSVC-ABI-compatible and the resulting php8embed.lib links into MSVC-target consumers unchanged. The build hard-fails unless the compiled VM kind is verifiably TAILCALL.

Status: experimental, but a measured performance win with the pinned toolchain. On PHP 8.5.7 CLI (Ryzen 9 5950X, 2026-08, best-of-5 hrtime loops), LLVM 22.1.8 clang-cl + TAILCALL runs the dispatch-bound int loop 1.7x faster than the MSVC lane's CALL VM (8.4 ms vs 14.2 ms) and wins on string-append and function-call loops too; the mixed reference loop drops from 4.4 ms to 2.7 ms — Windows lands within ~12% of the same loop on Linux (HYBRID VM). Two sharp edges, which is why this stays experimental:

  • The toolchain must be the pinned LLVM release. The VS-bundled clang-cl 19.1.5 generates Zend-engine code ~45–90% slower than MSVC — enough to swamp the VM-kind win and end up slower than the stock lane. The workflow pins LLVM_VERSION and hard-fails on mismatch.
  • The build hard-fails unless the compiled VM kind is verifiably TAILCALL (disassembled out of php8embed.lib), so it can never silently regress to the CALL VM.

It is never part of platform=all, never gates a release, and must not be moved into platforms_required until it has soaked as an opt-in lane.

macOS Zend VM kind

macOS SDKs do not ship the HYBRID VM. The macOS build uses clang (Homebrew LLVM), and clang defines __GNUC__ but fails PHP's HAVE_GCC_GLOBAL_REGS configure check, so the fast ZEND_VM_KIND_HYBRID interpreter is never selected. The result depends on the PHP minor:

macOS PHP Zend VM kind Notes
8.5+ TAILCALL (5) clang ≥ 19 musttail + preserve_none; ~HYBRID speed
8.4 CALL (1) slowest VM — no TAILCALL path before 8.5
8.3 CALL (1) slowest VM — no TAILCALL path before 8.5

HYBRID would require building PHP and its ~20 dependency libraries with real GCC on aarch64-apple-darwin. That is not a supported static-php-cli configuration: config/env.ini's [macos] section hardcodes CC=clang, and the toolchain classes only set the SPC_LINUX_DEFAULT_* vars the macOS section never reads — so SPC_TOOLCHAIN=GccNativeToolchain does not switch the macOS compiler. Wiring that up (plus getting Homebrew GCC to pass HAVE_GCC_GLOBAL_REGS for the aarch64 global registers and produce a working JIT, which the build already had to abandon Apple clang for) is a large spc change PHP upstream does not test on macOS. Until then: use macOS PHP 8.5 for CPU-bound workloads. The build logs the selected VM kind (macOS "Record Zend VM kind" step) and warns when a pre-8.5 macOS SDK lands on CALL.

Windows shared-engine SDK (-windows-x86_64-shared)

The default Windows SDK links PHP statically, and a static PHP cannot load extension DLLs: php_xdebug.dll and every other PHP extension import the engine from php8ts.dll, which a static build does not have. The -shared tarball is the alternative for consumers that need to load extensions (Xdebug first of all).

It is not compiled here. .github/workflows/windows-shared.yml runs tools/windows-shared/package.ps1, which repackages the official windows.php.net ZTS x64 build of the same release (the zip already contains a small static php8embed.lib whose objects import the engine through php8ts.lib; the matching devel pack carries php8ts.lib and the headers). Using the official engine means the official extension DLLs and xdebug.org's Xdebug DLLs are ABI-matched to it by construction. Every download is sha256-verified against tools/windows-shared/checksums.txt or the live windows.php.net checksum list, and the lane refuses to package unless a smoke test — the official php.exe run from a directory holding only the packaged bin/ payload — loads every extension of the static SDK's set plus Xdebug.

include/php/             headers (devel pack)
lib/php8embed.lib        embed SAPI, static, imports the engine
lib/php8ts.lib           import library for php8ts.dll
bin/php8ts.dll           the engine: ship it next to the consuming .exe
bin/*.dll                engine + extension dependency DLLs (OpenSSL, ICU, ...)
bin/ext/php_*.dll        official shared extensions
ephpm-sdk.json           php_version, php_api_no, zts, crt, and `preload`
LICENSE-PHP.txt, readme-redist-bins.txt, sbom/

Extensions the static SDK compiles in but the official build ships as DLLs (mbstring, openssl, curl, intl, pdo_mysql, ... and opcache on 8.3 and 8.4) are listed, in a dependency-safe load order, under preload in ephpm-sdk.json; a consumer loads them at startup to get the same extension set as the static SDK. The VS toolset is the one windows.php.net uses for the minor (vs16 for 8.3, vs17 for 8.4+), recorded as crt.

The same run publishes ephpm-ext-<ver>-windows-x86_64.tar.gz to the ext-<ver> release: php_xdebug.dll plus a manifest.json in the Linux catalog's schema (with "libc": "msvc", "os": "windows").

gh workflow run windows-shared.yml -f php_version=8.5.7

Older releases move to windows.php.net's archives/, which publishes no checksum list — pin their hashes in checksums.txt (the script prints the hash to pin when one is missing). A new Xdebug version needs its three php_xdebug-<ver>-<minor>-ts-<vs>-x86_64.dll hashes from xdebug.org added there too.

Tarball layout

lib/                       libphp.a (or php8embed.lib) + every static dep archive
include/php/
  main/                    includes php_config.h (ZTS marker) and php_version.h
  Zend/
  TSRM/
  ext/
  sapi/embed/php_embed.h
bin/php-config             Linux and macOS only
THIRD-PARTY-NOTICES.txt

Assets are named:

php-sdk-{php_version}-{os}-{arch}[-gnu][-nts[-shared]][-clang|-shared].tar.gz

bin/php-config

A relocatable POSIX-sh shim, generated by tools/mk-sdk-metadata.sh at package time. It resolves its own prefix at run time, so the tarball can be extracted anywhere. It exists because build scripts that expect a system PHP (php-config --includes) run it unconditionally.

Supported: --prefix, --includes, --include-dir, --ldflags, --libs, --extension-dir, --version, --vernum, --php-sapis, --help.

Deliberately refused, with a message and a non-zero exit, rather than answered with a plausible lie:

  • --php-binary — this SDK ships no PHP executable.
  • --configure-options — static-php-cli strips the configure line from the build.

--libs enumerates the static archives in lib/. It is a starting point, not a verified link line: whole-archive and symbol-ordering decisions belong to the consumer. See ePHPm's crates/ephpm/build.rs for a working static link against these tarballs, including the -Wl,--export-dynamic that lets extension=-loaded .so files resolve zend_* symbols against the host binary.

No php-config is generated on Windows — it is a shell script and nothing in the MSVC toolchain consumes one.

Extensions

Compiled into libphp.a on Linux and macOS:

bcmath, bz2, calendar, ctype, curl, dom, exif, fileinfo, filter, ftp, gd,
gettext, gmp, hash, iconv, intl, mbregex, mbstring, mysqli, mysqlnd, opcache,
openssl, pcntl, pcre, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, phar,
posix, session, shmop, simplexml, soap, sockets, sodium, sqlite3, sysvmsg,
sysvsem, sysvshm, tokenizer, xml, xmlreader, xmlwriter, xsl, zip, zlib

The NTS variants additionally include ffi.

Windows drops the Unix-only entries (pcntl, posix, sysv*), plus gmp, pgsql and pdo_pgsql, which static-php-cli cannot build statically on Windows today. It also does not yet carry shmop or sockets — PHP supports both there, but neither has been validated through spc's Windows toolchain from this repo.

ffi is not in the shared extension set on purpose. static-php-cli's ext-ffi passes --enable-zend-signals, and extension arguments land after the target arguments on the configure line, so it would override the --disable-zend-signals that spc emits for ZTS builds — silently changing signal behavior in the SDK ePHPm links. NTS builds emit no --disable-zend-signals, so ffi rides along there.

Loadable shared ZTS extensions (igbinary, msgpack, apcu, redis, mongodb) are a separate artifact built by .github/workflows/extensions.yml and published under ext-<version> tags. That catalog is ZTS-only by design: Debian and Sury publish no ZTS builds, which is the gap it fills. There is no NTS catalog. Windows has a catalog entry for Xdebug only (ephpm-ext-<ver>-windows-x86_64.tar.gz, from windows-shared.yml); it loads only into a binary linked against the -windows-x86_64-shared SDK.

Building

# full release
gh workflow run build.yml -f php_version=8.5.2

# one platform (uploads to the existing release, leaves other assets alone)
gh workflow run build.yml -f php_version=8.5.2 -f platform=linux-aarch64-gnu

# non-thread-safe variant
gh workflow run build.yml -f php_version=8.4.23 -f platform=linux-x86_64-gnu-nts

Pre-release (beta/RC) builds

static-php-cli resolves php-src from php.net's GA-only release feed, and its --with-php accepts only numeric x.y/x.y.z. So a beta/RC cannot go through the normal path. Set php_src_url to the upstream pre-release tarball (the release manager's QA dir, downloads.php.net/~<RM>/); every OS leg then fetches php-src from that URL and hands it to spc via --custom-url/ --custom-local, while --with-php gets the numeric major.minor. The resulting release tag (v<php_version>) is marked prerelease.

gh workflow run build.yml \
  -f php_version=8.6.0beta1 -f platform=all \
  -f php_src_url=https://downloads.php.net/~svpernova09/php-8.6.0beta1.tar.gz

Betas are not added to versions.json minors (watch-php.yml polls the GA feed and would error on a non-GA minor) and never gate a stable release. This lane is experimental: no spc release has explicit 8.6 support — it builds because spc has no upper-version cap and reads the real PHP_VERSION_ID from the supplied source. The Windows embed leg for a brand-new beta is unproven.

Any dispatch from a branch other than main MUST set release_tag_suffix. On 2026-07-13 a validation build from a side branch uploaded to the plain v8.5.7 tag and overwrote four production assets. The input exists to make that impossible:

gh workflow run build.yml --ref my-branch \
  -f php_version=8.5.7 -f platform=linux-x86_64-gnu -f release_tag_suffix=-mytest

watch-php.yml polls php.net every 6 hours, mirrors new php-src tarballs to the php-sources release tag, and dispatches builds for anything missing.

Dependency mirror

Builds do not fetch PHP or its GNU dependencies from upstream. Both come from this repo's own php-sources release tag, fetched with plain curl and handed to static-php-cli as file:// custom URLs.

Asset on php-sources Populated by Consumed by
php-X.Y.Z.tar.gz watch-php.yml build.yml, extensions.yml
libiconv-*, gettext-*, ncurses-*, libunistring-*, libidn2-* mirror-deps.yml build.yml, extensions.yml
gnu-deps-manifest.txt, SHA256SUMS mirror-deps.yml build.yml, extensions.yml

Those libraries resolve through static-php-cli type: filelist sources pointed at https://ftpmirror.gnu.org/gnu/<pkg>/. That host is a redirector to volunteer backend mirrors and returns HTTP errors when it picks a bad one. None of the five has a source-mirror fallback in spc, so a single bad backend fails the whole spc download and therefore the build. That took every scheduled build in this repo down from 2026-08-06 onward.

gmp is also GNU-hosted but is not in the mirror: it is the one GNU dependency with its own source-mirror fallback in spc, so mirror-deps.yml resolved it from there and correctly did not select it. The mirrored set is whatever spc actually resolved from ftpmirror.gnu.org, not a fixed list.

Refresh the mirror when you want newer dependency versions:

gh workflow run mirror-deps.yml                      # resolve + upload
gh workflow run mirror-deps.yml -f dry_run=true      # resolve + print only

mirror-deps.yml runs on a GitHub-hosted runner (different egress from the self-hosted fleet) but resolves inside the same almalinux:8 container with the same SPC_TARGET, so it picks exactly the artifacts the fleet would have. The set is derived from spc's own downloads/.cache.json — every artifact whose source URL is on ftpmirror.gnu.org.

Note the tradeoff: filelist sources normally track the newest tarball in the directory index on every build. Once mirrored, builds pin to whatever was current when mirror-deps.yml last ran. That is the same tradeoff php-src already makes, and it stops builds drifting underneath us.

If the manifest is missing from the release tag, builds log a warning and fall back to fetching from upstream GNU mirrors — the pre-mirror behaviour, and the known failure mode.

Consuming

ePHPm's release workflow:

- name: Download PHP SDK
  run: |
    gh release download v${{ matrix.php }} \
      --repo ephpm/php-sdk \
      --pattern "php-sdk-*-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" \
      --output php-sdk.tar.gz
    mkdir -p php-sdk && tar xzf php-sdk.tar.gz -C php-sdk

then builds with PHP_SDK_PATH=php-sdk cargo build --release.

Licensing

The MIT license in this repository covers the workflows and scripts here. It does not cover the contents of the published tarballs: those are an aggregate of PHP (PHP License 3.01 / Zend Engine License 2.00) and roughly twenty upstream libraries, each under its own terms — including a bundled libstdc++.a carrying the GCC Runtime Library Exception.

Every tarball ships a THIRD-PARTY-NOTICES.txt listing the components, their SPDX identifiers, and where to get the full license texts. Read it before redistributing anything built from this SDK.

The -windows-x86_64-shared tarball is the exception: it is the official windows.php.net binary distribution repackaged unmodified, so it carries that distribution's own notices instead — LICENSE-PHP.txt (PHP's license.txt), readme-redist-bins.txt (the bundled third-party DLLs and their licenses) and the upstream SBOM under sbom/. Xdebug (ephpm-ext-<ver>-windows-x86_64.tar.gz) is under the Xdebug License.

About

Prebuilt static PHP embed SDKs (libphp.a / php8embed.lib + headers) for ePHPm, per OS/arch/PHP version

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages