feat(abi): backfill per-object flat-slot safety verdicts (#202)
All checks were successful
ci/crow/manual/backfill-flat-safety/17 Pipeline was successful

## Why

bincraft #128 lets `union_index_records()` keep a carried flat record when that specific object is recorded safe. On its own it changes nothing for the records already being dropped, because they are legacy objects with no verdict:

```
4.4: 16398 dropped across 9 indexes  (~1822 each)
4.5: 20076 dropped across 9 indexes  (~2230 each)
4.6: 15532 dropped across 9 indexes  (~1726 each)
```

Every one of those resolves to CRAN source for clients the routing serves correctly. This backfill inspects the objects already in the bucket and writes their verdicts, which is what actually restores the coverage.

## How the verdict is made

By symbols, not heuristics. A binary fails at `dyn.load()` under minor M exactly when it references a symbol M does not export, and the volatile set is derived by diffing the `libR.so` exports of the R builds installed in the image — so a new R release updates it with no curated list to maintain.

That matters here because the source-level classifier calls **93%** of compiled packages risky (`LinkingTo: Rcpp` alone is enough), while a symbol scan of the same slot found **17** packages that break under 4.6 and **72** under 4.4. Recording the real answer per object is what makes the difference between ~2000 dropped records and the few dozen that genuinely need dropping.

## Safety

- **Refuses to run where fewer than two R minors are installed.** With nothing to compare, every verdict would read "safe" — writing a bucket full of false clearances is far worse than writing none.
- Skips packages with no compiled code: they cannot fail this way and are never dropped, so downloading them to prove it is wasted.
- Resumable; already-recorded objects cost one query.

`crow lint` passes.

## Sequencing

Merge and release bincraft #128 first, then run this per slot, then re-index. Verifying the drop counts fall is the point of the exercise, so that re-index is where the fix is confirmed rather than assumed.

Reviewed-on: #202
This commit is contained in:
Patrick Schratz 2026-09-05 09:35:47 +00:00 • committed by Patrick Schratz
commit c216e44578

View file

@ -0,0 +1,178 @@
# Record, per flat-slot object, whether it loads under every supported R minor.
#
# `union_index_records()` keeps a carried flat record when that object is
# recorded safe and drops it otherwise. Objects uploaded before that record
# existed have no verdict, so they are dropped: a full re-index removed roughly
# 2000 records per per-minor index (16398 across the 4.4 indexes, 20076 across
# 4.5, 15532 across 4.6), each one resolving to CRAN source for clients the
# routing serves correctly.
#
# This inspects what is already in the bucket and writes the verdict, which is
# what restores that coverage. Decided by symbols rather than heuristics: a
# binary fails at dyn.load() under minor M exactly when it references a symbol M
# does not export, and the volatile set is derived by diffing the R builds in
# the image.
#
# Re-running is cheap; recorded objects are skipped.
#
# crow pipeline create devxy/build-cran-binaries --var flat_safety=all
variables:
# A manual pipeline creation instantiates every file in .crow/, so the default
# must match no matrix row.
flat_safety:
description: "Inspect target: a specific <os>-<arch>, 'all' for every slot, or 'none'."
options:
- none
- all
- alpine-322-amd64
- alpine-322-arm64
- alpine-323-amd64
- alpine-323-arm64
- alpine-324-amd64
- alpine-324-arm64
- redhat-8-amd64
- redhat-8-arm64
- redhat-9-amd64
- redhat-9-arm64
- redhat-10-amd64
- redhat-10-arm64
- ubuntu-2204-amd64
- ubuntu-2204-arm64
- ubuntu-2404-amd64
- ubuntu-2404-arm64
- ubuntu-2604-amd64
- ubuntu-2604-arm64
default: none
FLAT_SAFETY_LIMIT:
description: 'Inspect at most this many objects per slot (0 = no limit).'
default: '0'
when:
- event: manual
evaluate: 'flat_safety == "all" || flat_safety == "${OS}-${ARCH}"'
skip_clone: true
labels:
group: rpkgs-${ARCH}
matrix:
include:
- OS: alpine-322
ARCH: amd64
R_VERSION: 4.5.3
IMG: alpine:3.22
- OS: alpine-322
ARCH: arm64
R_VERSION: 4.5.3
IMG: alpine:3.22
- OS: alpine-323
ARCH: amd64
R_VERSION: 4.5.3
IMG: alpine:3.23
- OS: alpine-323
ARCH: arm64
R_VERSION: 4.5.3
IMG: alpine:3.23
- OS: alpine-324
ARCH: amd64
R_VERSION: 4.5.3
IMG: alpine:3.24
- OS: alpine-324
ARCH: arm64
R_VERSION: 4.5.3
IMG: alpine:3.24
- OS: redhat-8
ARCH: amd64
R_VERSION: 4.4.3
IMG: redhat:8
- OS: redhat-8
ARCH: arm64
R_VERSION: 4.4.3
IMG: redhat:8
- OS: redhat-9
ARCH: amd64
R_VERSION: 4.4.3
IMG: redhat:9
- OS: redhat-9
ARCH: arm64
R_VERSION: 4.4.3
IMG: redhat:9
- OS: redhat-10
ARCH: amd64
R_VERSION: 4.5.3
IMG: redhat:10
- OS: redhat-10
ARCH: arm64
R_VERSION: 4.5.3
IMG: redhat:10
- OS: ubuntu-2204
ARCH: amd64
R_VERSION: 4.4.3
IMG: ubuntu:jammy
- OS: ubuntu-2204
ARCH: arm64
R_VERSION: 4.4.3
IMG: ubuntu:jammy
- OS: ubuntu-2404
ARCH: amd64
R_VERSION: 4.4.3
IMG: ubuntu:noble
- OS: ubuntu-2404
ARCH: arm64
R_VERSION: 4.4.3
IMG: ubuntu:noble
- OS: ubuntu-2604
ARCH: amd64
R_VERSION: 4.4.3
IMG: ubuntu:resolute
- OS: ubuntu-2604
ARCH: arm64
R_VERSION: 4.4.3
IMG: ubuntu:resolute
steps:
- name: 'Inspect flat objects and record safety'
image: reg.devxy.io/rpkgs/build-env-${IMG}
pull: true
environment:
OTEL_R_TRACES_EXPORTER: none
OTEL_R_LOGS_EXPORTER: none
OTEL_R_METRICS_EXPORTER: none
RED_HAT_DEV_PW:
from_secret: RED_HAT_DEV_PW
B2_S3_ACCESS_KEY:
from_secret: B2_S3_ACCESS_KEY
B2_S3_SECRET_KEY:
from_secret: B2_S3_SECRET_KEY
PGPASS:
from_secret: PGPASS
REPO_RO_TOKEN:
from_secret: REPO_RO_TOKEN
GIT_USER: pat-s
R_LIBS_USER: /tmp/R-pkgs
FLAT_SAFETY_ARCH: ${ARCH}
FLAT_SAFETY_LIMIT: ${FLAT_SAFETY_LIMIT}
commands:
- git clone -q https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git .
- mkdir -p /tmp/R-pkgs
- /opt/R/$R_VERSION/bin/Rscript local/install-bincraft.R
- |
set -eu
LIMIT=""
if [ "$$FLAT_SAFETY_LIMIT" != "0" ]; then
LIMIT="$$FLAT_SAFETY_LIMIT"
fi
# The codename comes from the image's /etc/os-release, exactly as the
# re-index step derives it, so it cannot drift from the matrix.
CODENAME=$(/opt/R/$R_VERSION/bin/Rscript -e 'cat(bincraft::set_codename(NULL))')
echo "slot: $$FLAT_SAFETY_ARCH/$$CODENAME"
/opt/R/$R_VERSION/bin/Rscript local/backfill-flat-safety.R \
"$$FLAT_SAFETY_ARCH" "$$CODENAME" $$LIMIT
backend_options:
docker:
resources:
requests:
memory: 4Gi
cpu: 1000m