From 31426edc127f191c4e4d8e68a9b5b3b4cc564ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 08:36:00 +0200 Subject: [PATCH 01/10] PHP 8.6: Restore PIE and memcached support # Conflicts: # php/Dockerfile-8.6 # php/Dockerfile-8.6-cli --- CLAUDE.md | 5 +++-- README.md | 4 ++-- build-notes.md | 10 ++++++++++ php/Dockerfile-8.6 | 8 ++++---- php/Dockerfile-8.6-cli | 8 ++++---- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2081782..c0291d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ The project is maintained primarily to serve the maintainer's own development wo All build scripts follow the same four-phase pattern (pull → build → test → push), each phase skippable via ENV flags: `NO_PULL=1`, `NO_BUILD=1`, `NO_TEST=1`, `NO_PUSH=1`, `NO_ASSETS=1`. ```bash -./build-php.sh # build all current PHP versions (8.2–8.5, apache + cli variants) +./build-php.sh # build all current PHP versions (8.2–8.5 + 8.6 RC, apache + cli variants) ./php/build-php-8.5.sh # build a single PHP version (apache + debug variants) ./php/build-php-8.5-cli.sh # build a single CLI variant ./php/legacy/build-php-legacy.sh # legacy PHP versions (many disabled — Debian repos shut down) @@ -27,7 +27,7 @@ Common examples: `NO_PUSH=1 ./build-php.sh` (local build only), `NO_PULL=1 NO_BU ## Tests ```bash -./test-php.sh # run test suite inside every current PHP image (8.2–8.5, apache + cli) +./test-php.sh # run test suite inside every current PHP image (8.2–8.5 + 8.6 RC, apache + cli) ``` Tests live in `tools/tests/` (Nette Tester, one `*Test.php` per PHP extension in `tools/tests/Extensions/`). To run in a single image: @@ -58,6 +58,7 @@ docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 c - PHP: the `-rc` suffix is hard-coded in the script, combined with the pre-release label (e.g. alpha/RC name) which can be read programmatically from the PHP binary itself. - MariaDB: RC builds have no additional sub-versions. Based on upstream's observed release mechanism, when a release channel goes stable the patch version is bumped to `2` and the `-rc` suffix is dropped. - During a PHP RC phase, components without upstream support for the new version (typically Xdebug → the `-debug` variant, PIE, individual extensions) are temporarily left out or commented in the Dockerfiles. They are re-enabled as soon as a *functional* upstream version exists — a working dev/beta/dev-master is enough, don't wait for a final release. Track the remaining work in a GitHub issue cross-linked with the version's PR. +- PHP 8.6 RC requires PIE 1.5+; refresh `php/build/pie.phar` with `prepare-assets.sh` before a build using `NO_PULL=1` if the local asset is older. See [build-notes.md](build-notes.md#php-86-rc). - All images are `linux/amd64` only. Building for ARM would require hardware the maintainer doesn't have (and renting a VPS for it is not planned); this will change only if the maintainer's own environment moves to ARM. ## Defaults baked into images diff --git a/README.md b/README.md index 8e12091..f8012c4 100644 --- a/README.md +++ b/README.md @@ -162,8 +162,8 @@ The pre-release of PHP 8.6 images have the `-rc` suffix, example: - `jakubboucek/lamp-devstack-php:8.6-rc` - `jakubboucek/lamp-devstack-php:8.6-rc-cli` -> Note: The 8.6 pre-release images don't contain the `memcached` extension yet and have no `-debug` variant — -> upstream support (PIE, Xdebug) for PHP 8.6 is not available yet. +> The 8.6 pre-release images include `memcached` from its development branch, installed with PIE 1.5 or newer. +> A `-debug` variant is not available yet. ### Using MariaDB MariaDB server starts at the same time as the web server. diff --git a/build-notes.md b/build-notes.md index 48d0576..d8865b8 100644 --- a/build-notes.md +++ b/build-notes.md @@ -11,6 +11,16 @@ To build only specific PHP version run only the target script on [`php` folder]( To build older PHP versions run [`php/legacy/build-php-legacy.sh`](php/legacy/build-php-legacy.sh). +### PHP 8.6 RC + +PHP 8.6 requires PIE 1.5 or newer ([upstream support](https://github.com/php/pie/pull/667)). +Run `./prepare-assets.sh` before rebuilding with `NO_PULL=1` if `php/build/pie.phar` +was downloaded before that release. The normal build flow downloads the latest stable PIE. + +Memcached uses `dev-master` for the [PHP 8.6 API fixes](https://github.com/php-memcached-dev/php-memcached/pull/580). +The stable 3.4.0 release does not compile on PHP 8.6. Return to stable package selection +when a release containing those fixes is available. + ## MariaDB Run [`build-mariadb.sh`](build-mariadb.sh) script to build all of MariaDB versions. diff --git a/php/Dockerfile-8.6 b/php/Dockerfile-8.6 index d9ad91b..e296952 100644 --- a/php/Dockerfile-8.6 +++ b/php/Dockerfile-8.6 @@ -25,8 +25,8 @@ ARG EXTENSION_DEV_DEPS=" \ libgmp-dev \ libicu-dev \ libjpeg62-turbo-dev \ -# libmemcached-dev \ libonig-dev \ + libmemcached-dev \ libpng-dev \ libsodium-dev \ libssl-dev \ @@ -50,8 +50,8 @@ ARG EXTENSION_RUNTIME_DEPS=" \ libicu76 \ libjpeg62-turbo \ liblzma5 \ -# libmemcached11t64 \ libonig5 \ + libmemcached11t64 \ libpng16-16t64 \ librav1e0.7 \ libsharpyuv0 \ @@ -69,6 +69,7 @@ ARG EXTENSION_RUNTIME_DEPS=" \ zlib1g \ " +# PHP 8.6 API compatibility fixes are currently available in memcached dev-master only. # OS binaries install && update critical binaries RUN set -eux; \ apt-get update; \ @@ -104,8 +105,7 @@ RUN set -eux; \ sysvshm \ xsl \ zip; \ -# Memcached disabled: PIE does not support PHP 8.6 yet (requires PHP <= 8.5) -# pie install php-memcached/php-memcached; \ + pie install php-memcached/php-memcached:dev-master; \ a2enmod \ expires \ headers \ diff --git a/php/Dockerfile-8.6-cli b/php/Dockerfile-8.6-cli index 5179739..f7eb12b 100644 --- a/php/Dockerfile-8.6-cli +++ b/php/Dockerfile-8.6-cli @@ -25,8 +25,8 @@ ARG EXTENSION_DEV_DEPS=" \ libgmp-dev \ libicu-dev \ libjpeg62-turbo-dev \ -# libmemcached-dev \ libonig-dev \ + libmemcached-dev \ libpng-dev \ libsodium-dev \ libssl-dev \ @@ -50,8 +50,8 @@ ARG EXTENSION_RUNTIME_DEPS=" \ libicu76 \ libjpeg62-turbo \ liblzma5 \ -# libmemcached11t64 \ libonig5 \ + libmemcached11t64 \ libpng16-16t64 \ librav1e0.7 \ libsharpyuv0 \ @@ -69,6 +69,7 @@ ARG EXTENSION_RUNTIME_DEPS=" \ zlib1g \ " +# PHP 8.6 API compatibility fixes are currently available in memcached dev-master only. # OS binaries install && update critical binaries RUN set -eux; \ apt-get update; \ @@ -104,8 +105,7 @@ RUN set -eux; \ sysvshm \ xsl \ zip; \ -# Memcached disabled: PIE does not support PHP 8.6 yet (requires PHP <= 8.5) -# pie install php-memcached/php-memcached; \ + pie install php-memcached/php-memcached:dev-master; \ apt-mark manual ${EXTENSION_RUNTIME_DEPS}; \ apt-get purge -y --auto-remove ${EXTENSION_DEV_DEPS}; \ apt-get clean -y && \ From 1a812a4257bc2eb3a986b9a3bf8cce5eb7e62bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 08:43:37 +0200 Subject: [PATCH 02/10] PHP 8.6: Add debug image with Xdebug and SPX --- CLAUDE.md | 4 ++-- README.md | 5 +++-- build-notes.md | 19 +++++++++++++++++++ php/Dockerfile-8.6-debug | 37 +++++++++++++++++++++++++++++++++++++ php/build-php-8.6.sh | 11 +++++++++++ test-php.sh | 1 + 6 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 php/Dockerfile-8.6-debug diff --git a/CLAUDE.md b/CLAUDE.md index c0291d4..c8b071d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ Common examples: `NO_PUSH=1 ./build-php.sh` (local build only), `NO_PULL=1 NO_BU ## Tests ```bash -./test-php.sh # run test suite inside every current PHP image (8.2–8.5 + 8.6 RC, apache + cli) +./test-php.sh # run test suite inside every current PHP image (8.2–8.5 + 8.6 RC, apache + cli, plus 8.6 RC debug) ``` Tests live in `tools/tests/` (Nette Tester, one `*Test.php` per PHP extension in `tools/tests/Extensions/`). To run in a single image: @@ -58,7 +58,7 @@ docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 c - PHP: the `-rc` suffix is hard-coded in the script, combined with the pre-release label (e.g. alpha/RC name) which can be read programmatically from the PHP binary itself. - MariaDB: RC builds have no additional sub-versions. Based on upstream's observed release mechanism, when a release channel goes stable the patch version is bumped to `2` and the `-rc` suffix is dropped. - During a PHP RC phase, components without upstream support for the new version (typically Xdebug → the `-debug` variant, PIE, individual extensions) are temporarily left out or commented in the Dockerfiles. They are re-enabled as soon as a *functional* upstream version exists — a working dev/beta/dev-master is enough, don't wait for a final release. Track the remaining work in a GitHub issue cross-linked with the version's PR. -- PHP 8.6 RC requires PIE 1.5+; refresh `php/build/pie.phar` with `prepare-assets.sh` before a build using `NO_PULL=1` if the local asset is older. See [build-notes.md](build-notes.md#php-86-rc). +- For PHP 8.6 RC package requirements, development versions, and temporary SPX compiler workarounds, see [build-notes.md](build-notes.md#php-86-rc). Keep these exceptions limited to the 8.6 Dockerfiles and revisit them as upstream releases land. - All images are `linux/amd64` only. Building for ARM would require hardware the maintainer doesn't have (and renting a VPS for it is not planned); this will change only if the maintainer's own environment moves to ARM. ## Defaults baked into images diff --git a/README.md b/README.md index f8012c4..b788383 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ of base images designed to cover most standard development workflows — with no - unsupported versions of **MariaDB** 12.2, 12.1, 12.0, 11.7, 11.6, 11.5, 11.3, 11.2, 11.1, 11.0, 10.10, 10.9, 10.8, 10.7, 10.5, 10.4 and 10.3 (unmaintained) - current version of **Apache** 2.4 (in non-CLI images) -- current version of **Xdebug** 3.5 (in debug images) +- **Xdebug** 3.5 in stable debug images; 3.6 alpha in PHP 8.6 RC debug images - current version of [**SPX Profiler**](https://github.com/NoiseByNorthwest/php-spx) (in debug images) - extra PHP extensions: [`bmath`](https://www.php.net/manual/en/book.bc.php), @@ -161,9 +161,10 @@ All PHP images also have alternative CLI variants, use `-cli` tag suffix, exampl The pre-release of PHP 8.6 images have the `-rc` suffix, example: - `jakubboucek/lamp-devstack-php:8.6-rc` - `jakubboucek/lamp-devstack-php:8.6-rc-cli` +- `jakubboucek/lamp-devstack-php:8.6-rc-debug` > The 8.6 pre-release images include `memcached` from its development branch, installed with PIE 1.5 or newer. -> A `-debug` variant is not available yet. +> The `-debug` variant includes Xdebug 3.6 alpha and SPX from its `v0.5` development branch. ### Using MariaDB MariaDB server starts at the same time as the web server. diff --git a/build-notes.md b/build-notes.md index d8865b8..498a3db 100644 --- a/build-notes.md +++ b/build-notes.md @@ -21,6 +21,25 @@ Memcached uses `dev-master` for the [PHP 8.6 API fixes](https://github.com/php-m The stable 3.4.0 release does not compile on PHP 8.6. Return to stable package selection when a release containing those fixes is available. +The debug variant uses `xdebug/xdebug:^3.6@alpha` and `noisebynorthwest/php-spx:0.5.x-dev`. +Xdebug 3.6.0alpha1 supports PHP 8.6; SPX support is in the `v0.5` branch, not `master`. +Stable SPX 0.4.22 and `dev-master` still declare PHP <8.6. + +The SPX build needs two compatibility flags on PHP 8.6 RC3 / Debian Trixie: +`-Wno-error=old-style-declaration` keeps a ZTS TLS declaration warning non-fatal, +and `-Dtypeof=__typeof__` allows the PHP headers' GNU type operator under SPX's strict +C11 mode. `CI=true` disables SPX's `-march=native` so the image remains portable across +amd64 CPUs. Revisit these workarounds when upstream fixes its build configuration. + +The 8.6 debug Dockerfile uses the bundled Dockerfile frontend (no external `syntax` directive). +With Docker 29.8.2 / BuildKit 0.33.1 and the containerd image store, the external frontend +resolved the published base instead of the freshly built local image. The bundled frontend +used the local base correctly; the build smoke checks explicitly require memcached in the +debug image to catch this regression. + +Upstream references: [Xdebug 3.6 alpha](https://xdebug.org/announcements/2026-09-08), +[SPX PHP 8.6 metadata](https://github.com/NoiseByNorthwest/php-spx/pull/349). + ## MariaDB Run [`build-mariadb.sh`](build-mariadb.sh) script to build all of MariaDB versions. diff --git a/php/Dockerfile-8.6-debug b/php/Dockerfile-8.6-debug new file mode 100644 index 0000000..b7a9765 --- /dev/null +++ b/php/Dockerfile-8.6-debug @@ -0,0 +1,37 @@ +# check=skip=SecretsUsedInArgOrEnv + +FROM jakubboucek/lamp-devstack-php:8.6-rc + +LABEL org.label-schema.name="PHP 8.6 (Pre-release, Apache module + Xdebug)" + +# Configure Xdebug +COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini + +# Setup PIE working directory +ARG PIE_WORKING_DIRECTORY=/tmp/.pie + +# Prevent interactive block +ARG DEBIAN_FRONTEND=noninteractive + +# PHP 8.6 support requires Xdebug 3.6 alpha and the SPX v0.5 development branch. +# SPX v0.5 uses an old-style TLS declaration under ZTS; keep the GCC warning non-fatal. +# PHP 8.6 headers use typeof; SPX forces strict C11, so use the equivalent GNU spelling. +# CI=true keeps SPX portable by disabling its -march=native optimization. +# Install Xdebug and SPX +RUN set -eux; \ + apt-get update; \ + apt-get install --no-install-recommends -y zlib1g-dev; \ + pie install xdebug/xdebug:^3.6@alpha; \ + CFLAGS="${CFLAGS:-} -Wno-error=old-style-declaration -Dtypeof=__typeof__" CI=true pie install noisebynorthwest/php-spx:0.5.x-dev; \ + apt-get remove --purge -y zlib1g-dev; \ + apt-get clean -y && \ + apt-get autoclean -y && \ + apt-get autoremove -y && \ + rm -rf /var/cache/* /var/lib/apt/lists/* /var/lib/log/* /tmp/* /var/tmp/* ~/.composer/cache/*; + +# Configure SPX +ENV PHP_SPX_ENABLED=0 +ENV PHP_SPX_HTTP_KEY="dev" +ENV PHP_SPX_HTTP_IP_WHITELIST="*" + +COPY spx.ini /usr/local/etc/php/conf.d/spx.ini diff --git a/php/build-php-8.6.sh b/php/build-php-8.6.sh index bdd504c..bf05f89 100755 --- a/php/build-php-8.6.sh +++ b/php/build-php-8.6.sh @@ -17,15 +17,23 @@ fi if [ "${NO_BUILD:-0}" -ne "1" ]; then docker build --progress plain -f ./Dockerfile-8.6 -t jakubboucek/lamp-devstack-php:8.6-rc ./ + docker build --progress plain -f ./Dockerfile-8.6-debug -t jakubboucek/lamp-devstack-php:8.6-rc-debug ./ PHP_RELEASE=$(docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo PHP_RELEASE_VERSION;") PHP_RELEASE_EXTRA=$(docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo PHP_EXTRA_VERSION;") docker tag jakubboucek/lamp-devstack-php:8.6-rc jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc + docker tag jakubboucek/lamp-devstack-php:8.6-rc-debug jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-debug docker tag jakubboucek/lamp-devstack-php:8.6-rc jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-${PHP_RELEASE_EXTRA} + docker tag jakubboucek/lamp-devstack-php:8.6-rc-debug jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-${PHP_RELEASE_EXTRA}-debug fi if [ "${NO_TEST:-0}" -ne "1" ]; then docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php --version + docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" + docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" + docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri memcached + docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri xdebug + docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri spx docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "var_export(gd_info()) . PHP_EOL;" fi @@ -33,6 +41,9 @@ if [ "${NO_PUSH:-0}" -ne "1" ]; then PHP_RELEASE=$(docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo PHP_RELEASE_VERSION;") PHP_RELEASE_EXTRA=$(docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo PHP_EXTRA_VERSION;") docker push jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-${PHP_RELEASE_EXTRA} + docker push jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-${PHP_RELEASE_EXTRA}-debug docker push jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc + docker push jakubboucek/lamp-devstack-php:8.6.${PHP_RELEASE}-rc-debug docker push jakubboucek/lamp-devstack-php:8.6-rc + docker push jakubboucek/lamp-devstack-php:8.6-rc-debug fi diff --git a/test-php.sh b/test-php.sh index fa38079..42ef486 100755 --- a/test-php.sh +++ b/test-php.sh @@ -15,3 +15,4 @@ docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 c docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5-cli composer -d tools/ test; docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc composer -d tools/ test; docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc-cli composer -d tools/ test; +docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc-debug composer -d tools/ test; From 3f769cd059b0d5fdba7118af8c81596832e3ed41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 08:53:56 +0200 Subject: [PATCH 03/10] docs: require PHP 8.6 RC workaround cleanup before stable --- build-notes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build-notes.md b/build-notes.md index 498a3db..78681f9 100644 --- a/build-notes.md +++ b/build-notes.md @@ -40,6 +40,19 @@ debug image to catch this regression. Upstream references: [Xdebug 3.6 alpha](https://xdebug.org/announcements/2026-09-08), [SPX PHP 8.6 metadata](https://github.com/NoiseByNorthwest/php-spx/pull/349). +#### Required cleanup when PHP 8.6 becomes stable + +- Remove the temporary SPX `CFLAGS` override (`-Wno-error=old-style-declaration` + and `-Dtypeof=__typeof__`) and its explanatory comments. These are accepted only + for the RC phase, not as permanent build configuration. +- Replace the memcached `dev-master`, Xdebug `^3.6@alpha`, and SPX `0.5.x-dev` + selectors with compatible stable releases, then update the RC-specific documentation. +- Rebuild and verify the images without the workarounds. If upstream still requires + any exception, explicitly discuss it with the maintainer rather than silently + carrying it into the stable images. +- Review `CI=true` separately: it prevents CPU-specific SPX builds and must not be + removed unless the resulting image remains portable across amd64 CPUs. + ## MariaDB Run [`build-mariadb.sh`](build-mariadb.sh) script to build all of MariaDB versions. From 8a9c70f1e0c5eec962f598f933eb6790527afff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 09:28:22 +0200 Subject: [PATCH 04/10] php 8.6: remove unwanted checks --- php/build-php-8.6.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/php/build-php-8.6.sh b/php/build-php-8.6.sh index bf05f89..b6590a6 100755 --- a/php/build-php-8.6.sh +++ b/php/build-php-8.6.sh @@ -31,9 +31,6 @@ if [ "${NO_TEST:-0}" -ne "1" ]; then docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" - docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri memcached - docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri xdebug - docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --ri spx docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "var_export(gd_info()) . PHP_EOL;" fi From fd374b10b1e769834e8693f2da13291b8431d51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 14:14:51 +0200 Subject: [PATCH 05/10] docs: keep PHP 8.6 build guidance concise --- CLAUDE.md | 2 +- build-notes.md | 45 ++++++--------------------------------------- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c8b071d..6f83b00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 c - PHP: the `-rc` suffix is hard-coded in the script, combined with the pre-release label (e.g. alpha/RC name) which can be read programmatically from the PHP binary itself. - MariaDB: RC builds have no additional sub-versions. Based on upstream's observed release mechanism, when a release channel goes stable the patch version is bumped to `2` and the `-rc` suffix is dropped. - During a PHP RC phase, components without upstream support for the new version (typically Xdebug → the `-debug` variant, PIE, individual extensions) are temporarily left out or commented in the Dockerfiles. They are re-enabled as soon as a *functional* upstream version exists — a working dev/beta/dev-master is enough, don't wait for a final release. Track the remaining work in a GitHub issue cross-linked with the version's PR. -- For PHP 8.6 RC package requirements, development versions, and temporary SPX compiler workarounds, see [build-notes.md](build-notes.md#php-86-rc). Keep these exceptions limited to the 8.6 Dockerfiles and revisit them as upstream releases land. +- For PHP 8.6 build prerequisites, see [build-notes.md](build-notes.md#php-86-rc); the RC-to-stable checklist is maintained in [#100](https://github.com/jakubboucek/docker-lamp-devstack/issues/100). - All images are `linux/amd64` only. Building for ARM would require hardware the maintainer doesn't have (and renting a VPS for it is not planned); this will change only if the maintainer's own environment moves to ARM. ## Defaults baked into images diff --git a/build-notes.md b/build-notes.md index 78681f9..8746147 100644 --- a/build-notes.md +++ b/build-notes.md @@ -13,45 +13,12 @@ To build older PHP versions run [`php/legacy/build-php-legacy.sh`](php/legacy/bu ### PHP 8.6 RC -PHP 8.6 requires PIE 1.5 or newer ([upstream support](https://github.com/php/pie/pull/667)). -Run `./prepare-assets.sh` before rebuilding with `NO_PULL=1` if `php/build/pie.phar` -was downloaded before that release. The normal build flow downloads the latest stable PIE. - -Memcached uses `dev-master` for the [PHP 8.6 API fixes](https://github.com/php-memcached-dev/php-memcached/pull/580). -The stable 3.4.0 release does not compile on PHP 8.6. Return to stable package selection -when a release containing those fixes is available. - -The debug variant uses `xdebug/xdebug:^3.6@alpha` and `noisebynorthwest/php-spx:0.5.x-dev`. -Xdebug 3.6.0alpha1 supports PHP 8.6; SPX support is in the `v0.5` branch, not `master`. -Stable SPX 0.4.22 and `dev-master` still declare PHP <8.6. - -The SPX build needs two compatibility flags on PHP 8.6 RC3 / Debian Trixie: -`-Wno-error=old-style-declaration` keeps a ZTS TLS declaration warning non-fatal, -and `-Dtypeof=__typeof__` allows the PHP headers' GNU type operator under SPX's strict -C11 mode. `CI=true` disables SPX's `-march=native` so the image remains portable across -amd64 CPUs. Revisit these workarounds when upstream fixes its build configuration. - -The 8.6 debug Dockerfile uses the bundled Dockerfile frontend (no external `syntax` directive). -With Docker 29.8.2 / BuildKit 0.33.1 and the containerd image store, the external frontend -resolved the published base instead of the freshly built local image. The bundled frontend -used the local base correctly; the build smoke checks explicitly require memcached in the -debug image to catch this regression. - -Upstream references: [Xdebug 3.6 alpha](https://xdebug.org/announcements/2026-09-08), -[SPX PHP 8.6 metadata](https://github.com/NoiseByNorthwest/php-spx/pull/349). - -#### Required cleanup when PHP 8.6 becomes stable - -- Remove the temporary SPX `CFLAGS` override (`-Wno-error=old-style-declaration` - and `-Dtypeof=__typeof__`) and its explanatory comments. These are accepted only - for the RC phase, not as permanent build configuration. -- Replace the memcached `dev-master`, Xdebug `^3.6@alpha`, and SPX `0.5.x-dev` - selectors with compatible stable releases, then update the RC-specific documentation. -- Rebuild and verify the images without the workarounds. If upstream still requires - any exception, explicitly discuss it with the maintainer rather than silently - carrying it into the stable images. -- Review `CI=true` separately: it prevents CPU-specific SPX builds and must not be - removed unless the resulting image remains portable across amd64 CPUs. +PHP 8.6 builds require PIE 1.5 or newer. When reusing assets with `NO_PULL=1`, +run `./prepare-assets.sh` first if the cached `php/build/pie.phar` is older. +Build the Apache image before debug; the debug Dockerfile uses the bundled frontend +and the locally built base. Package selectors and SPX compatibility flags are defined +and explained in the Dockerfiles. The [stable-transition checklist](https://github.com/jakubboucek/docker-lamp-devstack/issues/100) +is maintained in #100. ## MariaDB Run [`build-mariadb.sh`](build-mariadb.sh) script to build all of MariaDB versions. From 1187c00c07cacdb11577d9a410558db044563d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 12:24:20 +0200 Subject: [PATCH 06/10] docs: draft extension inventory with NEON compatibility findings --- tools/tests/expected-extensions.neon | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tools/tests/expected-extensions.neon diff --git a/tools/tests/expected-extensions.neon b/tools/tests/expected-extensions.neon new file mode 100644 index 0000000..ea74c73 --- /dev/null +++ b/tools/tests/expected-extensions.neon @@ -0,0 +1,90 @@ +# DRAFT: not wired into builds. See build-notes.md for dependency blockers. +# Profiles describe image tags, not PHP_SAPI. base means neither debug nor cli. +# mbstring exceptions record observations and require review before adoption. +profiles: + - '8.4' + - 8.4-cli + - 8.4-debug + - '8.5' + - 8.5-cli + - 8.5-debug + - 8.6-rc + - 8.6-rc-cli + - 8.6-rc-debug + +common: + - bcmath + - bz2 + - calendar + - core + - ctype + - curl + - date + - dom + - exif + - fileinfo + - filter + - gd + - gettext + - gmp + - hash + - iconv + - intl + - json + - libxml + - mbstring + - memcached + - mysqli + - mysqlnd + - openssl + - pcntl + - pcre + - pdo + - pdo_mysql + - pdo_sqlite + - phar + - posix + - random + - readline + - reflection + - session + - simplexml + - soap + - sockets + - sodium + - spl + - sqlite3 + - standard + - sysvmsg + - sysvsem + - sysvshm + - tokenizer + - xml + - xmlreader + - xmlwriter + - xsl + - zend opcache + - zip + - zlib + +rules: + - when: ['>=8.5'] + add: [lexbor, uri] + + - when: [+debug] + add: [xdebug, spx] + +# Applied last, only to the exact named profile; never to future stable 8.6. +exceptions: + 8.6-rc: + remove: [mbstring] + reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. + removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. + 8.6-rc-cli: + remove: [mbstring] + reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. + removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. + 8.6-rc-debug: + remove: [mbstring] + reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. + removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. From 5425f28c84b9a59b4c16fd2b06702b79b83d759d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 12:36:02 +0200 Subject: [PATCH 07/10] docs: select verified stable NEON and require mbstring in PHP 8.6 --- tools/tests/expected-extensions.neon | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/tools/tests/expected-extensions.neon b/tools/tests/expected-extensions.neon index ea74c73..dfaec25 100644 --- a/tools/tests/expected-extensions.neon +++ b/tools/tests/expected-extensions.neon @@ -1,7 +1,13 @@ -# DRAFT: not wired into builds. See build-notes.md for dependency blockers. +# DRAFT: not wired into builds. Parser choice and validation evidence: build-notes.md. # Profiles describe image tags, not PHP_SAPI. base means neither debug nor cli. -# mbstring exceptions record observations and require review before adoption. +# mbstring is mandatory, including PHP 8.6 RC; current images need repair. profiles: + - '8.2' + - 8.2-cli + - 8.2-debug + - '8.3' + - 8.3-cli + - 8.3-debug - '8.4' - 8.4-cli - 8.4-debug @@ -74,17 +80,5 @@ rules: - when: [+debug] add: [xdebug, spx] -# Applied last, only to the exact named profile; never to future stable 8.6. -exceptions: - 8.6-rc: - remove: [mbstring] - reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. - removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. - 8.6-rc-cli: - remove: [mbstring] - reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. - removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. - 8.6-rc-debug: - remove: [mbstring] - reason: Observed missing in local PHP 8.6.0RC3; intent is not yet approved. - removeWhen: Resolve mbstring policy before enabling the inventory check; revisit at 8.6 stable. +# Exact-profile exceptions apply last. Missing mbstring is a build defect, not an exception. +exceptions: {} From 5ba4900e57f5a06db72a4a73de262a4ae0f4448e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 17:41:28 +0200 Subject: [PATCH 08/10] tests: enforce PHP extension inventories from NEON rules Use stable NEON 3.4.8 and validate recursive conditions, exact exceptions and rule conflicts. Derive the variant from DEVSTACK_IMAGE and the version from PHP, without a profile allowlist. Report missing and unexpected modules without skipping the inventory check. Validated rule behavior for future versions, nested conditions, exceptions and malformed data. Full suites passed in all 15 maintained image variants; negative container probes rejected wrong variants and missing identity. PhpStorm inspections reported no warnings. --- tools/composer.json | 5 +- tools/src/ExtensionInventory.php | 199 +++++++++++++++++++++++++++ tools/tests/ExtensionsTest.php | 18 +++ tools/tests/Inventory/RulesTest.php | 87 ++++++++++++ tools/tests/bootstrap.php | 1 + tools/tests/expected-extensions.neon | 21 +-- 6 files changed, 309 insertions(+), 22 deletions(-) create mode 100644 tools/src/ExtensionInventory.php create mode 100644 tools/tests/ExtensionsTest.php create mode 100644 tools/tests/Inventory/RulesTest.php diff --git a/tools/composer.json b/tools/composer.json index b18db1e..ec5aa41 100644 --- a/tools/composer.json +++ b/tools/composer.json @@ -12,9 +12,10 @@ "license": "MIT", "type": "project", "require": { - "php": ">=8.1 <=8.5", + "php": ">=8.1", "nette/tester": "^2.5", - "tracy/tracy": "^2" + "tracy/tracy": "^2", + "nette/neon": "3.4.8" }, "config": { "platform": { diff --git a/tools/src/ExtensionInventory.php b/tools/src/ExtensionInventory.php new file mode 100644 index 0000000..be87514 --- /dev/null +++ b/tools/src/ExtensionInventory.php @@ -0,0 +1,199 @@ + $rule) { + self::keys($rule, ['when', 'add', 'remove'], "rules.$i"); + self::condition($rule['when'] ?? null, "rules.$i.when"); + self::changes($rule, "rules.$i"); + } + if (!is_array($config['exceptions'] ?? null)) { + throw new InvalidArgumentException('exceptions must be a map'); + } + foreach ($config['exceptions'] as $profile => $exception) { + if (!is_string($profile) || !preg_match('~^\d+\.\d+(?:-rc)?(?:-(?:debug|cli))?$~D', $profile)) { + throw new InvalidArgumentException('Exception keys must be exact minor profiles, e.g. 8.6-rc-debug'); + } + self::keys($exception, ['add', 'remove', 'reason', 'removeWhen'], "exceptions.$profile"); + self::changes($exception, "exceptions.$profile"); + foreach (['reason', 'removeWhen'] as $key) { + if (!is_string($exception[$key] ?? null) || trim($exception[$key]) === '') { + throw new InvalidArgumentException("exceptions.$profile.$key must explain the exception"); + } + } + } + $this->config = $config; + } + + /** Resolve image identity independently of its loaded extensions and PHP_SAPI. */ + public static function context(string $image, string $phpVersion): array + { + if (!preg_match('~^(\d+)\.(\d+)\.\d+(.*)$~D', $phpVersion, $runtime)) { + throw new InvalidArgumentException('Invalid PHP runtime version'); + } + $minor = $runtime[1] . '.' . $runtime[2]; + $rc = $runtime[3] !== ''; + if (!preg_match('~^[^\s@]+:([^/:]+)$~D', $image, $reference)) { + throw new InvalidArgumentException('DEVSTACK_IMAGE must contain the full image reference with a tag'); + } + $tag = $reference[1]; + if (preg_match('~^(\d+)(?:\.(\d+)(?:\.\d+)?)?(?:(-rc)(?:-(?:alpha|beta|RC)\d+)?)?(?:-(debug|cli))?$~D', $tag, $parts)) { + if ($parts[1] !== $runtime[1] || (isset($parts[2]) && $parts[2] !== '' && $parts[2] !== $runtime[2])) { + throw new InvalidArgumentException("Image tag $tag does not match PHP $phpVersion"); + } + if (($parts[3] ?? '') !== ($rc ? '-rc' : '')) { + throw new InvalidArgumentException("Image tag $tag does not match PHP pre-release status $phpVersion"); + } + $variant = ($parts[4] ?? '') ?: 'base'; + } elseif (in_array($tag, ['latest', 'debug', 'cli'], true)) { + $variant = $tag === 'latest' ? 'base' : $tag; + } else { + throw new InvalidArgumentException("Unsupported image tag $tag; use a version tag with optional -rc and -debug/-cli"); + } + $flags = [$variant]; + if ($rc) { + $flags[] = 'rc'; + } + return [ + 'version' => $minor, + 'flags' => $flags, + 'profile' => $minor . ($rc ? '-rc' : '') . ($variant === 'base' ? '' : '-' . $variant), + ]; + } + + public function expected(array $context): array + { + $add = $remove = []; + foreach ($this->config['rules'] as $rule) { + if (self::matches($rule['when'], $context)) { + $add = array_merge($add, self::extensions($rule['add'] ?? [], 'add')); + $remove = array_merge($remove, self::extensions($rule['remove'] ?? [], 'remove')); + } + } + $conflicts = array_intersect($add, $remove); + if ($conflicts) { + throw new InvalidArgumentException('Conflicting matching rules: ' . implode(', ', array_unique($conflicts))); + } + $expected = array_diff(array_merge(self::extensions($this->config['common'], 'common'), $add), $remove); + $exception = $this->config['exceptions'][$context['profile']] ?? []; + $expected = array_diff( + array_merge($expected, self::extensions($exception['add'] ?? [], 'add')), + self::extensions($exception['remove'] ?? [], 'remove'), + ); + $expected = array_values(array_unique($expected)); + sort($expected); + return $expected; + } + + public function difference(array $context, array $loaded): array + { + $expected = $this->expected($context); + $actual = array_map('strtolower', $loaded); + $missing = array_values(array_diff($expected, $actual)); + $unexpected = array_values(array_unique(array_diff($actual, $expected))); + sort($unexpected); + return ['missing' => $missing, 'unexpected' => $unexpected]; + } + + private static function keys(mixed $value, array $allowed, string $path): void + { + if (!is_array($value) || array_diff(array_keys($value), $allowed)) { + throw new InvalidArgumentException("$path must be a map with only: " . implode(', ', $allowed)); + } + } + + private static function extensions(mixed $value, string $path): array + { + if (!is_array($value) || !array_is_list($value)) { + throw new InvalidArgumentException("$path must be a list of extension names"); + } + $names = []; + foreach ($value as $name) { + if (!is_string($name) || !preg_match('~^[a-zA-Z][a-zA-Z0-9_ ]*$~D', $name) || trim($name) !== $name) { + throw new InvalidArgumentException("$path contains an invalid extension name"); + } + $names[] = strtolower($name); + } + if (count($names) !== count(array_unique($names))) { + throw new InvalidArgumentException("$path contains duplicate extension names"); + } + return $names; + } + + private static function changes(array $value, string $path): void + { + if (!array_key_exists('add', $value) && !array_key_exists('remove', $value)) { + throw new InvalidArgumentException("$path needs add or remove"); + } + $add = self::extensions(array_key_exists('add', $value) ? $value['add'] : [], "$path.add"); + $remove = self::extensions(array_key_exists('remove', $value) ? $value['remove'] : [], "$path.remove"); + if (array_intersect($add, $remove)) { + throw new InvalidArgumentException("$path adds and removes the same extension"); + } + } + + private static function condition(mixed $value, string $path): void + { + if (is_string($value)) { + if (!preg_match('~^(?:[+-](?:base|debug|cli|rc)|(?:>=|<=|!=|=|>|<)\d+(?:\.\d+)?)$~D', $value)) { + throw new InvalidArgumentException("Invalid condition at $path: $value"); + } + return; + } + if (!is_array($value) || !$value) { + throw new InvalidArgumentException("$path must be a condition or a nonempty condition list"); + } + if (!array_is_list($value)) { + if (count($value) !== 1 || !in_array(array_key_first($value), ['all', 'any'], true)) { + throw new InvalidArgumentException("Invalid map at $path; quote version comparisons containing ="); + } + $key = array_key_first($value); + $value = $value[$key]; + $path .= '.' . $key; + if (!is_array($value) || !array_is_list($value) || !$value) { + throw new InvalidArgumentException("$path must be a nonempty list"); + } + } + foreach ($value as $i => $child) { + self::condition($child, "$path.$i"); + } + } + + private static function matches(string|array $condition, array $context): bool + { + if (is_string($condition)) { + if ($condition[0] === '+' || $condition[0] === '-') { + return in_array(substr($condition, 1), $context['flags'], true) === ($condition[0] === '+'); + } + preg_match('~^(>=|<=|!=|=|>|<)(\d+)(?:\.(\d+))?$~D', $condition, $parts); + return version_compare($context['version'], $parts[2] . '.' . ($parts[3] ?? '0'), $parts[1]); + } + $any = array_key_exists('any', $condition); + $children = $condition['any'] ?? $condition['all'] ?? $condition; + foreach ($children as $child) { + $match = self::matches($child, $context); + if ($any && $match) { + return true; + } + if (!$any && !$match) { + return false; + } + } + return !$any; + } +} diff --git a/tools/tests/ExtensionsTest.php b/tools/tests/ExtensionsTest.php new file mode 100644 index 0000000..a23bac6 --- /dev/null +++ b/tools/tests/ExtensionsTest.php @@ -0,0 +1,18 @@ +difference($context, get_loaded_extensions()); +Assert::true($difference === ['missing' => [], 'unexpected' => []], + 'PHP ' . PHP_VERSION . ', profile ' . $context['profile'] + . '; missing: ' . implode(', ', $difference['missing']) + . '; unexpected: ' . implode(', ', $difference['unexpected'])); diff --git a/tools/tests/Inventory/RulesTest.php b/tools/tests/Inventory/RulesTest.php new file mode 100644 index 0000000..faa7eaf --- /dev/null +++ b/tools/tests/Inventory/RulesTest.php @@ -0,0 +1,87 @@ + ['Core'], 'rules' => $rules, 'exceptions' => $exceptions]); +} + +function context(string $tag = '8.6-rc-debug', string $version = '8.6.0RC3'): array +{ + return ExtensionInventory::context('registry.example:5000/dev/php:' . $tag, $version); +} + +$debug = context(); +Assert::same(['debug', 'rc'], $debug['flags']); +Assert::same('8.6-rc-debug', $debug['profile']); +Assert::same(['base'], context('8.5', '8.5.11')['flags']); +Assert::same(['cli'], context('8-cli', '8.5.11')['flags']); +Assert::same('8.6-rc-debug', context('8.6.0-rc-RC3-debug')['profile']); +Assert::same('8.7-debug', context('debug', '8.7.0')['profile']); + +$rules = [ + ['when' => ['>=8.6', ['any' => ['+debug', '+cli']]], 'add' => ['intl']], + ['when' => ['all' => ['+debug', '-cli']], 'add' => ['Xdebug', 'SPX']], + ['when' => ['+base'], 'remove' => ['core']], +]; +$matrix = inventory($rules); +Assert::same(['core', 'intl', 'spx', 'xdebug'], $matrix->expected($debug)); +Assert::same(['core', 'intl'], $matrix->expected(context('8.7-cli', '8.7.0'))); +Assert::same(['core', 'intl', 'spx', 'xdebug'], $matrix->expected(context('8.7-debug', '8.7.0'))); +Assert::same([], $matrix->expected(context('8.5', '8.5.11'))); +Assert::same(['missing' => ['intl'], 'unexpected' => ['extra']], + $matrix->difference($debug, ['Core', 'SPX', 'Xdebug', 'extra'])); +Assert::same(['missing' => [], 'unexpected' => []], + $matrix->difference($debug, ['Xdebug', 'SPX', 'intl', 'Core'])); + +foreach (['=8.4' => true, '!=8.4' => false, '>8.4' => false, '>=8.4' => true, + '<8.4' => false, '<=8.4' => true, '<8' => false, '>8' => true] as $condition => $matches) { + $i = inventory([['when' => [$condition], 'add' => ['intl']]]); + Assert::same($matches ? ['core', 'intl'] : ['core'], $i->expected(context('8.4', '8.4.26'))); +} +Assert::same(['core', 'intl'], inventory([['when' => ['>8.9'], 'add' => ['intl']]]) + ->expected(context('8.10', '8.10.0'))); + +$exception = ['8.6-rc-debug' => [ + 'remove' => ['xdebug'], 'add' => ['json'], + 'reason' => 'Test fixture', 'removeWhen' => 'When the fixture ends', +]]; +Assert::same(['core', 'intl', 'json', 'spx'], inventory($rules, $exception)->expected($debug)); +Assert::same(['core', 'intl', 'spx', 'xdebug'], inventory($rules, $exception) + ->expected(context('8.6-debug', '8.6.0'))); +Assert::exception(fn() => inventory([ + ['when' => ['+debug'], 'add' => ['SPX']], + ['when' => ['>=8'], 'remove' => ['spx']], +])->expected($debug), InvalidArgumentException::class, 'Conflicting matching rules: spx'); + +foreach (['', '8.6-rc-cli-debug', '8.6-rc-dev'] as $tag) { + Assert::exception(fn() => context($tag), InvalidArgumentException::class); +} +Assert::exception(fn() => ExtensionInventory::context('', '8.6.0RC3'), InvalidArgumentException::class); +Assert::exception(fn() => context('8.5-debug'), InvalidArgumentException::class); +Assert::exception(fn() => context('8.6-debug'), InvalidArgumentException::class); + +foreach ([[], 8.6, ['>' => 8.6], ['all' => []], ['any' => ['+debug'], 'all' => ['+cli']], + ['any' => '+debug'], ['+debig'], ['>=8.6.0'], [1 => '+debug'], ['all' => [['wat' => ['+cli']]]]] as $bad) { + Assert::exception(fn() => inventory([['when' => $bad, 'add' => ['intl']]]), InvalidArgumentException::class); +} +$unquoted = Neon::decode("when:\n\t- >=8.6\n\t- -cli"); +Assert::exception(fn() => inventory([['when' => $unquoted['when'], 'add' => ['intl']]]), + InvalidArgumentException::class, 'Invalid map at rules.0.when.0; quote version comparisons containing ='); +foreach ([ + ['common' => ['Core', 'core'], 'rules' => [], 'exceptions' => []], + ['common' => [], 'rules' => [], 'exceptions' => [], 'profiles' => []], + ['common' => [], 'rules' => [['when' => ['+cli'], 'add' => null]], 'exceptions' => []], + ['common' => [], 'rules' => [['when' => ['+cli'], 'add' => ['x'], 'remove' => ['X']]], 'exceptions' => []], + ['common' => [], 'rules' => [], 'exceptions' => ['8.6-rc' => ['remove' => ['x']]]], +] as $bad) { + Assert::exception(fn() => new ExtensionInventory($bad), InvalidArgumentException::class); +} diff --git a/tools/tests/bootstrap.php b/tools/tests/bootstrap.php index 642f038..6c69168 100644 --- a/tools/tests/bootstrap.php +++ b/tools/tests/bootstrap.php @@ -4,4 +4,5 @@ require __DIR__ . '/../vendor/autoload.php'; Tester\Environment::setup(); +Tester\Dumper::$dumpDir = sys_get_temp_dir() . '/devstack-test-output'; date_default_timezone_set('Europe/Prague'); diff --git a/tools/tests/expected-extensions.neon b/tools/tests/expected-extensions.neon index dfaec25..85508fc 100644 --- a/tools/tests/expected-extensions.neon +++ b/tools/tests/expected-extensions.neon @@ -1,23 +1,4 @@ -# DRAFT: not wired into builds. Parser choice and validation evidence: build-notes.md. -# Profiles describe image tags, not PHP_SAPI. base means neither debug nor cli. -# mbstring is mandatory, including PHP 8.6 RC; current images need repair. -profiles: - - '8.2' - - 8.2-cli - - 8.2-debug - - '8.3' - - 8.3-cli - - 8.3-debug - - '8.4' - - 8.4-cli - - 8.4-debug - - '8.5' - - 8.5-cli - - 8.5-debug - - 8.6-rc - - 8.6-rc-cli - - 8.6-rc-debug - +# Format: ../../docs/extension-inventory.md common: - bcmath - bz2 From 0210bb2e1635c9f8062694736adb1e198d296e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 17:42:00 +0200 Subject: [PATCH 09/10] build: run image-aware extension checks before publishing Run all configured tags or one requested tag through test-php.sh, passing DEVSTACK_IMAGE and aggregating failures. Invoke the same suite from all maintained PHP build scripts before push, preserving NO_TEST. Mount tooling read-only and document the NEON contract; keep investigation notes ignored. All ten real build test phases passed with NO_PULL=1 NO_BUILD=1 NO_PUSH=1. Mocked Docker checks confirmed that failed tests prevent any push, successful tests precede push, and NO_TEST bypass works. No images were rebuilt or published. --- .gitignore | 1 + CLAUDE.md | 21 +++++++-- docs/extension-inventory.md | 93 +++++++++++++++++++++++++++++++++++++ php/build-php-8.2-cli.sh | 1 + php/build-php-8.2.sh | 2 + php/build-php-8.3-cli.sh | 1 + php/build-php-8.3.sh | 2 + php/build-php-8.4-cli.sh | 1 + php/build-php-8.4.sh | 2 + php/build-php-8.5-cli.sh | 1 + php/build-php-8.5.sh | 2 + php/build-php-8.6-cli.sh | 1 + php/build-php-8.6.sh | 2 + test-php.sh | 53 ++++++++++++++------- 14 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 docs/extension-inventory.md diff --git a/.gitignore b/.gitignore index ab94291..d8ead09 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /php/build /tools/composer.lock /tools/vendor +/docs/agent-proposal/ diff --git a/CLAUDE.md b/CLAUDE.md index 6f83b00..3c9903f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,14 @@ Source for the prebuilt Docker images `jakubboucek/lamp-devstack-php` and `jakub The project is maintained primarily to serve the maintainer's own development workflow and is published because others found it useful. Its feature set is intentionally opinionated: components and extensions are included only when the maintainer actually needs them, and deliberately omitted otherwise. Any apparent imbalance in the package scope is a design choice, not an oversight. Suggestions and critical feedback on the scope are welcome — but never add new components or extensions on your own initiative; always consult the maintainer first. +## Documentation + +Keep tracked documentation focused on the current project and configuration formats. +Store implementation plans, investigation notes and measurement snapshots in the +ignored `docs/agent-proposal/` directory. Record one-off validation results in commit +messages or PR descriptions. The extension inventory declaration format is described +in [docs/extension-inventory.md](docs/extension-inventory.md). + ## Build commands All build scripts follow the same four-phase pattern (pull → build → test → push), each phase skippable via ENV flags: `NO_PULL=1`, `NO_BUILD=1`, `NO_TEST=1`, `NO_PUSH=1`, `NO_ASSETS=1`. @@ -27,14 +35,17 @@ Common examples: `NO_PUSH=1 ./build-php.sh` (local build only), `NO_PULL=1 NO_BU ## Tests ```bash -./test-php.sh # run test suite inside every current PHP image (8.2–8.5 + 8.6 RC, apache + cli, plus 8.6 RC debug) +./test-php.sh # test every tag listed at the top of the script (base, cli, debug) +./test-php.sh 8.6-rc-debug # test a single tag ``` -Tests live in `tools/tests/` (Nette Tester, one `*Test.php` per PHP extension in `tools/tests/Extensions/`). To run in a single image: +Prepare dependencies with `composer -d tools install` before running tests or build +checks. Build scripts run the same suite before pushing, unless `NO_TEST=1`. -```bash -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 composer -d tools/ test -``` +Tests live in `tools/tests/` (Nette Tester, one `*Test.php` per PHP extension in +`tools/tests/Extensions/`). The script passes the full image reference through +`DEVSTACK_IMAGE`, runs without an interactive terminal, and returns a nonzero status +if any container fails while continuing through the remaining tags. `tools/compute-minimal-ext-deps.php` is a build helper: run inside an image to compute the minimal runtime package list (`apt-mark` command) that replaces `-dev` packages in Dockerfiles — use it when changing extension deps. diff --git a/docs/extension-inventory.md b/docs/extension-inventory.md new file mode 100644 index 0000000..b80a06d --- /dev/null +++ b/docs/extension-inventory.md @@ -0,0 +1,93 @@ +# Extension inventory declaration + +`tools/tests/expected-extensions.neon` declares the expected PHP extensions for each +image profile. `tools/tests/ExtensionsTest.php` checks this declaration as part of +Nette Tester. Current build scripts invoke the suite before publishing images. + +Prepare tooling with `composer -d tools install`, then use `./test-php.sh` or +`./test-php.sh 8.6-rc-debug`. Dependencies are installed on the host and mounted +read-only into containers; they are not added to published images. Composer resolves +for PHP 8.1 via `config.platform.php`. NEON 3.4.8 is pinned: its declared upper bound +is 8.5, but its runtime behavior has also been verified for this project on PHP 8.6. + +The runner passes `DEVSTACK_IMAGE` with the full image reference. The test reads the +actual PHP version, derives base/CLI/debug from the tag, and checks that numeric tags +match the runtime major/minor and pre-release status. Numeric tags may include patch +versions and RC release labels; aliases `latest`, `debug`, `cli`, and major tags such +as `8-cli` also work. Exact exceptions use the canonical minor profile (e.g. +`8.6-rc-debug`), independent of the patch tag used to launch the container. + +Every pre-release runtime (alpha, beta, RC or dev) receives the `rc` flag. Unknown +future PHP versions use the same rules without registration; unrecognized tag +suffixes and missing image identity are errors. The functional extension tests retain +their skips, but the inventory test never skips missing extensions. + +## Structure + +- `common`: extensions expected in every supported profile. +- `rules`: a list of conditions (`when`) and extension lists to `add` or `remove`. + All matching rules apply. Conflicting additions/removals between matching rules are + errors; rule order does not resolve conflicts. +- `exceptions`: a map keyed by exact profile, applied after ordinary rules. Each entry + has `add` and/or `remove`, a `reason`, and a `removeWhen` condition for retiring it. + Use `{}` when no exceptions are needed. Quote numeric-looking keys such as `'8.5'`. + +Extension names are compared case-insensitively (`SPX` becomes `spx`, `Zend OPcache` +becomes `zend opcache`). Missing and unexpected extensions both fail the check; +`remove` means the extension must be absent, not optional. + +## Conditions + +A plain condition list means AND. Explicit `all` (AND) and `any` (OR) accept lists +of conditions and may be nested. For example, PHP 8.6 or later with debug or CLI: + +```neon +when: + - '>=8.6' + - any: [+debug, +cli] +``` + +PHP 8.6 or later in any non-CLI variant: + +```neon +when: + - '>=8.6' + - -cli +``` + +Version operators are `=`, `!=`, `>`, `>=`, `<`, and `<=`. They compare PHP minor +series: `=8.4` includes every 8.4.x, `>8.4` starts at 8.5, and `<8` means below 8.0. +Pre-release status is a flag and does not affect numeric version comparisons. + +`+flag` requires a flag; `-flag` excludes it. Flags are `debug`, `cli`, `rc`, and +synthetic `base` (neither debug nor CLI): + +| Profile | Flags | +| --- | --- | +| `8.5` | base | +| `8.5-debug` | debug | +| `8.5-cli` | cli | +| `8.6-rc` | base, rc | +| `8.6-rc-debug` | debug, rc | + +Thus `-debug` also matches CLI, while `+base` includes RC base images. Unknown flags +and unsupported combinations such as CLI plus debug are errors. + +Unquoted conditions are allowed when NEON parses them as strings. Operators containing +`=` require quotes because NEON otherwise interprets them as mappings or syntax errors. +Validate recursively: accept string leaves, nonempty lists with consecutive integer +keys starting at zero, and maps containing exactly one `all` or `any` key with a +nonempty condition list. Reject other maps, non-string leaves and empty logical nodes. +Never reconstruct a comparator from a parsed mapping: numeric conversion may have +already changed a version such as `8.10` to `8.1`. + +The configuration validator checks every rule, including inactive rules, and rejects +unknown fields, duplicate extension names and malformed exception entries. String +conditions also work directly in `when`. An exception cannot resolve a conflict between +ordinary matching rules: fix the conflicting rules first. + +A failure reports the runtime PHP version, canonical profile and sorted missing and +unexpected modules. Build scripts honor `NO_TEST=1` as an explicit bypass; otherwise +a failing suite prevents that build script from reaching its push phase. This does +not make a multi-version release atomic. The suite inspects CLI-loaded modules in +each image; HTTP/Apache behavior remains a separate concern. diff --git a/php/build-php-8.2-cli.sh b/php/build-php-8.2-cli.sh index 04471c2..c5f1ad0 100755 --- a/php/build-php-8.2-cli.sh +++ b/php/build-php-8.2-cli.sh @@ -23,6 +23,7 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.2-cli docker run --rm jakubboucek/lamp-devstack-php:8.2-cli php --version docker run --rm jakubboucek/lamp-devstack-php:8.2-cli php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.2-cli php -r "var_export(gd_info()) . PHP_EOL;" diff --git a/php/build-php-8.2.sh b/php/build-php-8.2.sh index e24510b..ee347d5 100755 --- a/php/build-php-8.2.sh +++ b/php/build-php-8.2.sh @@ -24,6 +24,8 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.2 + ../test-php.sh 8.2-debug docker run --rm jakubboucek/lamp-devstack-php:8.2 php --version docker run --rm jakubboucek/lamp-devstack-php:8.2-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.2 php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" diff --git a/php/build-php-8.3-cli.sh b/php/build-php-8.3-cli.sh index add935a..406505e 100755 --- a/php/build-php-8.3-cli.sh +++ b/php/build-php-8.3-cli.sh @@ -22,6 +22,7 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.3-cli docker run --rm jakubboucek/lamp-devstack-php:8.3-cli php --version docker run --rm jakubboucek/lamp-devstack-php:8.3-cli php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.3-cli php -r "var_export(gd_info()) . PHP_EOL;" diff --git a/php/build-php-8.3.sh b/php/build-php-8.3.sh index 2a2681f..409db64 100755 --- a/php/build-php-8.3.sh +++ b/php/build-php-8.3.sh @@ -24,6 +24,8 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.3 + ../test-php.sh 8.3-debug docker run --rm jakubboucek/lamp-devstack-php:8.3 php --version docker run --rm jakubboucek/lamp-devstack-php:8.3-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.3 php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" diff --git a/php/build-php-8.4-cli.sh b/php/build-php-8.4-cli.sh index 54ad8d7..8b1beca 100755 --- a/php/build-php-8.4-cli.sh +++ b/php/build-php-8.4-cli.sh @@ -22,6 +22,7 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.4-cli docker run --rm jakubboucek/lamp-devstack-php:8.4-cli php --version docker run --rm jakubboucek/lamp-devstack-php:8.4-cli php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.4-cli php -r "var_export(gd_info()) . PHP_EOL;" diff --git a/php/build-php-8.4.sh b/php/build-php-8.4.sh index 6a77d07..93aa665 100755 --- a/php/build-php-8.4.sh +++ b/php/build-php-8.4.sh @@ -24,6 +24,8 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.4 + ../test-php.sh 8.4-debug docker run --rm jakubboucek/lamp-devstack-php:8.4 php --version docker run --rm jakubboucek/lamp-devstack-php:8.4-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.4 php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" diff --git a/php/build-php-8.5-cli.sh b/php/build-php-8.5-cli.sh index fa6e9b8..1cffcd0 100755 --- a/php/build-php-8.5-cli.sh +++ b/php/build-php-8.5-cli.sh @@ -24,6 +24,7 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.5-cli docker run --rm jakubboucek/lamp-devstack-php:8.5-cli php --version docker run --rm jakubboucek/lamp-devstack-php:8.5-cli php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.5-cli php -r "var_export(gd_info()) . PHP_EOL;" diff --git a/php/build-php-8.5.sh b/php/build-php-8.5.sh index 11603e8..5a2e4ba 100755 --- a/php/build-php-8.5.sh +++ b/php/build-php-8.5.sh @@ -28,6 +28,8 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.5 + ../test-php.sh 8.5-debug docker run --rm jakubboucek/lamp-devstack-php:8.5 php --version docker run --rm jakubboucek/lamp-devstack-php:8.5-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.5 php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" diff --git a/php/build-php-8.6-cli.sh b/php/build-php-8.6-cli.sh index f3c2bcb..dd5a6de 100755 --- a/php/build-php-8.6-cli.sh +++ b/php/build-php-8.6-cli.sh @@ -25,6 +25,7 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.6-rc-cli docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-cli php --version docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-cli php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-cli php -r "var_export(gd_info()) . PHP_EOL;" diff --git a/php/build-php-8.6.sh b/php/build-php-8.6.sh index b6590a6..0fe7769 100755 --- a/php/build-php-8.6.sh +++ b/php/build-php-8.6.sh @@ -27,6 +27,8 @@ if [ "${NO_BUILD:-0}" -ne "1" ]; then fi if [ "${NO_TEST:-0}" -ne "1" ]; then + ../test-php.sh 8.6-rc + ../test-php.sh 8.6-rc-debug docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php --version docker run --rm jakubboucek/lamp-devstack-php:8.6-rc-debug php --version docker run --rm jakubboucek/lamp-devstack-php:8.6-rc php -r "echo implode(', ', get_loaded_extensions()) . PHP_EOL;" diff --git a/test-php.sh b/test-php.sh index 42ef486..6add738 100755 --- a/test-php.sh +++ b/test-php.sh @@ -1,18 +1,39 @@ #!/bin/bash -# shellcheck disable=SC2086 -set -ux; - -cd "$(dirname $0)"; - -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.2 composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.2-cli composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.3 composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.3-cli composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.4 composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.4-cli composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5 composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.5-cli composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc-cli composer -d tools/ test; -docker run --rm -it -v "$(pwd)":/app -w /app jakubboucek/lamp-devstack-php:8.6-rc-debug composer -d tools/ test; +TAGS=( + 8.2 8.2-cli 8.2-debug + 8.3 8.3-cli 8.3-debug + 8.4 8.4-cli 8.4-debug + 8.5 8.5-cli 8.5-debug + 8.6-rc 8.6-rc-cli 8.6-rc-debug +) + +set -ux + +if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ -z "$1" ]; }; then + echo "Usage: $0 [tag]" >&2 + exit 2 +fi + +if [ "$#" -eq 1 ]; then + TAGS=("$1") +fi + +cd "$(dirname "$0")" || exit 1 + +if [ ! -f tools/vendor/autoload.php ]; then + echo "Install test dependencies first: composer -d tools install" >&2 + exit 1 +fi + +status=0 +for tag in "${TAGS[@]}"; do + image="jakubboucek/lamp-devstack-php:$tag" + if ! docker run --rm -v "$(pwd)":/app:ro -w /app \ + -e "DEVSTACK_IMAGE=$image" \ + "$image" composer -d tools/ test; then + status=1 + fi +done + +exit "$status" From f5ab0d146063f97d50016657a33d4803fa92e2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Tue, 13 Oct 2026 22:42:27 +0200 Subject: [PATCH 10/10] tests: finalize image identity and document inventory examples --- CLAUDE.md | 2 +- docs/extension-inventory.md | 131 ++++++++++++++++++++++++++- test-php.sh | 4 +- tools/src/ExtensionInventory.php | 2 +- tools/tests/ExtensionsTest.php | 6 +- tools/tests/expected-extensions.neon | 21 +++-- 6 files changed, 149 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c9903f..f5e4d2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ checks. Build scripts run the same suite before pushing, unless `NO_TEST=1`. Tests live in `tools/tests/` (Nette Tester, one `*Test.php` per PHP extension in `tools/tests/Extensions/`). The script passes the full image reference through -`DEVSTACK_IMAGE`, runs without an interactive terminal, and returns a nonzero status +`IMAGE_NAME`, runs without an interactive terminal, and returns a nonzero status if any container fails while continuing through the remaining tags. `tools/compute-minimal-ext-deps.php` is a build helper: run inside an image to compute the minimal runtime package list (`apt-mark` command) that replaces `-dev` packages in Dockerfiles — use it when changing extension deps. diff --git a/docs/extension-inventory.md b/docs/extension-inventory.md index b80a06d..bc66a78 100644 --- a/docs/extension-inventory.md +++ b/docs/extension-inventory.md @@ -10,7 +10,7 @@ read-only into containers; they are not added to published images. Composer reso for PHP 8.1 via `config.platform.php`. NEON 3.4.8 is pinned: its declared upper bound is 8.5, but its runtime behavior has also been verified for this project on PHP 8.6. -The runner passes `DEVSTACK_IMAGE` with the full image reference. The test reads the +The runner passes `IMAGE_NAME` with the full image reference. The test reads the actual PHP version, derives base/CLI/debug from the tag, and checks that numeric tags match the runtime major/minor and pre-release status. Numeric tags may include patch versions and RC release labels; aliases `latest`, `debug`, `cli`, and major tags such @@ -29,7 +29,7 @@ their skips, but the inventory test never skips missing extensions. All matching rules apply. Conflicting additions/removals between matching rules are errors; rule order does not resolve conflicts. - `exceptions`: a map keyed by exact profile, applied after ordinary rules. Each entry - has `add` and/or `remove`, a `reason`, and a `removeWhen` condition for retiring it. + has `add` and/or `remove`, a `reason`, and a human-readable `removeWhen` note for retiring it. Use `{}` when no exceptions are needed. Quote numeric-looking keys such as `'8.5'`. Extension names are compared case-insensitively (`SPX` becomes `spx`, `Zend OPcache` @@ -91,3 +91,130 @@ unexpected modules. Build scripts honor `NO_TEST=1` as an explicit bypass; other a failing suite prevents that build script from reaching its push phase. This does not make a multi-version release atomic. The suite inspects CLI-loaded modules in each image; HTTP/Apache behavior remains a separate concern. + +## Exceptions + +An exception targets one canonical minor profile, including its variant and release +status. It has no `when`: the map key selects the profile. For example, +`8.6-rc-debug` matches all patch tags for that RC debug profile, but not `8.6-rc`, +`8.6-rc-cli`, or stable `8.6-debug`. A quoted key `'8.5'` targets only the stable +8.5 base variant. Wildcards and version expressions are not supported as keys. + +Each exception needs at least one of `add` or `remove`, plus two nonempty strings: + +- `reason` explains why the normal expectations do not apply. +- `removeWhen` tells the maintainer when to delete the exception. It is **human-readable + documentation**, not an evaluated condition, date, or automatic expiration mechanism. + Even a string such as `'>=8.7'` would only be text here. Delete the entry manually + once its stated condition is satisfied. + +The following complete miniature configuration is illustrative; it does not describe +an actual missing extension in our current images: + +```neon +exceptions: + 8.6-rc-debug: + remove: [spx] + reason: The profiler cannot currently be built for this RC. + removeWhen: Delete after a compatible profiler is installed and verified. + +rules: + - when: +debug + add: [xdebug, spx] + +common: [core, json] +``` + +The debug rule first adds both profilers. For `8.6-rc-debug`, the exception then +removes `spx`: the final expected set is `core`, `json`, `xdebug`. A loaded `spx` +would fail as unexpected, reminding the maintainer to retire the exception after +restoring it. Other debug profiles still require both profilers. + +Exceptions apply last even when their section appears first in the file. An `add` +can restore an extension removed by an ordinary rule; a `remove` can exclude one +from `common` or a matching rule. A single exception cannot both add and remove the +same extension. Conflicting ordinary rules fail before exceptions are applied. +Use `exceptions: []` or `exceptions: {}` when there are no exceptions; both decode +to an empty PHP array. + +## Examples + +These independent snippets illustrate the rule language, not changes to the current +extension policy. When trying a snippet on its own, supply any omitted top-level +sections (`common`, `rules`, `exceptions`) with empty lists/maps. + +### Version range, alternative variants, and stable releases only + +```neon +rules: + - when: + - '>=8.4' + - '<8.7' + - -rc + - any: [+debug, +cli] + add: [intl] +``` + +Every outer list item must match. This adds `intl` for stable PHP 8.4, 8.5 and 8.6 +in either debug or CLI variants. It excludes base variants, every pre-release, +PHP 8.3 and PHP 8.7+. All patch versions of each matching minor series are included. + +### Alternatives with their own combined conditions + +```neon +rules: + - when: + any: + - all: ['>=8.6', +debug] + - all: ['=8.4', +cli, -rc] + add: [sockets] +``` + +This adds `sockets` if either branch matches: debug PHP 8.6 or later (including RC), +or stable CLI PHP 8.4. It does not match CLI PHP 8.6 or debug PHP 8.5. Each inner +`all` applies only to its own branch; the second branch's `-rc` does not restrict +the first branch. + +### Removing a common extension, then restoring it for one exact profile + +```neon +exceptions: + 8.6-rc: + add: [intl] + reason: This specific RC base profile retains intl for compatibility. + removeWhen: Delete when this profile adopts the general base-image policy. + +rules: + - when: ['>=8.6', +base] + remove: [intl] + +common: [core, intl] +``` + +The rule removes `intl` from base PHP 8.6 and later, both stable and RC. The exception +restores it only for `8.6-rc`. Consequently, stable `8.6` and `8.7-rc` require only +`core`, while `8.6-rc` requires both extensions. CLI and debug variants also retain +both because they never match `+base`. Removing an expectation does not make the +extension optional: it must be absent unless subsequently restored by the exception. + +### Accumulating matching rules without relying on their order + +```neon +rules: + - when: +debug + add: [xdebug, spx] + - when: ['>=8.5'] + add: [lexbor, uri] + - when: ['>=8.6', -cli, '!=8.7'] + add: [intl] +``` + +For `8.6-rc-debug`, all three rules match and their additions are combined with +`common`. For `8.7-debug`, only the first two match. For `8.8-cli`, only the second +matches; `-cli` excludes it from the third. For base `8.8`, the second and third +match. None of these rules excludes RC unless `-rc` is explicitly present. + +Repeated additions across matching rules are combined into one expected extension. +However, adding `remove: [spx]` to the third rule would create a conflict for +`8.6-rc-debug`, because the first rule adds `spx`. Moving either rule earlier or later +would not resolve it; use non-overlapping conditions or a justified exact exception. diff --git a/test-php.sh b/test-php.sh index 6add738..a913497 100755 --- a/test-php.sh +++ b/test-php.sh @@ -8,7 +8,7 @@ TAGS=( 8.6-rc 8.6-rc-cli 8.6-rc-debug ) -set -ux +set -u if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ -z "$1" ]; }; then echo "Usage: $0 [tag]" >&2 @@ -30,7 +30,7 @@ status=0 for tag in "${TAGS[@]}"; do image="jakubboucek/lamp-devstack-php:$tag" if ! docker run --rm -v "$(pwd)":/app:ro -w /app \ - -e "DEVSTACK_IMAGE=$image" \ + -e "IMAGE_NAME=$image" \ "$image" composer -d tools/ test; then status=1 fi diff --git a/tools/src/ExtensionInventory.php b/tools/src/ExtensionInventory.php index be87514..71c3ab8 100644 --- a/tools/src/ExtensionInventory.php +++ b/tools/src/ExtensionInventory.php @@ -49,7 +49,7 @@ public static function context(string $image, string $phpVersion): array $minor = $runtime[1] . '.' . $runtime[2]; $rc = $runtime[3] !== ''; if (!preg_match('~^[^\s@]+:([^/:]+)$~D', $image, $reference)) { - throw new InvalidArgumentException('DEVSTACK_IMAGE must contain the full image reference with a tag'); + throw new InvalidArgumentException('IMAGE_NAME must contain the full image reference with a tag'); } $tag = $reference[1]; if (preg_match('~^(\d+)(?:\.(\d+)(?:\.\d+)?)?(?:(-rc)(?:-(?:alpha|beta|RC)\d+)?)?(?:-(debug|cli))?$~D', $tag, $parts)) { diff --git a/tools/tests/ExtensionsTest.php b/tools/tests/ExtensionsTest.php index a23bac6..9aa16f7 100644 --- a/tools/tests/ExtensionsTest.php +++ b/tools/tests/ExtensionsTest.php @@ -9,7 +9,11 @@ require __DIR__ . '/bootstrap.php'; require_once __DIR__ . '/../src/ExtensionInventory.php'; -$context = ExtensionInventory::context(getenv('DEVSTACK_IMAGE') ?: '', PHP_VERSION); +$imageName = getenv('IMAGE_NAME'); + +Assert::type('string', $imageName, 'Docker Image `name:tag` not provided'); + +$context = ExtensionInventory::context(getenv('IMAGE_NAME') ?: '', PHP_VERSION); $inventory = new ExtensionInventory(Neon::decodeFile(__DIR__ . '/expected-extensions.neon')); $difference = $inventory->difference($context, get_loaded_extensions()); Assert::true($difference === ['missing' => [], 'unexpected' => []], diff --git a/tools/tests/expected-extensions.neon b/tools/tests/expected-extensions.neon index 85508fc..d33605c 100644 --- a/tools/tests/expected-extensions.neon +++ b/tools/tests/expected-extensions.neon @@ -1,4 +1,15 @@ # Format: ../../docs/extension-inventory.md + +# Exact-profile exceptions apply last. Missing mbstring is a build defect, not an exception. +exceptions: [] + +rules: + - when: ['>=8.5'] + add: [lexbor, uri] + + - when: [+debug] + add: [xdebug, spx] + common: - bcmath - bz2 @@ -53,13 +64,3 @@ common: - zend opcache - zip - zlib - -rules: - - when: ['>=8.5'] - add: [lexbor, uri] - - - when: [+debug] - add: [xdebug, spx] - -# Exact-profile exceptions apply last. Missing mbstring is a build defect, not an exception. -exceptions: {}