2026-10-21 09:43:45 -07:00
#!/bin/bash
## lsphp-src-overlay.sh — BUILD-TIME ONLY, EMERGENCY PATH. Applies the php.net
## build produced by lsphp-src-build.sh (staged in /lsphp-src-out by the
## `lsphp-src` stage) over the LiteSpeed-packaged lsphp${PHPVER} that this stage
## has just installed, then PROVES the result offers the same support.
##
## Usage: lsphp-src-overlay.sh runtime|dev PHPVER [OUT_DIR=/lsphp-src-out]
## runtime — the shipped stage: replace the php-src binaries (lsphp, php,
## php8.M, phar) and every php-src shared extension the packages
## installed. Everything else stays exactly as packaged: php.ini,
## mods-available/*.ini, the LiteSpeed PECL .so files (see
## lsphp-src-build.sh for why), helper scripts, man pages.
## dev — Dockerfile.lsphp's ext-build stage: runtime + the -dev files
## (headers, phpize, php-config, lib/php/build) so cac_path_parity is
## compiled against, and its .phpt suite run on, the php.net build.
##
## PHP_SRC_VERSION empty and no staged build => no-op (normal builds).
##
## WHAT IS REPLACED is decided by dpkg, not by a list kept here: a file is
## php-src output iff it is owned by an installed lsphp${PHPVER}* package whose
## version equals lsphp${PHPVER}'s (core, -common, -curl, -intl, -ldap, -mysql,
## -opcache, -dev ...; the PECL packages carry their own versions). Every ELF
## file among those MUST have a php.net counterpart — a missing one fails the
## build, so a new LiteSpeed sub-package can never keep an old binary silently.
##
## THE "SAME SUPPORT" GATE (all fatal): lsphp-src-probe.php is run under both
## SAPIs before and after; the loaded extension set (incl. Zend extensions) must
## be identical, every PECL extension must report its same version, every
## php-src extension the new version, no effective ini value may change or
## disappear (new directives are allowed and printed), and the
## gd/curl/openssl/icu/imagick/redis/memcached/password/hash/stream feature
## lines must be unchanged. `php -i` Configure Command must match the packaged
## one except CFLAGS. No ELF under the prefix may have an unresolved library.
set -euo pipefail
MODE = " ${ 1 :?usage: lsphp-src-overlay.sh runtime|dev PHPVER } "
PHPVER = " ${ 2 :?usage: lsphp-src-overlay.sh runtime|dev PHPVER } "
OUT = " ${ 3 :- /lsphp-src-out } "
case " $MODE " in runtime| dev) ;; *) echo "FATAL(lsphp-src-overlay): mode must be runtime|dev" >& 2; exit 1 ;; esac
2026-10-21 09:47:44 -07:00
[[ " $PHPVER " = ~ ^[ 0-9]{ 2} $ ]] || { echo "FATAL(lsphp-src-overlay): PHPVER=' $PHPVER ' is not NN" >& 2; exit 1; }
2026-10-21 09:43:45 -07:00
die() { echo "FATAL(lsphp-src-overlay): $* " >& 2; exit 1; }
log() { echo "lsphp-src-overlay[ $MODE ]: $* " ; }
LSPHP_SOURCE = " ${ LSPHP_SOURCE :- litespeed -apt } "
if [ ! -f " $OUT /meta.env" ] ; then
[ -z " ${ PHP_SRC_VERSION :- } " ] || die "PHP_SRC_VERSION= $PHP_SRC_VERSION but no staged php.net build in $OUT (lsphp-src stage did not run?)."
[ " $LSPHP_SOURCE " = "litespeed-apt" ] || die "LSPHP_SOURCE= $LSPHP_SOURCE requires PHP_SRC_VERSION."
log "no php.net build staged — packaged lsphp ${ PHPVER } kept as is."
exit 0
fi
[ " $LSPHP_SOURCE " = "php.net" ] || die "a php.net build is staged but LSPHP_SOURCE= $LSPHP_SOURCE (must be 'php.net' so the image label tells the truth)."
# shellcheck disable=SC1091
. " $OUT /meta.env"
[ " ${ PHP_SRC_VERSION :- } " = " $LSPHP_SRC_PHP_VERSION " ] || die "staged build is $LSPHP_SRC_PHP_VERSION but PHP_SRC_VERSION= ${ PHP_SRC_VERSION :- <empty> } ."
BRANCH = " ${ PHPVER : 0 : 1 } . ${ PHPVER : 1 } "
P = "/usr/local/lsws/lsphp ${ PHPVER } "
SRCROOT = " $OUT /root"
PROBE = /usr/local/share/lsphp-src-probe.php
[ -f " $PROBE " ] || die " $PROBE missing."
CORE = "lsphp ${ PHPVER } "
COREV = $( dpkg-query -W -f= '${Version}' " $CORE " )
[ " $COREV " = " $LSPHP_SRC_PKG_REFERENCE " ] || die "this stage has $CORE $COREV but the php.net build was configured from $LSPHP_SRC_PKG_REFERENCE (the LiteSpeed repo moved between stages? rebuild)."
probe() { # $1 = tag; writes $OUT/probe-<tag>-{cli,lsapi}.txt
" $P /bin/php" " $PROBE " >" $OUT /probe- $1 -cli.txt" || die "php probe failed ( $1 )."
" $P /bin/lsphp" " $PROBE " >" $OUT /probe- $1 -lsapi.txt" || die "lsphp probe failed ( $1 )."
local first
first = $( head -n1 " $OUT /probe- $1 -lsapi.txt" )
[ " $first " = "SAPI litespeed" ] || die "lsphp did not run the probe under the litespeed SAPI ( $1 ): ' $first '."
}
## Configure Command minus CFLAGS, whitespace-normalised (the CFLAGS argument
## sits in a different position in our ./configure invocation).
cfgcmd() { " $P /bin/php" -i | awk -F' => ' '$1 ~ /^Configure Command/ {print $2}' | sed -E "s/'CFLAGS=[^']*'//; s/[[:space:]]+/ /g; s/^ //; s/ $//" ; }
probe before
CFG_BEFORE = $( cfgcmd)
OLDV = $( awk '$1 == "VERSION" {print $2}' " $OUT /probe-before-cli.txt" )
[ " $OLDV " = " $LSPHP_SRC_PKG_PHP_VERSION " ] || die "packaged PHP is $OLDV , build meta says $LSPHP_SRC_PKG_PHP_VERSION ."
## ---- which files are php-src output? (dpkg decides) ---------------------------
mapfile -t SRC_PKGS < <( dpkg-query -W -f= '${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp ${ PHPVER } *" \
| awk -v v = " $COREV " '$1 == "ii" && $3 == v {print $2}' )
mapfile -t PECL_PKGS < <( dpkg-query -W -f= '${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp ${ PHPVER } *" \
| awk -v v = " $COREV " '$1 == "ii" && $3 != v {print $2 "=" $3}' )
if [ " $MODE " = runtime ] ; then
for p in " ${ SRC_PKGS [@] } " ; do
[ " $p " != "lsphp ${ PHPVER } -dev" ] || die "runtime mode but lsphp ${ PHPVER } -dev is installed in this stage."
done
fi
log "php-src packages at $COREV : ${ SRC_PKGS [*] } "
log "kept as packaged (own versions): ${ PECL_PKGS [*] :- none } "
REPLACED = 0
KEPT =()
while IFS = read -r f; do
[ -f " $f " ] && [ ! -L " $f " ] || continue
rel = " ${ f #/ } "
is_elf = 0
if [ " $( head -c4 " $f " | od -An -c | tr -d ' ' ) " = '177ELF' ] ; then is_elf = 1; fi
case " $f " in
" $P /bin/phar ${ BRANCH } .phar" | " $P " /include/php/*| " $P " /lib/php/build/*| " $P /bin/php-config" | " $P /bin/php-config ${ BRANCH } " | " $P /bin/phpize" | " $P /bin/phpize ${ BRANCH } " )
## text artefacts generated by the php-src build
[ " $MODE " = dev ] || [[ " $f " == " $P /bin/phar ${ BRANCH } .phar" ]] || { KEPT +=( " $f " ) ; continue ; }
src = " $SRCROOT / $rel "
case " $f " in
" $P /bin/php-config" ) src = " $SRCROOT$P /bin/php-config ${ BRANCH } " ;;
" $P /bin/phpize" ) src = " $SRCROOT$P /bin/phpize ${ BRANCH } " ;;
esac
if [ -f " $src " ] ; then
install -m " $( stat -c %a " $f " ) " " $src " " $f " ; REPLACED = $(( REPLACED + 1 ))
elif [[ " $f " == " $P " /include/php/* ]] ; then
log "header $f no longer exists in php.net $LSPHP_SRC_PHP_VERSION — removing the stale copy"
rm -f " $f "
else
die "php.net build has no counterpart for packaged $f ."
fi
;;
*)
if [ " $is_elf " = 1 ] ; then
src = " $SRCROOT / $rel "
[ " $f " = " $P /bin/php" ] && src = " $SRCROOT$P /bin/php ${ BRANCH } "
[ -f " $src " ] || die "packaged ELF $f (php-src output, owned by a $COREV package) has NO php.net counterpart at $src — refusing to ship an old binary next to the new PHP."
install -m " $( stat -c %a " $f " ) " " $src " " $f " ; REPLACED = $(( REPLACED + 1 ))
else
KEPT +=( " $f " )
fi
;;
esac
done < <( for p in " ${ SRC_PKGS [@] } " ; do dpkg -L " $p " ; done | sort -u)
if [ " $MODE " = dev ] ; then
## headers that are NEW in the php.net release (none for most patch releases)
while IFS = read -r -d '' h; do
dst = " ${ h # " $SRCROOT " } "
[ -e " $dst " ] || { mkdir -p " $( dirname " $dst " ) " ; install -m 0644 " $h " " $dst " ; log "added new header $dst " ; }
done < <( find " $SRCROOT$P /include/php" -type f -print0)
CFV = $( " $P /bin/php-config" --version)
[ " $CFV " = " $LSPHP_SRC_PHP_VERSION " ] || die "php-config reports $CFV after overlay."
fi
log "replaced $REPLACED php-src files; kept ${# KEPT [@] } packaging files (ini templates, helper scripts, docs, man pages)"
## ---- no unresolved shared libraries -------------------------------------------
while IFS = read -r -d '' f; do
if [ " $( head -c4 " $f " | od -An -c | tr -d ' ' ) " = '177ELF' ] ; then
LDD = $( ldd " $f " 2>& 1 || true )
[[ " $LDD " != *"not found" * ]] || die " $f has unresolved libraries:
$LDD "
fi
done < <( find " $P /bin" " $P /lib" -type f -print0)
## ---- the "same support" gate ----------------------------------------------------
probe after
CFG_AFTER = $( cfgcmd)
[ " $CFG_BEFORE " = " $CFG_AFTER " ] || die "Configure Command differs from the packaged build beyond CFLAGS:
packaged: $CFG_BEFORE
php.net : $CFG_AFTER "
FAIL = 0
for sapi in cli lsapi; do
B = " $OUT /probe-before- $sapi .txt" ; A = " $OUT /probe-after- $sapi .txt"
NEWV = $( awk '$1 == "VERSION" {print $2}' " $A " )
[ " $NEWV " = " $LSPHP_SRC_PHP_VERSION " ] || { echo "[ $sapi ] reports PHP $NEWV , expected $LSPHP_SRC_PHP_VERSION " >& 2; FAIL = 1; }
## extension sets + versions
R = $( awk -v o = " $OLDV " -v n = " $LSPHP_SRC_PHP_VERSION " '
FNR == NR { if ($1 == "EXT" || $1 == "ZEXT") b[$1" "$2] = $3; next }
($1 == "EXT" || $1 == "ZEXT") { a[$1" "$2] = $3 }
END {
for (k in b) {
if (!(k in a)) { print "MISSING " k; continue }
want = (b[k] == o) ? n : b[k]
if (a[k] != want) print "VERSION " k " " b[k] " -> " a[k] " (want " want ")"
}
for (k in a) if (!(k in b)) print "EXTRA " k " " a[k]
}' " $B " " $A " | sort)
if [ -n " $R " ] ; then echo "[ $sapi ] extension surface changed:" $'\n' " $R " >& 2; FAIL = 1; fi
## feature lines must be byte-identical
R = $( diff <( grep '^FEAT ' " $B " ) <( grep '^FEAT ' " $A " ) || true )
if [ -n " $R " ] ; then echo "[ $sapi ] feature surface changed:" $'\n' " $R " >& 2; FAIL = 1; fi
## effective ini (packaged php.ini + mods-available/*.ini): no directive may
## vanish or change value; directives NEW in the release are printed.
R = $( awk 'FNR == NR { if ($1 == "INI") b[$2] = $0; next } $1 == "INI" { a[$2] = $0 }
END { for (k in b) { if (!(k in a)) print "REMOVED " b[k]; else if (a[k] != b[k]) print "CHANGED " b[k] " -> " a[k] } }' " $B " " $A " | sort)
if [ -n " $R " ] ; then echo "[ $sapi ] effective ini changed:" $'\n' " $R " >& 2; FAIL = 1; fi
R = $( awk 'FNR == NR { if ($1 == "INI") b[$2] = 1; next } $1 == "INI" && !($2 in b) { print " " $0 }' " $B " " $A " )
[ -z " $R " ] || log "[ $sapi ] ini directives new in $LSPHP_SRC_PHP_VERSION (informational):" $'\n' " $R "
log "[ $sapi ] $( grep -c '^EXT ' " $A " ) extensions + $( grep -c '^ZEXT ' " $A " ) zend extensions, identical to packaged; $( grep -c '^FEAT ' " $A " ) feature lines identical"
done
[ " $FAIL " = 0 ] || die "the php.net build does NOT offer the same support as packaged lsphp ${ PHPVER } $COREV (details above)."
printf '%s\n' " $LSPHP_SRC_PHP_VERSION " >/etc/cac-php-version
cat >/etc/cac-lsphp-source <<EOF
source=php.net
php_version=$LSPHP_SRC_PHP_VERSION
tarball_sha256=$LSPHP_SRC_SHA256
signer_primary_fpr=$LSPHP_SRC_SIGNER
replaces_packaged=$CORE=$COREV (PHP $OLDV)
pecl_kept=${PECL_PKGS[*]:-none}
EOF
log "OK: lsphp ${ PHPVER } is now php.net $LSPHP_SRC_PHP_VERSION (was packaged $OLDV ); /etc/cac-lsphp-source written"
## $OUT is a BuildKit bind mount (rw = writes discarded); nothing to clean up.