diff --git a/Dockerfile.lsphp b/Dockerfile.lsphp index 19f0e8e..16420d6 100644 --- a/Dockerfile.lsphp +++ b/Dockerfile.lsphp @@ -26,26 +26,63 @@ ARG PHPVER=83 ## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced ## from RINIT so a customer's .user.ini cannot displace it — see ## ext/cac-path-parity/cac_path_parity.c for why this is an extension and not an -## auto_prepend_file. Built against THIS image's own lsphp so the API/ABI -## (`PHP API` / extension_dir) always match; a PHP version bump in the base -## image therefore recompiles rather than silently loading a stale .so. +## auto_prepend_file. +## +## WHICH lsphp THE .so IS BUILT AGAINST — read this before touching the apt lines. +## `lsphp${PHPVER}-dev` is NOT available at the version the base image ships: +## the LiteSpeed apt repo carries only the CURRENT release, and every prebuilt +## OLS base image is behind it (measured 2026-08-05 on OLS 1.8.4: +## lsphp81 8.1.33 base / 8.1.34 repo, lsphp83 8.3.28 / 8.3.32, +## lsphp85 8.5.0 / 8.5.8). +## Pinning -dev to the base version fails on all three with +## `E: Version '' for 'lsphp-dev' was not found`. +## +## So installing -dev necessarily UPGRADES lsphp in this stage. The parity we can +## have — and the one this file now guarantees — is the other direction: the +## shipped runtime is pinned to whatever version this stage compiled against. +## That version is recorded here and consumed by stage 2, so the two apt layers +## are cache-locked to each other. Without this, `COPY ./ext` invalidating only +## THIS stage while stage 2's apt layer stayed cached produced a real, repeatable +## skew (reviewer measured a .so built on 8.3.32 shipped next to an 8.3.30 +## runtime, with lsphp83-common at 8.3.31 — the vendor family is not always +## uniformly versioned either). +## +## Benign in practice (PHP holds ABI stable across a patch series) but it is the +## riskier direction — headers NEWER than the runtime — and the runtime assertion +## in stage 2 catches only load failure, never silent struct-layout drift. ## ## Separate stage on purpose: the compiler + headers (~400MB) stay out of the ## shipped image, which gains only the ~40KB .so. Costs ~1-2 min of CI per PHP ## version; both stages share the same base layer, so no extra pull. FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS ext-build ARG PHPVER=83 -COPY ./ext/cac-path-parity /usr/src/cac-path-parity -RUN apt-get update && \ + +## Toolchain layer, deliberately BEFORE the source COPY so editing the extension +## does not re-resolve the PHP version (which is what caused the skew above). +## Records the exact lsphp version the headers belong to; stage 2 pins to it. +RUN set -e; \ + apt-get update; \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential autoconf pkg-config \ - lsphp${PHPVER}-dev && \ - cd /usr/src/cac-path-parity && \ - /usr/local/lsws/lsphp${PHPVER}/bin/phpize && \ + lsphp${PHPVER}-dev; \ + RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \ + DEVV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-dev); \ + if [ "$RTV" != "$DEVV" ]; then \ + echo "FATAL: lsphp${PHPVER}=$RTV but lsphp${PHPVER}-dev=$DEVV — the headers" >&2; \ + echo " do not belong to the PHP in this stage. Refusing to build." >&2; \ + exit 1; \ + fi; \ + mkdir -p /build-out; \ + printf '%s' "$RTV" > /build-out/lsphp.version; \ + echo "cac_path_parity will be compiled against lsphp${PHPVER} $RTV" + +COPY ./ext/cac-path-parity /usr/src/cac-path-parity +RUN set -e; \ + cd /usr/src/cac-path-parity; \ + /usr/local/lsws/lsphp${PHPVER}/bin/phpize; \ ./configure --enable-cac-path-parity \ - --with-php-config=/usr/local/lsws/lsphp${PHPVER}/bin/php-config && \ - make -j"$(nproc)" && \ - mkdir -p /build-out && \ + --with-php-config=/usr/local/lsws/lsphp${PHPVER}/bin/php-config; \ + make -j"$(nproc)"; \ cp modules/cac_path_parity.so /build-out/ ## ---- stage 2: the shipped sidecar image ------------------------------------ @@ -57,12 +94,40 @@ ENV PHPVER=${PHPVER} ## base lacks is lsphpNN-ldap. setpriv (util-linux) is already on the Ubuntu ## base; we add nothing else the sidecar doesn't need. All apt cache cleaned in ## the same layer to keep the image small. -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ +## +## VERSION LOCKSTEP: `apt-get install lsphpNN-ldap` pulls lsphpNN-common forward, +## which drags the whole lsphpNN family to the repo's current release — the same +## upgrade the ext-build stage gets. Left implicit, the two stages resolve that +## independently and Docker caches them independently, so they drift apart (see +## the long comment on stage 1). Copying stage 1's recorded version in BEFORE +## this layer makes the version part of this layer's cache key: same version => +## cache hit, new version => this layer re-runs and lands on the same one. The +## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead +## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05). +COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version +RUN set -e; \ + V=$(cat /etc/cac-lsphp-build.version); \ + apt-get update; \ + if ! DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates \ - lsphp${PHPVER}-ldap && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* + lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \ + echo "FATAL: lsphp${PHPVER} $V is what cac_path_parity was compiled against," >&2; \ + echo " but the LiteSpeed repo no longer offers it (it keeps only the" >&2; \ + echo " current release). The ext-build stage is almost certainly a stale" >&2; \ + echo " cache hit — rebuild with --no-cache." >&2; \ + exit 1; \ + fi; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ + RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \ + CMV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-common); \ + if [ "$RTV" != "$V" ] || [ "$CMV" != "$V" ]; then \ + echo "FATAL: cac_path_parity.so was compiled against lsphp${PHPVER} $V but this" >&2; \ + echo " image would ship lsphp${PHPVER}=$RTV / -common=$CMV." >&2; \ + echo " Rebuild with --no-cache so both stages resolve the same release." >&2; \ + exit 1; \ + fi; \ + echo "runtime lsphp${PHPVER} pinned to $V (the version cac_path_parity was built against)" ## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed ## image — same runtime, same tuning). Scripts layer last (they change most). @@ -89,7 +154,10 @@ RUN bash -c 'set -e; \ ## ## The trailing `lsphp -i | grep` is a BUILD-TIME ASSERTION: if the .so fails to ## load (ABI drift after a base-image PHP bump, bad build) the image build fails -## here rather than shipping a sidecar that silently lost path parity. +## here rather than shipping a sidecar that silently lost path parity. Note its +## limit: it proves the .so LOADS, not that it was built against these exact +## structs — silent layout drift would sail straight through. The version lockstep +## above is what actually removes that possibility; this stays as the backstop. ## NOTE: probe lsphp with `-i` ONLY. The lsphp binary is the LSAPI SAPI, not the ## CLI — it accepts just -[b|c|n|h|i|q|s|v|?] and answers anything else (`-m`, ## `-r`) by printing its usage text and exiting 0. A `lsphp -m | grep` check