Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
68f775d
ci: generate and test windows-x64 artifacts (#59)
claude Aug 8, 2026
4712ff4
docs: document the windows-x64 support surface (#59)
claude Aug 8, 2026
f5ab1ac
fix(core): resolve engine artifacts and the CRT on Windows (#59)
claude Aug 8, 2026
b0fcf00
test: make the suite runnable on Windows (#59)
claude Aug 8, 2026
fca70b6
feat(gen): generate windows-x64 artifacts natively (#59)
claude Aug 8, 2026
5c617bc
fix(gen): run the probe by absolute path; enable ext-zip on Windows CI
claude Aug 8, 2026
fd3e1d3
fix(gen): define ZEND_DEBUG explicitly on Windows
claude Aug 8, 2026
75a3bba
Generate windows FFI engine definitions for PHP 8.4 (#59)
github-actions[bot] Aug 8, 2026
9e783e1
fix: LF-normalize windows artifacts, read MSVC atomic flags, bind the…
claude Aug 8, 2026
8cc0c30
Generate windows FFI engine definitions for PHP 8.4 (#59)
github-actions[bot] Aug 8, 2026
f9f59cc
ci: run against the LF-normalized windows artifacts
claude Aug 8, 2026
8419047
Merge pull request #180 from lisachenko/claude/z-engine-windows-support
lisachenko Aug 8, 2026
2c70d43
Merge remote-tracking branch 'origin/8.4' into claude/cascade-8.4-int…
claude Aug 8, 2026
1ea603f
fix(gen): declare the intsafe.h overflow helpers for clang on Windows
claude Aug 8, 2026
5b0dee9
docs: note the preload path does not exist on Windows
claude Aug 8, 2026
0eadf6a
fix(gen): enable intsafe.h signed helpers via the build define
claude Aug 8, 2026
453ba44
Generate windows FFI engine definitions for PHP 8.5 (#59)
github-actions[bot] Aug 8, 2026
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
148 changes: 148 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,92 @@ jobs:
if: steps.artifacts.outputs.present == 'true'
run: php -d ffi.enable=1 -d zend.assertions=1 -d opcache.jit=off tools/examples/worker-loop.php

tests-windows:
name: Tests (Windows x64, ${{ matrix.ts }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- { ts: nts, dll: php8.dll }
- { ts: zts, dll: php8ts.dll }
defaults:
run:
# Git Bash: keeps these step bodies identical to the other test jobs'.
shell: bash
steps:
- uses: actions/checkout@v4

# The windows artifacts can only be generated on Windows runners (see
# the "Generate windows headers" workflow). Until they are committed
# this leg warns and skips instead of failing, so the matrix keeps
# Windows visible without blocking unrelated work; once the artifacts
# land, every step below runs for real.
- name: Check the generated definitions are committed
id: artifacts
run: |
if [ -f "include/${PHP_MINOR}/windows-x64-${{ matrix.ts }}/engine.h" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::warning::include/${PHP_MINOR}/windows-x64-${{ matrix.ts }} is not committed yet - run the 'Generate windows headers' workflow (issue #59)"
fi

# Same setup as the Linux tests job: opcache loaded but inactive in the
# runner process, JIT off everywhere (see the comment there).
- name: Set up PHP
if: steps.artifacts.outputs.present == 'true'
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
if: steps.artifacts.outputs.present == 'true'
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
if: steps.artifacts.outputs.present == 'true'
run: |
php -r 'exit(extension_loaded("Zend OPcache") ? 0 : 1);' \
|| { echo "::error::Zend OPcache is not loaded - the opcache tests would silently skip"; exit 1; }

# Unlike Linux/macOS there is no process-image symbol resolution on
# Windows: FFI binds the engine DLL explicitly (Core passes it to
# FFI::cdef; the generated engine.h names it as FFI_LIB for the preload
# path). Prove the DLL binds and that this FFI build supports
# __vectorcall (ZEND_FASTCALL) with its @@N export mangling.
- name: FFI symbol-resolution preflight
if: steps.artifacts.outputs.present == 'true'
run: |
php -d ffi.enable=1 -r '$e = FFI::cdef("const char *get_zend_version(void);", "${{ matrix.dll }}"); echo $e->get_zend_version(), PHP_EOL;' \
|| { echo "::error::FFI cannot resolve engine symbols from ${{ matrix.dll }} on this build"; exit 1; }
php -d ffi.enable=1 -r 'FFI::cdef("extern unsigned long long __vectorcall zend_string_hash_func(void *str);", "${{ matrix.dll }}");' \
|| { echo "::error::FFI cannot bind a __vectorcall symbol from ${{ matrix.dll }} - this build lacks vectorcall support"; exit 1; }

- name: Install dependencies
if: steps.artifacts.outputs.present == 'true'
uses: ramsey/composer-install@v3

- name: Run test suite
if: steps.artifacts.outputs.present == 'true'
run: composer test

# No opcache/SHM non-skip gate here (unlike Linux/macOS): the file-cache
# relocator does not support Windows payloads and opcache.preload does
# not exist on Windows, so those tests self-skip by design. The gate
# lands together with the relocator port (issue #119).

- name: Worker-loop soak (flat memory after warm-up)
if: steps.artifacts.outputs.present == 'true'
run: php -d ffi.enable=1 -d zend.assertions=1 -d opcache.jit=off tools/examples/worker-loop.php

static-analysis:
name: PHPStan (level max)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -340,3 +426,65 @@ jobs:
echo "::error::Generated darwin engine definitions are out of date. Run the 'Generate darwin headers' workflow and merge its commit."
exit 1
fi

# The windows counterpart of header-drift: regenerates natively on a
# Windows runner (docker cannot produce windows artifacts) and diffs only
# the directory this runner can regenerate.
header-drift-windows:
name: Generated windows headers up to date (${{ matrix.ts }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
ts: [nts, zts]
defaults:
run:
shell: bash
steps:
# The committed artifacts are LF; keep the checkout byte-exact so the
# regeneration diff below compares content, not line endings.
- name: Keep line endings byte-exact
run: git config --global core.autocrlf input
- uses: actions/checkout@v4

- name: Check the generated definitions are committed
id: artifacts
run: |
if [ -f "include/${PHP_MINOR}/windows-x64-${{ matrix.ts }}/engine.h" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::warning::include/${PHP_MINOR}/windows-x64-${{ matrix.ts }} is not committed yet - run the 'Generate windows headers' workflow (issue #59)"
fi

- name: Set up PHP
if: steps.artifacts.outputs.present == 'true'
uses: shivammathur/setup-php@v2
env:
phpts: ${{ matrix.ts == 'zts' && 'ts' || 'nts' }}
with:
php-version: ${{ env.PHP_MINOR }}
# zip: generate.php extracts the PHP devel pack with ZipArchive
extensions: ffi, zip
ini-values: ffi.enable=1
coverage: none

# clang locates the MSVC CRT and Windows SDK headers through the INCLUDE
# environment variable this step exports.
- name: Set up the MSVC toolchain environment
if: steps.artifacts.outputs.present == 'true'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64

- name: Regenerate engine definitions natively
if: steps.artifacts.outputs.present == 'true'
run: php tools/generator/generate.php --native --php=${{ env.PHP_MINOR }} --ts=${{ matrix.ts }}

- name: Fail if the committed artifacts are stale
if: steps.artifacts.outputs.present == 'true'
run: |
if ! git diff --exit-code -- "include/${PHP_MINOR}/windows-x64-${{ matrix.ts }}"; then
echo "::error::Generated windows engine definitions are out of date. Run the 'Generate windows headers' workflow and merge its commit."
exit 1
fi
179 changes: 179 additions & 0 deletions .github/workflows/generate-windows-headers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: Generate windows headers

# Generates the windows-x64-{nts,zts} FFI engine definitions on real Windows
# runners and commits them back to the branch. Docker cannot produce these
# (containers are Linux by construction), so this workflow is the canonical
# way to create or refresh windows artifacts (issue #59).
#
# Triggers:
# - pull_request touching the generator: bootstraps/refreshes the artifacts on
# the PR branch itself (same-repo PRs only - fork PRs get the uploaded
# artifacts but no push).
# - workflow_dispatch: manual refresh; run it against the branch whose
# artifacts need regenerating.
#
# A push made with GITHUB_TOKEN does not retrigger workflows, so the commit
# job cannot loop; the next human push re-runs full CI against the artifacts.

on:
workflow_dispatch:
pull_request:
paths:
- 'tools/generator/**'
- '.github/workflows/generate-windows-headers.yml'

permissions:
contents: write

# This branch targets PHP 8.5 only - engine structures are version-specific.
env:
PHP_MINOR: '8.5'

jobs:
generate:
name: Generate windows-x64-${{ matrix.ts }}
strategy:
fail-fast: false
matrix:
# Official Windows builds are x64 only (no arm64 on windows.php.net);
# the engine lives in php8.dll (NTS) / php8ts.dll (ZTS), which the
# generated FFI_LIB line and the preflights below must name exactly.
include:
- { ts: nts, dll: php8.dll }
- { ts: zts, dll: php8ts.dll }
runs-on: windows-latest
defaults:
run:
# Git Bash: keeps these step bodies identical to the darwin workflow's.
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}

- name: Set up PHP
uses: shivammathur/setup-php@v2
env:
phpts: ${{ matrix.ts == 'zts' && 'ts' || 'nts' }}
with:
php-version: ${{ env.PHP_MINOR }}
# zip: generate.php extracts the PHP devel pack with ZipArchive
extensions: ffi, opcache, zip
ini-values: ffi.enable=1
coverage: none

- name: Assert the build matches the matrix leg (Windows, x64)
run: |
php -r 'exit(PHP_OS_FAMILY === "Windows" ? 0 : 1);' \
|| { echo "::error::expected a Windows build"; exit 1; }
php -r 'exit(PHP_INT_SIZE === 8 ? 0 : 1);' \
|| { echo "::error::expected a 64-bit build"; exit 1; }

# setup-php cannot provide every thread-safety mode on every runner. A
# leg whose requested mode is unavailable skips cleanly instead of
# failing: the commit job pushes whatever the other legs produced, and
# the missing artifacts self-heal on a later run once the build exists -
# CI's presence guards keep the gap visible as warnings, never silent
# green.
- name: Check the requested thread-safety mode is available
id: build
run: |
if php -r 'exit(ZEND_THREAD_SAFE === ("${{ matrix.ts }}" === "zts") ? 0 : 1);'; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::warning::setup-php has no ${{ matrix.ts }} PHP ${PHP_MINOR} build for windows-x64 - skipping this leg"
fi

# clang (in PATH on the runner image) targets x86_64-pc-windows-msvc but
# locates the MSVC CRT and Windows SDK headers through the INCLUDE
# environment variable, which this step exports for the whole job.
- name: Set up the MSVC toolchain environment
if: steps.build.outputs.available == 'true'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64

# Unlike Linux/macOS there is no process-image symbol resolution on
# Windows: FFI must bind the engine DLL explicitly. Prove both halves up
# front with clear messages - that the DLL binds at all, and that this
# FFI build supports __vectorcall (ZEND_FASTCALL) with its @@N export
# mangling, without which none of the fastcall engine symbols resolve.
- name: FFI symbol-resolution preflight
if: steps.build.outputs.available == 'true'
run: |
php -d ffi.enable=1 -r '$e = FFI::cdef("const char *get_zend_version(void);", "${{ matrix.dll }}"); echo $e->get_zend_version(), PHP_EOL;' \
|| { echo "::error::FFI cannot resolve engine symbols from ${{ matrix.dll }} on this build"; exit 1; }
php -d ffi.enable=1 -r 'FFI::cdef("extern unsigned long long __vectorcall zend_string_hash_func(void *str);", "${{ matrix.dll }}");' \
|| { echo "::error::FFI cannot bind a __vectorcall symbol from ${{ matrix.dll }} - this build lacks vectorcall support"; exit 1; }

- name: Generate windows artifacts
if: steps.build.outputs.available == 'true'
run: php tools/generator/generate.php --native --php=${{ env.PHP_MINOR }} --ts=${{ matrix.ts }}

# On failure, surface the intermediate build dir (emitted engine.h,
# preprocessed source, probe) so the broken declaration can be diagnosed
# from the run page without reproducing on Windows hardware.
- name: Stash the build directory for diagnosis
if: failure()
run: |
mkdir -p "$RUNNER_TEMP/gen-debug"
cp -R "$(cygpath -u "${TEMP:-$RUNNER_TEMP}")/"z-engine-generator-* "$RUNNER_TEMP/gen-debug/" 2>/dev/null || true
- uses: actions/upload-artifact@v4
if: failure()
with:
name: gen-debug-windows-x64-${{ matrix.ts }}
path: ${{ runner.temp }}/gen-debug
if-no-files-found: ignore

- name: Smoke-check the artifacts
if: steps.build.outputs.available == 'true'
run: |
dir="include/${PHP_MINOR}/windows-x64-${{ matrix.ts }}"
for file in engine.h constants.php layouts.json probe.c; do
test -s "${dir}/${file}" || { echo "::error::${dir}/${file} is missing or empty"; exit 1; }
done
grep -q 'FFI_SCOPE "ZEngine"' "${dir}/engine.h" \
|| { echo "::error::${dir}/engine.h has no FFI_SCOPE declaration"; exit 1; }
grep -q 'FFI_LIB "${{ matrix.dll }}"' "${dir}/engine.h" \
|| { echo "::error::${dir}/engine.h does not name ${{ matrix.dll }} as FFI_LIB - symbols would not resolve on Windows"; exit 1; }

- uses: actions/upload-artifact@v4
if: steps.build.outputs.available == 'true'
with:
name: windows-x64-${{ matrix.ts }}
path: include/${{ env.PHP_MINOR }}/windows-x64-${{ matrix.ts }}
if-no-files-found: error

# A single commit job (instead of each runner pushing) serializes all
# matrix legs into one commit and eliminates the push race between them.
commit:
name: Commit generated artifacts
needs: generate
# Pushing requires same-repo credentials; fork PRs stop at the uploads.
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}

- uses: actions/download-artifact@v4
with:
pattern: windows-*
path: include/${{ env.PHP_MINOR }}/

- name: Commit and push if the artifacts changed
env:
TARGET_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -- "include/${PHP_MINOR}"
if git diff --cached --quiet; then
echo "Generated artifacts are identical to the committed ones - nothing to do."
exit 0
fi
git commit -m "Generate windows FFI engine definitions for PHP ${PHP_MINOR} (#59)"
git pull --rebase origin "${TARGET_REF}"
git push origin "HEAD:${TARGET_REF}"
Loading
Loading