Skip to content

Feature/exp log improvements #88

Feature/exp log improvements

Feature/exp log improvements #88

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
# Cancel in-progress runs on the same branch to save CI minutes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# =========================================================================
# Linux matrix: Ubuntu (22.04 / 24.04) × PHP (8.1–8.5) × CPU / CUDA
# 20 combinations.
# =========================================================================
test:
name: "PHP ${{ matrix.php }} / ${{ matrix.os }} / ${{ matrix.cuda && 'CUDA' || 'CPU' }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
cuda: [false, true]
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: gd
tools: none
coverage: none
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y \
build-essential \
autoconf \
liblapacke-dev \
libopenblas-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
libfreetype6-dev \
libxpm-dev
# nvidia-cuda-toolkit provides libcublas + development headers on Ubuntu 22/24/26.
# GPU tests are skipped at runtime (no hardware on standard runners); this job
# validates that the CUDA build path compiles and links correctly.
- name: Install CUDA toolkit
if: matrix.cuda
run: sudo apt-get install -y nvidia-cuda-toolkit
- name: Build extension
run: |
phpize
./configure ${{ matrix.cuda && '--with-cuda' || '' }}
make -j$(nproc)
# When cublas is detected, the standard PHP build links ndarray.so without
# the .cu objects (cuda_math.cu / cuda_dnn.cu) — only install-cuda does that,
# and it targets the system extension dir. cuda-modules rebuilds everything
# with nvcc and drops the result into modules/ndarray.so so that make test
# picks it up without a system install.
- name: Link CUDA objects into extension
if: matrix.cuda
run: |
if grep -q "HAVE_CUBLAS 1" config.h; then
echo "cublas detected — rebuilding modules/ndarray.so with CUDA objects"
make cuda-modules
else
echo "::warning::cublas not found; extension built without GPU support"
fi
# `make test` builds tmp-php.ini from the host PHP's loaded ini + conf.d,
# but the upstream phpize Makefile filters out every `^extension=` line via
# PHP_DEPRECATED_DIRECTIVES_REGEX — so even though setup-php enabled `gd`
# globally, the resulting tmp-php.ini lands GD-less and every
# `tests/image/*.phpt` skips with "GD extension not loaded".
#
# Fix: bypass the Makefile's tmp-php.ini layer for GD by injecting it via
# PHP_TEST_SHARED_EXTENSIONS (the variable run-tests.php propagates as `-d`
# to each child test process). We must also re-include the project's own
# `-d extension=ndarray.so` because we're replacing the default value of
# the variable, not appending to it.
#
# The probe matters: Ubuntu's shivammathur PHP loads GD dynamically via
# `/etc/php/.../conf.d/20-gd.ini`, so `php -n -m` does NOT list `gd` and
# we inject the absolute `$extension_dir/gd.so` path. macOS Homebrew PHP
# links GD statically into the binary, so `php -n -m` lists `gd` and
# `extension_dir` points at the PECL dir where no separate gd.so file
# exists — injecting the path there triggers a startup warning that
# corrupts every test's expected output. Detecting "already loaded"
# keeps both matrices clean.
#
# REPORT_EXIT_STATUS=1 is required: without it run-tests.php always exits 0.
- name: Run tests
run: |
EXTRA_EXT=""
if ! php -n -m 2>/dev/null | grep -qi '^gd$'; then
GD_PATH=$(php -r 'echo ini_get("extension_dir") . "/gd.so";')
if [ -f "$GD_PATH" ]; then
EXTRA_EXT=" -d extension=$GD_PATH"
fi
fi
REPORT_EXIT_STATUS=1 make test \
PHP_TEST_SHARED_EXTENSIONS="$EXTRA_EXT -d extension=ndarray.so" \
TESTS="-q --show-diff"
- name: Upload failure diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: "diffs-php${{ matrix.php }}-${{ matrix.os }}-${{ matrix.cuda && 'cuda' || 'cpu' }}"
path: "**/*.diff"
if-no-files-found: ignore
# =========================================================================
# macOS matrix: macos-14 / macos-15 (both Apple Silicon ARM64) × PHP 8.1–8.5.
# 10 combinations. CPU only — CUDA is not available on macOS.
#
# Apple clang on ARM64 falls through the long-double fallback in
# src/ndarray_types.h (no __float128, no libquadmath). The HAVE_AVX2 macro
# is left undefined on non-x86 hosts by config.m4, so the AVX2 kernels are
# excluded from the build. OpenBLAS + LAPACK come from Homebrew.
# =========================================================================
macos-test:
name: "PHP ${{ matrix.php }} / ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-14, macos-15]
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: gd
tools: none
coverage: none
# Homebrew ships its own openblas + lapack kegs that include LAPACKE
# headers and the libopenblas.dylib symbol set we need (cblas_sdot,
# LAPACKE_sgesdd). Apple's vecLib/Accelerate framework would be lighter
# weight but doesn't export the LAPACKE_* C bindings.
- name: Install OpenBLAS + LAPACK (Homebrew)
run: |
brew update
brew install openblas lapack autoconf
- name: Build extension
run: |
OPENBLAS_PREFIX=$(brew --prefix openblas)
LAPACK_PREFIX=$(brew --prefix lapack)
export CPPFLAGS="-I${OPENBLAS_PREFIX}/include -I${LAPACK_PREFIX}/include ${CPPFLAGS}"
export LDFLAGS="-L${OPENBLAS_PREFIX}/lib -L${LAPACK_PREFIX}/lib ${LDFLAGS}"
export LIBRARY_PATH="${OPENBLAS_PREFIX}/lib:${LAPACK_PREFIX}/lib:${LIBRARY_PATH}"
export PKG_CONFIG_PATH="${OPENBLAS_PREFIX}/lib/pkgconfig:${LAPACK_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}"
phpize
./configure
make -j$(sysctl -n hw.ncpu)
# Same GD-loading workaround as the Linux job — see the long comment
# there for why `make test`'s tmp-php.ini lands GD-less and how
# PHP_TEST_SHARED_EXTENSIONS gets it back in front of each child test.
# On macOS, Homebrew PHP links GD statically; the probe below skips
# the inject so we don't fight a non-existent /opt/homebrew/.../gd.so.
- name: Run tests
run: |
EXTRA_EXT=""
if ! php -n -m 2>/dev/null | grep -qi '^gd$'; then
GD_PATH=$(php -r 'echo ini_get("extension_dir") . "/gd.so";')
if [ -f "$GD_PATH" ]; then
EXTRA_EXT=" -d extension=$GD_PATH"
fi
fi
REPORT_EXIT_STATUS=1 make test \
PHP_TEST_SHARED_EXTENSIONS="$EXTRA_EXT -d extension=ndarray.so" \
TESTS="-q --show-diff"
- name: Upload failure diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: "diffs-php${{ matrix.php }}-${{ matrix.os }}"
path: "**/*.diff"
if-no-files-found: ignore
# =========================================================================
# Windows matrix: Windows Server 2022 / 2025 × PHP 8.1–8.5 × CPU / CUDA.
# 20 combinations. CUDA jobs install the NVIDIA Toolkit via Jimver's action,
# run build-cuda-windows.bat to pre-compile the .cu sources, then nmake
# links them in. GitHub-hosted Windows runners have no GPU, so the GPU
# PHPT tests SKIP at runtime — same behaviour as the Linux CUDA matrix.
# =========================================================================
windows-test:
name: "PHP ${{ matrix.php }} / ${{ matrix.os }} / ${{ matrix.cuda && 'CUDA' || 'CPU' }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
cuda: [false, true]
steps:
- uses: actions/checkout@v4
# Sets %PHP_SDK_ROOT% / %PHP_PREFIX%, downloads the matching PHP devel
# pack, configures the right Visual Studio toolset (vs16/vs17) and
# exposes phpize.bat + nmake on PATH for subsequent steps.
- name: Set up PHP build environment
uses: php/setup-php-sdk@v0.12
id: php-sdk
with:
version: ${{ matrix.php }}
arch: x64
ts: nts
- name: Set up MSVC developer prompt
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
toolset: ${{ steps.php-sdk.outputs.toolset }}
# OpenBLAS for Windows: download the upstream pre-built x64 release.
# The ZIP contains include/, lib/libopenblas.lib + libopenblas.dll.a,
# bin/libopenblas.dll — config.w32 finds these via --with-openblas-dir.
- name: Cache OpenBLAS
id: openblas-cache
uses: actions/cache@v4
with:
path: openblas
key: openblas-0.3.30-windows-x64
- name: Download OpenBLAS prebuilt
if: steps.openblas-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
$url = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.30/OpenBLAS-0.3.30-x64.zip"
Invoke-WebRequest -Uri $url -OutFile openblas.zip
New-Item -ItemType Directory -Path openblas -Force | Out-Null
Expand-Archive -Path openblas.zip -DestinationPath openblas -Force
# CUDA Toolkit (network installer with a minimal sub-package list so the
# runner only downloads ~1.5 GB instead of the full 3+ GB). The action
# exposes %CUDA_PATH% for the rest of the steps. cuDNN is not installed
# by default — the GPU DNN tests will skip, matching the cudnn-less
# build path on the Linux CUDA matrix runners.
- name: Install CUDA Toolkit
if: matrix.cuda
uses: Jimver/cuda-toolkit@v0.2.19
id: cuda-toolkit
with:
cuda: '12.5.0'
method: 'network'
sub-packages: '["nvcc", "cublas", "cublas_dev", "cudart", "thrust", "curand", "curand_dev", "cusolver", "cusolver_dev", "cusparse", "nvjitlink", "visual_studio_integration"]'
- name: phpize
shell: cmd
run: |
phpize
- name: configure (CPU)
if: '!matrix.cuda'
shell: cmd
run: |
configure --enable-ndarray --with-openblas-dir=%CD%\openblas --with-prefix=${{ steps.php-sdk.outputs.prefix }}
- name: configure (CUDA)
if: matrix.cuda
shell: cmd
env:
PHP_PREFIX: ${{ steps.php-sdk.outputs.prefix }}
run: |
configure --enable-ndarray --with-openblas-dir=%CD%\openblas --with-cuda --with-cuda-dir="%CUDA_PATH%" --with-prefix=%PHP_PREFIX%
# Pre-compile the .cu sources into ndarray_cuda.lib. Must run BEFORE
# nmake, since config.w32 added ndarray_cuda.lib to LIBS_NDARRAY and
# nmake will fail at link time if the file is missing. The .bat reads
# %CUDA_PATH% (set by the cuda-toolkit action) and %PHP_PREFIX% (set
# below) directly from the environment — passing them as CLI flags
# would hit cmd.exe's quirk of treating `=` as an argument delimiter.
- name: Build CUDA objects
if: matrix.cuda
shell: cmd
env:
PHP_PREFIX: ${{ steps.php-sdk.outputs.prefix }}
run: |
call build-cuda-windows.bat
- name: nmake
shell: cmd
run: |
nmake
# The PHP test runner loads the freshly-built php_ndarray.dll from the
# build output dir; the OpenBLAS DLL (and on CUDA builds the cublas /
# cudart / cudnn DLLs) have to be co-located or on PATH for the loader
# to resolve cblas_* / LAPACKE_* / cublas* / cudnn* at runtime.
- name: Stage runtime DLLs next to extension
shell: pwsh
run: |
# PHP on Windows calls SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
# early in startup, which kills PATH-based search for extension transitive
# deps. After that, dependency resolution only looks in:
# 1. The directory of php.exe (= %PHP_PREFIX% = php-bin\)
# 2. C:\Windows\System32
# 3. Dirs added via AddDllDirectory() — PHP adds none.
# So every runtime DLL must end up in php-bin\. The copies into x64\Release\
# are belt & suspenders for tools that bypass the hardened loader.
$buildDir = if (Test-Path 'x64\Release') { 'x64\Release' } else { 'x64\Release_TS' }
$phpBin = "${{ steps.php-sdk.outputs.prefix }}"
Write-Host "=== Staging targets ==="
Write-Host " buildDir = $buildDir"
Write-Host " phpBin = $phpBin"
Write-Host "=== Copying libopenblas.dll ==="
Copy-Item openblas\bin\libopenblas.dll "$buildDir\" -Force -Verbose
Copy-Item openblas\bin\libopenblas.dll "$phpBin\" -Force -Verbose
if ($env:RUNNER_CUDA -eq 'true') {
$cudaBin = Join-Path $env:CUDA_PATH 'bin'
Write-Host "=== Source CUDA bin: $cudaBin ==="
Get-ChildItem -Path $cudaBin -Filter '*.dll' | Select-Object -ExpandProperty Name | Sort-Object | Out-Host
$filters = @(
'cublas64_*.dll', # direct link dep
'cudart64_*.dll', # direct link dep
'cublasLt64_*.dll', # transitive dep of cublas
'cusolver64_*.dll', # direct link dep
'curand64_*.dll', # direct link dep
'cusparse64_*.dll', # transitive dep of cusolver
'nvJitLink_*.dll', # transitive dep of cublas/cusolver on CUDA 12+
'cudnn64_*.dll' # optional, when cuDNN sub-package was installed
)
foreach ($filter in $filters) {
$matched = Get-ChildItem -Path $cudaBin -Filter $filter -ErrorAction SilentlyContinue
if (-not $matched) {
Write-Host " $filter -> no match"
continue
}
foreach ($f in $matched) {
Write-Host " Copying $($f.Name) -> $buildDir\ and $phpBin\"
Copy-Item -Path $f.FullName -Destination "$buildDir\" -Force
Copy-Item -Path $f.FullName -Destination "$phpBin\" -Force
}
}
}
Write-Host ""
Write-Host "=== Files in $phpBin (final) ==="
Get-ChildItem -Path $phpBin -Filter '*.dll' | Select-Object -ExpandProperty Name | Sort-Object | Out-Host
env:
RUNNER_CUDA: ${{ matrix.cuda }}
- name: Show extension's transitive DLL deps
shell: pwsh
run: |
$buildDir = if (Test-Path 'x64\Release') { 'x64\Release' } else { 'x64\Release_TS' }
$dll = Join-Path $buildDir 'php_ndarray.dll'
Write-Host "=== dumpbin /dependents $dll ==="
dumpbin /dependents $dll
if ($env:RUNNER_CUDA -eq 'true') {
# Also dump deps of the CUDA libs themselves — these are transitive
# deps of php_ndarray.dll that the loader needs to resolve too.
# If something here is missing from php-bin\ or System32, that's
# the source of "module could not be found".
$phpBin = "${{ steps.php-sdk.outputs.prefix }}"
foreach ($name in @('cublas64_12.dll', 'cusolver64_11.dll', 'cudart64_12.dll', 'curand64_10.dll')) {
$p = Join-Path $phpBin $name
if (Test-Path $p) {
Write-Host ""
Write-Host "=== dumpbin /dependents $p ==="
dumpbin /dependents $p
}
}
}
env:
RUNNER_CUDA: ${{ matrix.cuda }}
- name: Run tests
shell: cmd
env:
REPORT_EXIT_STATUS: 1
run: |
nmake test TESTS="-q --show-diff"
- name: Upload failure diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: "diffs-php${{ matrix.php }}-${{ matrix.os }}-win-x64-${{ matrix.cuda && 'cuda' || 'cpu' }}"
path: "**/*.diff"
if-no-files-found: ignore
# =========================================================================
# Gate job — the ONLY required status check for branch protection.
# =========================================================================
all-pass:
name: All required tests passed
needs: [test, macos-test, windows-test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check required matrix results
run: |
fail=0
for label in "linux:${{ needs.test.result }}" \
"macos:${{ needs.macos-test.result }}" \
"windows:${{ needs.windows-test.result }}"; do
name="${label%%:*}"
result="${label##*:}"
echo "${name}: ${result}"
if [[ "${result}" != "success" ]]; then
fail=1
fi
done
if [[ $fail -ne 0 ]]; then
echo "::error::One or more required matrices failed or were cancelled."
exit 1
fi
echo "All required Linux + macOS + Windows combinations passed."