Skip to content

Build PHP CLI Binaries #34

Build PHP CLI Binaries

Build PHP CLI Binaries #34

name: Build PHP CLI Binaries
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 macOS dependency builds
required: true
default: 2.8.5
debug:
description: Enable verbose build logs
required: true
type: boolean
default: false
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
archive_extension: tar.gz
binary_name: php
- target: macos-x86_64
runner: macos-15-intel
artifact_platform: macos
artifact_arch: x86_64
archive_extension: tar.gz
binary_name: php
- target: windows-x86_64
runner: windows-latest
artifact_platform: windows
artifact_arch: x86_64
archive_extension: zip
binary_name: php.exe
env:
PHP_VERSION: ${{ inputs.php_version }}
XDEBUG_VERSION: ${{ inputs.xdebug_version }}
SPC_REF: ${{ inputs.spc_ref }}
PHP_CLI_ARTIFACT_PLATFORM: ${{ matrix.artifact_platform }}
PHP_CLI_ARTIFACT_ARCH: ${{ matrix.artifact_arch }}
PHP_CLI_ARCHIVE_EXTENSION: ${{ matrix.archive_extension }}
PHP_CLI_BINARY_NAME: ${{ matrix.binary_name }}
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: Install macOS build dependencies
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
brew update
brew install \
autoconf \
automake \
bison \
cmake \
libtool \
ninja \
pkg-config \
re2c
- 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 dependencies 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
php_extensions="bz2,curl,dom,gd,iconv,intl,mbstring,mysqli,openssl,pdo_mysql,pdo_sqlite,simplexml,sqlite3,xml,xmlreader,xmlwriter,zip,zlib"
libraries="bzip2,curl,freetype,icu,libiconv,libjpeg,libpng,libxml2,libzip,onig,openssl,sqlite,zlib"
php bin/spc doctor --auto-fix
php bin/spc download \
--with-php="$PHP_VERSION" \
--for-extensions="$php_extensions" \
--for-libs="$libraries" \
--ignore-cache-sources=php-src
php bin/spc build:libs "$libraries"
- name: Build PHP runtime on macOS
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
if [ "$BUILD_DEBUG" = "true" ]; then
set -x
fi
source_url="https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
xdebug_url="https://xdebug.org/files/xdebug-${XDEBUG_VERSION}.tgz"
work_dir="$RUNNER_TEMP/php-build"
staging_dir="$RUNNER_TEMP/php-artifact"
spc_root="$GITHUB_WORKSPACE/.cache/static-php-cli/buildroot"
rm -rf "$work_dir" "$staging_dir"
mkdir -p "$work_dir" "$staging_dir"
curl --fail --location --retry 3 "$source_url" --output "$work_dir/php.tar.gz"
tar -xzf "$work_dir/php.tar.gz" -C "$work_dir"
spc_pkg_config="$(find "$GITHUB_WORKSPACE/.cache/static-php-cli/pkgroot" -path '*/bin/pkg-config' -type f -perm -111 | head -n 1)"
if [ -z "$spc_pkg_config" ]; then
echo "static-php-cli pkg-config was not found in pkgroot" >&2
exit 1
fi
pkg_config_static="$work_dir/pkg-config-static"
cat > "$pkg_config_static" <<SH
#!/bin/sh
exec "$spc_pkg_config" --static "\$@"
SH
chmod +x "$pkg_config_static"
export PATH="$(brew --prefix bison)/bin:$spc_root/bin:$PATH"
export PKG_CONFIG="$pkg_config_static"
export PKG_CONFIG_LIBDIR="$spc_root/lib/pkgconfig"
export PKG_CONFIG_PATH="$spc_root/lib/pkgconfig"
export CPPFLAGS="-I$spc_root/include ${CPPFLAGS:-}"
export LDFLAGS="-L$spc_root/lib ${LDFLAGS:-}"
export LIBRARY_PATH="$spc_root/lib:${LIBRARY_PATH:-}"
"$PKG_CONFIG" --version
"$PKG_CONFIG" --modversion libxml-2.0
curl_frameworks="-framework CoreFoundation -framework CoreServices -framework SystemConfiguration"
export CURL_CFLAGS="$("$PKG_CONFIG" --cflags libcurl)"
export CURL_LIBS="$("$PKG_CONFIG" --libs libcurl) $curl_frameworks"
export LIBS="$curl_frameworks ${LIBS:-}"
cd "$work_dir/php-${PHP_VERSION}"
curl_config="$PWD/ext/curl/config.m4"
perl -0pi -e 's/(PHP_CHECK_LIBRARY\(\[curl\],\s*\[curl_easy_perform\],)/save_LIBS="\$LIBS"\n LIBS="\$LIBS \$CURL_LIBS"\n\1/s' "$curl_config"
perl -0pi -e 's/(PHP_CHECK_LIBRARY\(\[curl\],\s*\[curl_easy_perform\],.*?\)\n)/\1 LIBS="\$save_LIBS"\n/s' "$curl_config"
grep -q 'LIBS="$LIBS $CURL_LIBS"' "$curl_config"
./buildconf --force
curl_static_link="$spc_root/lib/libcurl.a $curl_frameworks"
CURL_STATIC_LINK="$curl_static_link" perl -0pi -e 's/-lcurl/$ENV{CURL_STATIC_LINK}/g' configure
grep -Fq "$spc_root/lib/libcurl.a" configure
./configure \
--prefix="$staging_dir" \
--with-config-file-path="$staging_dir/etc" \
--with-config-file-scan-dir="$staging_dir/etc/conf.d" \
--disable-cgi \
--disable-debug \
--disable-phpdbg \
--enable-bcmath=shared \
--enable-calendar=shared \
--enable-cli \
--enable-ctype=shared \
--enable-dom=shared \
--enable-exif=shared \
--enable-fileinfo=shared \
--enable-filter=shared \
--enable-gd=shared \
--enable-intl=shared \
--enable-mbstring=shared \
--enable-opcache=shared \
--enable-pdo=shared \
--enable-phar=shared \
--enable-session=shared \
--enable-simplexml=shared \
--enable-tokenizer=shared \
--enable-xml=shared \
--enable-xmlreader=shared \
--enable-xmlwriter=shared \
--with-bz2=shared,$spc_root \
--with-curl \
--with-freetype \
--with-iconv=shared,$spc_root \
--with-jpeg \
--with-mysqli=shared,mysqlnd \
--with-openssl=shared \
--with-pdo-mysql=shared,mysqlnd \
--with-pdo-sqlite=shared \
--with-sqlite3=shared \
--with-zip=shared \
--with-zlib=shared,$spc_root
grep -E 'CoreFoundation|SystemConfiguration|CoreServices' Makefile
make -j"$(sysctl -n hw.ncpu)"
make install
mkdir -p "$staging_dir/etc/conf.d"
curl --fail --location --retry 3 "$xdebug_url" --output "$work_dir/xdebug.tgz"
tar -xzf "$work_dir/xdebug.tgz" -C "$work_dir"
xdebug_source="$(find "$work_dir" -maxdepth 1 -type d -name 'xdebug-*' | head -n 1)"
cd "$xdebug_source"
"$staging_dir/bin/phpize"
./configure --with-php-config="$staging_dir/bin/php-config"
make -j"$(sysctl -n hw.ncpu)"
make install
extension_dir="$("$staging_dir/bin/php-config" --extension-dir)"
relative_extension_dir="${extension_dir#"$staging_dir"/}"
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": "bin/php",
"extensionDir": "$relative_extension_dir",
"libraryDir": "lib",
"xdebug": "$relative_extension_dir/xdebug.so"
}
JSON
"$staging_dir/bin/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/bin/php" -n \
-r 'if ( ! extension_loaded( "curl" ) ) { fwrite( STDERR, "curl is not loaded\n" ); exit( 1 ); } echo PHP_VERSION, PHP_EOL;'
smoke_extensions=(gd mbstring pdo pdo_sqlite sqlite3)
for extension in "${smoke_extensions[@]}"; do
echo "Smoke testing extension: $extension"
otool -L "$extension_dir/$extension.so"
"$staging_dir/bin/php" -n \
-d "extension_dir=$extension_dir" \
-d "extension=$extension" \
-r 'echo PHP_VERSION, PHP_EOL;'
done
smoke_extension_args=()
for extension in "${smoke_extensions[@]}"; do
smoke_extension_args+=(-d "extension=$extension")
echo "Smoke testing extension set: ${smoke_extensions[*]:0:${#smoke_extension_args[@]}}"
"$staging_dir/bin/php" -n \
-d "extension_dir=$extension_dir" \
"${smoke_extension_args[@]}" \
-r 'echo PHP_VERSION, PHP_EOL;'
done
- name: Package macOS artifact
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
out_dir="$GITHUB_WORKSPACE/out/php-binaries"
staging_dir="$RUNNER_TEMP/php-artifact"
artifact="php-${PHP_VERSION}-cli-${PHP_CLI_ARTIFACT_PLATFORM}-${PHP_CLI_ARTIFACT_ARCH}.${PHP_CLI_ARCHIVE_EXTENSION}"
mkdir -p "$out_dir"
tar -czf "$out_dir/$artifact" -C "$staging_dir" .
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
$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=curl" `
-d "extension=gd" `
-d "extension=mbstring" `
-d "extension=pdo" `
-d "extension=pdo_sqlite" `
-d "extension=sqlite3" `
-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.$env:PHP_CLI_ARCHIVE_EXTENSION"
$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/*