Skip to content

Build PHP 8.3.31 with Xdebug 3.5.1 #41

Build PHP 8.3.31 with Xdebug 3.5.1

Build PHP 8.3.31 with Xdebug 3.5.1 #41

name: Build PHP CLI Binaries
run-name: Build PHP ${{ inputs.php_version }} with Xdebug ${{ inputs.xdebug_version }}
on:
workflow_dispatch:
inputs:
php_version:
description: PHP patch version to build
required: true
default: 8.4.20
xdebug_version:
description: Xdebug version to package
required: true
default: 3.5.1
spc_ref:
description: static-php-cli ref to use for the macOS build
required: true
default: 2.8.5
debug:
description: Enable verbose build logs
required: true
type: boolean
default: false
upload_to_apps_cdn:
description: Upload to Apps CDN
required: true
type: boolean
default: false
apps_cdn_visibility:
description: Apps CDN visibility
required: true
type: choice
options:
- internal
- external
default: internal
permissions:
contents: read
env:
GITHUB_TOKEN: ${{ github.token }}
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 240
strategy:
fail-fast: false
matrix:
include:
- target: macos-aarch64
runner: macos-15
artifact_platform: macos
artifact_arch: aarch64
extensions: &macos_extensions apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gettext,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,shmop,simplexml,sockets,sodium,sqlite3,ssh2,tokenizer,xml,xmlreader,xmlwriter,xsl,yaml,zip,zlib
- target: macos-x86_64
runner: macos-15-intel
artifact_platform: macos
artifact_arch: x86_64
extensions: *macos_extensions
- target: windows-x86_64
runner: windows-latest
artifact_platform: windows
artifact_arch: x86_64
extensions: apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pdo,pdo_mysql,pdo_sqlite,phar,redis,session,shmop,simplexml,sockets,sodium,sqlite3,ssh2,tokenizer,xml,xmlreader,xmlwriter,yaml,zip,zlib
env:
PHP_VERSION: ${{ inputs.php_version }}
XDEBUG_VERSION: ${{ inputs.xdebug_version }}
SPC_REF: ${{ inputs.spc_ref }}
PHP_CLI_EXTENSIONS: ${{ matrix.extensions }}
PHP_CLI_ARTIFACT_PLATFORM: ${{ matrix.artifact_platform }}
PHP_CLI_ARTIFACT_ARCH: ${{ matrix.artifact_arch }}
BUILD_DEBUG: ${{ inputs.debug }}
steps:
- name: Checkout Studio
uses: actions/checkout@v6
- name: Checkout static-php-cli
if: runner.os == 'macOS'
uses: actions/checkout@v6
with:
repository: crazywhalecc/static-php-cli
ref: ${{ env.SPC_REF }}
path: .cache/static-php-cli
- name: Validate PHP version
shell: bash
run: |
set -euo pipefail
if [[ ! "$PHP_VERSION" =~ ^8\.(2|3|4|5)\.[0-9]+$ ]]; then
echo "Unsupported PHP version: $PHP_VERSION" >&2
echo "Supported PHP minors: 8.2, 8.3, 8.4, 8.5" >&2
exit 1
fi
php_minor="${PHP_VERSION%.*}"
echo "PHP_MINOR=$php_minor" >> "$GITHUB_ENV"
- name: Setup PHP for static-php-cli
if: runner.os == 'macOS'
uses: shivammathur/setup-php@v2
with:
coverage: none
tools: composer:v2
php-version: 8.4
ini-values: memory_limit=-1
extensions: curl, openssl, mbstring
env:
phpts: nts
- name: Install static-php-cli dependencies
if: runner.os == 'macOS'
working-directory: .cache/static-php-cli
run: composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative
- name: Build macOS PHP runtime with static-php-cli
if: runner.os == 'macOS'
working-directory: .cache/static-php-cli
shell: bash
run: |
set -euo pipefail
if [ "$BUILD_DEBUG" = "true" ]; then
set -x
fi
# mbregex, readline, and zlib declare target: ["static"] in SPC's
# config/ext.json and must be baked into PHP. mbstring is forced static
# to work around a missing-space bug in SPC's mbregex validation
# (src/SPC/builder/extension/mbregex.php:25 emits `php -n-d ...` when
# mbstring is shared). Everything else — including Xdebug, which is
# target: ["shared"] — is built as a loadable .so under buildroot/modules.
forced_static_exts="mbregex mbstring readline zlib"
static_list=()
shared_list=()
IFS=',' read -ra all_exts <<< "$PHP_CLI_EXTENSIONS"
for ext in "${all_exts[@]}"; do
ext_trimmed="${ext// /}"
is_static=false
for s in $forced_static_exts; do
if [ "$ext_trimmed" = "$s" ]; then
is_static=true
break
fi
done
if $is_static; then
static_list+=("$ext_trimmed")
else
shared_list+=("$ext_trimmed")
fi
done
static_extensions=$(IFS=','; echo "${static_list[*]}")
shared_extensions=$(IFS=','; echo "${shared_list[*]}")
download_args=(
--with-php="$PHP_VERSION"
--for-extensions="${PHP_CLI_EXTENSIONS}"
--custom-url="xdebug:https://xdebug.org/files/xdebug-${XDEBUG_VERSION}.tgz"
--ignore-cache-sources=php-src
--prefer-pre-built
)
build_args=(
"$static_extensions"
--build-shared="$shared_extensions"
--build-cli
)
if [ "$BUILD_DEBUG" = "true" ]; then
download_args+=(--debug)
build_args+=(--debug)
fi
php bin/spc doctor --auto-fix
php bin/spc download "${download_args[@]}"
php bin/spc build "${build_args[@]}"
- name: Package macOS artifact
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
spc_root="$GITHUB_WORKSPACE/.cache/static-php-cli/buildroot"
staging_dir="$RUNNER_TEMP/php-artifact"
out_dir="$GITHUB_WORKSPACE/out/php-binaries"
artifact="php-${PHP_VERSION}-cli-${PHP_CLI_ARTIFACT_PLATFORM}-${PHP_CLI_ARTIFACT_ARCH}.zip"
rm -rf "$staging_dir"
mkdir -p "$staging_dir/ext" "$out_dir"
cp "$spc_root/bin/php" "$staging_dir/php"
chmod +x "$staging_dir/php"
cp "$spc_root/modules"/*.so "$staging_dir/ext/"
extension_dir="$staging_dir/ext"
find_macho_files() {
find "$staging_dir" -type f \( -name '*.dylib' -o -name '*.so' -o -perm -111 \) -print | while read -r candidate; do
if file "$candidate" | grep -q 'Mach-O'; then
printf '%s\n' "$candidate"
fi
done
}
find_macho_files | while read -r file; do
if otool -L "$file" | grep -E '/opt/homebrew/|/usr/local/'; then
echo "Found non-portable Homebrew dependency in $file" >&2
exit 1
fi
if otool -L "$file" | grep -F "$spc_root"; then
echo "Found non-portable static-php-cli dependency in $file" >&2
exit 1
fi
done
xattr -cr "$staging_dir"
find_macho_files | while read -r file; do
codesign --force --sign - "$file"
codesign --verify --strict "$file"
done
cat > "$staging_dir/runtime.json" <<JSON
{
"phpVersion": "$PHP_VERSION",
"phpMinor": "$PHP_MINOR",
"threadSafety": "nts",
"platform": "$PHP_CLI_ARTIFACT_PLATFORM",
"arch": "$PHP_CLI_ARTIFACT_ARCH",
"binary": "php",
"extensionDir": "ext",
"xdebug": "ext/xdebug.so"
}
JSON
"$staging_dir/php" -n \
-d "extension_dir=$extension_dir" \
-d "zend_extension=$extension_dir/xdebug.so" \
-d "xdebug.mode=debug" \
--ri xdebug
echo "Smoke testing built-in extension: curl"
"$staging_dir/php" -n \
-r 'if ( ! extension_loaded( "curl" ) ) { fwrite( STDERR, "curl is not loaded\n" ); exit( 1 ); } echo PHP_VERSION, PHP_EOL;'
smoke_extensions=(apcu gd gettext igbinary pdo pdo_sqlite redis sodium sqlite3 ssh2 xsl yaml)
for extension in "${smoke_extensions[@]}"; do
echo "Smoke testing extension: $extension"
otool -L "$extension_dir/$extension.so"
"$staging_dir/php" -n \
-d "extension_dir=$extension_dir" \
-d "extension=$extension" \
-r 'echo PHP_VERSION, PHP_EOL;'
done
smoke_extension_args=()
loaded_count=0
for extension in "${smoke_extensions[@]}"; do
smoke_extension_args+=(-d "extension=$extension")
loaded_count=$((loaded_count + 1))
echo "Smoke testing extension set: ${smoke_extensions[*]:0:loaded_count}"
"$staging_dir/php" -n \
-d "extension_dir=$extension_dir" \
"${smoke_extension_args[@]}" \
-r 'echo PHP_VERSION, PHP_EOL;'
done
( cd "$staging_dir" && zip -qr "$out_dir/$artifact" . )
shasum -a 256 "$out_dir/$artifact" | awk '{print $1}' > "$out_dir/$artifact.sha256"
- name: Resolve Windows runtime metadata
if: runner.os == 'Windows'
shell: pwsh
run: |
$phpMinor = $env:PHP_MINOR
switch ( $phpMinor ) {
'8.2' { $vsVersion = 'vs16' }
'8.3' { $vsVersion = 'vs16' }
'8.4' { $vsVersion = 'vs17' }
'8.5' { $vsVersion = 'vs17' }
default {
Write-Error "Unsupported PHP minor: $phpMinor"
exit 1
}
}
"PHP_WINDOWS_VS_VERSION=$vsVersion" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Assemble PHP runtime on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
if ( $env:BUILD_DEBUG -eq 'true' ) {
Set-PSDebug -Trace 1
}
$workDir = Join-Path $env:RUNNER_TEMP 'php-build'
$stagingDir = Join-Path $env:RUNNER_TEMP 'php-artifact'
$phpArchive = Join-Path $workDir 'php.zip'
$phpReleaseUrl = "https://windows.php.net/downloads/releases/php-$env:PHP_VERSION-nts-Win32-$env:PHP_WINDOWS_VS_VERSION-x64.zip"
$phpArchiveUrl = "https://windows.php.net/downloads/releases/archives/php-$env:PHP_VERSION-nts-Win32-$env:PHP_WINDOWS_VS_VERSION-x64.zip"
$xdebugUrl = "https://xdebug.org/files/php_xdebug-$env:XDEBUG_VERSION-$env:PHP_MINOR-nts-$env:PHP_WINDOWS_VS_VERSION-x86_64.dll"
Remove-Item -Recurse -Force @($workDir, $stagingDir) -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $workDir, $stagingDir | Out-Null
try {
Invoke-WebRequest -Uri $phpReleaseUrl -OutFile $phpArchive -ErrorAction Stop
} catch {
Invoke-WebRequest -Uri $phpArchiveUrl -OutFile $phpArchive -ErrorAction Stop
}
Expand-Archive -Path $phpArchive -DestinationPath $stagingDir
$xdebugPath = Join-Path $stagingDir 'ext\php_xdebug.dll'
Invoke-WebRequest -Uri $xdebugUrl -OutFile $xdebugPath
function Add-PECLExtension {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Version
)
Write-Host "::group::Fetch PECL $Name $Version"
$url = "https://windows.php.net/downloads/pecl/releases/$Name/$Version/php_$Name-$Version-$env:PHP_MINOR-nts-$env:PHP_WINDOWS_VS_VERSION-x64.zip"
$zipPath = Join-Path $workDir "pecl-$Name.zip"
$extractDir = Join-Path $workDir "pecl-$Name"
if ( Test-Path $extractDir ) { Remove-Item -Recurse -Force $extractDir }
Invoke-WebRequest -Uri $url -OutFile $zipPath -ErrorAction Stop
Expand-Archive -Path $zipPath -DestinationPath $extractDir
$extDll = Get-ChildItem -Path $extractDir -Filter "php_$Name.dll" -Recurse | Select-Object -First 1
if ( $null -eq $extDll ) {
throw "Expected php_$Name.dll in PECL package for $Name $Version"
}
Copy-Item -Path $extDll.FullName -Destination (Join-Path $stagingDir "ext\php_$Name.dll") -Force
Get-ChildItem -Path $extractDir -Filter '*.dll' -Recurse |
Where-Object { $_.Name -ne "php_$Name.dll" } |
ForEach-Object { Copy-Item -Path $_.FullName -Destination (Join-Path $stagingDir $_.Name) -Force }
Write-Host "::endgroup::"
}
Add-PECLExtension -Name apcu -Version 5.1.24
Add-PECLExtension -Name igbinary -Version 3.2.16
Add-PECLExtension -Name redis -Version 6.0.2
Add-PECLExtension -Name ssh2 -Version 1.4.1
Add-PECLExtension -Name yaml -Version 2.2.4
$runtimeJson = @{
phpVersion = $env:PHP_VERSION
phpMinor = $env:PHP_MINOR
threadSafety = 'nts'
platform = $env:PHP_CLI_ARTIFACT_PLATFORM
arch = $env:PHP_CLI_ARTIFACT_ARCH
binary = 'php.exe'
extensionDir = 'ext'
xdebug = 'ext/php_xdebug.dll'
} | ConvertTo-Json -Depth 4
Set-Content -Path (Join-Path $stagingDir 'runtime.json') -Value $runtimeJson
$env:PATH = "$stagingDir;$env:PATH"
& (Join-Path $stagingDir 'php.exe') -n `
-d "extension_dir=$(Join-Path $stagingDir 'ext')" `
-d "zend_extension=$xdebugPath" `
-d "xdebug.mode=debug" `
--ri xdebug
if ( $LASTEXITCODE -ne 0 ) {
exit $LASTEXITCODE
}
& (Join-Path $stagingDir 'php.exe') -n `
-d "extension_dir=$(Join-Path $stagingDir 'ext')" `
-d "extension=apcu" `
-d "extension=curl" `
-d "extension=gd" `
-d "extension=igbinary" `
-d "extension=mbstring" `
-d "extension=pdo" `
-d "extension=pdo_sqlite" `
-d "extension=redis" `
-d "extension=sodium" `
-d "extension=sqlite3" `
-d "extension=ssh2" `
-d "extension=yaml" `
-r "echo PHP_VERSION, PHP_EOL;"
if ( $LASTEXITCODE -ne 0 ) {
exit $LASTEXITCODE
}
- name: Package Windows artifact
if: runner.os == 'Windows'
shell: pwsh
run: |
$outDir = Join-Path $env:GITHUB_WORKSPACE 'out/php-binaries'
$stagingDir = Join-Path $env:RUNNER_TEMP 'php-artifact'
$artifact = "php-$env:PHP_VERSION-cli-$env:PHP_CLI_ARTIFACT_PLATFORM-$env:PHP_CLI_ARTIFACT_ARCH.zip"
$archive = Join-Path $outDir $artifact
New-Item -ItemType Directory -Force $outDir | Out-Null
Compress-Archive -Force -Path (Join-Path $stagingDir '*') -DestinationPath $archive
(Get-FileHash $archive -Algorithm SHA256).Hash.ToLower() | Set-Content -NoNewline "$archive.sha256"
- name: Upload PHP CLI artifact
uses: actions/upload-artifact@v7
with:
name: php-${{ env.PHP_VERSION }}-cli-${{ matrix.target }}
path: out/php-binaries/*
- name: Upload static-php-cli logs
if: ${{ failure() && runner.os == 'macOS' }}
uses: actions/upload-artifact@v7
with:
name: spc-logs-${{ matrix.target }}
path: .cache/static-php-cli/log/*.log
if-no-files-found: ignore
publish-apps-cdn:
name: Publish Apps CDN artifacts
needs: build
runs-on: ubuntu-latest
env:
PHP_VERSION: ${{ inputs.php_version }}
PHP_CLI_VISIBILITY: ${{ inputs.apps_cdn_visibility }}
DRY_RUN: ${{ inputs.upload_to_apps_cdn && 'false' || 'true' }}
WPCOM_API_TOKEN: ${{ secrets.WPCOM_API_TOKEN }}
steps:
- name: Checkout Studio
uses: actions/checkout@v6
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Download PHP CLI artifacts
uses: actions/download-artifact@v4
with:
pattern: php-${{ env.PHP_VERSION }}-cli-*
path: out/php-binaries
merge-multiple: true
- name: Publish PHP CLI artifacts to Apps CDN
run: |
set -euo pipefail
if [ "$DRY_RUN" != "true" ] && [ -z "${WPCOM_API_TOKEN:-}" ]; then
echo "WPCOM_API_TOKEN secret is required to upload PHP CLI artifacts to Apps CDN."
exit 1
fi
upload_results_file="${PWD}/out/php-binaries/apps-cdn-upload-results.json"
bundle exec fastlane publish_php_cli_binaries \
version:"${PHP_VERSION}" \
artifacts_dir:"${PWD}/out/php-binaries" \
visibility:"${PHP_CLI_VISIBILITY}" \
output_file:"${upload_results_file}"
echo "### Apps CDN PHP CLI upload" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" = "true" ]; then
echo "Dry run completed for PHP ${PHP_VERSION} CLI artifacts with ${PHP_CLI_VISIBILITY} visibility." >> "$GITHUB_STEP_SUMMARY"
else
echo "Uploaded PHP ${PHP_VERSION} CLI artifacts with ${PHP_CLI_VISIBILITY} visibility." >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
PHP_CLI_UPLOAD_RESULTS_FILE="$upload_results_file" ruby -rjson -e '
results = JSON.parse(File.read(ENV.fetch("PHP_CLI_UPLOAD_RESULTS_FILE")))
File.open(ENV.fetch("GITHUB_STEP_SUMMARY"), "a") do |summary|
summary.puts "| File | Apps CDN URL |"
summary.puts "| --- | --- |"
results.each do |file_name, result|
url = result["cdn_url"].to_s
url_output = url.empty? ? "Dry run (not uploaded)" : "<#{url}>"
summary.puts "| `#{file_name}` | #{url_output} |"
end
end
'