From 0529b5525460f6ca1cb95766132c2b970a61dc58 Mon Sep 17 00:00:00 2001 From: pat-s Date: Mon, 31 Aug 2026 12:58:58 +0000 Subject: [PATCH] test(verify): fail a slot still serving an ABI-risky binary of another minor The comprehensive production sweep passed 236/0 while 2608 records were being served that crash at load: the exact failure this whole effort was about. Nothing in the suite looked for them. The guard in bincraft drops them at index time, so it only takes effect where a reindex has run since it shipped. amd64/resolute is clean; fourteen slots are not, and no check said so. A package carrying a Path in any per-minor index is risky by construction, which is the signal for the ones absent from this minor's index. Serving such a package from the generic slot with a binary built under a different minor is the crash; that count must be zero. --- scripts/verify-r-minor-routing.sh | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/verify-r-minor-routing.sh b/scripts/verify-r-minor-routing.sh index 2192cdc..190245a 100755 --- a/scripts/verify-r-minor-routing.sh +++ b/scripts/verify-r-minor-routing.sh @@ -140,6 +140,23 @@ fallback_counts() { ' } +# Packages this index serves from the generic slot with a binary built under a +# different R minor, while some other per-minor slot carries a build of them - +# which proves the ABI classifier called them risky. Serving those is the +# load-time crash the per-minor slots exist to prevent. bincraft drops them at +# index time, so a non-zero count means the slot has not been reindexed since +# that guard shipped. +abi_unsafe_count() { + local minor_file=$1 minor=$2 risky_file=$3 + gunzip -c "$minor_file" 2>/dev/null | awk -v m="$minor" ' + /^Package:/ { pkg = $2; path = ""; built = "" } + /^Path:/ { path = $2 } + /^Built:/ { built = $2 " " $3 } + /^$/ { if (pkg != "" && path == "" && built != "" && built !~ ("^R " m "\\.")) print pkg; pkg = "" } + ' | sort -u > "$minor_file.mismatched" + comm -12 "$minor_file.mismatched" "$risky_file" | wc -l +} + # How many packages a client of would receive as source through # per-minor routing while the generic slot holds a binary built under that very # minor. Zero is the bar for enabling a slot. @@ -327,6 +344,30 @@ for arch in $ARCHES; do fi fi + # Packages carrying a Path in any per-minor index are risky by construction. + : > "$WORK/${arch}-${distro}.risky" + for minor in $MINORS; do + f="$WORK/${arch}-${distro}-${minor}.gz" + [ -s "$f" ] || continue + gunzip -c "$f" 2>/dev/null | awk ' + /^Package:/ { pkg = $2; path = "" } + /^Path:/ { path = $2 } + /^$/ { if (pkg != "" && path != "") print pkg; pkg = "" } + ' >> "$WORK/${arch}-${distro}.risky" + done + sort -u -o "$WORK/${arch}-${distro}.risky" "$WORK/${arch}-${distro}.risky" + + for minor in $MINORS; do + f="$WORK/${arch}-${distro}-${minor}.gz" + [ -s "$f" ] || continue + unsafe=$(abi_unsafe_count "$f" "$minor" "$WORK/${arch}-${distro}.risky") + if [ "${unsafe:-0}" -gt 0 ]; then + bad "$slot R $minor serves $unsafe ABI-risky package(s) built under another R minor - reindex this slot" + else + ok "$slot R $minor serves no ABI-risky package from another minor" + fi + done + # Excluded minors: no published index, and under --live a redirect to CRAN. for minor in $EXCLUDED_MINORS; do ex_url="$BASE/$slot/latest/src/contrib/$minor/PACKAGES.gz"