diff --git a/.github/workflows/build-php-linux.yml b/.github/workflows/build-php-linux.yml new file mode 100644 index 0000000..396bd0a --- /dev/null +++ b/.github/workflows/build-php-linux.yml @@ -0,0 +1,149 @@ +# Builds one PHP release for Linux (x86-64 or arm64) with static-php-cli in its glibc container, +# tests it, and publishes it on the php- release as php--build-.tar.gz: +# bin/php and bin/php-fpm with the compiled-in extensions and the shared ones in ext/ from +# php.linux in versions.json (build number: php.linux.build). +# Called by release.yml for every version without that build yet; run it by hand to test a build +# without publishing. Nothing is published unless every check passed, and a published asset is +# never replaced. +name: Build PHP Linux + +on: + workflow_call: + inputs: + version: + type: string + required: true + platform: + type: string + required: true + publish: + type: boolean + default: false + workflow_dispatch: + inputs: + version: + description: PHP version (major.minor.patch) + required: true + platform: + description: Platform + type: choice + options: [linux-x64, linux-arm64] + publish: + description: Publish the package as a release asset + type: boolean + default: false + +permissions: + contents: read + +env: + VERSION: ${{ inputs.version }} + PLATFORM: ${{ inputs.platform }} + +jobs: + build: + name: build php ${{ inputs.version }} ${{ inputs.platform }} + runs-on: ${{ inputs.platform == 'linux-arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + timeout-minutes: 150 + outputs: + asset: ${{ steps.config.outputs.asset }} + ok: ${{ steps.done.outputs.ok }} + env: + GITHUB_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v7 + + - name: Read versions.json + id: config + run: | + jq -r '.php.linux | "SPC_VERSION=\(.spc)\nMIN_GLIBC=\(.min_glibc)\nSTATIC_EXTS=\(.static_extensions)\nSHARED_EXTS=\(.shared_extensions)"' versions.json >> "$GITHUB_ENV" + echo "MINOR=$(cut -d. -f1-2 <<<"$VERSION")" >> "$GITHUB_ENV" + build="$(jq -r .php.linux.build versions.json)" + echo "BUILD=$build" >> "$GITHUB_ENV" + asset="php-$VERSION-build$build-$PLATFORM.tar.gz" + echo "ASSET=$asset" >> "$GITHUB_ENV" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + + - name: Install patchelf + run: sudo apt-get update -q && sudo apt-get install -yq patchelf + + - name: Cache static-php-cli downloads + uses: actions/cache@v6 + with: + path: spc-src/downloads + key: spc-linux-downloads-${{ env.SPC_VERSION }}-${{ inputs.platform }}-${{ inputs.version }}-${{ hashFiles('versions.json') }} + restore-keys: spc-linux-downloads-${{ env.SPC_VERSION }}-${{ inputs.platform }}-${{ env.MINOR }} + + - name: Build + run: bash packages/php/linux/build.sh "$VERSION" out + + - name: Portability (readelf) + run: bash scripts/verify-linux.sh out "$MIN_GLIBC" | tee -a "$GITHUB_STEP_SUMMARY" + + # Every shared extension in versions.json has to build and load for a release. + - name: Smoke test + run: bash scripts/smoke-php-package.sh php out + + - name: Package + run: | + jq -n --arg version "$VERSION" --argjson build "$BUILD" --arg platform "$PLATFORM" --arg commit "$GITHUB_SHA" \ + --arg spc "$SPC_VERSION" --arg min_glibc "$MIN_GLIBC" --arg static "$STATIC_EXTS" --arg shared "$SHARED_EXTS" \ + --arg built "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '{package: "php", version: $version, build: $build, platform: $platform, min_glibc: $min_glibc, + built: $built, commit: $commit, static_php_cli: $spc, + extensions: {compiled_in: ($static | split(",")), shared: ($shared | split(","))}}' \ + > out/flock-package.json + tar -C out -czf "$ASSET" . + sha256sum "$ASSET" | tee -a "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v7 + with: + name: ${{ env.ASSET }} + path: ${{ env.ASSET }} + retention-days: 7 + - id: done + run: echo ok=true >> "$GITHUB_OUTPUT" + + # The oldest glibc the build targets, without anything the runner has installed. + portable: + name: run php ${{ inputs.version }} ${{ inputs.platform }} on CentOS 7 (glibc 2.17) + needs: build + if: needs.build.outputs.ok == 'true' + runs-on: ${{ inputs.platform == 'linux-arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + outputs: + ok: ${{ steps.done.outputs.ok }} + env: + ASSET: ${{ needs.build.outputs.asset }} + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: ${{ env.ASSET }} + - name: Read versions.json + run: jq -r '.php.linux | "STATIC_EXTS=\(.static_extensions)\nSHARED_EXTS=\(.shared_extensions)"' versions.json >> "$GITHUB_ENV" + # In a container: the runner's own glibc and libraries would hide what the package lacks. As + # the runner's user, like PHP-FPM on a user's machine (as root it wants a pool user). + - name: Smoke test + run: | + mkdir pkg && tar -C pkg -xzf "$ASSET" + docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/w" -w /w -e STATIC_EXTS -e SHARED_EXTS centos:7 \ + bash scripts/smoke-php-package.sh php pkg + - id: done + run: echo ok=true >> "$GITHUB_OUTPUT" + + publish: + needs: [build, portable] + if: inputs.publish && needs.portable.outputs.ok == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + ASSET: ${{ needs.build.outputs.asset }} + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: ${{ env.ASSET }} + - run: bash scripts/publish.sh php "$VERSION" "$ASSET" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a69532a..c871014 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -32,6 +32,6 @@ jobs: - name: Check run: | go -C tools/flockrel run . check -versions ../../versions.json -php-build ../../packages/php/macos/build.sh \ - -out "$RUNNER_TEMP/findings.json" 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" + -php-build-linux ../../packages/php/linux/build.sh -out "$RUNNER_TEMP/findings.json" 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" - name: Issues run: bash scripts/upstream-issues.sh "$RUNNER_TEMP/findings.json" | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index bbbc3b1..7030d04 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -35,7 +35,7 @@ on: platform: description: Platform type: choice - options: [macos-arm64, windows-x64] + options: [macos-arm64, windows-x64, linux-x64, linux-arm64] publish: description: Publish the package as a release asset type: boolean @@ -52,7 +52,8 @@ env: jobs: fetch: name: fetch ${{ inputs.package }} ${{ inputs.version }} ${{ inputs.platform }} - runs-on: ubuntu-latest + # MySQL for Linux is repacked with libraries from an AlmaLinux container of its architecture. + runs-on: ${{ inputs.platform == 'linux-arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} timeout-minutes: 30 outputs: asset: ${{ steps.config.outputs.asset }} @@ -81,12 +82,13 @@ jobs: path: | ${{ steps.config.outputs.asset }} ${{ inputs.package }}-${{ inputs.version }}.json + sources/* retention-days: 7 test: name: run ${{ inputs.package }} ${{ inputs.version }} ${{ inputs.platform }} needs: fetch - runs-on: ${{ inputs.platform == 'windows-x64' && 'windows-2022' || 'macos-15' }} + runs-on: ${{ fromJSON('{"windows-x64":"windows-2022","linux-x64":"ubuntu-24.04","linux-arm64":"ubuntu-24.04-arm"}')[inputs.platform] || 'macos-15' }} timeout-minutes: 20 env: ASSET: ${{ needs.fetch.outputs.asset }} @@ -103,7 +105,7 @@ jobs: if: runner.os == 'macOS' run: sudo mv /opt/homebrew /opt/homebrew.hidden - name: Smoke test - if: runner.os == 'macOS' + if: runner.os != 'Windows' run: bash scripts/smoke-service.sh "$PKG" "$ASSET" - name: Smoke test if: runner.os == 'Windows' @@ -126,4 +128,5 @@ jobs: - uses: actions/download-artifact@v8 with: name: ${{ env.ASSET }} - - run: bash scripts/publish.sh "$PKG" "$VERSION" "$ASSET" "$PKG-$VERSION.json" + # sources/: what a mirror.sh adds for its license (MySQL for Linux: libaio's and numactl's source). + - run: bash scripts/publish.sh "$PKG" "$VERSION" "$ASSET" "$PKG-$VERSION.json" sources/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97ccda7..360a081 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ # Flock downloads https://github.com//releases/download/manifest/manifest.json (+ .sig), or # manifest-beta.json / manifest-experimental.json for those channels. # php-macos PHP and FrankenPHP for macOS (build-php-macos.yml) +# php-linux PHP for Linux, x86-64 and arm64 (build-php-linux.yml) # php-exts-windows extension packs for Windows PHP, one per minor (build-php-exts-windows.yml) # frankenphp-windows FrankenPHP for Windows (build-frankenphp-windows.yml) # builds services built from source: PostgreSQL, MariaDB (macOS), Valkey, Redis (build-service.yml) @@ -34,6 +35,7 @@ jobs: outputs: any: ${{ steps.plan.outputs.any }} php_macos: ${{ steps.plan.outputs.php_macos }} + php_linux: ${{ steps.plan.outputs.php_linux }} php_exts_windows: ${{ steps.plan.outputs.php_exts_windows }} frankenphp_windows: ${{ steps.plan.outputs.frankenphp_windows }} builds: ${{ steps.plan.outputs.builds }} @@ -67,6 +69,21 @@ jobs: publish: true optional: ${{ matrix.optional == true }} + php-linux: + needs: plan + if: needs.plan.outputs.php_linux != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.plan.outputs.php_linux) }} + permissions: + contents: write + uses: ./.github/workflows/build-php-linux.yml + with: + version: ${{ matrix.version }} + platform: ${{ matrix.platform }} + publish: true + php-exts-windows: needs: plan if: needs.plan.outputs.php_exts_windows != '[]' @@ -132,7 +149,7 @@ jobs: publish: true manifest: - needs: [plan, php-macos, php-exts-windows, frankenphp-windows, builds, mirrors] + needs: [plan, php-macos, php-linux, php-exts-windows, frankenphp-windows, builds, mirrors] # Also after failed builds: whatever did get published belongs in the manifests. if: always() && needs.plan.result == 'success' permissions: diff --git a/README.md b/README.md index cd697d6..926cf24 100644 --- a/README.md +++ b/README.md @@ -7,31 +7,43 @@ signed manifest, so nothing has to be compiled on your machine. ## What we publish -Platforms are macOS on Apple Silicon (`macos-arm64`, macOS 13+) and Windows x64 (`windows-x64`). +Platforms are macOS on Apple Silicon (`macos-arm64`, macOS 13+), Windows x64 (`windows-x64`) and, +for some packages so far, Linux on x86-64 and arm64 (`linux-x64`, `linux-arm64`). Every package is either **built** here from verified upstream source, or **mirrored**: the vendor's own binaries, checked against the vendor's checksum or signature. -| Package | macOS arm64 | Windows x64 | Versions (`versions.json`) | -|---|---|---|---| -| `php` | built: cli + fpm, extensions compiled in, optional ones in `ext/` | mirrored: the official thread-safe build from windows.php.net | macOS 8.1–8.5, Windows 7.4–8.5 | -| `php-exts` | – | built: extension DLLs for the Windows build (below) | one pack per Windows minor | -| `frankenphp` | built: FrankenPHP with the same PHP and extensions as `php` | built: FrankenPHP next to the official PHP build it links against | PHP 8.2–8.5 | -| `postgres` | built: server, client tools (psql, pg_dump, …), contrib | built | 14–18 | -| `mariadb` | built | mirrored | 10.11, 11.4, 11.8, 12.3 | -| `mysql` | mirrored | mirrored | 8.0, 8.4, 9.7 | -| `mongodb` | mirrored | mirrored | 7.0, 8.0, 9.0 | -| `valkey` | built | built with MSYS2 (`fork()` for persistence) | 9.1 | -| `redis` | built | – (Valkey only) | 7.2, 7.4, 8.4, 8.10 | -| `meilisearch` | mirrored (Community Edition) | mirrored (Community Edition) | 1 | -| `typesense` | mirrored | – (no Windows build upstream) | 29, 30 | -| `rustfs` | mirrored | mirrored | 1 | -| `node` | mirrored | mirrored | 16, 18, 20, 22, 24, 26 | +| Package | macOS arm64 | Windows x64 | Linux x64, arm64 | Versions (`versions.json`) | +|---|---|---|---|---| +| `php` | built: cli + fpm, extensions compiled in, optional ones in `ext/` | mirrored: the official thread-safe build from windows.php.net | built, like macOS (glibc 2.17+, see below) | macOS 8.1–8.5, Linux 8.2–8.5, Windows 7.4–8.5 | +| `php-exts` | – | built: extension DLLs for the Windows build (below) | – | one pack per Windows minor | +| `frankenphp` | built: FrankenPHP with the same PHP and extensions as `php` | built: FrankenPHP next to the official PHP build it links against | – | PHP 8.2–8.5 | +| `postgres` | built: server, client tools (psql, pg_dump, …), contrib | built | – | 14–18 | +| `mariadb` | built | mirrored | – | 10.11, 11.4, 11.8, 12.3 | +| `mysql` | mirrored | mirrored | mirrored, with libaio, libnuma and ncurses added (below) | 8.0, 8.4, 9.7 | +| `mongodb` | mirrored | mirrored | mirrored: the Ubuntu 22.04 build (below) | 7.0, 8.0, 9.0 | +| `valkey` | built | built with MSYS2 (`fork()` for persistence) | – | 9.1 | +| `redis` | built | – (Valkey only) | – | 7.2, 7.4, 8.4, 8.10 | +| `meilisearch` | mirrored (Community Edition) | mirrored (Community Edition) | mirrored (Community Edition) | 1 | +| `typesense` | mirrored | – (no Windows build upstream) | mirrored | 29, 30 | +| `rustfs` | mirrored | mirrored | mirrored (static musl build) | 1 | +| `node` | mirrored | mirrored | mirrored | 16, 18, 20, 22, 24, 26 | The versions column lists release lines: every day the newest upstream release of each line is looked up (`tools/flockrel/upstream.go`, php.net and windows.php.net for PHP) and built or mirrored when it isn't published yet. Patch releases come in on their own; a new line (PostgreSQL 19, Node.js 28, PHP 8.6) is one entry in `versions.json`. +### PHP for Linux + +`php` for `linux-x64` and `linux-arm64` is built like the macOS one, with static-php-cli and the +same extensions (`php.linux`), but in static-php-cli's glibc container (`bin/spc-gnu-docker`: CentOS +7, glibc 2.17). Every library but glibc's is compiled in, and unlike a fully static musl build, PHP +loads the shared extensions in `ext/`. Before publishing, `scripts/verify-linux.sh` checks that no +binary needs a newer glibc or another shared library, and the smoke test runs again in a `centos:7` +container. Linux starts at PHP 8.2: 8.1 had reached its end of life, and its headers need +`_GNU_SOURCE`, which the mongodb extension's `-std=c17` build doesn't get from 8.1's `php_config.h`. +FrankenPHP isn't built for Linux yet. + ### PHP release candidates A PHP minor in `versions.json` that has no release yet gets its release candidates instead @@ -75,13 +87,13 @@ read its configure option from, so `--enable-opentelemetry` is passed explicitly ### Package layouts -Archives are `.tar.gz` on macOS and `.zip` on Windows. Mirrored packages keep the vendor's layout, +Archives are `.tar.gz` on macOS and Linux and `.zip` on Windows. Mirrored packages keep the vendor's layout, usually one top-level folder (`mysql-8.4.11-macos15-arm64/bin/mysqld`); unpack and drop it. What we build has `flock-package.json` at the top (what it is, how it was built) and `LICENSES/`. | Package | Layout | |---|---| -| `php` (macOS) | `bin/php`, `bin/php-fpm`, `ext/*.so` | +| `php` (macOS, Linux) | `bin/php`, `bin/php-fpm`, `ext/*.so` | | `php` (Windows) | the official zip: `php.exe`, `php-cgi.exe`, `ext/` | | `php-exts` | `ext/php_.dll` into PHP's `ext\`; `*.dll` next to `php.exe` (ImageMagick) | | `frankenphp` (macOS) | `bin/frankenphp`, `ext/*.so` | @@ -94,6 +106,14 @@ build has `flock-package.json` at the top (what it is, how it was built) and `LI ### Things to know - **MySQL on macOS** needs macOS 15: Oracle builds it for no older release. +- **MySQL on Linux** is Oracle's generic glibc build, repacked as tar.gz with libaio, libnuma and + ncurses from AlmaLinux 8 in `lib/private/`, where its binaries look first: Ubuntu 24.04 renamed + libaio's library, so mysqld wouldn't start there otherwise. libaio's and numactl's source packages + (LGPL) are published next to it. On arm64 Oracle has no minimal build, so the debug server and + plugins are dropped and debug info stripped. +- **MongoDB on Linux** is the Ubuntu 22.04 build: it needs glibc 2.35, OpenSSL 3 and libcurl, which + current distributions have. MongoDB publishes no build that runs everywhere, and its RHEL 8 build + needs OpenSSL 1.1, which reached its end of life. - **PostgreSQL on Windows** refuses to run as an administrator: start it with `pg_ctl`, which drops the administrator rights first. - **PHP on Windows** is only mirrored while it is windows.php.net's current build of its minor @@ -126,6 +146,7 @@ the same version. | `promote.yml` | records a channel decision (promote, hold, yank) in `channels.json` | | `check.yml` | weekly: new release lines, end of life, newer pinned versions as issues | | `build-php-macos.yml` | `php` and `frankenphp` for macOS (static-php-cli) | +| `build-php-linux.yml` | `php` for Linux (static-php-cli, glibc container) | | `build-php-exts-windows.yml` | `php-exts` for one PHP minor | | `build-frankenphp-windows.yml` | `frankenphp` for Windows | | `build-service.yml` | PostgreSQL, MariaDB (macOS), Valkey, Redis from source | @@ -162,7 +183,7 @@ Every published build is in one of three channels, and each channel has its own listing the builds at its level or above: | Channel | Manifest | Who | Contains | -|---|---|---|---| +|---|---|---|---|---| | stable | `manifest.json` | every Flock installation | what has been promoted | | beta | `manifest-beta.json` | opt-in in Flock | stable, patches in their waiting time, approved new lines, pre-releases | | experimental | `manifest-experimental.json` | testing before promotion | everything published | @@ -268,5 +289,6 @@ scripts/publish.sh upload a package (and its sources) to its relea scripts/ready-issue.sh keep the Ready for testing issue up to date scripts/upstream-issues.sh turn the weekly check's findings into issues scripts/verify-macos.sh fails when a Mach-O file links outside macOS or targets macOS > 13 +scripts/verify-linux.sh fails when an ELF file needs a library beyond glibc's or glibc > 2.17 scripts/smoke-*.sh smoke tests run against every package before it is published ``` diff --git a/packages/meilisearch/mirror.sh b/packages/meilisearch/mirror.sh index 52bf9f4..d1b8a1b 100755 --- a/packages/meilisearch/mirror.sh +++ b/packages/meilisearch/mirror.sh @@ -10,6 +10,8 @@ V="$1"; PLATFORM="$2"; OUT="$3" case "$PLATFORM" in macos-arm64) asset=meilisearch-macos-apple-silicon; bin=meilisearch ;; windows-x64) asset=meilisearch-windows-amd64.exe; bin=meilisearch.exe ;; + linux-x64) asset=meilisearch-linux-amd64; bin=meilisearch ;; + linux-arm64) asset=meilisearch-linux-aarch64; bin=meilisearch ;; esac dir="$(mktemp -d)" fetch "https://github.com/meilisearch/meilisearch/releases/download/v$V/$asset" "$dir/$bin" diff --git a/packages/mongodb/mirror.sh b/packages/mongodb/mirror.sh index b9d71ea..fb13a27 100755 --- a/packages/mongodb/mirror.sh +++ b/packages/mongodb/mirror.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# Mirrors MongoDB Community Server's archive unchanged, checked against MongoDB's SHA-256. +# Mirrors MongoDB Community Server's archive unchanged, checked against MongoDB's SHA-256. On Linux, +# MongoDB builds per distribution; the Ubuntu 22.04 build needs glibc 2.35, OpenSSL 3 and libcurl, +# which current distributions have (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9+, Arch). The RHEL 8 +# build would need OpenSSL 1.1, which reached its end of life. # Usage: packages/mongodb/mirror.sh set -euo pipefail . "$(dirname "$0")/../../scripts/lib.sh" @@ -8,6 +11,8 @@ V="$1"; PLATFORM="$2"; OUT="$3" case "$PLATFORM" in macos-arm64) url="https://fastdl.mongodb.org/osx/mongodb-macos-arm64-$V.tgz" ;; windows-x64) url="https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-$V.zip" ;; + linux-x64) url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-$V.tgz" ;; + linux-arm64) url="https://fastdl.mongodb.org/linux/mongodb-linux-aarch64-ubuntu2204-$V.tgz" ;; esac fetch "$url" "$OUT" check_sha256 "$OUT" "$(fetch_text "$url.sha256" | cut -d' ' -f1)" diff --git a/packages/mysql/mirror.sh b/packages/mysql/mirror.sh index 854e985..b0979a8 100755 --- a/packages/mysql/mirror.sh +++ b/packages/mysql/mirror.sh @@ -1,5 +1,14 @@ #!/usr/bin/env bash # Mirrors Oracle's MySQL Community Server archive unchanged (macOS tar.gz, Windows zip). +# +# Linux: Oracle's generic glibc build, repacked as tar.gz with what it needs from the system added +# to lib/private, where its binaries look first ($ORIGIN/../lib/private): libaio and libnuma +# (mysqld) and ncurses (the client), taken from AlmaLinux 8, the glibc 2.28 system Oracle builds +# against (its packages are signature-checked by dnf). libaio is the reason: Ubuntu 24.04 renamed +# its library, so mysqld can't start there otherwise. libaio and numactl are LGPL: their source +# packages go to /sources/, published next to the archive. On arm64 Oracle publishes no +# minimal build, so the debug server and plugins are dropped and debug info stripped, which is +# what the minimal x86-64 build is. Needs Docker, on a machine of the platform's architecture. # Usage: packages/mysql/mirror.sh set -euo pipefail . "$(dirname "$0")/../../scripts/lib.sh" @@ -9,8 +18,66 @@ case "$PLATFORM" in # Oracle names macOS builds after the oldest macOS they support, which moves over time. macos-arm64) names=("mysql-$V-macos15-arm64.tar.gz" "mysql-$V-macos14-arm64.tar.gz" "mysql-$V-macos26-arm64.tar.gz") ;; windows-x64) names=("mysql-$V-winx64.zip") ;; + # 8.0 is built against glibc 2.17, later lines against 2.28. + linux-x64) names=("mysql-$V-linux-glibc2.28-x86_64-minimal.tar.xz" "mysql-$V-linux-glibc2.17-x86_64-minimal.tar.xz") ;; + linux-arm64) names=("mysql-$V-linux-glibc2.28-aarch64.tar.xz") ;; esac + +if [[ "$PLATFORM" != linux-* ]]; then + for name in "${names[@]}"; do + mysql_fetch "$V" "$name" "$OUT" && exit 0 + done + exit 1 +fi + +tmp="$(mktemp -d)" for name in "${names[@]}"; do - mysql_fetch "$V" "$name" "$OUT" && exit 0 + mysql_fetch "$V" "$name" "$tmp/$name" && break + rm -f "$tmp/$name" done -exit 1 +[[ -f "$tmp/$name" ]] || exit 1 +tar -C "$tmp" -xJf "$tmp/$name" +dir="$tmp/${name%.tar.xz}" +[[ -d "$dir/lib/private" ]] || { echo "::error::$name has no lib/private" >&2; exit 1; } + +if [[ "$PLATFORM" == linux-arm64 ]]; then + rm -f "$dir/bin/mysqld-debug" + rm -rf "$dir/lib/plugin/debug" + while IFS= read -r -d '' f; do + [[ "$(head -c 4 "$f" | tr -d '\0')" == $'\x7fELF' ]] && strip --strip-debug "$f" + done < <(find "$dir/bin" "$dir/lib" -type f -print0) + echo "✓ dropped the debug server and plugins, stripped debug info" +fi + +# The libraries the binaries need of these: ncurses 6, or 5 for the glibc 2.17 build of 8.0. +libs="$(find "$dir/bin" "$dir/lib" -type f -exec sh -c 'readelf -d "$1" 2>/dev/null' _ {} \; \ + | sed -n 's/.*(NEEDED).*\[\(libaio\|libnuma\|libncurses\|libtinfo\)\(\.so\.[0-9]*\)\]/\1\2/p' | sort -u | tr '\n' ' ')" +echo "adding $libs" +sources="$(dirname "$OUT")/sources" +mkdir -p "$sources" +docker run --rm -e libs="$libs" -v "$dir/lib/private:/out" -v "$sources:/sources" almalinux:8 bash -euc ' + # tsflags: the image skips documentation, where libaio keeps its license. ncurses-libs has none: + # ncurses-base, built from the same source, does. numactl ships none at all (below). + dnf install -y -q --setopt=tsflags= libaio numactl-libs ncurses-libs ncurses-compat-libs >/dev/null + for lib in $libs; do cp -L "/usr/lib64/$lib" /out/; done + rpm -q --qf "%{SOURCERPM}\n" libaio numactl-libs >/sources/srpms.txt + rpm -q --qf "%{VERSION}" numactl-libs >/sources/numactl-version + mkdir -p /out/LICENSES + for pkg in libaio ncurses-base; do + for f in $(rpm -qL "$pkg"; rpm -qd "$pkg" | grep -Ei "/(copying|license)[^/]*$"); do + [[ -f "$f" ]] && cp "$f" "/out/LICENSES/$pkg-$(basename "$f")" + done + done + chown -R "$(stat -c %u:%g /out)" /out /sources' +while read -r srpm; do + fetch "https://vault.almalinux.org/8.10/BaseOS/Source/Packages/$srpm" "$sources/$srpm" + echo "✓ $srpm" +done <"$sources/srpms.txt" +rm "$sources/srpms.txt" +fetch "https://raw.githubusercontent.com/numactl/numactl/v$(cat "$sources/numactl-version")/LICENSE.LGPL2.1" \ + "$dir/lib/private/LICENSES/numactl-LICENSE.LGPL2.1" +rm "$sources/numactl-version" +echo "✓ added libaio, libnuma and ncurses from AlmaLinux 8 to lib/private" + +tar -C "$tmp" -czf "$OUT" "$(basename "$dir")" +rm -rf "$tmp" diff --git a/packages/node/mirror.sh b/packages/node/mirror.sh index 5662cdf..4c00da6 100755 --- a/packages/node/mirror.sh +++ b/packages/node/mirror.sh @@ -10,6 +10,8 @@ V="$1"; PLATFORM="$2"; OUT="$3" case "$PLATFORM" in macos-arm64) name="node-v$V-darwin-arm64.tar.gz" ;; windows-x64) name="node-v$V-win-x64.zip" ;; + linux-x64) name="node-v$V-linux-x64.tar.gz" ;; + linux-arm64) name="node-v$V-linux-arm64.tar.gz" ;; esac tmp="$(mktemp -d)" base="https://nodejs.org/dist/v$V" diff --git a/packages/php/linux/build.sh b/packages/php/linux/build.sh new file mode 100755 index 0000000..53c3935 --- /dev/null +++ b/packages/php/linux/build.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Builds PHP for Linux with static-php-cli in its glibc container (bin/spc-gnu-docker: CentOS 7, +# glibc 2.17): php + php-fpm (NTS) with the compiled-in extensions, plus shared extensions as .so. +# The binaries link only against glibc (everything else is compiled in), so they run on glibc +# distributions from 2014 on, but not on musl ones (Alpine). Unlike a fully static musl build, +# PHP can load the shared extensions. +# +# Usage: packages/php/linux/build.sh +# major.minor builds the newest patch release; major.minor.patch exactly that release. +# SPC_VERSION static-php-cli release to use (required) +# STATIC_EXTS comma-separated extensions compiled in (required) +# SHARED_EXTS comma-separated extensions built as .so (optional) +# DOWNLOAD_ARGS extra arguments for `spc download`, e.g. --custom-url=xdebug:https://… (optional) +# WORK_DIR static-php-cli source checkout, the container's workspace (default: ./spc-src) +# GITHUB_TOKEN lets static-php-cli query GitHub without the anonymous rate limit +# +# Needs Docker and patchelf. Produces /bin/php, /bin/php-fpm, /ext/*.so and +# /LICENSES/. A shared extension that fails to build is reported, not fatal: PHP is still +# usable without it. +set -euo pipefail + +PHP_VERSION="${1:?usage: build.sh }" +PHP_MINOR="$(cut -d. -f1-2 <<<"$PHP_VERSION")" +OUT="$(mkdir -p "${2:?usage: build.sh }" && cd "$2" && pwd)" +: "${SPC_VERSION:?SPC_VERSION is required}" +: "${STATIC_EXTS:?STATIC_EXTS is required}" +SHARED_EXTS="${SHARED_EXTS:-}" + +# The same Xdebug and opentelemetry releases as packages/php/macos/build.sh (see the reasons there). +xdebug=3.5.3 +[[ "$PHP_MINOR" == 8.6 ]] && xdebug=3.6.0alpha1 +DOWNLOAD_ARGS="${DOWNLOAD_ARGS:-} --custom-url=xdebug:https://xdebug.org/files/xdebug-$xdebug.tgz" +DOWNLOAD_ARGS="$DOWNLOAD_ARGS --custom-url=opentelemetry:https://pecl.php.net/get/opentelemetry-1.4.2.tgz" +# OpenLDAP 2.7 puts declarations right after labels, which the container's GCC 10 rejects (C23 +# allows it, GCC 11 accepts it); static-php-cli takes the newest release, so pin the 2.6 line. +DOWNLOAD_ARGS="$DOWNLOAD_ARGS --custom-url=ldap:https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-2.6.15.tgz" + +echo "::group::static-php-cli $SPC_VERSION (glibc container)" +# The container script and the image it builds come with the source, not the spc binary. +mkdir -p "${WORK_DIR:=$PWD/spc-src}" +cd "$WORK_DIR" +if [[ ! -x bin/spc-gnu-docker ]]; then + curl -fsSL "https://github.com/crazywhalecc/static-php-cli/archive/refs/tags/$SPC_VERSION.tar.gz" | tar xz --strip-components 1 +fi +# Downloads run on the host, with the spc binary: CentOS 7's curl can't reach servers that only +# speak TLS 1.3 (xdebug.org). SPC_TARGET has it pick the pre-built libraries for the container's +# glibc (--linux-glibc-2.17.txz). +if [[ ! -x ./spc ]]; then + curl -fsSL "https://github.com/crazywhalecc/static-php-cli/releases/download/$SPC_VERSION/spc-linux-$(uname -m).tar.gz" | tar xz + chmod +x spc +fi +download() { SPC_TARGET=native-native-gnu.2.17 ./spc download "$@"; } +# The container mounts these; created here, they belong to the runner's user rather than root. +mkdir -p buildroot source dist downloads pkgroot log +spc() { bin/spc-gnu-docker "$@"; } +spc --version +echo "::endgroup::" + +# The container runs as root: what it leaves in the mounted folders can only be removed with sudo. +remove() { rm -rf "$@" 2>/dev/null || sudo rm -rf "$@"; } + +all="$STATIC_EXTS${SHARED_EXTS:+,$SHARED_EXTS}" + +echo "::group::Download sources for PHP $PHP_VERSION" +# A release candidate (8.6.0RC3) isn't on php.net/distributions: php.net's list of release +# candidates has its tarball (on the release manager's downloads.php.net page) and SHA-256. +rc_sum="" +with_php="$PHP_VERSION" +if [[ "$PHP_VERSION" =~ RC[0-9]+$ ]]; then + read -r rc_url rc_sum < <(curl -fsSL --retry 3 'https://www.php.net/pre-release-builds.php?format=json' \ + | jq -r --arg v "$PHP_VERSION" '.[] | objects | .release | objects | select(.version == $v) | "\(.files.xz.path) \(.files.xz.sha256)"') + [[ -n "${rc_url:-}" ]] || { echo "::error::php.net doesn't list PHP $PHP_VERSION as a release candidate"; exit 1; } + DOWNLOAD_ARGS="$DOWNLOAD_ARGS --custom-url=php-src:$rc_url" + with_php="$PHP_MINOR" # static-php-cli only takes x.y or x.y.z; the source is the RC tarball +fi +# shellcheck disable=SC2086 +download --with-php="$with_php" --for-extensions="$all" --prefer-pre-built --retry=3 ${DOWNLOAD_ARGS:-} +if [[ -n "$rc_sum" ]]; then + rc_file="downloads/$(basename "$rc_url")" + [[ -f "$rc_file" ]] || { echo "::error::static-php-cli didn't save $rc_file"; ls downloads; exit 1; } + got="$(sha256sum "$rc_file" | cut -d' ' -f1)" + [[ "$got" == "$rc_sum" ]] || { echo "::error::$rc_file: SHA-256 $got, expected $rc_sum"; exit 1; } + echo "✓ $(basename "$rc_file") SHA-256 $got" +fi +echo "::endgroup::" + +# spc hides compiler output; it lands in log/ (wiped by the next spc command), so print the errors. +print_spc_errors() { + echo "::group::static-php-cli log: errors" + grep -h -E ' error:|fatal error|undefined reference|\*\*\* \[' log/*.log 2>/dev/null | sort -u | head -80 || true + echo "::endgroup::" + echo "::group::static-php-cli log: last 80 lines" + tail -n 80 log/spc.shell.log 2>/dev/null || true + echo "::endgroup::" +} + +rm -rf "$OUT" && mkdir -p "$OUT/bin" "$OUT/ext" "$OUT/LICENSES" + +# static-php-cli builds the shared extensions one after another and stops at the first that +# fails. Rebuild without that one until every remaining extension built (at most 4 attempts). +pending="$SHARED_EXTS" +for attempt in 1 2 3 4; do + echo "::group::Build PHP $PHP_VERSION (attempt $attempt${pending:+, shared: $pending})" + # Start clean: libraries built for an extension we dropped would otherwise be linked + # without their dependencies. Pre-built libraries unpack quickly. + remove buildroot source/php-src + mkdir -p buildroot + ok=1 + # static-php-cli's post-build sanity checks are stricter than we need; the smoke test verifies. + spc build "$STATIC_EXTS" ${pending:+--build-shared="$pending"} --build-cli --build-fpm 2>&1 | tee spc-build.out || ok=0 + echo "::endgroup::" + [[ $ok -eq 1 ]] || print_spc_errors + + for bin in php php-fpm; do + [[ -x "buildroot/bin/$bin" ]] || { echo "::error::$bin missing"; exit 1; } + cp "buildroot/bin/$bin" "$OUT/bin/" + done + + remaining="" + for ext in ${pending//,/ }; do + so="$(find buildroot/modules buildroot/lib/php -name "$ext.so" 2>/dev/null | head -1 || true)" + if [[ -n "$so" ]]; then cp "$so" "$OUT/ext/"; else remaining="${remaining:+$remaining,}$ext"; fi + done + [[ -z "$remaining" ]] && break + + failed="$(sed -n 's/.*Failed module: .*shared extension \([a-z0-9_]*\) builder.*/\1/p' spc-build.out | tail -1)" + if [[ -z "$failed" ]]; then + echo "::warning::not built and no failing extension reported: $remaining" + break + fi + echo "::warning::$failed.so failed to build, retrying without it" + pending="$(tr ',' '\n' <<<"$remaining" | { grep -vx "$failed" || true; } | paste -sd, -)" + [[ -n "$pending" ]] || break +done +for ext in ${SHARED_EXTS//,/ }; do + [[ -f "$OUT/ext/$ext.so" ]] || echo "::warning::$ext.so was not built" +done + +if [[ "$PHP_VERSION" == *.*.* ]]; then + built="$("$OUT/bin/php" -n -r 'echo PHP_VERSION;')" + [[ "$built" == "$PHP_VERSION" ]] || { echo "::error::asked for PHP $PHP_VERSION, built $built"; exit 1; } +fi + +# The binaries keep the container's library path (/app/buildroot/lib) as RPATH: nothing is there +# on a user's machine, and whoever could create it would have PHP load their libraries. +find "$OUT/bin" "$OUT/ext" -type f -exec patchelf --remove-rpath {} \; + +# Inside the container, so into a mounted folder. +remove dist/licenses +if spc dump-license --for-extensions="$all" --dump-dir=dist/licenses; then + cp -R dist/licenses/. "$OUT/LICENSES/" +else + echo "::warning::license dump failed" +fi + +ls -la "$OUT/bin" +echo "built PHP $PHP_VERSION into $OUT" diff --git a/packages/rustfs/mirror.sh b/packages/rustfs/mirror.sh index 62e446c..ca001e5 100755 --- a/packages/rustfs/mirror.sh +++ b/packages/rustfs/mirror.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Mirrors the RustFS binary, checked against GitHub's SHA-256 of the release asset. Upstream's zip -# holds only the executable, so it's repacked with the Apache-2.0 license. +# holds only the executable, so it's repacked with the Apache-2.0 license. On Linux, the static musl +# build, which runs on any distribution. # Usage: packages/rustfs/mirror.sh set -euo pipefail . "$(dirname "$0")/../../scripts/lib.sh" @@ -9,6 +10,8 @@ V="$1"; PLATFORM="$2"; OUT="$3" case "$PLATFORM" in macos-arm64) asset="rustfs-macos-aarch64-v$V.zip" ;; windows-x64) asset="rustfs-windows-x86_64-v$V.zip" ;; + linux-x64) asset="rustfs-linux-x86_64-musl-v$V.zip" ;; + linux-arm64) asset="rustfs-linux-aarch64-musl-v$V.zip" ;; esac tmp="$(mktemp -d)"; dir="$tmp/pkg"; mkdir "$dir" fetch "https://github.com/rustfs/rustfs/releases/download/$V/$asset" "$tmp/$asset" diff --git a/packages/typesense/mirror.sh b/packages/typesense/mirror.sh index 1dc08a0..6603dc7 100755 --- a/packages/typesense/mirror.sh +++ b/packages/typesense/mirror.sh @@ -1,15 +1,20 @@ #!/usr/bin/env bash -# Mirrors the Typesense server binary for macOS (there is no native Windows build). Typesense -# publishes no checksum file; the binary is checked against the MD5 shipped inside the tarball. -# Repacked with the GPLv3 license, which the upstream tarball doesn't include. -# Usage: packages/typesense/mirror.sh macos-arm64 +# Mirrors the Typesense server binary for macOS and Linux (there is no native Windows build). +# Typesense publishes no checksum file; the binary is checked against the MD5 shipped inside the +# tarball. Repacked with the GPLv3 license, which the upstream tarball doesn't include. +# Usage: packages/typesense/mirror.sh set -euo pipefail . "$(dirname "$0")/../../scripts/lib.sh" V="$1"; PLATFORM="$2"; OUT="$3" -[[ "$PLATFORM" == macos-arm64 ]] || { echo "Typesense has no Windows build" >&2; exit 1; } +case "$PLATFORM" in + macos-arm64) name=darwin-arm64 ;; + linux-x64) name=linux-amd64 ;; + linux-arm64) name=linux-arm64 ;; + *) echo "Typesense has no Windows build" >&2; exit 1 ;; +esac tmp="$(mktemp -d)"; dir="$tmp/pkg"; mkdir "$dir" -fetch "https://dl.typesense.org/releases/$V/typesense-server-$V-darwin-arm64.tar.gz" "$tmp/t.tar.gz" +fetch "https://dl.typesense.org/releases/$V/typesense-server-$V-$name.tar.gz" "$tmp/t.tar.gz" tar -C "$dir" -xzf "$tmp/t.tar.gz" check_md5 "$dir/typesense-server" "$(cut -d' ' -f1 "$dir/typesense-server.md5.txt")" fetch "https://raw.githubusercontent.com/typesense/typesense/v$V/LICENSE.txt" "$dir/LICENSE.txt" diff --git a/scripts/smoke-php-package.sh b/scripts/smoke-php-package.sh index df540b5..acfad64 100755 --- a/scripts/smoke-php-package.sh +++ b/scripts/smoke-php-package.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Smoke-tests a macOS PHP or FrankenPHP package directory against php.macos in versions.json: -# every compiled-in extension loaded, every shared extension in ext/ built and loading. +# Smoke-tests a macOS or Linux PHP package directory, or a macOS FrankenPHP one, against php.macos in +# versions.json (or the lists in STATIC_EXTS and SHARED_EXTS, which the Linux workflow takes from +# php.linux): every compiled-in extension loaded, every shared extension in ext/ built and loading. # Usage: scripts/smoke-php-package.sh set -euo pipefail PKG="${1:?usage: smoke-php-package.sh }" diff --git a/scripts/smoke-service.sh b/scripts/smoke-service.sh index a7f4add..3e23934 100644 --- a/scripts/smoke-service.sh +++ b/scripts/smoke-service.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Smoke-tests a service package on macOS the way Flock runs it: unpack (dropping a single +# Smoke-tests a service package on macOS or Linux the way Flock runs it: unpack (dropping a single # top-level folder, as Flock does), initialise a data directory, start on 127.0.0.1, talk to it # with its own client, shut down. With SIGNER_MACOS set, the main binary must carry a valid code -# signature whose authority matches that regex (SIGNER_MACOS in versions.json) (the vendor's, for mirrored packages). +# signature whose authority matches that regex (SIGNER_MACOS in versions.json) (the vendor's, for mirrored packages); +# Linux binaries carry none. # Usage: scripts/smoke-service.sh set -uo pipefail @@ -34,7 +35,7 @@ entries=("$D"/*) if [[ ${#entries[@]} -eq 1 && -d "${entries[0]}" ]]; then D="${entries[0]}"; fi echo "### $PKG: $(basename "$ARCHIVE")" | tee -a "$summary" -if [[ -n "${SIGNER_MACOS:-}" ]]; then +if [[ -n "${SIGNER_MACOS:-}" && "$(uname -s)" == Darwin ]]; then case "$PKG" in mysql) main=bin/mysqld ;; mongodb) main=bin/mongod ;; node) main=bin/node ;; *) main="" ;; esac if codesign --verify --strict "$D/$main" 2>/dev/null \ && authority="$(codesign -dvv "$D/$main" 2>&1 | sed -n 's/^Authority=//p' | head -1)" \ diff --git a/scripts/verify-linux.sh b/scripts/verify-linux.sh new file mode 100755 index 0000000..ce13587 --- /dev/null +++ b/scripts/verify-linux.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Checks that every ELF file under a directory runs on other Linux machines: +# - it needs only glibc's libraries, the dynamic loader, libgcc_s and libstdc++ (on every glibc +# distribution); everything else is compiled in +# - it needs no glibc symbol newer than MAX_GLIBC (default 2.17) +# - it has no RPATH or RUNPATH (which would point into the build tree) +# Usage: scripts/verify-linux.sh [max-glibc] +set -euo pipefail + +DIR="${1:?usage: verify-linux.sh [max-glibc]}" +MAX_GLIBC="${2:-2.17}" + +ver_gt() { [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" == "$1" && "$1" != "$2" ]]; } + +failed=0 +checked=0 +while IFS= read -r -d '' f; do + [[ "$(head -c 4 "$f" | tr -d '\0')" == $'\x7fELF' ]] || continue + checked=$((checked + 1)) + + while read -r lib; do + case "$lib" in + libc.so.6 | libm.so.6 | libdl.so.2 | libpthread.so.0 | librt.so.1 | libresolv.so.2 | libutil.so.1 | \ + ld-linux-*.so.* | libgcc_s.so.1 | libstdc++.so.6) ;; + *) echo "✗ $f needs $lib"; failed=1 ;; + esac + done < <(readelf -d "$f" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p') + + if rpath="$(readelf -d "$f" | grep -E '\((RPATH|RUNPATH)\)')"; then + echo "✗ $f has $rpath"; failed=1 + fi + + newest="$(readelf -V --wide "$f" | grep -o 'GLIBC_[0-9][0-9.]*' | sed 's/GLIBC_//' | sort -V | tail -1 || true)" + if [[ -n "$newest" ]] && ver_gt "$newest" "$MAX_GLIBC"; then + echo "✗ $f needs glibc $newest (max $MAX_GLIBC)" + failed=1 + fi +done < <(find "$DIR" -type f -print0) + +if [[ $checked -eq 0 ]]; then + echo "✗ no ELF files found in $DIR" + exit 1 +fi +if [[ $failed -ne 0 ]]; then + exit 1 +fi +echo "✓ $checked ELF files are portable (glibc ≤ $MAX_GLIBC, no other shared libraries)" diff --git a/tools/flockrel/check.go b/tools/flockrel/check.go index 6e7c1fd..a5b4a08 100644 --- a/tools/flockrel/check.go +++ b/tools/flockrel/check.go @@ -22,6 +22,7 @@ func runCheck(args []string) error { fs := flag.NewFlagSet("check", flag.ExitOnError) versionsPath := fs.String("versions", "versions.json", "versions file") buildSh := fs.String("php-build", "packages/php/macos/build.sh", "PHP macOS build script (pins xdebug and opentelemetry)") + linuxBuildSh := fs.String("php-build-linux", "packages/php/linux/build.sh", "PHP Linux build script (pins xdebug and opentelemetry)") out := fs.String("out", "findings.json", "where to write the findings") fs.Parse(args) @@ -33,7 +34,11 @@ func runCheck(args []string) error { if err != nil { return err } - findings, problems := check(v, string(script), liveSources{}, time.Now().UTC()) + linuxScript, err := os.ReadFile(*linuxBuildSh) + if err != nil { + return err + } + findings, problems := check(v, string(script), string(linuxScript), liveSources{}, time.Now().UTC()) for _, p := range problems { fmt.Fprintf(os.Stderr, "::warning::%s\n", p) } @@ -176,7 +181,7 @@ func newestOf(vs []string) string { } // check returns the findings, and problems (sources that couldn't be read) to warn about. -func check(v *versions, phpBuildScript string, src sources, now time.Time) (findings []finding, problems []string) { +func check(v *versions, phpBuildScript, linuxPHPBuildScript string, src sources, now time.Time) (findings []finding, problems []string) { today := now.Format(time.DateOnly) // lineFindings reports, for one product, lines newer than the newest a config builds and lines @@ -281,6 +286,7 @@ func check(v *versions, phpBuildScript string, src sources, now time.Time) (find } lineFindings("PHP", []lineConfig{ {"`php.macos.minors`", v.PHP.MacOS.Minors, ""}, + {"`php.linux.minors`", v.PHP.Linux.Minors, ""}, {"`php.windows.minors`", v.PHP.Windows.Minors, " (check that the extensions in `php.windows.extensions` support it)"}, {"`frankenphp.minors`", v.FrankenPHP.Minors, ""}, }, cycles, upstream, rcs, false, false) @@ -316,12 +322,20 @@ func check(v *versions, phpBuildScript string, src sources, now time.Time) (find {"FrankenPHP", v.FrankenPHP.Version, "php/frankenphp", "", "`frankenphp.version` (then raise `frankenphp.*.build`)", ""}, {"static-php-cli", v.PHP.MacOS.SPC, "crazywhalecc/static-php-cli", "", "`php.macos.spc` (then raise `php.macos.build` and `frankenphp.macos.build`)", ""}, } + if v.PHP.Linux.SPC != "" { + pins = append(pins, pin{"static-php-cli (Linux)", v.PHP.Linux.SPC, "crazywhalecc/static-php-cli", "", "`php.linux.spc` (then raise `php.linux.build`)", ""}) + } if ossl := opensslPin(v); ossl != "" { pins = append(pins, pin{"OpenSSL", ossl, "openssl/openssl", "openssl-", "`OPENSSL_VERSION` of postgres and mariadb (then raise their macOS `build`)", truncate(ossl, 2)}) } - for _, ext := range []struct{ name, repo string }{{"xdebug", "xdebug/xdebug"}, {"opentelemetry", "open-telemetry/opentelemetry-php-instrumentation"}} { - if m := regexp.MustCompile(ext.name + `-([0-9.]+)\.tgz`).FindStringSubmatch(phpBuildScript); m != nil { - pins = append(pins, pin{ext.name + " (macOS)", m[1], ext.repo, "", "`packages/php/macos/build.sh` (then raise `php.macos.build` and `frankenphp.macos.build`)", ""}) + for _, script := range []struct{ platform, dir, text, raise string }{ + {"macOS", "macos", phpBuildScript, "`php.macos.build` and `frankenphp.macos.build`"}, + {"Linux", "linux", linuxPHPBuildScript, "`php.linux.build`"}, + } { + for _, ext := range []struct{ name, repo string }{{"xdebug", "xdebug/xdebug"}, {"opentelemetry", "open-telemetry/opentelemetry-php-instrumentation"}} { + if m := regexp.MustCompile(ext.name + `-([0-9.]+)\.tgz`).FindStringSubmatch(script.text); m != nil { + pins = append(pins, pin{ext.name + " (" + script.platform + ")", m[1], ext.repo, "", "`packages/php/" + script.dir + "/build.sh` (then raise " + script.raise + ")", ""}) + } } } exts := v.PHP.Windows.Extensions diff --git a/tools/flockrel/check_test.go b/tools/flockrel/check_test.go index ebe3da1..36fd92b 100644 --- a/tools/flockrel/check_test.go +++ b/tools/flockrel/check_test.go @@ -28,6 +28,7 @@ func TestCheck(t *testing.T) { var v versions if err := json.Unmarshal([]byte(`{ "php": {"macos": {"minors": ["8.4", "8.5"], "spc": "2.8.5"}, + "linux": {"minors": ["8.4", "8.5"], "spc": "2.8.5"}, "windows": {"minors": ["8.1", "8.4", "8.5"], "extensions": { "from_source": {"igbinary": {"repo": "igbinary/igbinary", "ref": "3.2.16", "refs": {"8.5": "3.2.17RC1"}}, "apcu": {"repo": "krakjoe/apcu", "ref": "v5.1.28"}}, @@ -65,7 +66,8 @@ func TestCheck(t *testing.T) { }, rcs: map[string]string{"8.5.13": "8.5.13RC1", "8.7.0": "8.7.0RC1", "8.6.1": "8.6.1RC1"}, } - findings, problems := check(&v, "--custom-url=xdebug:https://xdebug.org/files/xdebug-3.5.3.tgz\n--custom-url=opentelemetry:https://pecl.php.net/get/opentelemetry-1.4.2.tgz", src, now) + script := "--custom-url=xdebug:https://xdebug.org/files/xdebug-3.5.3.tgz\n--custom-url=opentelemetry:https://pecl.php.net/get/opentelemetry-1.4.2.tgz" + findings, problems := check(&v, script, script, src, now) if len(problems) > 0 { t.Errorf("problems: %v", problems) } @@ -95,7 +97,7 @@ func TestCheck(t *testing.T) { } t.Errorf("%d findings, want %d", len(findings), len(want)) } - if b := got["New release line: PHP 8.6"].Body; !strings.Contains(b, "`php.macos.minors`, `php.windows.minors` (check that the extensions") || !strings.Contains(b, "`frankenphp.minors`") { + if b := got["New release line: PHP 8.6"].Body; !strings.Contains(b, "`php.macos.minors`, `php.linux.minors`, `php.windows.minors` (check that the extensions") || !strings.Contains(b, "`frankenphp.minors`") { t.Errorf("PHP 8.6 body: %s", b) } if b := got["New release line: PHP 8.7"].Body; !strings.Contains(b, "release candidate phase (8.7.0RC1)") || !strings.Contains(b, "beta channel only") { @@ -110,6 +112,7 @@ func TestCheck(t *testing.T) { "| OpenSSL | 3.5.9 | 3.5.10 |", // the same line, not 4.0 "| xdebug (macOS) | 3.5.3 | 3.6.0 |", "| opentelemetry (macOS) | 1.4.2 | 1.5.0 |", + "| xdebug (Linux) | 3.5.3 | 3.6.0 |", "| igbinary for PHP 8.5 (Windows) | 3.2.17RC1 | 3.2.17 |", "| igbinary (Windows) | 3.2.16 | 3.2.17 |", } { diff --git a/tools/flockrel/main_test.go b/tools/flockrel/main_test.go index bf16869..88a17a1 100644 --- a/tools/flockrel/main_test.go +++ b/tools/flockrel/main_test.go @@ -37,6 +37,7 @@ func TestTagAndAssetNames(t *testing.T) { assets := map[string]parsed{ "php-8.4.26-build1-macos-arm64.tar.gz": {1, "macos-arm64", true}, "php-8.4.26-build12-windows-x64.zip": {12, "windows-x64", true}, + "php-8.4.26-build3-linux-arm64.tar.gz": {3, "linux-arm64", true}, "php-8.4.26-macos-arm64.tar.gz": {}, "php-8.4.26-build0-macos-arm64.tar.gz": {}, "php-8.4.26-build1-macos-arm64.tar.xz": {}, diff --git a/tools/flockrel/plan.go b/tools/flockrel/plan.go index f82d437..70cb3c8 100644 --- a/tools/flockrel/plan.go +++ b/tools/flockrel/plan.go @@ -22,6 +22,7 @@ type build struct { // runPlan prints, and writes to $GITHUB_OUTPUT, the builds whose release asset doesn't exist yet: // // php_macos=[{"package":"php","version":"8.4.26","platform":"macos-arm64","build":1}, ...] (php and frankenphp) +// php_linux=[{"package":"php","version":"8.4.26","platform":"linux-arm64","build":1}, ...] // php_exts_windows=[{"package":"php-exts","version":"8.4","platform":"windows-x64","build":1}, ...] // frankenphp_windows=[{"package":"frankenphp","version":"8.4.26","platform":"windows-x64","build":1}, ...] // builds=[{"package":"postgres","version":"18.6","platform":"windows-x64","build":1}, ...] @@ -69,18 +70,18 @@ func runPlan(args []string) error { return true } - phpMacOS, phpExtsWindows, frankenWindows := []build{}, []build{}, []build{} + phpMacOS, phpLinux, phpExtsWindows, frankenWindows := []build{}, []build{}, []build{}, []build{} builds, mirrors := []build{}, []build{} - // PHP versions per minor: php.net for macOS (built from source), windows.php.net for Windows - // (mirrored, and the base of the extension packs and FrankenPHP), which can trail by a day. - macOSPHP := map[string]string{} - macOSVersion := func(minor string) (string, error) { - if v, ok := macOSPHP[minor]; ok { + // PHP versions per minor: php.net for macOS and Linux (built from source), windows.php.net for + // Windows (mirrored, and the base of the extension packs and FrankenPHP), which can trail by a day. + phpNetPHP := map[string]string{} + phpNetVersion := func(minor string) (string, error) { + if v, ok := phpNetPHP[minor]; ok { return v, nil } latest, err := latestPHP(minor) - macOSPHP[minor] = latest + phpNetPHP[minor] = latest return latest, err } var windowsPHP map[string]string @@ -100,7 +101,7 @@ func runPlan(args []string) error { // The release is out, but windows.php.net doesn't have its build yet: wait for it rather // than building the release candidate it still lists. if ok && newMinorRC.MatchString(ver) { - if released, err := macOSVersion(minor); err == nil && !newMinorRC.MatchString(released) { + if released, err := phpNetVersion(minor); err == nil && !newMinorRC.MatchString(released) { fmt.Fprintf(os.Stderr, "waiting PHP %s for Windows (windows.php.net still lists %s)\n", released, ver) return "", false } @@ -114,7 +115,7 @@ func runPlan(args []string) error { phpVersions = strings.Split(*only, ",") } else { for _, minor := range v.PHP.MacOS.Minors { - latest, err := macOSVersion(minor) + latest, err := phpNetVersion(minor) if err != nil { return err } @@ -127,6 +128,25 @@ func runPlan(args []string) error { phpMacOS = append(phpMacOS, b) } } + var linuxVersions []string + if *only != "" { + linuxVersions = strings.Split(*only, ",") + } else { + for _, minor := range v.PHP.Linux.Minors { + latest, err := phpNetVersion(minor) + if err != nil { + return err + } + linuxVersions = append(linuxVersions, latest) + } + } + for _, ver := range linuxVersions { + for _, platform := range []string{"linux-x64", "linux-arm64"} { + if b := (build{Package: "php", Version: strings.TrimSpace(ver), Platform: platform, Build: v.PHP.Linux.Build}); needed(b) { + phpLinux = append(phpLinux, b) + } + } + } if *only == "" { for _, minor := range v.PHP.Windows.Minors { if ver, ok := windowsVersion(minor); ok { @@ -153,7 +173,7 @@ func runPlan(args []string) error { // FrankenPHP embeds PHP, so it is built per PHP version on both platforms. if selected("frankenphp") && *only == "" { for _, minor := range v.FrankenPHP.Minors { - latest, err := macOSVersion(minor) + latest, err := phpNetVersion(minor) if err != nil { return err } @@ -178,7 +198,7 @@ func runPlan(args []string) error { fmt.Fprintf(os.Stderr, "::warning::%s\n", msg) } for _, ver := range vers { - for _, platform := range []string{"macos-arm64", "windows-x64"} { + for _, platform := range platforms { c, ok := p.Platforms[platform] if !ok { continue @@ -200,11 +220,11 @@ func runPlan(args []string) error { for _, o := range []struct { name string list []build - }{{"php_macos", phpMacOS}, {"php_exts_windows", phpExtsWindows}, {"frankenphp_windows", frankenWindows}, {"builds", builds}, {"mirrors", mirrors}} { + }{{"php_macos", phpMacOS}, {"php_linux", phpLinux}, {"php_exts_windows", phpExtsWindows}, {"frankenphp_windows", frankenWindows}, {"builds", builds}, {"mirrors", mirrors}} { j, _ := json.Marshal(o.list) fmt.Fprintf(&out, "%s=%s\n", o.name, j) } - fmt.Fprintf(&out, "any=%t\n", len(phpMacOS)+len(phpExtsWindows)+len(frankenWindows)+len(builds)+len(mirrors) > 0) + fmt.Fprintf(&out, "any=%t\n", len(phpMacOS)+len(phpLinux)+len(phpExtsWindows)+len(frankenWindows)+len(builds)+len(mirrors) > 0) fmt.Print(out.String()) if path := os.Getenv("GITHUB_OUTPUT"); path != "" { f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) diff --git a/tools/flockrel/versions.go b/tools/flockrel/versions.go index de106d9..3006e45 100644 --- a/tools/flockrel/versions.go +++ b/tools/flockrel/versions.go @@ -21,6 +21,15 @@ type versions struct { StaticExtensions string `json:"static_extensions"` SharedExtensions string `json:"shared_extensions"` } `json:"macos"` + // Linux is built like macOS, in static-php-cli's glibc container, for linux-x64 and linux-arm64. + Linux struct { + Minors []string `json:"minors"` + Build int `json:"build"` + SPC string `json:"spc"` + MinGlibc string `json:"min_glibc"` + StaticExtensions string `json:"static_extensions"` + SharedExtensions string `json:"shared_extensions"` + } `json:"linux"` // Windows mirrors the official thread-safe x64 builds from windows.php.net. Windows struct { Build int `json:"build"` @@ -76,7 +85,10 @@ type platformConfig struct { Build int `json:"build"` } -var platformExt = map[string]string{"macos-arm64": "tar.gz", "windows-x64": "zip"} +// platforms are the platforms we publish for, in the order the plan lists them. +var platforms = []string{"macos-arm64", "windows-x64", "linux-x64", "linux-arm64"} + +var platformExt = map[string]string{"macos-arm64": "tar.gz", "windows-x64": "zip", "linux-x64": "tar.gz", "linux-arm64": "tar.gz"} func loadVersions(path string) (*versions, error) { data, err := os.ReadFile(path) diff --git a/versions.json b/versions.json index b8c2fb3..fe6d88b 100644 --- a/versions.json +++ b/versions.json @@ -8,6 +8,14 @@ "static_extensions": "bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,gd,gmp,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,sysvmsg,sysvsem,sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib,apcu", "shared_extensions": "xdebug,imagick,pcov,mongodb,memcached,opentelemetry" }, + "linux": { + "minors": ["8.2", "8.3", "8.4", "8.5"], + "build": 1, + "spc": "2.8.5", + "min_glibc": "2.17", + "static_extensions": "bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,gd,gmp,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,sysvmsg,sysvsem,sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib,apcu", + "shared_extensions": "xdebug,imagick,pcov,mongodb,memcached,opentelemetry" + }, "windows": { "minors": ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"], "build": 1, @@ -70,7 +78,9 @@ "lines": ["8.0", "8.4", "9.7"], "platforms": { "macos-arm64": { "how": "mirror", "build": 1 }, - "windows-x64": { "how": "mirror", "build": 1 } + "windows-x64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } }, "env": { "SIGNER_MACOS": "Oracle", "SIGNER_WINDOWS": "Oracle" }, "publish_source": true @@ -79,7 +89,9 @@ "lines": ["7.0", "8.0", "9.0"], "platforms": { "macos-arm64": { "how": "mirror", "build": 1 }, - "windows-x64": { "how": "mirror", "build": 1 } + "windows-x64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } }, "env": { "SIGNER_MACOS": "MongoDB" }, "publish_source": true @@ -88,13 +100,17 @@ "lines": ["1"], "platforms": { "macos-arm64": { "how": "mirror", "build": 1 }, - "windows-x64": { "how": "mirror", "build": 1 } + "windows-x64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } } }, "typesense": { "lines": ["29", "30"], "platforms": { - "macos-arm64": { "how": "mirror", "build": 1 } + "macos-arm64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } }, "publish_source": true }, @@ -102,14 +118,18 @@ "lines": ["1"], "platforms": { "macos-arm64": { "how": "mirror", "build": 1 }, - "windows-x64": { "how": "mirror", "build": 1 } + "windows-x64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } } }, "node": { "lines": ["16", "18", "20", "22", "24", "26"], "platforms": { "macos-arm64": { "how": "mirror", "build": 1 }, - "windows-x64": { "how": "mirror", "build": 1 } + "windows-x64": { "how": "mirror", "build": 1 }, + "linux-x64": { "how": "mirror", "build": 1 }, + "linux-arm64": { "how": "mirror", "build": 1 } }, "env": { "SIGNER_MACOS": "Node.js|OpenJS", "SIGNER_WINDOWS": "Node.js|OpenJS" } }