2026-10-21 09:43:45 -07:00
#!/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 } "
2026-10-21 09:47:44 -07:00
[[ " $PHPVER " = ~ ^[ 0-9]{ 2} $ ]] || die "PHPVER=' $PHPVER ' is not NN."
2026-10-21 09:43:45 -07:00
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 } )"