feat(ci): verify what clients actually receive from every published slot #209
2 changed files with 346 additions and 0 deletions
156
.crow/verify-repo-routing.yaml
Normal file
156
.crow/verify-repo-routing.yaml
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Install a fixed set of packages from a published slot under every R minor the
|
||||||
|
# image carries, through each URL form the website tells people to configure,
|
||||||
|
# and check that each one loads.
|
||||||
|
#
|
||||||
|
# This is the end-to-end counterpart to the ABI bookkeeping: `abi_flat_safety`
|
||||||
|
# records what the binaries say about themselves, this proves what a client
|
||||||
|
# actually receives once the CDN, the per-minor indexes and the `Path:` field
|
||||||
|
# have had their say. It runs on the build agents rather than a workstation so
|
||||||
|
# arm64 is covered on real hardware.
|
||||||
|
#
|
||||||
|
# crow pipeline create devxy/build-cran-binaries --var verify_routing=all
|
||||||
|
|
||||||
|
variables:
|
||||||
|
# A manual pipeline creation instantiates every file in .crow/, so the default
|
||||||
|
# must match no matrix row.
|
||||||
|
verify_routing:
|
||||||
|
description: "Verify 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
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: manual
|
||||||
|
evaluate: 'verify_routing == "all" || verify_routing == "${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: 'Verify routing serves loadable binaries'
|
||||||
|
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
|
||||||
|
REPO_RO_TOKEN:
|
||||||
|
from_secret: REPO_RO_TOKEN
|
||||||
|
VERIFY_ARCH: ${ARCH}
|
||||||
|
commands:
|
||||||
|
- git clone -q https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git .
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
CODENAME=$(/opt/R/$R_VERSION/bin/Rscript -e 'cat(bincraft::set_codename(NULL))')
|
||||||
|
rc=0
|
||||||
|
for RBIN in /opt/R/[0-9]*/bin/R; do
|
||||||
|
RV=$(basename "$(dirname "$(dirname "$$RBIN")")")
|
||||||
|
for FORM in arch generic flat; do
|
||||||
|
echo "=== R $$RV via $$FORM ==="
|
||||||
|
"$(dirname "$$RBIN")/Rscript" local/verify-repo-routing.R \
|
||||||
|
"$$VERIFY_ARCH" "$$CODENAME" "$$FORM" || rc=1
|
||||||
|
done
|
||||||
|
done
|
||||||
|
exit $$rc
|
||||||
|
backend_options:
|
||||||
|
docker:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 4Gi
|
||||||
|
cpu: 1000m
|
||||||
190
local/verify-repo-routing.R
Normal file
190
local/verify-repo-routing.R
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
# Check that a published slot serves an installable, loadable binary for the R
|
||||||
|
# running this script, through one of the URL forms a user can configure.
|
||||||
|
#
|
||||||
|
# Rscript local/verify-repo-routing.R <arch> <codename> <form>
|
||||||
|
#
|
||||||
|
# One process per form on purpose: `available.packages()` caches its index in
|
||||||
|
# `tempdir()` keyed by repository URL, and the `arch` and `flat` forms share a
|
||||||
|
# URL. In a single process the second form would silently read the first form's
|
||||||
|
# index and the run would prove nothing.
|
||||||
|
#
|
||||||
|
# Forms:
|
||||||
|
# arch https://cran.rpkgs.com/<arch>/<codename>/latest with R's own agent
|
||||||
|
# generic https://cran.rpkgs.com/ with an agent naming the distribution
|
||||||
|
# flat the arch URL with a curl agent, so the edge cannot read an R minor
|
||||||
|
# from the request and the flat slot answers instead of a per-minor
|
||||||
|
# one
|
||||||
|
#
|
||||||
|
# `arch` and `generic` are contractual: they are what the website tells people
|
||||||
|
# to configure, so a failure there fails the run. `flat` measures what a client
|
||||||
|
# hiding its R minor is exposed to; it is reported and never fatal.
|
||||||
|
|
||||||
|
args <- commandArgs(trailingOnly = TRUE)
|
||||||
|
if (length(args) != 3L) {
|
||||||
|
stop("usage: verify-repo-routing.R <arch> <codename> <form>", call. = FALSE)
|
||||||
|
}
|
||||||
|
arch <- args[[1L]]
|
||||||
|
codename <- args[[2L]]
|
||||||
|
form <- args[[3L]]
|
||||||
|
|
||||||
|
# Compiled packages that exercise the R C API broadly, plus one pure-R package
|
||||||
|
# as a control: if `brew` fails too, the problem is the repository rather than
|
||||||
|
# the ABI.
|
||||||
|
PACKAGES <- c(
|
||||||
|
"Rcpp",
|
||||||
|
"data.table",
|
||||||
|
"stringi",
|
||||||
|
"xml2",
|
||||||
|
"curl",
|
||||||
|
"jsonlite",
|
||||||
|
"digest",
|
||||||
|
"base64enc",
|
||||||
|
"yaml",
|
||||||
|
"lobstr",
|
||||||
|
"brew"
|
||||||
|
)
|
||||||
|
|
||||||
|
arch_url <- sprintf("https://cran.rpkgs.com/%s/%s/latest", arch, codename)
|
||||||
|
|
||||||
|
# The agent the website's generic snippet builds, so this exercises the string
|
||||||
|
# real users send rather than an approximation of it.
|
||||||
|
generic_agent <- function() {
|
||||||
|
os_release <- readLines("/etc/os-release", warn = FALSE)
|
||||||
|
field <- function(key) {
|
||||||
|
line <- grep(sprintf("^%s=", key), os_release, value = TRUE)
|
||||||
|
if (length(line) == 0L) {
|
||||||
|
return("")
|
||||||
|
}
|
||||||
|
gsub('"', "", sub("^[^=]+=", "", line[[1L]]))
|
||||||
|
}
|
||||||
|
os <- paste(field("NAME"), field("VERSION_ID"))
|
||||||
|
codename_id <- field("VERSION_CODENAME")
|
||||||
|
if (nzchar(codename_id)) {
|
||||||
|
os <- sprintf("%s; codename=%s", os, codename_id)
|
||||||
|
}
|
||||||
|
sprintf(
|
||||||
|
"R/%s (%s) (%s)",
|
||||||
|
getRversion(),
|
||||||
|
os,
|
||||||
|
paste(R.version["platform"], R.version["arch"], R.version["os"])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings <- switch(
|
||||||
|
form,
|
||||||
|
arch = list(repo = arch_url, agent = getOption("HTTPUserAgent")),
|
||||||
|
generic = list(repo = "https://cran.rpkgs.com/", agent = generic_agent()),
|
||||||
|
flat = list(repo = arch_url, agent = "curl/8.5.0"),
|
||||||
|
stop(sprintf("unknown form '%s'", form), call. = FALSE)
|
||||||
|
)
|
||||||
|
|
||||||
|
# No CRAN in `repos`: a package missing from our index must fail loudly rather
|
||||||
|
# than be quietly compiled from source, which would hide exactly the gap this
|
||||||
|
# script exists to find.
|
||||||
|
options(HTTPUserAgent = settings$agent, repos = c(rpkgs = settings$repo))
|
||||||
|
|
||||||
|
r_minor <- paste(getRversion()[1L, 1:2], collapse = ".")
|
||||||
|
rscript <- file.path(R.home("bin"), "Rscript")
|
||||||
|
|
||||||
|
emit <- function(package, status, built = "", secs = NA_real_) {
|
||||||
|
cat(
|
||||||
|
sprintf(
|
||||||
|
"ROW\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
form,
|
||||||
|
arch,
|
||||||
|
codename,
|
||||||
|
r_minor,
|
||||||
|
package,
|
||||||
|
status,
|
||||||
|
sprintf("%s|%.1fs", built, secs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# `library()` in this process would abort the whole run on a bad `dyn.load()`,
|
||||||
|
# and the point of the exercise is to survive that and report it.
|
||||||
|
loads <- function(package, lib) {
|
||||||
|
status <- suppressWarnings(system2(
|
||||||
|
rscript,
|
||||||
|
c("-e", shQuote(sprintf('library("%s")', package))),
|
||||||
|
env = sprintf("R_LIBS=%s", lib),
|
||||||
|
stdout = FALSE,
|
||||||
|
stderr = FALSE
|
||||||
|
))
|
||||||
|
identical(as.integer(status), 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
built_field <- function(package, lib) {
|
||||||
|
desc <- file.path(lib, package, "DESCRIPTION")
|
||||||
|
if (!file.exists(desc)) {
|
||||||
|
return("")
|
||||||
|
}
|
||||||
|
value <- read.dcf(desc, fields = "Built")[[1L]]
|
||||||
|
if (is.na(value)) "" else sub(";.*$", "", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
available <- tryCatch(
|
||||||
|
rownames(available.packages()),
|
||||||
|
error = function(e) {
|
||||||
|
message(sprintf(
|
||||||
|
"index unreadable at %s: %s",
|
||||||
|
settings$repo,
|
||||||
|
conditionMessage(e)
|
||||||
|
))
|
||||||
|
character()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
failures <- 0L
|
||||||
|
for (package in PACKAGES) {
|
||||||
|
if (!package %in% available) {
|
||||||
|
emit(package, "MISSING_FROM_INDEX")
|
||||||
|
failures <- failures + 1L
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
lib <- file.path(tempdir(), sprintf("lib-%s-%s", form, package))
|
||||||
|
dir.create(lib, showWarnings = FALSE, recursive = TRUE)
|
||||||
|
started <- Sys.time()
|
||||||
|
ok <- tryCatch(
|
||||||
|
{
|
||||||
|
# Hard dependencies come from the same repository and must be present
|
||||||
|
# for `R CMD INSTALL` to succeed, so they are installed too; Suggests are
|
||||||
|
# not, since a missing one does not stop a package loading.
|
||||||
|
install.packages(
|
||||||
|
package,
|
||||||
|
lib = lib,
|
||||||
|
dependencies = c("Depends", "Imports", "LinkingTo"),
|
||||||
|
quiet = TRUE
|
||||||
|
)
|
||||||
|
package %in% rownames(installed.packages(lib.loc = lib))
|
||||||
|
},
|
||||||
|
error = function(e) FALSE
|
||||||
|
)
|
||||||
|
secs <- as.numeric(difftime(Sys.time(), started, units = "secs"))
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
emit(package, "INSTALL_FAILED", secs = secs)
|
||||||
|
failures <- failures + 1L
|
||||||
|
} else if (!loads(package, lib)) {
|
||||||
|
emit(package, "LOAD_FAILED", built_field(package, lib), secs)
|
||||||
|
failures <- failures + 1L
|
||||||
|
} else {
|
||||||
|
emit(package, "OK", built_field(package, lib), secs)
|
||||||
|
}
|
||||||
|
unlink(lib, recursive = TRUE, force = TRUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
message(sprintf(
|
||||||
|
"%s/%s R %s via %s: %d of %d packages usable",
|
||||||
|
arch,
|
||||||
|
codename,
|
||||||
|
r_minor,
|
||||||
|
form,
|
||||||
|
length(PACKAGES) - failures,
|
||||||
|
length(PACKAGES)
|
||||||
|
))
|
||||||
|
|
||||||
|
if (failures > 0L && !identical(form, "flat")) {
|
||||||
|
quit(status = 1L)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue