Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 75 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,21 @@ env:

jobs:
tests:
name: Tests (release, NTS)
name: Tests (release, ${{ matrix.ts_label }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- ts: nts
ts_label: NTS
opcache_gate: composer test:opcache
- ts: zts
ts_label: ZTS
# The file-cache relocator does not support ZTS payloads yet
# (issue #118): its tests self-skip on ZTS, so the non-skip gate
# excludes that group while still proving the SHM tests ran
opcache_gate: composer test:opcache-zts
steps:
- uses: actions/checkout@v4

Expand All @@ -28,12 +41,19 @@ jobs:
# off everywhere: it rewrites the executor internals z-engine hooks into
# (AGENTS.md).
uses: shivammathur/setup-php@v2
env:
phpts: ${{ matrix.ts == 'zts' && 'ts' || 'nts' }}
with:
php-version: ${{ env.PHP_MINOR }}
extensions: ffi, opcache
ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0
coverage: none

- name: Assert the thread-safety mode matches the matrix leg
run: |
php -r 'exit(ZEND_THREAD_SAFE === ("'"${{ matrix.ts }}"'" === "zts") ? 0 : 1);' \
|| { echo "::error::PHP thread-safety mode does not match the ${{ matrix.ts }} matrix leg"; exit 1; }

- name: Assert the opcache extension is available to the test runner
run: |
php -r 'exit(extension_loaded("Zend OPcache") ? 0 : 1);' \
Expand All @@ -50,7 +70,7 @@ jobs:
# the child process, fixture not published from shared memory), so without
# this step a broken opcache setup reads as a green run.
- name: Opcache/SHM coverage must not silently skip
run: composer test:opcache
run: ${{ matrix.opcache_gate }}

- name: Worker-loop soak (flat memory after warm-up)
run: php -d ffi.enable=1 -d zend.assertions=1 -d opcache.jit=off tools/examples/worker-loop.php
Expand Down Expand Up @@ -83,14 +103,26 @@ jobs:
- run: composer cs:check

tests-internal-debug:
name: Internal group (debug build)
name: Internal group (debug build, ${{ matrix.ts_label }})
runs-on: ubuntu-latest
# The segfault-prone destructive tests run only against a --enable-debug
# PHP build, where corruption surfaces as assertion failures. There is no
# official debug PHP image, so we build one here from the official image's
# own source with a plain `docker build` (the daemon's built-in BuildKit -
# no buildx setup, no moby/buildkit pull, fewer flaky Docker Hub round trips)
# and run the tests in it.
# own source (a full PHP compile) and run the tests in it. The build goes
# through buildx so its layers can be cached across runs with actions/cache
# - a warm cache turns the multi-minute compile into a layer restore.
strategy:
fail-fast: false
matrix:
include:
- ts: nts
ts_label: NTS
# ZTS excludes the relocator tests (no ZTS payload support yet,
# issue #118) but still gates the SHM coverage against silent skips
opcache_args: --group opcache --fail-on-skipped
- ts: zts
ts_label: ZTS
opcache_args: --group opcache --exclude-group opcache-relocator --fail-on-skipped
steps:
- uses: actions/checkout@v4

Expand All @@ -106,13 +138,36 @@ jobs:
- name: Install dependencies
uses: ramsey/composer-install@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Restore the debug image layer cache
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: docker-debug-${{ matrix.ts }}-${{ env.PHP_MINOR }}-${{ hashFiles('tools/docker/php-debug.Dockerfile') }}
restore-keys: |
docker-debug-${{ matrix.ts }}-${{ env.PHP_MINOR }}-

- name: Build the debug PHP image
run: |
docker build \
mkdir -p /tmp/.buildx-cache
docker buildx build \
-f tools/docker/php-debug.Dockerfile \
--build-arg "PHP_VERSION=${PHP_MINOR}" \
--build-arg "PHP_TS=${{ matrix.ts }}" \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache-new,mode=max" \
--load \
-t z-engine-php:debug .

# Rotate instead of appending: a reused local cache dir accumulates stale
# layers without bound, so each run saves only the layers it actually used
- name: Rotate the layer cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

- name: Run internal group with process isolation
run: |
docker run --rm -v "$PWD:/app" -w /app z-engine-php:debug \
Expand All @@ -125,7 +180,7 @@ jobs:
- name: Run opcache/SHM group on the debug build
run: |
docker run --rm -v "$PWD:/app" -w /app z-engine-php:debug \
vendor/bin/phpunit --group opcache --fail-on-skipped
vendor/bin/phpunit ${{ matrix.opcache_args }}

header-drift:
name: Generated headers up to date
Expand All @@ -134,8 +189,20 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Caches the generator image's toolchain layers (apt/clang/ext-ffi); the
# emit stage itself always re-runs - it is the step that produces the
# artifacts being drift-checked. generate.php rotates per-target subdirs.
- name: Restore the generator image layer cache
uses: actions/cache@v4
with:
path: /tmp/.buildx-gen-cache
key: docker-gen-${{ env.PHP_MINOR }}-${{ hashFiles('tools/generator/Dockerfile') }}
restore-keys: |
docker-gen-${{ env.PHP_MINOR }}-
- name: Regenerate engine definitions
run: composer gen-headers
env:
Z_ENGINE_BUILDX_CACHE_DIR: /tmp/.buildx-gen-cache
- name: Fail if the committed artifacts are stale
run: |
if ! git diff --exit-code -- include/; then
Expand Down
19 changes: 17 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ Everything under `include/<minor>/<os>-<arch>-<ts>/` is generated:
| `layouts.json` | `sizeof`/`offsetof` of every dereferenced struct, from the C compiler |
| `probe.c` | the generated C probe (kept so a probe-only run can reuse it) |

Regenerate them with:
This branch maintains **two thread-safety targets**: `linux-x64-nts` and
`linux-x64-zts` (the manifest in `tools/generator/symbols.php` is
thread-safety-aware — on ZTS the per-thread EG/CG are reached through the TSRM
offsets instead of the plain extern symbols, see issue #60). Regenerate them
with:

```bash
composer gen-headers # all targets for this branch (needs Docker)
Expand Down Expand Up @@ -82,12 +86,19 @@ structs (`Zend/zend_closures.c`, `ext/opcache/ZendAccelerator.h`,
PHP version, ext-ffi. Fetch the matching `Zend/zend_closures.c`,
`ext/opcache/ZendAccelerator.h` and `ext/opcache/zend_file_cache.c` (and
nothing else) from `raw.githubusercontent.com/php/php-src/php-<version>/`.
For the `zts` target the running PHP must itself be a matching
`--enable-zts` **release** build of the same minor (emit.php derives the
thread-safety mode, the TSRM symbols and the layouts from the interpreter
it runs under).
2. First run against the **committed** `symbols.php` into a scratch directory
and `diff` against `include/<minor>/<platform>/` — the output must be
byte-identical (it was, verified on Ubuntu clang-18 vs the trixie image:
the emitter normalizes declarations from the clang AST, so compiler
version does not leak into the artifacts). If the diff is clean, host
regeneration is equivalent to the Docker/CI pipeline for this host.
regeneration is equivalent to the Docker/CI pipeline for this host. An
NTS pre-check validates the host pipeline for the ZTS target too — the
emitter and probe code paths are identical, only the manifest branch
differs.
3. Only then apply the `symbols.php` change and regenerate into `include/`
for real: `php -d memory_limit=2G tools/generator/emit.php
--php-src=<dir> [--out=...]`.
Expand Down Expand Up @@ -115,6 +126,10 @@ composer test:internal # destructive/segfault-prone group, process-isolated
- `ZENGINE_STRICT_LAYOUT_CHECK=1` (set in the test bootstrap) makes
`Core::init()` verify every struct layout against `layouts.json` before
touching engine memory — the anti-segfault airbag. Keep it on in development.
- On **ZTS** builds the file-cache relocator tests (`opcache-relocator` group)
self-skip — ZTS payloads are not supported yet (issue #118). The non-skip
gate for the remaining opcache/SHM coverage is `composer test:opcache-zts`;
CI runs both release and debug test legs on NTS **and** ZTS.

## Quality gates (all enforced in CI)

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ FFI lets PHP load shared libraries, call C functions, and read C structures with
## Requirements & support matrix

- PHP with the **FFI** extension enabled
- **x64, non-thread-safe (NTS)** builds
- **x64** builds, **NTS and ZTS** (on ZTS the opcache file-cache relocator is not yet supported — [#118](https://github.com/lisachenko/z-engine/issues/118))

Engine memory layouts change between every PHP minor version, so each PHP minor has its own generated definitions and its own branch.

| PHP | OS / Arch / TS | Branch | Status |
|-----|----------------|--------|--------|
| 8.5 | linux-x64-nts | `master` | ✅ supported |
| 8.4 | linux-x64-nts | `8.4` | ✅ supported |
| 8.5 | linux-x64-nts, linux-x64-zts | `master` | ✅ supported |
| 8.4 | linux-x64-nts, linux-x64-zts | `8.4` | ✅ supported |
| 8.0 | linux-x64-nts | `8.0` | 🧊 frozen (legacy) |
| macOS / Windows / ZTS | — | — | 📋 [tracked in issues](https://github.com/lisachenko/z-engine/issues) |
| macOS / Windows | — | — | 📋 [tracked in issues](https://github.com/lisachenko/z-engine/issues) |

> **Version matching is not optional.** Running Z-Engine against a PHP minor it was not built for corrupts memory. `Core::init()` enforces the match and aborts with a clear message rather than letting you crash.

Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"test": "phpunit",
"test:internal": "phpunit --group internal --process-isolation",
"test:opcache": "phpunit --group opcache --fail-on-skipped",
"test:opcache-zts": "phpunit --group opcache --exclude-group opcache-relocator --fail-on-skipped",
"phpstan": "phpstan analyse",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
Expand All @@ -45,6 +46,7 @@
"test": "Run the test suite (segfault-prone internal group excluded)",
"test:internal": "Run the segfault-prone internal test group with process isolation (use a debug PHP build)",
"test:opcache": "Run the opcache/shared-memory tests and FAIL if any of them skipped (they are self-skipping when opcache is unavailable)",
"test:opcache-zts": "Same non-skip gate for ZTS builds: excludes the file-cache relocator tests, which do not support ZTS payloads yet (issue #118)",
"phpstan": "Run static analysis at the maximum level",
"cs:check": "Check coding standards without fixing",
"cs:fix": "Fix coding standards",
Expand Down
Loading
Loading