#!/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 [[ "$PHPVER" =~ ^[0-9]{2}$ ]] || { echo "FATAL(lsphp-src-overlay): PHPVER='$PHPVER' is not NN" >&2; exit 1; } 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:-}." 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--{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 <