diff --git a/.github/scripts/install-alpine-php.sh b/.github/scripts/install-alpine-php.sh new file mode 100644 index 00000000000..50c673377a3 --- /dev/null +++ b/.github/scripts/install-alpine-php.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Installs PHP $PHP_MINOR and the extension's build tools into the Alpine +# containers of the musl turbo legs in phar.yml. With PHP_ZTS=1 it builds a +# thread-safe PHP from the release tarball instead (build-php.sh): Alpine +# packages no thread-safe PHP, while the official php:*-alpine Docker images +# are thread-safe from 8.6 on. +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 + PHP_MINOR="$PHP_MINOR" PHP_ZTS=1 sh "$(dirname "$0")/../turbo-build/build-php.sh" + exit 0 +fi + +V="$(echo "$PHP_MINOR" | tr -d .)" +# Alpine's php86 packages are currently only in edge/testing. +if [ "$PHP_MINOR" = "8.6" ]; then + echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories +fi +apk add --no-cache \ + "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ + "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ + "php$V-dom" "php$V-xml" "php$V-xmlwriter" "php$V-simplexml" +ln -sf "/usr/bin/php$V" /usr/local/bin/php +ln -sf "/usr/bin/php-config$V" /usr/local/bin/php-config diff --git a/.github/turbo-build/Dockerfile b/.github/turbo-build/Dockerfile index 0694cb5bd6d..bd63e6dc6fb 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; each a linux/amd64 + linux/arm64 -# manifest). +# gnu-php8.4, gnu-php8.5, gnu-php8.6, 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, @@ -14,18 +14,19 @@ FROM ubuntu:22.04 ARG PHP_MINOR +# 1 builds a thread-safe PHP (the -zts tags) +ARG PHP_ZTS=0 ENV DEBIAN_FRONTEND=noninteractive -# 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 -# (20260924) — an extension built against one refuses to load into the -# other. The 8.6 image therefore builds the official release tarball on -# the same base. Drop these and the source branch below once ondrej/php -# ships 8.6.0. -ARG PHP86_VERSION=8.6.0RC2 -ARG PHP86_URL=https://downloads.php.net/~mbeccati/php-8.6.0RC2.tar.xz -ARG PHP86_SHA256=ef3fba21c311e9bbace0e2102702446d322c275b8a10e6b33b28f2561299671c +# These images build PHP from the official release tarball pinned in +# build-php.sh 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 +# (20260924) — an extension built against one refuses to load into the +# other. Switch the NTS 8.6 image to the PPA once it ships 8.6.0. +# - ondrej/php packages no thread-safe PHP at all, so the ZTS images always +# build from source. # The source label links the GHCR package to this repository. LABEL org.opencontainers.image.source="https://github.com/phpstan/phpstan-src" @@ -48,9 +49,9 @@ RUN test -n "$PHP_MINOR"; \ return 1; \ }; \ apt_install software-properties-common gnupg ca-certificates curl git make g++ patch unzip zstd \ - && if [ "$PHP_MINOR" = "8.6" ]; then \ + && 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 \ - && PHP_VERSION="$PHP86_VERSION" PHP_URL="$PHP86_URL" PHP_SHA256="$PHP86_SHA256" sh /tmp/build-php.sh; \ + && PHP_MINOR="$PHP_MINOR" PHP_ZTS="$PHP_ZTS" sh /tmp/build-php.sh; \ else \ add-apt-repository -y ppa:ondrej/php \ && apt_install "php$PHP_MINOR-cli" "php$PHP_MINOR-dev" "php$PHP_MINOR-curl" "php$PHP_MINOR-mbstring" "php$PHP_MINOR-xml" \ diff --git a/.github/turbo-build/build-php.sh b/.github/turbo-build/build-php.sh index 8b09500aa04..6e51aae9de8 100644 --- a/.github/turbo-build/build-php.sh +++ b/.github/turbo-build/build-php.sh @@ -1,7 +1,9 @@ #!/bin/sh -# Builds and installs PHP $PHP_VERSION into /usr/local from the official -# release tarball at $PHP_URL, verified against $PHP_SHA256. Used by the -# Dockerfile for the PHP minors ondrej/php does not ship yet. +# 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 +# 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. # # The extensions match what the turbo-compile legs use from the PPA # images: tokenizer for the parser tests, pcntl + posix for the fork @@ -9,6 +11,27 @@ # 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 + ;; + *) + echo "build-php.sh: no tarball pinned for PHP $PHP_MINOR" >&2 + exit 1 + ;; +esac +export PHP_VERSION +PHP_ZTS="${PHP_ZTS:-0}" +export PHP_ZTS +ZTS_FLAG="" +if [ "$PHP_ZTS" = "1" ]; then + ZTS_FLAG="--enable-zts" +fi + cd /tmp curl -fsSLo php.tar.xz "$PHP_URL" echo "$PHP_SHA256 php.tar.xz" | sha256sum -c - @@ -17,6 +40,7 @@ tar -xJf php.tar.xz -C php-src --strip-components=1 rm php.tar.xz cd php-src +# shellcheck disable=SC2086 # ZTS_FLAG is empty or a single word ./configure \ --prefix=/usr/local \ --disable-cgi \ @@ -27,7 +51,8 @@ cd php-src --enable-pcntl \ --with-curl \ --with-openssl \ - --with-zlib + --with-zlib \ + $ZTS_FLAG make -j"$(nproc)" make install @@ -41,4 +66,5 @@ cd /tmp rm -rf php-src php -r 'if (PHP_VERSION !== getenv("PHP_VERSION")) { fwrite(STDERR, "built PHP " . PHP_VERSION . ", expected " . getenv("PHP_VERSION") . "\n"); exit(1); }' +php -r 'if ((int) PHP_ZTS !== (int) getenv("PHP_ZTS")) { fwrite(STDERR, "built PHP_ZTS=" . PHP_ZTS . ", expected " . getenv("PHP_ZTS") . "\n"); exit(1); }' php -m diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 762bea72250..541f327dd99 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -24,7 +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. - TURBO_RETIRED_BINARIES: "linux-gnu-x86_64/phpstan_turbo-*-zts.so linux-gnu-arm64/phpstan_turbo-*-zts.so" + # 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" # 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" @@ -340,20 +342,25 @@ jobs: # separate turbo-compile-musl-arm64 job below: JavaScript-based actions # cannot run in Alpine containers on arm64 runners (the runner only # ships an x64 musl Node), so that job drives the Alpine container - # through docker exec instead of a `container:` leg. There are no Linux - # ZTS legs: every distro, ppa:ondrej/php, sury, Remi's default `php`, the - # official Docker image and setup-php ship an NTS CLI, and thread-safe - # PHP on Linux (FrankenPHP, php:*-zts images) is a few percent of hosts — - # not worth two binaries per PHP minor. Their binaries are deleted from - # phpstan/phpstan by the commit job (TURBO_RETIRED_BINARIES). Windows - # keeps its ZTS leg: XAMPP, WampServer and Scoop default to TS PHP. - # The gnu-php8.6 image builds the official 8.6 prerelease tarball (see the - # Dockerfile). Alpine's php86 packages are currently only in edge/testing. - container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}', matrix.php-version) || matrix.target.family == 'musl' && matrix.php-version == '8.6' && 'alpine:edge' || matrix.target.container }} + # through docker exec instead of a `container:` leg. + # 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). Windows has ZTS legs for + # every minor: XAMPP, WampServer and Scoop default to TS PHP. + # 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 }} timeout-minutes: 30 env: PHP_MINOR: ${{ matrix.php-version }} + PHP_ZTS: ${{ matrix.target.suffix == '-zts' && '1' || '0' }} strategy: fail-fast: false @@ -381,6 +388,31 @@ jobs: runs-on: "macos-latest" family: "macos" artifact: "phpstan_turbo" + # 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" + artifact: "phpstan_turbo" + suffix: "-zts" + - php-version: "8.6" + target: + name: "linux-gnu-arm64" + runs-on: "ubuntu-24.04-arm" + family: "gnu" + artifact: "phpstan_turbo" + suffix: "-zts" + - php-version: "8.6" + target: + name: "linux-musl-x86_64" + runs-on: "ubuntu-latest" + container: "alpine:3.24" + family: "musl" + artifact: "phpstan_turbo" + suffix: "-zts" steps: - name: Harden the runner (Audit all outbound calls) @@ -389,23 +421,12 @@ jobs: with: egress-policy: audit - # The Alpine leg installs PHP and tooling before checkout: - # actions/checkout needs git inside the container, and the build needs - # php/php-config symlinks pointing at the matrix PHP version. The GNU + # actions/checkout needs git inside the Alpine container; PHP itself is + # installed after checkout, by a script from the repository. The GNU # container legs have the toolchain baked in. - - name: "Install PHP and build tools (Alpine container)" + - name: "Install git (Alpine container)" if: matrix.target.family == 'musl' - run: | - V="$(echo "$PHP_MINOR" | tr -d .)" - if [ "$PHP_MINOR" = "8.6" ]; then - echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories - fi - apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd \ - "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ - "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ - "php$V-dom" "php$V-xml" "php$V-xmlwriter" "php$V-simplexml" - ln -sf "/usr/bin/php$V" /usr/local/bin/php - ln -sf "/usr/bin/php-config$V" /usr/local/bin/php-config + run: apk add --no-cache git - name: "Checkout" uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -424,6 +445,11 @@ jobs: - name: "Trust the checkout despite the container/host uid mismatch" run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + # The build needs php/php-config on PATH for the matrix PHP version. + - name: "Install PHP and build tools (Alpine container)" + if: matrix.target.family == 'musl' + run: sh .github/scripts/install-alpine-php.sh + - name: "Install PHP (macOS)" if: matrix.target.family == 'macos' uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 @@ -463,6 +489,10 @@ jobs: EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" [ "$REPORTED" = "$EXPECTED" ] + # the loader rejects a ts-mismatched binary, so php loading it above + # proves the binary matches the interpreter; assert the interpreter + # itself so a -zts artifact cannot silently carry an NTS build + [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 if: matrix.target.family == 'macos' @@ -509,7 +539,7 @@ jobs: - name: "Upload extension artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}" + name: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" path: "turbo-ext/phpstan_turbo.so" if-no-files-found: "error" @@ -521,17 +551,23 @@ jobs: # ships an x64 musl Node). Instead the workflow steps run on the arm64 # host and only the build and test commands run inside a long-lived # Alpine container via docker exec — native arm64, no QEMU. The steps - # mirror the musl leg of turbo-compile one for one. + # mirror the musl legs of turbo-compile one for one. runs-on: "ubuntu-24.04-arm" timeout-minutes: 30 env: PHP_MINOR: ${{ matrix.php-version }} + PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} strategy: 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" steps: - name: Harden the runner (Audit all outbound calls) @@ -551,27 +587,15 @@ jobs: - name: "Start the Alpine build container" env: - ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && 'alpine:edge' || 'alpine:3.24' }} + ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && matrix.ts == 'nts' && 'alpine:edge' || 'alpine:3.24' }} run: | docker run -d --name alpine-build \ -v "$PWD:/work" -w /work \ - -e PHP_MINOR -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ + -e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ "$ALPINE_IMAGE" sleep 7200 - name: "Install PHP and build tools" - run: | - docker exec -i alpine-build sh -e <<'EOF' - V="$(echo "$PHP_MINOR" | tr -d .)" - if [ "$PHP_MINOR" = "8.6" ]; then - echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories - fi - apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd \ - "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ - "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ - "php$V-dom" "php$V-xml" "php$V-xmlwriter" "php$V-simplexml" - ln -sf "/usr/bin/php$V" /usr/local/bin/php - ln -sf "/usr/bin/php-config$V" /usr/local/bin/php-config - EOF + run: docker exec alpine-build sh .github/scripts/install-alpine-php.sh # The workspace volume is owned by the host runner user while the # container runs as root, so git refuses the repo ("dubious @@ -593,6 +617,7 @@ jobs: EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" [ "$REPORTED" = "$EXPECTED" ] + [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] EOF - name: "Install Composer dependencies (pinned composer)" @@ -632,7 +657,7 @@ jobs: - name: "Upload extension artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}" + name: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" path: "turbo-ext/phpstan_turbo.so" if-no-files-found: "error" @@ -659,17 +684,20 @@ jobs: # The 8.3 images are pinned to one release of each known numbering; # 8.4/8.5 float on the minor tag and 8.6 on the prerelease tag so a future # release with a surprising build is picked up on the next run without - # a workflow change. Legs run on linux-gnu-x86_64 only: the token - # numbering comes from the bison that generated the tarball's parser, - # not from arch or libc, and per-arch coverage stays with turbo-run - # and the compile jobs. No macOS/Windows legs: those runners cannot - # run Linux containers, and no alternative-numbered official build is - # distributed for either platform today. + # a workflow change. Legs run on x86_64 only: the token numbering comes + # from the bison that generated the tarball's parser, not from arch or + # libc, and per-arch coverage stays with turbo-run and the compile jobs. + # 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 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' && 'phpstan-turbo-test:8.6' || matrix.image }} + IMAGE: ${{ matrix.php-version == '8.6' && format('phpstan-turbo-test:{0}', matrix.binary) || matrix.image }} permissions: contents: read @@ -683,18 +711,27 @@ jobs: - "php:8.4-cli-bookworm" - "php:8.5-cli-bookworm" - "php:8.6-rc-cli-bookworm" + - "php:8.6-rc-cli-alpine" check: ["tests", "phpstan"] include: - image: "php:8.3.21-cli-bookworm" php-version: "8.3" + binary: "linux-gnu-x86_64-php8.3" - image: "php:8.3.22-cli-bookworm" php-version: "8.3" + binary: "linux-gnu-x86_64-php8.3" - image: "php:8.4-cli-bookworm" php-version: "8.4" + binary: "linux-gnu-x86_64-php8.4" - image: "php:8.5-cli-bookworm" php-version: "8.5" + binary: "linux-gnu-x86_64-php8.5" - image: "php:8.6-rc-cli-bookworm" php-version: "8.6" + binary: "linux-gnu-x86_64-php8.6-zts" + - image: "php:8.6-rc-cli-alpine" + php-version: "8.6" + binary: "linux-musl-x86_64-php8.6-zts" # The tests legs invoke paratest directly instead of a literal # `make tests`: its install-paratest step needs a composer, which # the images do not carry — tests/vendor is installed on the @@ -735,7 +772,7 @@ jobs: - name: "Download extension artifact" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: "phpstan_turbo-linux-gnu-x86_64-php${{ matrix.php-version }}" + name: "phpstan_turbo-${{ matrix.binary }}" path: "turbo-ext" - name: "Add mbstring to the PHP 8.6 prerelease image" @@ -743,16 +780,38 @@ jobs: env: PHP_IMAGE: ${{ matrix.image }} # Unlike the stable images, the 8.6 prerelease omits mbstring, - # which PHPUnit and PHPStan's test fixtures require. + # which PHPUnit and PHPStan's test fixtures require. The Alpine + # images also drop the build tools after building PHP, and carry no + # bash, which the run step below needs, and no make, which + # `make phpstan` needs. run: | docker build --build-arg "PHP_IMAGE=$PHP_IMAGE" -t "$IMAGE" - <<'DOCKERFILE' ARG PHP_IMAGE FROM ${PHP_IMAGE} - RUN apt-get update && apt-get install -y --no-install-recommends libonig-dev \ - && docker-php-ext-install mbstring \ - && rm -rf /var/lib/apt/lists/* + 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/*; \ + fi DOCKERFILE + # Guards the matrix above: a binary of the wrong thread-safety would + # fail the probe below anyway, but with an undefined-symbol error + # instead of this message. + - name: "Verify the binary matches the image's thread-safety" + env: + BINARY: ${{ matrix.binary }} + run: | + IMAGE_ZTS="$(docker run --rm "$IMAGE" php -r 'echo (int) PHP_ZTS;')" + BINARY_ZTS=$([ "${BINARY%-zts}" != "$BINARY" ] && echo 1 || echo 0) + echo "image PHP_ZTS=$IMAGE_ZTS, binary $BINARY" + [ "$IMAGE_ZTS" = "$BINARY_ZTS" ] + # Fast fail with a one-line diagnosis: a token-numbering mismatch # would otherwise surface as thousands of unrelated-looking failures # in the suite run below. @@ -1011,7 +1070,9 @@ jobs: php-version: ["8.3", "8.4", "8.5", "8.6"] ts: ["nts", "zts"] script: ["make tests", "make phpstan"] - # Linux ships no ZTS binary (see turbo-compile); Windows does + # 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. exclude: - operating-system: "ubuntu-latest" ts: "zts" diff --git a/.github/workflows/turbo-build-image.yml b/.github/workflows/turbo-build-image.yml index 99ad200ff83..90426cde072 100644 --- a/.github/workflows/turbo-build-image.yml +++ b/.github/workflows/turbo-build-image.yml @@ -4,9 +4,10 @@ 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} -# 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. +# and the thread-safe gnu-php8.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. on: workflow_dispatch: @@ -34,7 +35,7 @@ concurrency: jobs: build: - name: "Build image (php${{ matrix.php-version }}, ${{ matrix.platform.arch }})" + name: "Build image (php${{ matrix.tag }}, ${{ matrix.platform.arch }})" if: github.repository == 'phpstan/phpstan-src' runs-on: ${{ matrix.platform.runs-on }} timeout-minutes: 30 @@ -45,7 +46,8 @@ jobs: strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + # [-zts]; a -zts tag builds a thread-safe PHP of that minor + tag: ["8.3", "8.4", "8.5", "8.6", "8.6-zts"] platform: - arch: "amd64" runs-on: "ubuntu-latest" @@ -53,7 +55,8 @@ jobs: runs-on: "ubuntu-24.04-arm" env: - IMAGE: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.php-version }}-${{ matrix.platform.arch }}" + IMAGE: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.tag }}-${{ matrix.platform.arch }}" + TAG: ${{ matrix.tag }} steps: - name: Harden the runner (Audit all outbound calls) @@ -67,13 +70,19 @@ jobs: persist-credentials: false - name: "Build" - run: docker build --build-arg "PHP_MINOR=${{ matrix.php-version }}" -t "$IMAGE" .github/turbo-build + run: | + MINOR="${TAG%-zts}" + ZTS=$([ "$MINOR" != "$TAG" ] && echo 1 || echo 0) + docker build --build-arg "PHP_MINOR=$MINOR" --build-arg "PHP_ZTS=$ZTS" -t "$IMAGE" .github/turbo-build - name: "Smoke test (interpreter matches the matrix, toolchain present)" run: | - VERSION="$(docker run --rm "$IMAGE" php -r 'echo PHP_VERSION;')" - echo "image reports PHP $VERSION, expected ${{ matrix.php-version }}.*" - [ "${VERSION%.*}" = "${{ matrix.php-version }}" ] + MINOR="${TAG%-zts}" + ZTS=$([ "$MINOR" != "$TAG" ] && echo 1 || echo 0) + VERSION="$(docker run --rm "$IMAGE" php -r 'echo PHP_VERSION, " ", (int) PHP_ZTS;')" + echo "image reports PHP $VERSION (version, ZTS), expected $MINOR.* $ZTS" + [ "${VERSION%.* *}" = "$MINOR" ] + [ "${VERSION##* }" = "$ZTS" ] docker run --rm "$IMAGE" sh -c 'php-config --version && git --version && g++ --version && make --version && curl --version' # PR runs stop here: build + smoke validate the Dockerfile change, @@ -87,7 +96,7 @@ jobs: run: docker push "$IMAGE" manifest: - name: "Publish multi-arch manifest (php${{ matrix.php-version }})" + name: "Publish multi-arch manifest (php${{ matrix.tag }})" if: github.repository == 'phpstan/phpstan-src' && github.event_name != 'pull_request' needs: build runs-on: "ubuntu-latest" @@ -97,10 +106,10 @@ jobs: strategy: matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + tag: ["8.3", "8.4", "8.5", "8.6", "8.6-zts"] env: - TAG: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.php-version }}" + TAG: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.tag }}" steps: - name: Harden the runner (Audit all outbound calls) diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 2f905b241bc..5bfbffcd861 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -29,9 +29,10 @@ * a source checkout loads its locally built extension through php.ini * instead. Only non-debug builds for PHP >= MINIMUM_PHP_VERSION_ID are * shipped; a ZTS variant (-zts filename - * suffix) exists for Windows only, where XAMPP, WampServer and Scoop default - * to thread-safe PHP — a thread-safe PHP on Linux or macOS finds no binary - * and runs without the extension. Workers run the regular + * 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 * entrypoint, so TurboExtensionEnabler still gates activation on the * expected extension version. *