From 9b61dd987e8bf577bd6cbc1861f0a61ca027bd0a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 19:35:27 +0000 Subject: [PATCH 1/2] feat: support PHP 8.4 and 8.5 in parallel (and install on 8.6 nightly) z-engine now bundles generated engine definitions for both supported minors and selects the right set at boot, hiding all version-dependent header complexity from this package. Accordingly: - composer.json widens php to ^8.4 (8.4, 8.5 and 8.6 nightly all install; the engine boots only on minors z-engine has definitions for) and takes z-engine as 8.4.x-dev || 8.5.x-dev - CI runs every engine-touching job on a {8.4, 8.5} matrix, plus an experimental analysis-only job on PHP 8.6 nightly (the PHPStan layer needs no engine) - the debug-build Dockerfile handles PHP 8.5's statically linked opcache (no opcache.so to load) - README badge/requirements, AGENTS.md and CONTRIBUTING describe the parallel-support model Verified: full suite green on PHP 8.4.24 (debug build, incl. the destructive internal group) and PHP 8.5.9 against the unified z-engine branch; PHPStan, stubs:check and cs:check green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt --- .github/workflows/ci.yml | 97 ++++++++++++++++++++++++------- AGENTS.md | 10 +++- CONTRIBUTING.md | 7 ++- README.md | 8 ++- composer.json | 4 +- tools/docker/php-debug.Dockerfile | 12 ++-- 6 files changed, 101 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 312ec3f..8bc1d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,22 +6,25 @@ on: - main pull_request: -env: - # Engine structures are version-specific; this package tracks one PHP minor at a time, - # exactly like the z-engine branch it builds on. - PHP_MINOR: '8.4' - +# Engine structures are version-specific, but z-engine bundles generated +# definitions for every supported minor and selects the right set at boot - +# so this package runs on PHP 8.4 and 8.5 in parallel, and every +# engine-touching job runs against both minors. jobs: tests: - name: Tests (release, NTS) + name: Tests (release, NTS, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi ini-values: ffi.enable=1, zend.assertions=1, opcache.jit=off coverage: none @@ -32,15 +35,19 @@ jobs: run: composer test tests-opcache: - name: Tests (opcache, save_comments=0) + name: Tests (opcache, save_comments=0, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi # save_comments=0 is a normal production setting that makes doc comments # unavailable at runtime; this job is the permanent guard that nothing in the @@ -63,15 +70,19 @@ jobs: composer test:preload benchmarks-smoke: - name: Benchmarks (smoke) + name: Benchmarks (smoke, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi ini-values: ffi.enable=1, zend.assertions=1, opcache.jit=off coverage: none @@ -84,8 +95,12 @@ jobs: run: composer bench:smoke analysis-tests: - name: PHPStan extension tests (FFI disabled) + name: PHPStan extension tests (FFI disabled, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 @@ -96,7 +111,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} ini-values: ffi.enable=0 coverage: none @@ -105,16 +120,46 @@ jobs: - name: Run the analysis suite run: composer test:analysis + # The analysis layer needs no engine, so it is the one thing that can (and should) + # already run on the NEXT minor: an early warning for parser/reflection changes, not + # a support statement. The engine-touching jobs stay off nightly until z-engine + # bundles definitions for it. + analysis-tests-nightly: + name: PHPStan extension tests (PHP 8.6 nightly, experimental) + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v7 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.6' + ini-values: ffi.enable=0 + coverage: none + + # z-engine's platform constraint stops at the minors it has definitions for; the + # analysis suite never boots it, so installing past the constraint is safe here. + - name: Install dependencies (ignore the PHP platform requirement) + run: composer install --no-interaction --no-progress --ignore-platform-req=php+ + + - name: Run the analysis suite + run: composer test:analysis + generated-stubs: - name: Generated stubs are up to date + name: Generated stubs are up to date (PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi ini-values: ffi.enable=1 coverage: none @@ -123,13 +168,17 @@ jobs: # The project analyses itself with stubs its own generator produced. Regenerating here # makes a generator change that stops matching them a failing build rather than a stale - # file nobody re-runs. + # file nobody re-runs. Running it per minor proves the stubs are version-independent. - name: Regenerate and compare run: composer stubs:check tests-internal-debug: - name: Internal group (debug build) + name: Internal group (debug build, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] # The destructive eviction tests run destroy_zend_class() over a copied class entry while # its template is still live. On a release build a copy that released a block the template # still owns usually survives the test and crashes somewhere unrelated later; on a @@ -149,7 +198,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi coverage: none @@ -159,7 +208,7 @@ jobs: run: | docker build \ -f tools/docker/php-debug.Dockerfile \ - --build-arg "PHP_VERSION=${PHP_MINOR}" \ + --build-arg "PHP_VERSION=${{ matrix.php }}" \ -t generics-php:debug . - name: Confirm the image really is a debug build @@ -188,15 +237,19 @@ jobs: || { echo "::error::${skipped} destructive test(s) skipped - the debug container cannot reach the engine"; exit 1; } static-analysis: - name: PHPStan (level max) + name: PHPStan (level max, PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] steps: - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: ${{ matrix.php }} extensions: ffi ini-values: ffi.enable=1 coverage: none @@ -215,7 +268,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.PHP_MINOR }} + php-version: '8.5' coverage: none - uses: ramsey/composer-install@v4 diff --git a/AGENTS.md b/AGENTS.md index b29e56a..dbcb32f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,9 +79,13 @@ needs a reason, not a refactor. Each is explained in full in the README. ## 1. Version matching is still non-negotiable -Engine struct layouts are version-specific. This package tracks **one PHP minor at a time**, the same -one the z-engine branch it depends on tracks. Never run the suite against a different minor, and never -try to defeat `Core::init()`'s guard. +Engine struct layouts are version-specific. z-engine hides that complexity: it bundles generated +definitions for **every minor it supports (currently PHP 8.4 and 8.5, in parallel)** and selects +the set matching the running interpreter at boot. This package therefore runs on both minors from +a single install — CI exercises the whole matrix — but the rule is unchanged: never run the suite +on a minor z-engine has no definitions for, and never try to defeat `Core::init()`'s guard. +PHP 8.6 nightly may `composer install` (the analysis layer works there), but nothing that touches +the engine is supported until z-engine ships 8.6 definitions. Running anything that touches the engine requires: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2e5416..978327b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,9 @@ about the engine itself. ## Before you start -- **Match your PHP version to the branch.** Engine struct layouts are version-specific; this package - tracks one PHP minor at a time. +- **Run only supported PHP minors.** Engine struct layouts are version-specific; z-engine bundles + definitions for PHP 8.4 and 8.5 and picks the right set at boot, so this package supports both + minors in parallel. Anything newer (8.6 nightly) installs but cannot touch the engine yet. - **Develop against a debug build** (`--enable-debug`, FFI on). It turns silent memory corruption into loud assertion failures. - FFI must be enabled (`ffi.enable=1`) and the JIT disabled (`opcache.jit=off`). @@ -48,7 +49,7 @@ PHP build, so you can work on the PHPStan extension anywhere. ## Pull request checklist -- [ ] `composer test` passes on the matching PHP minor +- [ ] `composer test` passes on every supported PHP minor (CI runs 8.4 and 8.5) - [ ] `composer phpstan` and `composer cs:check` are green - [ ] `composer stubs:check` is green if you touched a template or the generator - [ ] tests added or updated, with unique specialized class names diff --git a/README.md b/README.md index 0f938bf..602fdb6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ real class whose `int` is enforced by the Zend Engine itself — the same `TypeError` you get from a hand-written class, on a class that did not exist a microsecond ago.** -[![PHP Version](https://img.shields.io/badge/php-8.4-8892BF.svg)](https://www.php.net/) +[![PHP Version](https://img.shields.io/badge/php-8.4%20%7C%208.5-8892BF.svg)](https://www.php.net/) [![License](https://img.shields.io/packagist/l/lisachenko/userland-php-generics.svg)](LICENSE) [![PHPStan](https://img.shields.io/badge/PHPStan-level%20max-brightgreen.svg)](https://phpstan.org/) @@ -208,10 +208,12 @@ autoloader is a side effect a library should not impose. ## Requirements and installation -- PHP 8.4, NTS, x86-64 +- PHP 8.4 or 8.5 (supported in parallel), NTS, x86-64 - `ext-ffi` with `ffi.enable=1` - `opcache.jit=off` (the JIT rewrites the executor internals z-engine hooks into) -- [`lisachenko/z-engine`](https://github.com/lisachenko/z-engine) on the matching branch +- [`lisachenko/z-engine`](https://github.com/lisachenko/z-engine) — one install covers both + minors: it bundles generated engine definitions per PHP version and selects the right set + at boot, so this package never deals with version-dependent headers itself ```bash composer require --dev lisachenko/userland-php-generics diff --git a/composer.json b/composer.json index 646fae2..704f650 100644 --- a/composer.json +++ b/composer.json @@ -21,9 +21,9 @@ } ], "require": { - "php": "~8.4.0", + "php": "^8.4", "ext-ffi": "*", - "lisachenko/z-engine": "~8.4.0" + "lisachenko/z-engine": "8.4.x-dev || 8.5.x-dev" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.75", diff --git a/tools/docker/php-debug.Dockerfile b/tools/docker/php-debug.Dockerfile index 8419c51..55b6594 100644 --- a/tools/docker/php-debug.Dockerfile +++ b/tools/docker/php-debug.Dockerfile @@ -67,14 +67,18 @@ RUN rm -f /usr/local/etc/php/conf.d/docker-php-ext-*.ini # explicitly in the child processes that exercise shared memory - which is where # a debug build turns the shared-memory corruption of issue #41 into a loud # zend_function_dtor() assertion instead of a silent wrong result. -# opcache is a shared zend_extension here (the debug build installs its own -# ABI-tagged extension directory), so it is loaded by absolute path - a bare +# Up to PHP 8.4 opcache is a shared zend_extension (the debug build installs +# its own ABI-tagged extension directory), loaded by absolute path - a bare # file name resolves against whatever extension_dir the base image left behind. +# Since PHP 8.5 opcache is linked statically into the binary: no opcache.so +# exists and no zend_extension line is needed. The sanity check below asserts +# the extension is present either way. RUN set -eux; \ extension_dir="$(php-config --extension-dir)"; \ - test -f "${extension_dir}/opcache.so"; \ { \ - echo "zend_extension=${extension_dir}/opcache.so"; \ + if [ -f "${extension_dir}/opcache.so" ]; then \ + echo "zend_extension=${extension_dir}/opcache.so"; \ + fi; \ echo 'ffi.enable=1'; \ echo 'zend.assertions=1'; \ echo 'report_memleaks=1'; \ From 943f17066782c4aa0b61e7b080dc7da7174affa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 19:48:48 +0000 Subject: [PATCH 2/2] docs: parallel support comes from composer resolving the z-engine line per minor Per review on z-engine#142, z-engine keeps its one-minor-per-release-line model; the 8.4.x-dev || 8.5.x-dev constraint is what makes this package run on PHP 8.4 and 8.5 in parallel (composer resolves the 8.4 branch on 8.4 and master on 8.5). Reword README, AGENTS, CONTRIBUTING and the CI header to describe that mechanism instead of a single dual-version z-engine install. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt --- .github/workflows/ci.yml | 9 +++++---- AGENTS.md | 15 ++++++++------- CONTRIBUTING.md | 7 ++++--- README.md | 7 ++++--- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc1d13..ca4be7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,11 @@ on: - main pull_request: -# Engine structures are version-specific, but z-engine bundles generated -# definitions for every supported minor and selects the right set at boot - -# so this package runs on PHP 8.4 and 8.5 in parallel, and every -# engine-touching job runs against both minors. +# Engine structures are version-specific and z-engine tracks one PHP minor per +# release line, but the "8.4.x-dev || 8.5.x-dev" constraint lets composer +# resolve the line matching each runner's PHP - so this package runs on +# PHP 8.4 and 8.5 in parallel, and every engine-touching job runs against +# both minors. jobs: tests: name: Tests (release, NTS, PHP ${{ matrix.php }}) diff --git a/AGENTS.md b/AGENTS.md index dbcb32f..5947dca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,13 +79,14 @@ needs a reason, not a refactor. Each is explained in full in the README. ## 1. Version matching is still non-negotiable -Engine struct layouts are version-specific. z-engine hides that complexity: it bundles generated -definitions for **every minor it supports (currently PHP 8.4 and 8.5, in parallel)** and selects -the set matching the running interpreter at boot. This package therefore runs on both minors from -a single install — CI exercises the whole matrix — but the rule is unchanged: never run the suite -on a minor z-engine has no definitions for, and never try to defeat `Core::init()`'s guard. -PHP 8.6 nightly may `composer install` (the analysis layer works there), but nothing that touches -the engine is supported until z-engine ships 8.6 definitions. +Engine struct layouts are version-specific, and z-engine tracks **one PHP minor per release +line** (`8.4` branch for PHP 8.4, `master` for PHP 8.5). This package supports **PHP 8.4 and +8.5 in parallel** by declaring z-engine as `8.4.x-dev || 8.5.x-dev`, so composer resolves the +line matching the running platform — z-engine owns all version-dependent header complexity, +and CI exercises the whole matrix. The rule is unchanged: never run the suite on a minor the +resolved z-engine does not target, and never try to defeat `Core::init()`'s guard. +PHP 8.6 nightly may `composer install` (the analysis layer works there), but nothing that +touches the engine is supported until a z-engine line for 8.6 exists. Running anything that touches the engine requires: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 978327b..0086c03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,10 @@ about the engine itself. ## Before you start -- **Run only supported PHP minors.** Engine struct layouts are version-specific; z-engine bundles - definitions for PHP 8.4 and 8.5 and picks the right set at boot, so this package supports both - minors in parallel. Anything newer (8.6 nightly) installs but cannot touch the engine yet. +- **Run only supported PHP minors.** Engine struct layouts are version-specific; z-engine tracks + one minor per release line, and the `8.4.x-dev || 8.5.x-dev` constraint makes composer resolve + the line matching your PHP, so this package supports both minors in parallel. Anything newer + (8.6 nightly) installs but cannot touch the engine yet. - **Develop against a debug build** (`--enable-debug`, FFI on). It turns silent memory corruption into loud assertion failures. - FFI must be enabled (`ffi.enable=1`) and the JIT disabled (`opcache.jit=off`). diff --git a/README.md b/README.md index 602fdb6..ed0ea93 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,10 @@ autoloader is a side effect a library should not impose. - PHP 8.4 or 8.5 (supported in parallel), NTS, x86-64 - `ext-ffi` with `ffi.enable=1` - `opcache.jit=off` (the JIT rewrites the executor internals z-engine hooks into) -- [`lisachenko/z-engine`](https://github.com/lisachenko/z-engine) — one install covers both - minors: it bundles generated engine definitions per PHP version and selects the right set - at boot, so this package never deals with version-dependent headers itself +- [`lisachenko/z-engine`](https://github.com/lisachenko/z-engine) — the + `8.4.x-dev || 8.5.x-dev` constraint lets composer resolve the z-engine line matching your + PHP minor (the `8.4` branch on PHP 8.4, `master` on PHP 8.5); z-engine owns all + version-dependent header complexity, so this package never deals with it ```bash composer require --dev lisachenko/userland-php-generics