#!/bin/bash ## lsphp-src-build.sh — BUILD-TIME ONLY, EMERGENCY PATH. Compiles lsphp from the ## official php.net release tarball, configured EXACTLY like the LiteSpeed-packaged ## lsphp${PHPVER} it will replace, for the window in which a PHP security release ## is out but rpms.litespeedtech.com/debian has not published it yet. ## ## Runs in the throwaway `lsphp-src` stage of Dockerfile.lsphp / Dockerfile.litespeed, ## FROM the same litespeedtech/openlitespeed base as the shipped stage. The output ## (/lsphp-src-out) is applied to the shipped stage by lsphp-src-overlay.sh, which ## is where the "same support as the packaged lsphp" assertions live. ## ## PHP_SRC_VERSION empty (every normal build) => does nothing but create an empty ## /lsphp-src-out, in well under a second. Nothing is downloaded or installed. ## ## WHAT "CONFIGURED EXACTLY LIKE THE PACKAGE" MEANS, and why it cannot drift: ## the configure arguments are NOT kept in this repo. They are read, at build ## time, from the `Configure Command` line of the packaged lsphp that the same ## build just installed from the LiteSpeed repo (php -i), and replayed verbatim — ## only CFLAGS is edited, to drop the two -f*-prefix-map flags that encode ## LiteSpeed's build directory. When LiteSpeed changes their flags, this follows ## automatically; if a new flag needs a -dev package we do not install, configure ## FAILS (loud), it does not silently drop the feature. CPPFLAGS/LDFLAGS are not ## recorded by PHP; they come from Ubuntu noble's dpkg-buildflags with ## hardening=+all, which reproduces the packaged binary's measured hardening ## (PIE, full RELRO, BIND_NOW, FORTIFY _chk symbols) — checked again below. ## ## TRUST: the tarball must (1) match PHP_SRC_SHA256 (CI takes it from ## https://www.php.net/releases/index.php?json&version=X.Y; the build fails if it ## is not supplied) AND (2) carry a valid OpenPGP signature (.asc from php.net) ## by a key in keys/php-src/php-keyring.asc (the official keyring from ## https://www.php.net/distributions/php-keyring.gpg, committed ## unmodified as php-keyring.asc — it is ASCII-armoured despite the name) whose PRIMARY ## fingerprint is listed for this branch in keys/php-src/release-managers.txt ## (transcribed from https://www.php.net/gpg-keys.php). Either check failing ## aborts the build. A new release manager key therefore needs a reviewed commit. ## ## The LSAPI SAPI itself is LiteSpeed's php-litespeed release (pinned by sha256 ## in keys/php-src/litespeed-sapi.txt), NOT php-src's bundled sapi/litespeed — ## that is what LiteSpeed's packages compile (measured, see that file). ## ## PECL extensions (igbinary, imagick, memcached, msgpack, redis) are NOT built ## here: the shipped stage keeps LiteSpeed's own packaged .so files — the same ## binaries the primary image ships, which LiteSpeed itself does not rebuild per ## PHP patch release (e.g. lsphp85-msgpack 3.0.1-1 is from June, core 8.5.11 from ## September). That is only safe while the Zend ABI is unchanged, so this script ## also runs an ABI gate (scripts/lsphp-abi-gate.sh: pahole over every struct the ## public PHP headers define, packaged -dev headers vs the new headers); any ## changed offset/size of an existing member fails the build. ## ## Usage (env): PHPVER=85 PHP_SRC_VERSION=8.5.12 PHP_SRC_SHA256=<64 hex> lsphp-src-build.sh set -euo pipefail HERE=$(cd "$(dirname "$0")" && pwd) # sibling scripts (lsphp-abi-gate.sh) OUT=/lsphp-src-out KEYDIR=${KEYDIR:-/usr/local/share/php-src-keys} mkdir -p "$OUT" die() { echo "FATAL(lsphp-src-build): $*" >&2; exit 1; } log() { echo "lsphp-src-build: $*"; } PHPVER="${PHPVER:?PHPVER required}" if [ -z "${PHP_SRC_VERSION:-}" ]; then [ -z "${PHP_SRC_SHA256:-}" ] || die "PHP_SRC_SHA256 given without PHP_SRC_VERSION." log "PHP_SRC_VERSION empty — packaged lsphp build, nothing to do." exit 0 fi BRANCH="${PHPVER:0:1}.${PHPVER:1}" # 85 -> 8.5 [[ "$PHP_SRC_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "PHP_SRC_VERSION='$PHP_SRC_VERSION' is not X.Y.Z (RCs/alphas are refused)." [ "${PHP_SRC_VERSION%.*}" = "$BRANCH" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION does not belong to PHPVER=$PHPVER (branch $BRANCH)." [[ "${PHP_SRC_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || die "PHP_SRC_SHA256 must be the 64-hex sha256 of php-${PHP_SRC_VERSION}.tar.xz from the php.net releases JSON." P="/usr/local/lsws/lsphp${PHPVER}" START=$(date +%s) ## ---- 1. toolchain + every -dev library the packaged configure line needs ----- ## Same script as the shipped stage: refuses stale/unreachable repo lists and ## brings lsphp${PHPVER}* (core + -dev) to the repo's current release, so the ## Configure Command we read is the one the shipped stage's packages carry ## (lsphp-src-overlay.sh re-checks that the versions match). ## -dev packages: one per library in the packaged Configure Command. Extras are ## harmless; a missing one makes configure fail, which is the loud outcome. DEVPKGS=( build-essential autoconf pkg-config file dwarves curl ca-certificates gpg gpgv xz-utils libxml2-dev libssl-dev libcurl4-openssl-dev libenchant-2-dev libgmp-dev libonig-dev libedit-dev libbz2-dev libxslt1-dev libzip-dev libsodium-dev libargon2-dev libjpeg-dev libxpm-dev libpng-dev libwebp-dev libfreetype-dev libldap-dev libsasl2-dev libpq-dev libsnmp-dev libsqlite3-dev freetds-dev libtidy-dev libicu-dev zlib1g-dev libkrb5-dev ) case "$PHPVER" in 81) DEVPKGS+=(libc-client2007e-dev libpspell-dev) ;; # --with-imap, --with-pspell 82|83) DEVPKGS+=(libpspell-dev) ;; # --with-pspell (gone in 8.4) esac /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" "${DEVPKGS[@]}" "lsphp${PHPVER}-dev" PKGV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}") DEVV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}-dev") [ "$PKGV" = "$DEVV" ] || die "lsphp${PHPVER}=$PKGV but lsphp${PHPVER}-dev=$DEVV — packaged headers do not match the packaged binary." PKG_PHPV=$("$P/bin/php" -r 'echo PHP_VERSION;') log "packaged reference: lsphp${PHPVER} ${PKGV} (PHP ${PKG_PHPV}); building php.net ${PHP_SRC_VERSION}" if [ "$(printf '%s\n%s\n' "$PKG_PHPV" "$PHP_SRC_VERSION" | sort -V | tail -n1)" != "$PHP_SRC_VERSION" ] \ || [ "$PKG_PHPV" = "$PHP_SRC_VERSION" ]; then die "PHP_SRC_VERSION=$PHP_SRC_VERSION is not NEWER than the packaged $PKG_PHPV — the emergency path is pointless (or a downgrade). Use the normal build." fi ## ---- 2. fetch + verify ------------------------------------------------------- WORK=/usr/src/php-src mkdir -p "$WORK" cd "$WORK" TARBALL="php-${PHP_SRC_VERSION}.tar.xz" for f in "$TARBALL" "$TARBALL.asc"; do curl -fsSL --retry 3 --max-time 600 -o "$f" "https://www.php.net/distributions/$f" \ || die "download of https://www.php.net/distributions/$f failed." done GOT=$(sha256sum "$TARBALL" | awk '{print $1}') [ "$GOT" = "$PHP_SRC_SHA256" ] || die "sha256 mismatch for $TARBALL: got $GOT, expected $PHP_SRC_SHA256 (php.net releases JSON)." log "sha256 OK $GOT" [ -s "$KEYDIR/php-keyring.asc" ] || die "pinned keyring $KEYDIR/php-keyring.asc missing." ## The committed file is php.net's keyring byte-for-byte (ASCII-armoured); gpgv ## only reads binary keyrings. gpg --batch --yes --dearmor -o "$WORK/php-keyring.gpg" "$KEYDIR/php-keyring.asc" || die "could not dearmor $KEYDIR/php-keyring.asc." [ -s "$KEYDIR/release-managers.txt" ] || die "pinned fingerprint list $KEYDIR/release-managers.txt missing." STATUS=$(gpgv --status-fd 1 --keyring "$WORK/php-keyring.gpg" "$TARBALL.asc" "$TARBALL" 2>"$WORK/gpgv.err") || { cat "$WORK/gpgv.err" >&2; die "OpenPGP signature verification of $TARBALL FAILED."; } cat "$WORK/gpgv.err" ## [GNUPG:] VALIDSIG PRIMARY=$(awk '$1 == "[GNUPG:]" && $2 == "VALIDSIG" {print $NF}' <<<"$STATUS") [ -n "$PRIMARY" ] || die "gpgv reported no VALIDSIG for $TARBALL." ALLOWED=$(awk -v b="$BRANCH" '$1 == b {print $2}' "$KEYDIR/release-managers.txt") [ -n "$ALLOWED" ] || die "no release-manager fingerprints pinned for PHP $BRANCH in release-managers.txt." OK=0 while read -r fp; do [ "$fp" = "$PRIMARY" ] && OK=1; done <<<"$ALLOWED" [ "$OK" = 1 ] || die "$TARBALL is validly signed by $PRIMARY, which is NOT a pinned PHP $BRANCH release manager (release-managers.txt)." log "signature OK: primary key $PRIMARY is a pinned PHP $BRANCH release manager" tar -xJf "$TARBALL" SRC="$WORK/php-${PHP_SRC_VERSION}" grep -F "PHP ${PHP_SRC_VERSION}" "$SRC/NEWS" >/dev/null || die "NEWS in $TARBALL has no 'PHP ${PHP_SRC_VERSION}' entry." ## ---- 2b. LiteSpeed's LSAPI SAPI, as the packages use (keys/php-src/litespeed-sapi.txt) read -r SAPI_URL SAPI_SHA < <(awk '!/^#/ && NF == 2 {print; exit}' "$KEYDIR/litespeed-sapi.txt") [ -n "${SAPI_URL:-}" ] && [[ "${SAPI_SHA:-}" =~ ^[0-9a-f]{64}$ ]] || die "no pinned LiteSpeed SAPI in $KEYDIR/litespeed-sapi.txt." curl -fsSL --retry 3 --max-time 300 -o "$WORK/lsapi.tgz" "$SAPI_URL" || die "download of $SAPI_URL failed." GOT=$(sha256sum "$WORK/lsapi.tgz" | awk '{print $1}') [ "$GOT" = "$SAPI_SHA" ] || die "LiteSpeed SAPI $SAPI_URL sha256 $GOT != pinned $SAPI_SHA (LiteSpeed republished it? re-measure against the packaged lsphp before re-pinning)." mkdir -p "$WORK/lsapi" tar -xzf "$WORK/lsapi.tgz" -C "$WORK/lsapi" ## Per its README: copy only the *.c/*.h over php-src's sapi/litespeed and keep ## php-src's config.m4 / Makefile.frag / arginfo. N=0 for f in "$WORK"/lsapi/*/*.c "$WORK"/lsapi/*/*.h; do [ -f "$f" ] || continue install -m 0644 "$f" "$SRC/sapi/litespeed/$(basename "$f")"; N=$((N + 1)) done [ "$N" -ge 4 ] || die "LiteSpeed SAPI archive had only $N .c/.h files." log "LiteSpeed SAPI $(basename "$SAPI_URL") (sha256 OK) replaces php-src sapi/litespeed sources ($N files)" ## ---- 2c. compiled-in ini defaults LiteSpeed patched ---------------------------- ## Measured 2026-10-21 with `php -n`: LiteSpeed's lsphp82/83 carry Debian's ## session.save_path patch (default '/var/lib/php/sessions' instead of php-src's ## ''); lsphp85 does not. Reproduce whatever the packaged binary reports. Any ## OTHER compiled-in default that differs is caught by the gate in step 4b. PKG_SAVE_PATH=$("$P/bin/php" -n -r 'echo ini_get("session.save_path");') if [ -n "$PKG_SAVE_PATH" ]; then [[ "$PKG_SAVE_PATH" =~ ^[A-Za-z0-9/._-]+$ ]] || die "unexpected packaged session.save_path default '$PKG_SAVE_PATH'." sed -i -E 's|(STD_PHP_INI_ENTRY\("session\.save_path",[[:space:]]*)""|\1"'"$PKG_SAVE_PATH"'"|' "$SRC/ext/session/session.c" grep -F "\"session.save_path\"" "$SRC/ext/session/session.c" | grep -F "\"$PKG_SAVE_PATH\"" >/dev/null \ || die "could not apply the packaged session.save_path default '$PKG_SAVE_PATH' to ext/session/session.c." log "compiled-in session.save_path default set to '$PKG_SAVE_PATH' (as the packaged lsphp${PHPVER})" fi ## ---- 3. replay the packaged configure line ----------------------------------- CFG_LINE=$("$P/bin/php" -i | awk -F' => ' '$1 == "Configure Command " || $1 == "Configure Command" {sub(/^[^=]*=> /, ""); print}') [ -n "$CFG_LINE" ] || die "could not read 'Configure Command' from the packaged $P/bin/php -i." ## xargs does the shell-style single-quote splitting without an eval. mapfile -t CFG_ARGS < <(printf '%s\n' "$CFG_LINE" | xargs -n1 printf '%s\n') [ "${CFG_ARGS[0]}" = "./configure" ] || die "unexpected Configure Command shape (first word '${CFG_ARGS[0]}')." CFG_ARGS=("${CFG_ARGS[@]:1}") ARGS=() PKG_CFLAGS="" for a in "${CFG_ARGS[@]}"; do case "$a" in CFLAGS=*) PKG_CFLAGS="${a#CFLAGS=}" ;; *) ARGS+=("$a") ;; esac done [ -n "$PKG_CFLAGS" ] || die "packaged Configure Command carries no CFLAGS — refusing to guess." CFLAGS="" for w in $PKG_CFLAGS; do case "$w" in -ffile-prefix-map=*|-fdebug-prefix-map=*) continue ;; esac CFLAGS+="${CFLAGS:+ }$w" done CFLAGS+=" -ffile-prefix-map=${SRC}=." export DEB_BUILD_MAINT_OPTIONS=hardening=+all CPPFLAGS=$(dpkg-buildflags --get CPPFLAGS) LDFLAGS="" for w in $(dpkg-buildflags --get LDFLAGS); do ## CFLAGS ends in -fno-lto (LiteSpeed's choice); keep the link step consistent. case "$w" in -flto*|-ffat-lto-objects) continue ;; esac LDFLAGS+="${LDFLAGS:+ }$w" done CXXFLAGS="$CFLAGS" export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS log "configure args (from packaged lsphp${PHPVER} ${PKGV}): ${ARGS[*]}" log "CFLAGS=$CFLAGS" log "CPPFLAGS=$CPPFLAGS" log "LDFLAGS=$LDFLAGS" ## Build-environment shim (builder stage only, never shipped): ext/pdo_dblib's ## config.m4 looks for libsybdb in $prefix/lib, not the multiarch dir Ubuntu's ## freetds-dev uses. pdo_dblib is built but not shipped (the packaged image has ## no lsphpNN-dblib), exactly as in the packaged build. MA=$(dpkg-architecture -qDEB_HOST_MULTIARCH) [ -e /usr/lib/libsybdb.so ] || ln -s "/usr/lib/$MA/libsybdb.so" /usr/lib/libsybdb.so cd "$SRC" if ! ./configure "${ARGS[@]}" "CFLAGS=$CFLAGS" >"$WORK/configure.log" 2>&1; then tail -n 60 "$WORK/configure.log" >&2 die "configure FAILED with the packaged flags — most likely a -dev package missing from DEVPKGS in this script (see the error above)." fi ## Unknown options are expected for a handful of historic flags LiteSpeed still ## passes (--enable-gd-native-ttf, --with-vpx-dir, ...); they are just as unknown ## to the packaged build. Print them so a NEW unknown flag is visible in the log. grep -E "^configure: WARNING: unrecognized options" "$WORK/configure.log" || true T0=$(date +%s) make -j"$(nproc)" >"$WORK/make.log" 2>&1 || { tail -n 80 "$WORK/make.log" >&2; die "make FAILED."; } log "make took $(( $(date +%s) - T0 ))s on $(nproc) CPUs" STAGE="$WORK/stage" make install INSTALL_ROOT="$STAGE" >"$WORK/install.log" 2>&1 || { tail -n 40 "$WORK/install.log" >&2; die "make install FAILED."; } SP="$STAGE$P" [ -x "$SP/bin/lsphp${BRANCH}" ] || die "make install produced no $P/bin/lsphp${BRANCH} (did --enable-litespeed vanish from the packaged flags?)." [ -x "$SP/bin/php${BRANCH}" ] || die "make install produced no $P/bin/php${BRANCH}." ## --program-suffix names everything 8.M; LiteSpeed's packages ship the ## SAPI as bin/lsphp and the CLI as both bin/php and bin/php8.M (two identical ## regular files, measured). Reproduce those names; the overlay copies only the ## files the packaged image actually has. mv "$SP/bin/lsphp${BRANCH}" "$SP/bin/lsphp" cp -p "$SP/bin/php${BRANCH}" "$SP/bin/php" ## Strip like dh_strip does for the packaged binaries (measured: no .comment / ## debug sections in the packaged lsphp). find "$SP" -type f \( -name '*.so' -o -path "*/bin/*" \) -print0 | while IFS= read -r -d '' f; do case "$(file -b "$f")" in ELF*shared\ object*|ELF*pie\ executable*|ELF*executable*) strip --remove-section=.comment --remove-section=.note --strip-unneeded "$f" ;; esac done NEWV=$("$SP/bin/php${BRANCH}" -n -r 'echo PHP_VERSION;' 2>/dev/null || true) [ "$NEWV" = "$PHP_SRC_VERSION" ] || die "built php reports '$NEWV', expected $PHP_SRC_VERSION." ## ---- 4b. compiled-in ini defaults must match the packaged binary --------------- ## `php -n` = no php.ini, no scan dir, no shared extensions: what is left is ## what the binary itself defaults to. New directives (a security release may ## add one, e.g. memory_hard_limit_percent in 8.x.y php2shell fix) are allowed ## and printed; a removed or changed default fails. ini_defaults() { "$1" -n -r 'foreach (ini_get_all(null, false) as $k => $v) echo $k, "=", var_export($v, true), "\n";' | sort; } INI_PKG=$(ini_defaults "$P/bin/php") INI_NEW=$(ini_defaults "$SP/bin/php${BRANCH}") INI_BAD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^<' || true) INI_ADD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^>' || true) [ -z "$INI_BAD" ] || die "compiled-in ini defaults differ from the packaged lsphp${PHPVER} (< packaged, > php.net build): $(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") || true) LiteSpeed patched a default this script does not reproduce (see step 2c)." log "compiled-in ini defaults identical to packaged ($(printf '%s\n' "$INI_PKG" | wc -l) directives)${INI_ADD:+; new in $PHP_SRC_VERSION: $(printf '%s' "$INI_ADD" | tr '\n' ' ')}" ## Same LSAPI SAPI as the packaged binary? Compare the LSAPI_*/LSCAPI_* names ## (its env knobs + feature switches) compiled into each. A difference means ## LiteSpeed moved to a SAPI release we have not pinned (or the copy failed). lsapi_names() { grep -aoE 'LS(C)?API_[A-Z0-9_]+' "$1" | sort -u; } LSAPI_DIFF=$(diff <(lsapi_names "$P/bin/lsphp") <(lsapi_names "$SP/bin/lsphp") || true) [ -z "$LSAPI_DIFF" ] || die "LSAPI SAPI differs from the packaged lsphp (< packaged, > php.net build): $LSAPI_DIFF" log "LSAPI SAPI matches the packaged lsphp ($(lsapi_names "$SP/bin/lsphp" | wc -l) LSAPI_* names identical)" ## ---- 4. ABI gate: may LiteSpeed's packaged PECL .so files load into this? ----- ## See scripts/lsphp-abi-gate.sh (and its test). Measured for 8.5.11 -> 8.5.12: ## the php2shell fix adds CG(interned_strings_fallible) in padding and appends ## EG(delayed_oom) / PS(in_rfc1867_update); no existing member moved. ABI_OUT=$(bash "$HERE/lsphp-abi-gate.sh" "$P/include" "$SP/include" "$WORK/abi" 2>&1) || { printf '%s\n' "$ABI_OUT" >&2 die "ABI gate: PHP $PHP_SRC_VERSION changed struct layouts relative to the packaged $PKG_PHPV headers. LiteSpeed's packaged PECL .so files (redis, imagick, memcached, igbinary, msgpack) were compiled against the old layout and must NOT be loaded into this build — they would have to be rebuilt from source, which this emergency path does not do (docs/lsphp-source-emergency.md)." } printf '%s\n' "$ABI_OUT" | sed 's/^/lsphp-src-build: /' ## ---- 5. hand-off ---------------------------------------------------------------- mkdir -p "$OUT/root" cp -a "$STAGE/." "$OUT/root/" cat >"$OUT/meta.env" <"$OUT/configure-args.txt" log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})"