Files
cloud-apache-container/scripts/lsphp-src-build.sh
T

303 lines
18 KiB
Bash
Raw Normal View History

#!/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 <signing-key-fpr> <date> <ts> <expire> <ver> <rsv> <pk-algo> <hash> <class> <PRIMARY-fpr>
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 <tool>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" <<EOF
LSPHP_SRC_PHP_VERSION=$PHP_SRC_VERSION
LSPHP_SRC_SHA256=$PHP_SRC_SHA256
LSPHP_SRC_SIGNER=$PRIMARY
LSPHP_SRC_PKG_REFERENCE=$PKGV
LSPHP_SRC_PKG_PHP_VERSION=$PKG_PHPV
EOF
printf '%s\n' "${ARGS[@]}" >"$OUT/configure-args.txt"
log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})"