feat(local): detect dependency-cascade failures generally, not just RcppParallel (#128)
## Why (from the #127 trial-build gate) The gate did its job: 0/3 passed, merge blocked. The log showed *why* -- BFpack, BayesERtools, GMLTM all fail while building their shared dependency **`rstan`**, not in their own code: ``` Failed to build source package rstan. .../StanHeaders/include/stan/math/prim/core/init_threadpool_tbb.hpp:9:10: fatal error: tbb/tbb_stddef.h: No such file or directory ``` So the per-package `-DTBB_INTERFACE_NEW` makevars entries the classifier proposed are useless for these packages -- they're blocked on `rstan` (which already has a registry entry). This is the **same dependency cascade** the RcppParallel `applies_to` guard catches, but `tbb-stddef-removed` is a generic signature with no such pin, so ~73 Stan packages kept getting proposed. ## What Generalise cascade detection beyond the RcppParallel special case: - `failing_dependency(error_text, package)` -- when the log names a **different** package as the one that failed to compile (`Failed to build source package X`, `compilation failed for package 'X'`, `dependency 'X' ... not available`), that package is the real cause. - `build_triage_report()` now blocks any package whose **every** failing build is such a cascade: reported as `blocked_on` that dependency, never proposed a bogus per-package entry. A package that fails in its **own** compilation is still proposed. - The `applies_to` (RcppParallel) and data-driven (rstan) cases are unified into one `blocked_packages` / `blocked_on` model; the report, proposer, and `blocked_summary` count the actually-blocked packages, and the blocked note shows even when a group also has genuine proposals. ## Effect Next auto-apply run will stop proposing the rstan-blocked Stan packages (and any future dependency cascade) and surface them as "blocked on rstan" instead. Fixing `rstan` once clears the whole cluster. ## Verification - New tests: `failing_dependency` (cascade vs own-compile vs none), and an end-to-end split where BFpack/GMLTM (blocked on rstan) are not proposed while an own-compile package still is. - Full suite: 112 tests pass; all pre-commit hooks pass. Refs #120, #127. (Separate follow-ups: fixing rstan's build itself, and quieting the gate's metadata-DB retry storm -- both root-caused to bincraft.) Reviewed-on: #128
This commit is contained in:
parent
875b086122
commit
f9d399fac0
1 changed files with 135 additions and 23 deletions
|
|
@ -155,6 +155,44 @@ fingerprint_error <- function(error_text, package = NULL, max_chars = 200L) {
|
|||
fp
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency-cascade detection
|
||||
# ---------------------------------------------------------------------------
|
||||
# If a build's error_text shows the failure was actually in a DIFFERENT package
|
||||
# (a dependency that would not compile), return that dependency's name;
|
||||
# otherwise NA. Used to avoid proposing a per-package fix for a package that
|
||||
# only fails because a shared dependency does not build (e.g. the ~73 Stan
|
||||
# packages that fail while building `rstan`). Generalises the RcppParallel
|
||||
# `applies_to` guard to any dependency named in the log.
|
||||
failing_dependency <- function(error_text, package) {
|
||||
if (length(error_text) == 0L || is.na(error_text) || !nzchar(error_text)) {
|
||||
return(NA_character_)
|
||||
}
|
||||
x <- as.character(error_text)
|
||||
# optional opening quote before the package name: apostrophe, double-quote,
|
||||
# backtick, or curly quotes -- written as \u escapes so the pattern stays
|
||||
# valid UTF-8 regardless of source encoding.
|
||||
q <- "[\u0027\u0022\u0060\u2018\u2019]?"
|
||||
name <- "([A-Za-z][A-Za-z0-9._]+)"
|
||||
# Markers R/pak emit naming the package that actually failed to compile.
|
||||
pats <- c(
|
||||
paste0("compilation failed for package ", q, name),
|
||||
paste0("Failed to build source package ", q, name),
|
||||
paste0("Error in building package ", q, name),
|
||||
paste0("dependenc(?:y|ies) ", q, name, q, "?[^\\n]*not available")
|
||||
)
|
||||
deps <- character(0L)
|
||||
for (p in pats) {
|
||||
hits <- regmatches(x, gregexpr(p, x, perl = TRUE))[[1L]]
|
||||
if (length(hits) > 0L) {
|
||||
deps <- c(deps, sub(p, "\\1", hits, perl = TRUE))
|
||||
}
|
||||
}
|
||||
deps <- sub("[._]+$", "", deps) # drop a trailing sentence period (e.g. "rstan.")
|
||||
deps <- setdiff(unique(deps), package) # a package failing on its OWN code is not a cascade
|
||||
if (length(deps) == 0L) NA_character_ else deps[[1L]]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -291,18 +329,38 @@ build_triage_report <- function(
|
|||
unregistered <- setdiff(pkgs, registered_pkgs)
|
||||
auto_proposable <- isTRUE(sig$auto) && sig$matched
|
||||
|
||||
# A signature whose fix is package-specific (`applies_to`) may only be
|
||||
# proposed for that package. Everything else matching it is a downstream
|
||||
# failure blocked on that package (e.g. RcppParallel dependents carrying
|
||||
# RcppParallel's own error text) -- never propose those a bogus entry.
|
||||
# Decide, per package, whether it is genuinely fixable or merely blocked on
|
||||
# a dependency (so a per-package entry would be useless). Two blocking modes:
|
||||
# 1. `applies_to`: a package-specific patch (e.g. RcppParallel's) is only
|
||||
# valid for its own package; other matches are downstream of it.
|
||||
# 2. data-driven cascade: the package's error_text shows a *different*
|
||||
# package failed to compile (e.g. the ~73 Stan packages blocked on rstan).
|
||||
# `blocked_map` maps a blocked package -> the dependency it waits on.
|
||||
g$blocked_dep <- vapply(
|
||||
seq_len(nrow(g)),
|
||||
function(i) failing_dependency(g$error_text[[i]], g$name[[i]]),
|
||||
character(1L)
|
||||
)
|
||||
proposable <- unregistered
|
||||
blocked_on <- NULL
|
||||
blocked_map <- list()
|
||||
if (!is.null(sig$applies_to)) {
|
||||
proposable <- intersect(unregistered, sig$applies_to)
|
||||
downstream <- setdiff(pkgs, sig$applies_to)
|
||||
if (length(downstream) > 0L) {
|
||||
blocked_on <- sig$applies_to
|
||||
for (p in setdiff(pkgs, sig$applies_to)) {
|
||||
blocked_map[[p]] <- sig$applies_to
|
||||
}
|
||||
proposable <- intersect(proposable, sig$applies_to)
|
||||
}
|
||||
for (p in proposable) {
|
||||
deps <- g$blocked_dep[g$name == p]
|
||||
if (!any(is.na(deps))) {
|
||||
# every failing build of p is a cascade -> blocked, not fixable here
|
||||
blocked_map[[p]] <- unique(deps)
|
||||
}
|
||||
}
|
||||
proposable <- setdiff(proposable, names(blocked_map))
|
||||
blocked_packages <- names(blocked_map)
|
||||
blocked_on <- unique(unlist(blocked_map, use.names = FALSE))
|
||||
if (length(blocked_on) == 0L) {
|
||||
blocked_on <- NULL
|
||||
}
|
||||
|
||||
proposed <- list()
|
||||
|
|
@ -326,6 +384,7 @@ build_triage_report <- function(
|
|||
suggested_fix = sig$fix,
|
||||
applies_to = sig$applies_to,
|
||||
blocked_on = blocked_on,
|
||||
blocked_packages = blocked_packages,
|
||||
fingerprint = names(fp_tab)[[1L]],
|
||||
fingerprint_variants = length(fp_tab),
|
||||
build_count = nrow(g),
|
||||
|
|
|
|||
Loading…
Reference in a new issue