Skip to content
Open
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
151 changes: 151 additions & 0 deletions .github/workflows/build-php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Build PHP binaries

on:
# Build PHP binaries whenever a new semantic-release tag is pushed
push:
tags:
- "v*.*.*"
# Allow manual runs
workflow_dispatch:

permissions:
contents: write

jobs:
build-php:
name: Build PHP ${{ matrix.php_minor }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php_minor: ["8.2", "8.3", "8.4", "8.5"]

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Read PHP build version from manifest
id: read_manifest
env:
PHP_MINOR: ${{ matrix.php_minor }}
run: |
set -euo pipefail
python - << 'PYCODE'
import json
import os
import pathlib

minor = os.environ["PHP_MINOR"]
manifest_path = pathlib.Path("php-versions.json")
data = json.loads(manifest_path.read_text(encoding="utf-8"))

info = data.get(minor)
if not info or "build" not in info:
raise SystemExit(f"No build field for {minor} in php-versions.json")

build = info["build"]
# Expose as step output and env for later steps
github_output = os.environ["GITHUB_OUTPUT"]
with open(github_output, "a", encoding="utf-8") as fh:
fh.write(f"php_build={build}\n")
PYCODE

- name: Cache SPC downloads
uses: actions/cache@v4
with:
path: |
~/.cache/spc
.spc
key: spc-${{ runner.os }}-${{ matrix.php_minor }}-${{ steps.read_manifest.outputs.php_build }}

- name: Download static-php-cli (spc)
run: |
set -euo pipefail
curl -fsSL -o spc.tgz https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64.tar.gz
tar -xzf spc.tgz
rm spc.tgz
chmod +x spc

- name: Build PHP with static-php-cli
env:
PHP_BUILD: ${{ steps.read_manifest.outputs.php_build }}
# Use a dynamic target on Linux so xdebug can be built as a shared extension
SPC_TARGET: native-native-gnu
run: |
set -euo pipefail
./spc doctor --auto-fix
./spc download --all --with-php="$PHP_BUILD" --prefer-pre-built
./spc build "bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,swoole,tokenizer,xml,xmlreader,xmlwriter,zip,zlib" --build-cli --build-shared="xdebug"

- name: Package PHP binary and xdebug
env:
PHP_MINOR: ${{ matrix.php_minor }}
run: |
set -euo pipefail

# Expect CLI binary at buildroot/bin/php
if [ ! -f buildroot/bin/php ]; then
echo "buildroot/bin/php not found" >&2
exit 1
fi

# Expect xdebug shared extension at buildroot/modules/xdebug.so
if [ ! -f buildroot/modules/xdebug.so ]; then
echo "buildroot/modules/xdebug.so not found (xdebug shared extension)" >&2
exit 1
fi

mkdir -p "dist/$PHP_MINOR/ext"
cp buildroot/bin/php "dist/$PHP_MINOR/php"
cp buildroot/modules/xdebug.so "dist/$PHP_MINOR/ext/xdebug.so"

tar -C "dist/$PHP_MINOR" -czf "php-${PHP_MINOR}-linux-x86_64.tar.gz" .

- name: Upload PHP tarball artifact
uses: actions/upload-artifact@v4
with:
name: php-${{ matrix.php_minor }}-linux-x86_64
path: php-${{ matrix.php_minor }}-linux-x86_64.tar.gz

publish:
name: Publish PHP binaries to latest-build release
runs-on: ubuntu-latest
needs: build-php

steps:
- name: Generate Token
id: generate_token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.BOT_APP_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ steps.generate_token.outputs.token }}

- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist

- name: Collect tarballs
run: |
set -euo pipefail
find dist -type f -name "php-*-linux-x86_64.tar.gz" -exec mv {} . \;
ls -1 php-*-linux-x86_64.tar.gz

- name: Upload to latest-build release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
tag_name: latest-build
files: |
php-8.2-linux-x86_64.tar.gz
php-8.3-linux-x86_64.tar.gz
php-8.4-linux-x86_64.tar.gz
php-8.5-linux-x86_64.tar.gz
php-versions.json
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

98 changes: 98 additions & 0 deletions .github/workflows/check-php-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Check PHP versions

on:
schedule:
# Check for new PHP patch releases daily
- cron: "0 3 * * *"
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
bump-php-versions:
runs-on: ubuntu-latest

steps:
- name: Generate Token
id: generate_token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.BOT_APP_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ steps.generate_token.outputs.token }}

- name: Update php-versions.json from static-php.dev
id: update
run: |
set -euo pipefail
python - << 'PYCODE'
import json
import pathlib
import re
import sys
from urllib.request import urlopen

manifest_path = pathlib.Path("php-versions.json")
if not manifest_path.is_file():
sys.exit("php-versions.json not found")

data = json.loads(manifest_path.read_text(encoding="utf-8"))

url = "https://dl.static-php.dev/static-php-cli/common/"
with urlopen(url) as resp:
html = resp.read().decode("utf-8", errors="replace")

# Extract versions like php-8.3.30-cli-linux-x86_64.tar.gz
versions = set(re.findall(r"php-(\\d+\\.\\d+\\.\\d+)-cli-linux-x86_64\\.tar\\.gz", html))
if not versions:
sys.exit("No PHP versions found on static-php.dev; aborting.")

def sort_key(v: str):
return tuple(int(x) for x in v.split("."))

updated = False

for minor, info in data.items():
prefix = minor + "."
matching = sorted((v for v in versions if v.startswith(prefix)), key=sort_key)
if not matching:
continue

latest = matching[-1]
current = info.get("build")
if current != latest:
info["build"] = latest
updated = True

