All checks were successful
ci/crow/cron/process-updates/9 Pipeline was successful
Implements steps 1 + 2 of #115: turn recorded build failures into triaged patch suggestions instead of hand-scraping Crow logs. ## What this adds A **read-only** reporting pipeline over the `single_builds` metadata table. It never writes to the DB or the registry. - `local/failing-builds-classify.R` — pure, DB-free helpers: - `normalise_error()` strips temp paths, version numbers, hex addresses, and the package name so the same root cause collapses to one fingerprint. - `fingerprint_error()` extracts the salient error line and normalises it. - `classify_error()` matches against a seed signature set; unmatched errors are never guessed at. - `propose_registry_entry()` renders a schema-valid `registry.json` entry. - `local/failing-builds-report.R` — entrypoint: queries `single_builds WHERE error_occurred = TRUE AND removed = FALSE`, groups by root cause (signature when classified, fingerprint otherwise), classifies each group, and prints a triaged report. Flags: `--platform`, `--arch`, `--min`, `--json`; `PLATFORM`/`ARCH` env fallbacks. - `local/tests/test-failing-builds-classify.R` — unit tests for the helpers. - `local/patches/README.md` — documents the workflow. ## Seed signatures Each rule carries a fix tier, confidence, and an auto/human-only flag: | Signature | Fix | Disposition | | --- | --- | --- | | `tbb/tbb_stddef.h: No such file` | makevars `-DTBB_INTERFACE_NEW` | auto-proposable | | RcppParallel bundled TBB (musl / new g++) | curated `disable-tbb.patch` | auto-proposable | | system `libuv.so` link leak | force vendored/static lib | **human triage** (novel source diff) | | unmatched | none | **human triage** | ## Guardrails honored - No autonomous novel source diffs: only known env/makevars levers and already-curated package patches are auto-proposable; anything needing a brand-new diff, and any unknown signature, is routed to human triage. - No DB or registry writes; no change to the public `src/contrib` index. - Reuses `single_builds.error_text`; no new failure-capture pipeline. ## Verification - All helper unit tests pass under the Dockerized R 4.5.3 build env. - Pre-commit hooks pass (`air-format`, `validate-patches`, prettier, etc.). - Smoke-tested the full report path with a stubbed DB; generated proposals pass the real `local/validate-patches.R`. Steps 3 (auto-open PRs) and 4 (feedback loop) are intentionally deferred, per the issue's suggestion to validate the signature set first. Closes #115 Reviewed-on: #116
204 lines
7.7 KiB
R
204 lines
7.7 KiB
R
# Pure, side-effect-free helpers for triaging `single_builds` failures:
|
|
# normalise a raw `error_text` into a stable fingerprint, and classify it
|
|
# against a seed set of known failure signatures (issue #115, steps 1 + 2).
|
|
#
|
|
# Kept free of DB/IO so it can be sourced by both `failing-builds-report.R`
|
|
# and the unit tests in `local/tests/`.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signature table
|
|
# ---------------------------------------------------------------------------
|
|
# Each rule maps a recurring compile/link/load error to a suggested fix tier.
|
|
# `pattern` is a case-insensitive regex matched against the raw `error_text`.
|
|
# `auto` marks whether the fix is a *known lever* safe to auto-propose as a PR
|
|
# (env / makevars / an already-curated package-specific patch). Rules that
|
|
# would require a brand-new source diff for a previously-unseen package stay
|
|
# `auto = FALSE` -> classified, but always routed to human triage, per the
|
|
# issue's guardrail against shipping autonomous novel source diffs.
|
|
#
|
|
# Seeded from the existing registry entries and known recurring failures; add
|
|
# a row here as new signatures are confirmed. Order matters: the first match
|
|
# wins, so keep more specific patterns above broader ones.
|
|
build_signatures <- function() {
|
|
list(
|
|
list(
|
|
id = "tbb-stddef-removed",
|
|
label = "removed TBB header tbb/tbb_stddef.h",
|
|
pattern = "tbb/tbb_stddef\\.h.*No such file",
|
|
tier = "makevars",
|
|
confidence = "high",
|
|
auto = TRUE,
|
|
fix = "add CPPFLAGS += -DTBB_INTERFACE_NEW so the source stops including the removed tbb/tbb_stddef.h header",
|
|
example = "StanHeaders / rstan (#114)",
|
|
registry = list(
|
|
env = NULL,
|
|
configure_args = NULL,
|
|
makevars = list(CPPFLAGS = "-DTBB_INTERFACE_NEW"),
|
|
patch = NULL,
|
|
reason = "package includes the removed tbb/tbb_stddef.h; -DTBB_INTERFACE_NEW selects the new oneTBB interface path"
|
|
)
|
|
),
|
|
list(
|
|
id = "rcppparallel-bundled-tbb",
|
|
label = "bundled Intel TBB build fails/hangs (musl / new g++)",
|
|
pattern = "USE_TBB[^\\n]*(not supported|unsupported)|RcppParallel[^\\n]*TBB|tbb[^\\n]*(Alpine|musl)",
|
|
tier = "patch",
|
|
confidence = "high",
|
|
auto = TRUE,
|
|
fix = "apply the curated RcppParallel/disable-tbb.patch so the bundled TBB build is skipped and the TinyThread backend is used",
|
|
example = "RcppParallel",
|
|
registry = list(
|
|
env = NULL,
|
|
configure_args = NULL,
|
|
makevars = NULL,
|
|
patch = "RcppParallel/disable-tbb.patch",
|
|
reason = "bundled Intel TBB build hangs/fails on musl (Alpine) and newer toolchains; patch forces the TinyThread backend"
|
|
)
|
|
),
|
|
list(
|
|
id = "system-libuv-link-leak",
|
|
label = "binary links system libuv (NEEDED libuv.so.1)",
|
|
pattern = "libuv\\.so",
|
|
tier = "patch",
|
|
confidence = "medium",
|
|
# A novel per-package source diff is required to force the vendored lib;
|
|
# never auto-propose, only surface for a human (guardrail).
|
|
auto = FALSE,
|
|
fix = "force the vendored/static library instead of the system one via a human-authored source patch (see fs/force-vendored-libuv.patch as precedent)",
|
|
example = "fs",
|
|
registry = list(
|
|
env = NULL,
|
|
configure_args = NULL,
|
|
makevars = NULL,
|
|
patch = "<package>/force-vendored-<lib>.patch",
|
|
reason = "binary links the system library and fails to dyn.load on consumer machines; force the vendored/static build"
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Normalisation
|
|
# ---------------------------------------------------------------------------
|
|
# Collapse build-specific noise (temp paths, version numbers, hex addresses,
|
|
# the package name) so the same root cause across packages/platforms/versions
|
|
# maps to one fingerprint bucket.
|
|
normalise_error <- function(error_text, package = NULL) {
|
|
if (length(error_text) == 0L || is.na(error_text) || !nzchar(error_text)) {
|
|
return("")
|
|
}
|
|
x <- as.character(error_text)
|
|
# Package-specific token first (before version/number stripping mangles it).
|
|
if (!is.null(package) && length(package) == 1L && nzchar(package)) {
|
|
# \Q..\E quotes the name literally so metachars (e.g. data.table's dot)
|
|
# are matched verbatim rather than as regex.
|
|
x <- gsub(paste0("\\b\\Q", package, "\\E\\b"), "<pkg>", x, perl = TRUE)
|
|
}
|
|
# R temp dirs/files: /tmp/RtmpAbC123, RtmpXXXX, /tmp/Rtmp.../file123.
|
|
x <- gsub("/tmp/[^ \t\n]*", "<tmp>", x, perl = TRUE)
|
|
x <- gsub("\\bRtmp[A-Za-z0-9]+", "Rtmp<x>", x, perl = TRUE)
|
|
# Hex addresses and version-like number runs.
|
|
x <- gsub("0x[0-9a-fA-F]+", "0x<addr>", x, perl = TRUE)
|
|
x <- gsub("[0-9]+(\\.[0-9]+)+", "<v>", x, perl = TRUE)
|
|
x <- gsub("\\b[0-9]{2,}\\b", "<n>", x, perl = TRUE)
|
|
# Whitespace and case.
|
|
x <- tolower(x)
|
|
x <- gsub("[ \t\r\n]+", " ", x, perl = TRUE)
|
|
trimws(x)
|
|
}
|
|
|
|
# Extract the single most informative line from a multi-line error, then
|
|
# normalise it. This is the grouping key; a short salient line groups far
|
|
# better than the whole (often huge) transcript.
|
|
fingerprint_error <- function(error_text, package = NULL, max_chars = 200L) {
|
|
if (length(error_text) == 0L || is.na(error_text) || !nzchar(error_text)) {
|
|
return("<empty>")
|
|
}
|
|
lines <- strsplit(as.character(error_text), "\n", fixed = TRUE)[[1L]]
|
|
lines <- trimws(lines)
|
|
lines <- lines[nzchar(lines)]
|
|
if (length(lines) == 0L) {
|
|
return("<empty>")
|
|
}
|
|
salient_re <- paste(
|
|
"error:",
|
|
"fatal error:",
|
|
"no such file",
|
|
"undefined reference",
|
|
"cannot find -l",
|
|
"cannot open shared object",
|
|
"configuration failed",
|
|
"non-zero exit",
|
|
"installation of package",
|
|
"compilation failed",
|
|
sep = "|"
|
|
)
|
|
hit <- lines[grepl(salient_re, lines, ignore.case = TRUE)]
|
|
chosen <- if (length(hit) > 0L) hit[[1L]] else lines[[length(lines)]]
|
|
fp <- normalise_error(chosen, package)
|
|
if (nchar(fp) > max_chars) {
|
|
fp <- paste0(substr(fp, 1L, max_chars), "...")
|
|
}
|
|
fp
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Classification
|
|
# ---------------------------------------------------------------------------
|
|
# Return the first matching signature (as a list) enriched with `matched`, or
|
|
# the unclassified fallback. Never guesses: an unmatched error is routed to a
|
|
# human, not assigned a fix.
|
|
classify_error <- function(error_text, signatures = build_signatures()) {
|
|
txt <- if (length(error_text) == 0L || is.na(error_text)) {
|
|
""
|
|
} else {
|
|
as.character(error_text)
|
|
}
|
|
for (sig in signatures) {
|
|
if (
|
|
nzchar(txt) && grepl(sig$pattern, txt, ignore.case = TRUE, perl = TRUE)
|
|
) {
|
|
sig$matched <- TRUE
|
|
return(sig)
|
|
}
|
|
}
|
|
list(
|
|
id = "unclassified",
|
|
label = "unknown signature",
|
|
tier = NA_character_,
|
|
confidence = NA_character_,
|
|
auto = FALSE,
|
|
fix = "no known signature; flag for human triage",
|
|
example = NA_character_,
|
|
registry = NULL,
|
|
matched = FALSE
|
|
)
|
|
}
|
|
|
|
# Render a suggested registry.json entry (as a pretty JSON string) for a
|
|
# classified group, filling package/versions/platforms from the observed
|
|
# failures. Only meaningful when the signature carries a `registry` template.
|
|
propose_registry_entry <- function(
|
|
signature,
|
|
package,
|
|
platforms,
|
|
versions = "*"
|
|
) {
|
|
if (is.null(signature$registry)) {
|
|
return(NULL)
|
|
}
|
|
tmpl <- signature$registry
|
|
entry <- list(
|
|
package = package,
|
|
versions = versions,
|
|
# I() keeps this a JSON array even when a single platform is affected.
|
|
platforms = I(as.character(platforms))
|
|
)
|
|
for (k in c("env", "configure_args", "makevars", "patch")) {
|
|
if (!is.null(tmpl[[k]])) {
|
|
entry[[k]] <- tmpl[[k]]
|
|
}
|
|
}
|
|
entry$reason <- tmpl$reason
|
|
jsonlite::toJSON(entry, auto_unbox = TRUE, pretty = TRUE, null = "null")
|
|
}
|