diff --git a/.github/scripts/install-alpine-php.sh b/.github/scripts/install-alpine-php.sh index 50c673377a3..9ab49c6086e 100644 --- a/.github/scripts/install-alpine-php.sh +++ b/.github/scripts/install-alpine-php.sh @@ -9,7 +9,7 @@ set -eu apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd if [ "${PHP_ZTS:-0}" = "1" ]; then - apk add --no-cache pkgconf xz libxml2-dev oniguruma-dev curl-dev openssl-dev zlib-dev + apk add --no-cache jq pkgconf xz libxml2-dev oniguruma-dev curl-dev openssl-dev zlib-dev PHP_MINOR="$PHP_MINOR" PHP_ZTS=1 sh "$(dirname "$0")/../turbo-build/build-php.sh" exit 0 fi diff --git a/.github/turbo-build/Dockerfile b/.github/turbo-build/Dockerfile index bd63e6dc6fb..1b3a143e76f 100644 --- a/.github/turbo-build/Dockerfile +++ b/.github/turbo-build/Dockerfile @@ -1,7 +1,7 @@ # Prebuilt image for the turbo-compile gnu legs in phar.yml, published to # ghcr.io/phpstan/turbo-build by turbo-build-image.yml (tags gnu-php8.3, -# gnu-php8.4, gnu-php8.5, gnu-php8.6, gnu-php8.6-zts; each a linux/amd64 + -# linux/arm64 manifest). +# gnu-php8.4, gnu-php8.5, gnu-php8.6 and the thread-safe gnu-php8.3-zts to +# gnu-php8.6-zts; each a linux/amd64 + linux/arm64 manifest). # # Baking the PHP toolchain in keeps apt and the ondrej/php PPA out of the # compile jobs entirely: ports.ubuntu.com (the only Ubuntu arm64 mirror, @@ -18,8 +18,9 @@ ARG PHP_MINOR ARG PHP_ZTS=0 ENV DEBIAN_FRONTEND=noninteractive -# These images build PHP from the official release tarball pinned in -# build-php.sh instead of installing it from ondrej/php: +# These images build PHP from the newest official tarball of the minor +# (build-php.sh resolves it from php.net) instead of installing it from +# ondrej/php: # - PHP 8.6 is a prerelease, and ondrej/php trails its tags by weeks: on # 2026-09-25 it still served 8.6.0beta3, whose module API (20250926) the # 8.6.0RC2 that the php:8.6-rc images ship had already replaced @@ -50,7 +51,7 @@ RUN test -n "$PHP_MINOR"; \ }; \ apt_install software-properties-common gnupg ca-certificates curl git make g++ patch unzip zstd \ && if [ "$PHP_MINOR" = "8.6" ] || [ "$PHP_ZTS" = "1" ]; then \ - apt_install pkg-config xz-utils libxml2-dev libonig-dev libcurl4-openssl-dev libssl-dev zlib1g-dev \ + apt_install jq pkg-config xz-utils libxml2-dev libonig-dev libcurl4-openssl-dev libssl-dev zlib1g-dev \ && PHP_MINOR="$PHP_MINOR" PHP_ZTS="$PHP_ZTS" sh /tmp/build-php.sh; \ else \ add-apt-repository -y ppa:ondrej/php \ diff --git a/.github/turbo-build/build-php.sh b/.github/turbo-build/build-php.sh index 6e51aae9de8..567962bde83 100644 --- a/.github/turbo-build/build-php.sh +++ b/.github/turbo-build/build-php.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Builds and installs PHP $PHP_MINOR into /usr/local from the official -# release tarball pinned below, thread-safe when $PHP_ZTS is 1. Used by the +# Builds and installs the newest release of PHP $PHP_MINOR into /usr/local +# from the official tarball, thread-safe when $PHP_ZTS is 1. Used by the # Dockerfile for the PHP builds ondrej/php does not ship (prerelease # minors, and every thread-safe build), and by the musl ZTS legs in # phar.yml, which run it in Alpine — Alpine packages no thread-safe PHP. @@ -11,19 +11,39 @@ # and curl, mbstring, openssl, xml and zlib for Composer. set -eu -# The tarball for each minor this script builds; one pin shared by the -# glibc image and the musl legs, so both build the same release. -case "$PHP_MINOR" in - 8.6) - PHP_VERSION=8.6.0RC2 - PHP_URL=https://downloads.php.net/~mbeccati/php-8.6.0RC2.tar.xz - PHP_SHA256=ef3fba21c311e9bbace0e2102702446d322c275b8a10e6b33b28f2561299671c - ;; +# The release is resolved from php.net at build time, so the weekly image +# rebuild and every musl leg pick up a new patch release (or the next +# release candidate of a prerelease minor) without a change here: an +# extension built against one patch release loads into every other of the +# minor. The sha256 published next to the tarball guards the download. A +# minor without a stable release yet is not in the releases API; its +# current release candidate is in the pre-release one, under .0. +php_net() { + curl -fsSL --retry 3 "$1" +} +RELEASE_JSON="$(php_net "https://www.php.net/releases/index.php?json&version=$PHP_MINOR")" +PHP_VERSION="$(printf '%s' "$RELEASE_JSON" | jq -r '.version // empty')" +if [ -n "$PHP_VERSION" ]; then + PHP_URL="https://www.php.net/distributions/php-$PHP_VERSION.tar.xz" + PHP_SHA256="$(printf '%s' "$RELEASE_JSON" | jq -r --arg file "php-$PHP_VERSION.tar.xz" '.source[] | select(.filename == $file) | .sha256')" +else + PRERELEASE_JSON="$(php_net "https://www.php.net/pre-release-builds.php?format=json" | jq --arg minor "$PHP_MINOR.0" '.[$minor].release // empty')" + PHP_VERSION="$(printf '%s' "$PRERELEASE_JSON" | jq -r '.version // empty')" + PHP_URL="$(printf '%s' "$PRERELEASE_JSON" | jq -r '.files.xz.path // empty')" + PHP_SHA256="$(printf '%s' "$PRERELEASE_JSON" | jq -r '.files.xz.sha256 // empty')" +fi +case "$PHP_VERSION" in + "$PHP_MINOR".*) ;; *) - echo "build-php.sh: no tarball pinned for PHP $PHP_MINOR" >&2 + echo "build-php.sh: php.net lists no release of PHP $PHP_MINOR (resolved \"$PHP_VERSION\")" >&2 exit 1 ;; esac +if [ -z "$PHP_URL" ] || [ -z "$PHP_SHA256" ]; then + echo "build-php.sh: php.net lists no tar.xz with a sha256 for PHP $PHP_VERSION" >&2 + exit 1 +fi +echo "build-php.sh: building PHP $PHP_VERSION from $PHP_URL" export PHP_VERSION PHP_ZTS="${PHP_ZTS:-0}" export PHP_ZTS diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 9173c27164c..c80e93dee06 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -24,9 +24,9 @@ env: # more (paths under turbo-ext/, shell globs). The commit job deletes them # from the dist; without this, the torn-set guard there would refuse every # version bump, since a retired binary has no freshly built replacement. - # Linux ZTS binaries are built from PHP 8.6 on again (see turbo-compile), - # so only the older minors' are retired. - TURBO_RETIRED_BINARIES: "linux-gnu-x86_64/phpstan_turbo-8.[345]-zts.so linux-gnu-arm64/phpstan_turbo-8.[345]-zts.so" + # None at the moment: the Linux ZTS binaries retired for 8.3-8.5 are built + # again (see turbo-compile). + TURBO_RETIRED_BINARIES: "" # The php-parser version whose grammar tables and semantic actions the # native parser engine (turbo-ext/src/parser/) was ported against. SUPPORTED_PHP_PARSER_VERSION: "v5.9.0" @@ -718,21 +718,22 @@ jobs: # the same core. runs-on: ${{ matrix.target.runs-on }} # Container legs pin the binaries' userland floor independently of the - # runner image, like turbo-compile-core. Linux ZTS legs (suffix -zts) - # exist from PHP 8.6 on: the official Docker images build every 8.6+ - # variant thread-safe, cli and alpine included (docker-library/php#1686), - # while distros, ppa:ondrej/php, sury, Remi and setup-php keep shipping - # an NTS CLI — so 8.6+ needs both. Up to 8.5 thread-safe PHP on Linux - # (FrankenPHP, php:*-zts images) is a few percent of hosts and gets no - # binary; the commit job deletes those from phpstan/phpstan - # (TURBO_RETIRED_BINARIES). The thread-safe extension links against the - # same core: the core never touches TSRM (turbo-shared-core-gate compares - # the two builds too). The gnu-php8.6 and gnu-php8.6-zts images build the - # official 8.6 prerelease tarball (see the Dockerfile); the musl ZTS legs - # build the same tarball in alpine:3.24, since Alpine packages no - # thread-safe PHP. Alpine's NTS php86 packages are currently only in - # edge/testing. - container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.target.suffix || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.target.suffix != '-zts' && 'alpine:edge' || matrix.target.container || '' }} + # runner image, like turbo-compile-core. Every Linux target has a ZTS + # leg (ts: zts, artifact suffix -zts) next to the NTS one for every + # minor: distros, ppa:ondrej/php, sury, Remi and setup-php ship an NTS + # CLI, while the official Docker images build every 8.6+ variant + # thread-safe, cli and alpine included (docker-library/php#1686), and + # their php:*-zts tags (the base of FrankenPHP) do so for older minors. + # The thread-safe extension is only the per-version shim — it links + # against the platform's one core, which never touches TSRM + # (turbo-shared-core-gate compares the two builds too) — so a ZTS leg + # compiles only those few sources. ondrej/php packages no thread-safe + # PHP, so the gnu-php-zts images build the minor's newest + # official tarball (build-php.sh; so does gnu-php8.6, a prerelease), and + # the musl ZTS legs build it in alpine:3.24, since Alpine packages no + # thread-safe PHP either. Alpine's NTS php86 packages are + # currently only in edge/testing. + container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.ts != 'zts' && 'alpine:edge' || matrix.target.container || '' }} needs: turbo-compile-core # one platform's failed core must not skip every platform's legs: a leg # whose core is missing fails at its download instead @@ -741,13 +742,14 @@ jobs: env: PHP_MINOR: ${{ matrix.php-version }} - PHP_ZTS: ${{ matrix.target.suffix == '-zts' && '1' || '0' }} - TURBO_ARTIFACT: "phpstan_turbo-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" + PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} + TURBO_ARTIFACT: "phpstan_turbo-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" strategy: fail-fast: false matrix: php-version: ["8.3", "8.4", "8.5", "8.6"] + # the musl arm64 legs are in turbo-compile-musl-arm64 target: - name: "linux-gnu-x86_64" runs-on: "ubuntu-latest" @@ -763,28 +765,15 @@ jobs: - name: "macos-arm64" runs-on: "macos-latest" family: "macos" - # thread-safe builds, 8.6+ only (see above); the musl arm64 one is in - # turbo-compile-musl-arm64 - include: - - php-version: "8.6" - target: - name: "linux-gnu-x86_64" - runs-on: "ubuntu-latest" - family: "gnu" - suffix: "-zts" - - php-version: "8.6" - target: - name: "linux-gnu-arm64" - runs-on: "ubuntu-24.04-arm" - family: "gnu" - suffix: "-zts" - - php-version: "8.6" - target: - name: "linux-musl-x86_64" - runs-on: "ubuntu-latest" - container: "alpine:3.24" - family: "musl" - suffix: "-zts" + ts: ["nts", "zts"] + # Homebrew and setup-php ship an NTS PHP on macOS, and no + # thread-safe distribution is common enough there to build for + exclude: + - target: + name: "macos-arm64" + runs-on: "macos-latest" + family: "macos" + ts: "zts" steps: - name: Harden the runner (Audit all outbound calls) @@ -899,11 +888,8 @@ jobs: fail-fast: false matrix: php-version: ["8.3", "8.4", "8.5", "8.6"] - ts: ["nts"] - # thread-safe from 8.6 on, see turbo-compile - include: - - php-version: "8.6" - ts: "zts" + # thread-safe for every minor too, see turbo-compile + ts: ["nts", "zts"] steps: - name: Harden the runner (Audit all outbound calls) @@ -1027,14 +1013,16 @@ jobs: # turbo-differential. The 8.6+ images are thread-safe # (docker-library/php#1686), so those legs load the -zts binaries, and # an Alpine leg covers the musl one — php:*-alpine is the other image - # family people run PHPStan in. No macOS/Windows legs: those runners + # family people run PHPStan in. The 8.5 php:*-zts pair (the base of + # FrankenPHP's images) does the same for the older minors' ZTS + # binaries, which are compiled against our own build of the tarball. No macOS/Windows legs: those runners # cannot run Linux containers, and no alternative-numbered official # build is distributed for either platform today. runs-on: "ubuntu-latest" timeout-minutes: 60 env: - IMAGE: ${{ matrix.php-version == '8.6' && format('phpstan-turbo-test:{0}', matrix.binary) || matrix.image }} + IMAGE: ${{ (matrix.php-version == '8.6' || contains(matrix.image, 'alpine')) && format('phpstan-turbo-test:{0}', matrix.binary) || matrix.image }} permissions: contents: read @@ -1047,6 +1035,8 @@ jobs: - "php:8.3.22-cli-bookworm" - "php:8.4-cli-bookworm" - "php:8.5-cli-bookworm" + - "php:8.5-zts-bookworm" + - "php:8.5-zts-alpine" - "php:8.6-rc-cli-bookworm" - "php:8.6-rc-cli-alpine" check: ["tests", "phpstan"] @@ -1063,6 +1053,12 @@ jobs: - image: "php:8.5-cli-bookworm" php-version: "8.5" binary: "linux-gnu-x86_64-php8.5" + - image: "php:8.5-zts-bookworm" + php-version: "8.5" + binary: "linux-gnu-x86_64-php8.5-zts" + - image: "php:8.5-zts-alpine" + php-version: "8.5" + binary: "linux-musl-x86_64-php8.5-zts" - image: "php:8.6-rc-cli-bookworm" php-version: "8.6" binary: "linux-gnu-x86_64-php8.6-zts" @@ -1112,8 +1108,8 @@ jobs: name: "phpstan_turbo-${{ matrix.binary }}" path: "turbo-ext" - - name: "Add mbstring to the PHP 8.6 prerelease image" - if: matrix.php-version == '8.6' + - name: "Add mbstring, bash and make to the 8.6 prerelease and Alpine images" + if: matrix.php-version == '8.6' || contains(matrix.image, 'alpine') env: PHP_IMAGE: ${{ matrix.image }} # Unlike the stable images, the 8.6 prerelease omits mbstring, @@ -1126,14 +1122,19 @@ jobs: ARG PHP_IMAGE FROM ${PHP_IMAGE} RUN if command -v apk > /dev/null; then \ - apk add --no-cache bash make oniguruma \ - && apk add --no-cache --virtual .mbstring-build-deps $PHPIZE_DEPS oniguruma-dev \ - && docker-php-ext-install mbstring \ - && apk del .mbstring-build-deps; \ - else \ - apt-get update && apt-get install -y --no-install-recommends libonig-dev \ - && docker-php-ext-install mbstring \ - && rm -rf /var/lib/apt/lists/*; \ + apk add --no-cache bash make; \ + fi \ + && if ! php -m | grep -qx mbstring; then \ + if command -v apk > /dev/null; then \ + apk add --no-cache oniguruma \ + && apk add --no-cache --virtual .mbstring-build-deps $PHPIZE_DEPS oniguruma-dev \ + && docker-php-ext-install mbstring \ + && apk del .mbstring-build-deps; \ + else \ + apt-get update && apt-get install -y --no-install-recommends libonig-dev \ + && docker-php-ext-install mbstring \ + && rm -rf /var/lib/apt/lists/*; \ + fi; \ fi DOCKERFILE @@ -1493,9 +1494,11 @@ jobs: # binary is built for. Kept out of the compile jobs so the commit job, # which only needs the binaries, does not wait for them (see there). # The musl binaries are tested in turbo-differential-musl. The linux-gnu - # 8.6 legs run in the image the binary is compiled in instead: setup-php's - # Linux 8.6 is a nightly snapshot that can predate the release the binary - # targets, and a module API mismatch refuses to load it at all. + # 8.6 and ZTS legs run in the image the binary is compiled in instead: + # setup-php's Linux 8.6 is a nightly snapshot that can predate the + # release the binary targets, and a module API mismatch refuses to load + # it at all; the ZTS legs so as not to depend on setup-php's thread-safe + # Linux builds. needs: - turbo-compile - turbo-compile-windows @@ -1516,13 +1519,8 @@ jobs: php-version: ["8.3", "8.4", "8.5", "8.6"] target: ["linux-gnu-x86_64", "linux-gnu-arm64", "macos-arm64", "windows-x86_64"] ts: ["nts", "zts"] - # Windows ships ZTS binaries for every minor, Linux from 8.6 on — - # added by the include entries below (see turbo-compile) + # macOS ships no ZTS binary (see turbo-compile) exclude: - - target: "linux-gnu-x86_64" - ts: "zts" - - target: "linux-gnu-arm64" - ts: "zts" - target: "macos-arm64" ts: "zts" include: @@ -1537,10 +1535,44 @@ jobs: runs-on: "windows-latest" - target: "linux-gnu-x86_64" php-version: "8.6" + ts: "nts" container: "ghcr.io/phpstan/turbo-build:gnu-php8.6" - target: "linux-gnu-arm64" php-version: "8.6" + ts: "nts" container: "ghcr.io/phpstan/turbo-build:gnu-php8.6" + # the linux-gnu ZTS legs, in the image the binary is compiled in + # (see above); the official php:*-zts images get turbo-docker-run + - target: "linux-gnu-x86_64" + php-version: "8.3" + ts: "zts" + runs-on: "ubuntu-latest" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.3-zts" + - target: "linux-gnu-arm64" + php-version: "8.3" + ts: "zts" + runs-on: "ubuntu-24.04-arm" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.3-zts" + - target: "linux-gnu-x86_64" + php-version: "8.4" + ts: "zts" + runs-on: "ubuntu-latest" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.4-zts" + - target: "linux-gnu-arm64" + php-version: "8.4" + ts: "zts" + runs-on: "ubuntu-24.04-arm" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.4-zts" + - target: "linux-gnu-x86_64" + php-version: "8.5" + ts: "zts" + runs-on: "ubuntu-latest" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.5-zts" + - target: "linux-gnu-arm64" + php-version: "8.5" + ts: "zts" + runs-on: "ubuntu-24.04-arm" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.5-zts" - target: "linux-gnu-x86_64" php-version: "8.6" ts: "zts" @@ -1677,15 +1709,8 @@ jobs: matrix: arch: ["x86_64", "arm64"] php-version: ["8.3", "8.4", "8.5", "8.6"] - ts: ["nts"] - # thread-safe from 8.6 on, see turbo-compile - include: - - arch: "x86_64" - php-version: "8.6" - ts: "zts" - - arch: "arm64" - php-version: "8.6" - ts: "zts" + # thread-safe for every minor too, see turbo-compile + ts: ["nts", "zts"] steps: - name: Harden the runner (Audit all outbound calls) @@ -1784,9 +1809,8 @@ jobs: php-version: ["8.3", "8.4", "8.5", "8.6"] ts: ["nts", "zts"] script: ["make tests", "make phpstan"] - # Linux ships ZTS binaries only from 8.6 on (see turbo-compile), and - # the ubuntu 8.6 legs are excluded below — turbo-docker-run runs the - # 8.6 ZTS binaries in the official, thread-safe 8.6 images instead. + # The Linux ZTS binaries run the same checks in turbo-docker-run, in + # the official thread-safe images, instead of under setup-php. exclude: - operating-system: "ubuntu-latest" ts: "zts" diff --git a/.github/workflows/turbo-build-image.yml b/.github/workflows/turbo-build-image.yml index 90426cde072..066dcb0cc9c 100644 --- a/.github/workflows/turbo-build-image.yml +++ b/.github/workflows/turbo-build-image.yml @@ -4,7 +4,7 @@ name: "Build Turbo compile images" # Builds .github/turbo-build/Dockerfile natively on x86_64 and arm64 # runners and publishes ghcr.io/phpstan/turbo-build:gnu-php{8.3,8.4,8.5,8.6} -# and the thread-safe gnu-php8.6-zts as multi-arch manifests. The +# and the thread-safe gnu-php{8.3,8.4,8.5,8.6}-zts as multi-arch manifests. The # turbo-compile gnu legs in phar.yml run in these images, so their only # network dependency is a GHCR pull — no apt, no ports.ubuntu.com, no PPA at # compile time. @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: # [-zts]; a -zts tag builds a thread-safe PHP of that minor - tag: ["8.3", "8.4", "8.5", "8.6", "8.6-zts"] + tag: ["8.3", "8.4", "8.5", "8.6", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] platform: - arch: "amd64" runs-on: "ubuntu-latest" @@ -106,7 +106,7 @@ jobs: strategy: matrix: - tag: ["8.3", "8.4", "8.5", "8.6", "8.6-zts"] + tag: ["8.3", "8.4", "8.5", "8.6", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] env: TAG: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.tag }}" diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 84f8f361dcd..b7e39640259 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -34,9 +34,9 @@ * instead. Only non-debug builds for PHP >= MINIMUM_PHP_VERSION_ID are * shipped; a ZTS variant (-zts filename * suffix) exists for Windows, where XAMPP, WampServer and Scoop default - * to thread-safe PHP, and for Linux from PHP 8.6 on, where the official - * Docker images are thread-safe — a thread-safe PHP on macOS, or on Linux - * before 8.6, finds no binary and runs without the extension. Workers run the regular + * to thread-safe PHP, and for Linux, where the official Docker images are + * thread-safe from 8.6 on (and the php:*-zts ones before) — a thread-safe + * PHP on macOS finds no binary and runs without the extension. Workers run the regular * entrypoint, so TurboExtensionEnabler still gates activation on the * expected extension version. *