fix(build): build packages requiring newer R on compatible minors

This commit is contained in:
Patrick Schratz 2026-09-13 09:34:09 +00:00
commit 2861d50446
No known key found for this signature in database
GPG key ID: 62050D5BC68AB6DC
3 changed files with 256 additions and 0 deletions

72
local/build-newer-r.R Normal file
View file

@ -0,0 +1,72 @@
source(file.path("local", "newer-r-helpers.R"))
source(file.path("local", "rebuild-missing-helpers.R"))
args <- commandArgs(trailingOnly = TRUE)
pos <- args[!startsWith(args, "--")]
if (!length(pos) %in% c(1L, 3L)) {
stop("Usage: build-newer-r.R PRIMARY_R [SPLIT_INTO SPLIT_INDEX] [--dry-run]")
}
primary_r <- pos[1L]
target_r <- as.character(getRversion())
pkgs <- newer_r_candidates(tools::CRAN_package_db(), primary_r, target_r)
exclude <- jsonlite::fromJSON("local/excluded-packages.json")[["package"]]
pkgs <- pkgs[!pkgs$Package %in% exclude, , drop = FALSE]
if (length(pos) == 3L) {
pkgs <- pkgs[
shard_slice(seq_len(nrow(pkgs)), pos[2L], pos[3L]),
,
drop = FALSE
]
}
message(sprintf(
"R %s: %d package(s) unsupported by primary R %s",
target_r,
nrow(pkgs),
primary_r
))
if ("--dry-run" %in% args) {
print(pkgs, row.names = FALSE)
} else {
options(crayon.enabled = TRUE, Ncpus = 4, future.globals.onReference = NULL)
for (i in seq_len(nrow(pkgs))) {
message(sprintf(
"[%d/%d] %s %s",
i,
nrow(pkgs),
pkgs$Package[i],
pkgs$Version[i]
))
bincraft::build_binary_package(
pkgs$Package[i],
tag = pkgs$Version[i],
# Keep bincraft's normal ABI routing: safe binaries can be shared;
# sensitive ones use this interpreter's minor for presence and output.
is_r_minor_sensitive = bincraft::classify_r_minor_sensitive(
pkgs$Package[i],
pkgs$Version[i],
metadata_db_host = "r-binaries.devxy.io",
metadata_db_name = "build_metadata",
metadata_db_user = "rpkgs",
metadata_db_password = Sys.getenv("PGPASS"),
metadata_db_sslmode = "require",
metadata_db_port = 15432
),
s3_endpoint = "https://s3.eu-central-003.backblazeb2.com",
s3_region = "eu-central-003",
s3_bucket = "devxy-rpkgs-binaries",
s3_access_key_id = Sys.getenv("B2_S3_ACCESS_KEY"),
s3_secret_access_key = Sys.getenv("B2_S3_SECRET_KEY"),
metadata_db_host = "r-binaries.devxy.io",
metadata_db_name = "build_metadata",
metadata_db_table = "single_builds",
metadata_db_user = "rpkgs",
metadata_db_password = Sys.getenv("PGPASS"),
metadata_db_sslmode = "require",
metadata_db_port = 15432,
archive = TRUE,
patches = "local/patches",
upload = TRUE,
store_build_metadata = TRUE
)
}
}

75
local/newer-r-helpers.R Normal file
View file

@ -0,0 +1,75 @@
# Evaluate every R constraint, including patch-level and upper bounds.
r_requirement_satisfied <- function(depends, r_version) {
if (is.na(depends) || !nzchar(trimws(depends))) {
return(TRUE)
}
entries <- trimws(strsplit(depends, ",", fixed = TRUE)[[1L]])
entries <- entries[grepl("^R(\\s|\\(|$)", entries)]
all(vapply(
entries,
function(entry) {
if (entry == "R") {
return(TRUE)
}
match <- regmatches(
entry,
regexec(
"^R\\s*\\(\\s*(>=|<=|==|>|<)\\s*([0-9]+(?:[.-][0-9]+)*)\\s*\\)$",
entry,
perl = TRUE
)
)[[1L]]
if (length(match) != 3L) {
stop("Cannot parse R dependency: ", entry)
}
comparison <- utils::compareVersion(r_version, match[3L])
switch(
match[2L],
">=" = comparison >= 0L,
"<=" = comparison <= 0L,
"==" = comparison == 0L,
">" = comparison > 0L,
"<" = comparison < 0L
)
},
logical(1L)
))
}
# Use unfiltered current CRAN metadata: available.packages() would hide exactly
# the packages that cannot run under the primary interpreter.
# Propagate R incompatibility through required dependencies, not Suggests.
newer_r_candidates <- function(db, primary_r, target_r) {
db <- as.data.frame(db, stringsAsFactors = FALSE)
# CRAN also lists R-specific Recommended copies (e.g. Matrix for R-devel).
# Prefer the ordinary contrib record, or a future-R copy can incorrectly
# block every package depending on Matrix on all released interpreters.
if ("Path" %in% names(db)) {
db <- db[order(!is.na(db$Path) & nzchar(db$Path)), , drop = FALSE]
}
db <- db[!duplicated(db$Package), , drop = FALSE]
deps <- tools::package_dependencies(
packages = db$Package,
db = as.matrix(db),
which = c("Depends", "Imports", "LinkingTo"),
recursive = FALSE
)
supported <- function(version) {
ok <- vapply(db$Depends, r_requirement_satisfied, logical(1L), version)
repeat {
blocked <- db$Package[!ok]
next_ok <- ok &
!vapply(deps, function(x) any(x %in% blocked), logical(1L))
if (identical(ok, next_ok)) {
return(ok)
}
ok <- next_ok
}
}
selected <- !supported(primary_r) & supported(target_r)
if ("OS_type" %in% names(db)) {
selected <- selected & (is.na(db$OS_type) | db$OS_type != "windows")
}
result <- db[selected, c("Package", "Version"), drop = FALSE]
result[order(result$Package), , drop = FALSE]
}

