Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ URL:
https://Require.predictiveecology.org,
https://github.com/PredictiveEcology/Require
Date: 2026-06-06
Version: 2.0.0.9034
Version: 2.0.0.9035
Authors@R: c(
person(given = "Eliot J B",
family = "McIntire",
Expand Down
13 changes: 13 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# Require 2.0.0.9035 (development version)

* The `identify-and-defer` install strategy now installs deferred build-failure
culprits in **dependency order** and **retries** them. Previously, when both a
dependency and its dependent were deferred (e.g. `LandR` and `LandR.CS`, where
`LandR.CS` Imports `LandR`), the final serial pass installed them in deferral
order, so the dependent could be built before its dependency and fail with
`dependency 'LandR' is not available for package 'LandR.CS'` — and was never
re-attempted once the dependency landed. Now: (1) deferred culprits are
topologically ordered using pak's own "dependency 'X' is not available for
package 'Y'" lines so X installs before Y, and (2) a retry pass re-attempts any
still-missing culprit while progress is being made (a newly-installed
dependency can unblock another dependent). (`orderRefsByMissingDepEdges()`.)

# Require 2.0.0.9034 (development version)

Expand Down
68 changes: 68 additions & 0 deletions R/pak.R
Original file line number Diff line number Diff line change
Expand Up @@ -2678,6 +2678,43 @@ extractBuildFailures <- function(output) {
unique(sub("Failed to build\\s+", "", m, perl = TRUE))
}

# Reorder `refs` so that, for every "dependency 'X' is not available for package
# 'Y'" build-error parseable from `msgs` whose BOTH ends are in `refs`, the
# dependency X installs before the dependent Y. Used to order the deferred-culprit
# serial install: when both a dependency (e.g. LandR) and its dependent (LandR.CS)
# get deferred, the serial pass must not build LandR.CS first. Best-effort and
# stable -- refs with no parseable edge keep their position; an unresolvable
# cycle bails out keeping the remaining order (never drops a ref). Index-based so
# duplicate bare names can't drop/duplicate a ref.
orderRefsByMissingDepEdges <- function(refs, msgs) {
if (length(refs) < 2L || !length(msgs) || !any(nzchar(msgs))) return(refs)
clean <- gsub("\033\\[[0-9;]*m", "", paste(msgs, collapse = "\n"))
pat <- paste0("dependency [\u2018'\"]?([A-Za-z0-9._]+)[\u2019'\"]? is not available ",
"for package [\u2018'\"]?([A-Za-z0-9._]+)")
m <- regmatches(clean, gregexpr(pat, clean, perl = TRUE))[[1]]
if (!length(m)) return(refs)
dep <- sub(paste0(".*", pat, ".*"), "\\1", m, perl = TRUE) # X (dependency)
pkg <- sub(paste0(".*", pat, ".*"), "\\2", m, perl = TRUE) # Y (dependent)
nms <- extractPkgName(refs)
edges <- unique(data.frame(dep = dep, pkg = pkg, stringsAsFactors = FALSE))
edges <- edges[edges$dep %in% nms & edges$pkg %in% nms & edges$dep != edges$pkg, ,
drop = FALSE]
if (!nrow(edges)) return(refs)
idx <- seq_along(refs)
ordered <- integer(0)
guard <- 0L
while (length(idx) && guard <= length(refs)) {
guard <- guard + 1L
remNms <- nms[idx]
ready <- !vapply(remNms, function(n) any(edges$dep[edges$pkg == n] %in% remNms),
logical(1))
if (!any(ready)) { ordered <- c(ordered, idx); break } # cycle -> keep rest as-is
ordered <- c(ordered, idx[ready])
idx <- idx[!ready]
}
refs[ordered]
}

# ---------------------------------------------------------------------------
# Parse pak's "Missing N system packages" block. Returns a named character
# vector: names = pkg the system dep is needed BY (e.g. "fs"), values = the
Expand Down Expand Up @@ -3803,12 +3840,43 @@ pakInstallFiltered <- function(pkgDT, libPaths, repos, standAlone, verbose,
# state from the failed plan(s), and each serial install benefits from
# a clean subprocess (see pakResetSubprocess() comment).
if (length(deferred)) {
# Order so a deferred dependency installs before a deferred dependent
# (e.g. LandR before LandR.CS). Without this, the serial pass could build a
# dependent before its dependency and hit a spurious
# "dependency 'X' is not available for package 'Y'" build-error.
deferred <- orderRefsByMissingDepEdges(deferred, allCapturedMsgs)
messageVerbose(
"identify-and-defer: installing ", length(deferred),
" deferred culprit(s) one at a time",
verbose = verbose, verboseLevel = 1)
pakResetSubprocess()
capturePak(pakSerialInstall(deferred, libPaths[1], repos, verbose))

# Retry pass: a culprit can still fail only because a dependency that is
# ALSO a deferred culprit had not yet been installed when it was attempted
# (an ordering we couldn't infer, or a dependency installed later in the
# same pass). Re-attempt the ones still missing while progress is being
# made -- each newly-installed dependency can unblock another dependent.
for (retry in seq_len(3L)) {
instNow <- tryCatch(
rownames(installed.packages(lib.loc = libPaths[1], noCache = TRUE)),
error = function(e) character(0))
stillMissing <- deferred[!extractPkgName(deferred) %in% instNow]
if (!length(stillMissing)) break
messageVerbose(
"identify-and-defer: retry ", retry, " -- re-attempting ",
length(stillMissing), " still-missing culprit(s) now that their ",
"dependencies may be installed",
verbose = verbose, verboseLevel = 1)
pakResetSubprocess()
capturePak(pakSerialInstall(stillMissing, libPaths[1], repos, verbose))
instAfter <- tryCatch(
rownames(installed.packages(lib.loc = libPaths[1], noCache = TRUE)),
error = function(e) character(0))
# No progress this pass (none of the still-missing got installed) -> stop.
if (sum(!extractPkgName(stillMissing) %in% instAfter) >= length(stillMissing))
break
}
}
}

Expand Down
28 changes: 28 additions & 0 deletions tests/testthat/test-17usePak.R
Original file line number Diff line number Diff line change
Expand Up @@ -1994,3 +1994,31 @@ test_that("getCRANrepos preserves non-CRAN repos when resolving @CRAN@", {
invisible(getCRANrepos(ind = 1))
testthat::expect_true(any(peUniv == unname(getOption("repos"))))
})

# ---------------------------------------------------------------------------
# orderRefsByMissingDepEdges: deferred build-failure culprits must install in
# dependency order. When both a dependency and its dependent are deferred (e.g.
# LandR + LandR.CS), the serial pass must build the dependency first, inferred
# from pak's "dependency 'X' is not available for package 'Y'" lines.
# ---------------------------------------------------------------------------
test_that("orderRefsByMissingDepEdges puts a deferred dependency before its dependent", {
ord <- Require:::orderRefsByMissingDepEdges
pn <- Require:::extractPkgName
refs <- c("ianmseddy/LandR.CS@development", "PredictiveEcology/LandR@development", "data.table")

# dependent listed before its dependency; edge from pak's (curly-quote) message
out <- ord(refs, "ERROR: dependency ‘LandR’ is not available for package ‘LandR.CS’")
testthat::expect_lt(which(pn(out) == "LandR"), which(pn(out) == "LandR.CS"))
testthat::expect_setequal(pn(out), pn(refs)) # no ref dropped/duplicated

# straight-quote variant parses the same
out2 <- ord(refs, "dependency 'LandR' is not available for package 'LandR.CS'")
testthat::expect_identical(pn(out2), pn(out))

# no parseable edge -> order unchanged
testthat::expect_identical(ord(refs, "an unrelated message"), refs)
# edge whose dependency is not in the ref set -> ignored, order unchanged
testthat::expect_identical(
ord(c("a", "b"), "dependency ‘zzz’ is not available for package ‘a’"),
c("a", "b"))
})
Loading