if updated:
manifest_path.write_text(json.dumps(data, indent=2) + "\\n", encoding="utf-8")
PYCODE

- name: Check for changes
id: git_diff
run: |
if git diff --quiet php-versions.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Create Pull Request
if: steps.git_diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.generate_token.outputs.token }}
commit-message: "fix(php): bumped php versions"
title: "fix(php): bumped php versions"
body: "Update php-versions.json with latest patch versions from static-php.dev."
branch: "chore/bump-php-versions"
base: "main"
add-paths: |
php-versions.json

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,11 @@ target/
# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules)
rust-project.json

# PHP build artifacts
spc
.spc/
buildroot/
dist/
php-*-linux-x86_64.tar.gz

# End of https://www.toptal.com/developers/gitignore/api/linux,rust,rust-analyzer,jetbrains+all
44 changes: 44 additions & 0 deletions build-php.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail

# Usage: ./build-php.sh [build] [PHP_MINOR]
# Example: ./build-php.sh build 8.4

COMMAND="${1:-}"
PHP_MINOR="${2:-8.3}"
IMAGE_NAME="pvm-build-env"

if [ "$COMMAND" == "build" ]; then
if [ -z "${GITHUB_TOKEN:-}" ]; then
echo "Error: GITHUB_TOKEN is not set."
echo "Please set it by running: export GITHUB_TOKEN=your_token"
exit 1
fi

echo "Building PHP $PHP_MINOR in Docker..."
docker build --network host \
--build-arg GITHUB_TOKEN="${GITHUB_TOKEN}" \
--build-arg PHP_MINOR="${PHP_MINOR}" \
-f build/Dockerfile -t "$IMAGE_NAME" build

echo "Extracting build artifact..."
mkdir -p output

# Create a temporary container to copy files out
TMP_CONTAINER=$(docker create "$IMAGE_NAME")

# Get the actual tarball name from the container
TARBALL_NAME=$(docker cp "$TMP_CONTAINER:/tarball-name.txt" - | tar -xO)

echo "Artifact name: $TARBALL_NAME"
docker cp "$TMP_CONTAINER:/php-artifact.tar.gz" "output/$TARBALL_NAME"
docker rm "$TMP_CONTAINER"

echo "Build finished! Check output/$TARBALL_NAME"
stat "output/$TARBALL_NAME"
exit 0
fi

echo "Usage: $0 build [PHP_MINOR]"
echo "Example: $0 build 8.3"
exit 1
88 changes: 88 additions & 0 deletions build/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
FROM ubuntu:24.04
ARG GITHUB_TOKEN
ARG PHP_MINOR=8.3

# Avoid interactive prompts during build
ENV DEBIAN_FRONTEND=noninteractive
ENV GITHUB_TOKEN=${GITHUB_TOKEN}
ENV PHP_MINOR=${PHP_MINOR}
ENV SPC_TARGET="native-native-gnu"
ENV SPC_BUILD_SHARED="xdebug"

# Install all build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
automake \
autopoint \
bison \
bzip2 \
ca-certificates \
cmake \
curl \
flex \
g++ \
gcc \
git \
jq \
libtool \
make \
patch \
pkg-config \
re2c \
sudo \
unzip \
xz-utils \
libunistring-dev \
libidn2-dev \
libedit-dev \
&& rm -rf /var/lib/apt/lists/*

USER root
WORKDIR /app
COPY --chmod=755 build-prepare-build.sh .
COPY --chmod=755 build-php-local.sh .
COPY --chmod=644 php-versions.json .
COPY --chmod=644 craft.yml .

RUN --mount=type=cache,target=/app/downloads \
--mount=type=cache,target=/app/source \
./build-prepare-build.sh

RUN --mount=type=cache,target=/app/downloads \
--mount=type=cache,target=/app/source \
spc doctor --auto-fix

# Dynamically update php-version in craft.yml based on PHP_MINOR
RUN sed -i "s/^php-version: .*/php-version: ${PHP_MINOR}/" craft.yml

# Download all required sources and extensions
RUN --mount=type=cache,target=/app/downloads \
--mount=type=cache,target=/app/source \
PHP_BUILD=$(jq -r --arg minor "${PHP_MINOR}" '.[$minor].build' php-versions.json) && \
spc download --with-php="$PHP_BUILD" --for-extensions="apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,mbstring,mbregex,msgpack,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib,xdebug" --prefer-pre-built

# Build PHP using spc build to explicitly control SAPIs and avoid embed failures
RUN --mount=type=cache,target=/app/downloads \
--mount=type=cache,target=/app/source \
spc build --build-cli \
--build-shared="xdebug" \
--enable-zts \
--with-suggested-libs \
--with-suggested-exts \
"apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,mbstring,mbregex,msgpack,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" \
--debug

# Verify PHP execution (without and with xdebug)
RUN ./buildroot/bin/php --version
RUN ./buildroot/bin/php -d zend_extension=./buildroot/modules/xdebug.so --version

# Package result
RUN PHP_BUILD=$(jq -r --arg minor "${PHP_MINOR}" '.[$minor].build' php-versions.json) && \
DIST_DIR="dist/${PHP_MINOR}" && \
mkdir -p "$DIST_DIR/ext" && \
cp buildroot/bin/php "$DIST_DIR/php" && \
cp buildroot/modules/xdebug.so "$DIST_DIR/ext/xdebug.so" && \
TARBALL="php-version-${PHP_BUILD}-linux-x86_64.tar.gz" && \
tar -C "$DIST_DIR" -czf "/php-artifact.tar.gz" . && \
echo "$TARBALL" > /tarball-name.txt

Loading