View file

@ -0,0 +1,81 @@
source(file.path("..", "newer-r-helpers.R"))
test_that("R constraints respect patch versions, bounds, and missing fields", {
expect_true(r_requirement_satisfied(NA_character_, "4.4.3"))
expect_true(r_requirement_satisfied(
"Matrix (>= 1.4-0), R (>= 4.5.0)",
"4.5.3"
))
expect_false(r_requirement_satisfied("R (>= 4.5.0)", "4.4.3"))
expect_false(r_requirement_satisfied("R (>= 4.5.4)", "4.5.3"))
expect_true(r_requirement_satisfied("R (>= 4.5), R (< 4.6)", "4.5.3"))
expect_false(r_requirement_satisfied("R (>= 4.5), R (< 4.6)", "4.6.0"))
expect_true(r_requirement_satisfied("R (== 4.5.3)", "4.5.3"))
expect_true(r_requirement_satisfied("R (> 4.5.2), R (<= 4.5.3)", "4.5.3"))
expect_true(r_requirement_satisfied("R, Rcpp (>= 1.0)", "4.4.3"))
expect_error(r_requirement_satisfied("R (~ 4.5)", "4.5.3"), "Cannot parse")
})
fixture <- function() {
data.frame(
Package = c(
"arules",
"arulesSequences",
"indirect",
"optional",
"ordinary",
"future",
"blocked"
),
Version = rep("1.0", 7L),
Depends = c(
"R (>= 4.5.0)",
"arules",
NA,
NA,
"R (>= 4.0)",
"R (>= 4.7)",
NA
),
Imports = c(NA, NA, "arulesSequences", NA, NA, NA, "arules, future"),
LinkingTo = NA_character_,
Suggests = c(NA, NA, NA, "arules", NA, NA, NA)
)
}
test_that("every compatible newer minor includes required dependents only", {
db <- fixture()
for (target in c("4.5.3", "4.6.0")) {
expect_identical(
newer_r_candidates(db, "4.4.3", target)$Package,
c("arules", "arulesSequences", "indirect")
)
}
expect_equal(nrow(newer_r_candidates(db, "4.4.3", "4.4.3")), 0L)
expect_equal(nrow(newer_r_candidates(db, "4.5.3", "4.4.3")), 0L)
expect_equal(nrow(newer_r_candidates(db, "4.5.3", "4.6.0")), 0L)
})
test_that("LinkingTo, dependency cycles, Windows and empty metadata are handled", {
db <- fixture()
db$LinkingTo[5L] <- "arules"
db$Imports[1L] <- "ordinary"
db$OS_type <- c(NA, "windows", rep(NA_character_, 5L))
expect_identical(
newer_r_candidates(db, "4.4.3", "4.6.0")$Package,
c("arules", "indirect", "ordinary")
)
expect_equal(nrow(newer_r_candidates(db[FALSE, ], "4.4.3", "4.6.0")), 0L)
})
test_that("R-devel Recommended copies cannot block ordinary CRAN dependencies", {
db <- fixture()
db$Path <- NA_character_
future_copy <- db[1L, ]
future_copy$Depends <- "R (>= 4.7)"
future_copy$Path <- "4.7.0/Recommended"
expect_identical(
newer_r_candidates(rbind(future_copy, db), "4.4.3", "4.6.1")$Package,
c("arules", "arulesSequences", "indirect")
)
})