From 0c8b5be908af788a2555764f417ba0e2ecd69183 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sat, 16 May 2026 18:09:52 -0700 Subject: [PATCH 1/2] fix(pak): honor type="source" in the pak path (binary-lag downgrade) type was collected by Install()/Require() but never threaded into the pak backbone, so pak did its default binary-preferring resolution. On Windows/Mac, when CRAN has the new source (e.g. reproducible 3.1.0) but only the old binary (3.0.0), pak would keep/install the stale binary -- either a no-op or a silent downgrade -- ignoring an explicit type="source". Thread type into pakDepsToPkgDT + pakInstallFiltered and pin pkg.platforms="source" for the resolve+install when type=="source" (pak ignores base R's pkgType; pkgdepends' platforms config is the real knob). on.exit restores prior options. No behavior change unless type is explicitly "source". PR #151's detection/warning unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- R/Require2.R | 7 ++++--- R/pak.R | 31 +++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/R/Require2.R b/R/Require2.R index 6153ce2b..dbecc30d 100644 --- a/R/Require2.R +++ b/R/Require2.R @@ -362,13 +362,13 @@ Require <- function(packages, withCallingHandlers( pkgDT <- pakDepsToPkgDT(packages, which = which, libPaths = libPaths, standAlone = standAlone, verbose = verbose, - purge = purge, install = install), + purge = purge, install = install, type = type), message = function(m) invokeRestart("muffleMessage") ) } else { pkgDT <- pakDepsToPkgDT(packages, which = which, libPaths = libPaths, standAlone = standAlone, verbose = verbose, - purge = purge, install = install) + purge = purge, install = install, type = type) } } else if (!skipDepResolution) { if (length(which)) { @@ -480,7 +480,8 @@ Require <- function(packages, } else { pkgDT <- pakInstallFiltered(pkgDT, libPaths = libPaths, repos = repos, standAlone = standAlone, verbose = verbose, - forceUpgrade = identical(install, "force")) + forceUpgrade = identical(install, "force"), + type = type) # Invalidate the dep-tree cache: installed state changed, so the next # call should re-resolve rather than use a stale cached result. pakDepsCacheInvalidate(pkgsForPak = trimVersionNumber(HEADtoNone(pkgDT$packageFullName)), diff --git a/R/pak.R b/R/pak.R index d7555fb9..cfc77dc5 100644 --- a/R/pak.R +++ b/R/pak.R @@ -95,6 +95,22 @@ pakCall <- function(expr, verbose = getOption("Require.verbose")) { } } +# When the caller explicitly requested type = "source", force pak to resolve +# AND install from source only. pak ignores base R's getOption("pkgType"); its +# source-vs-binary selection is driven by pkgdepends' `platforms` config, which +# is settable from the main process via options(pkg.platforms=) (read when pak +# builds the proposal, before the subprocess is spawned). The default is +# c(, "source"); pinning it to "source" stops pak from "keeping" +# or installing a stale CRAN binary when the source tree is newer -- the +# binary-lag downgrade where pak resolves reproducible 3.1.0 (source) but only +# the 3.0.0 binary exists on Windows/Mac, leaving the user silently downgraded. +# Returns the previous options() list for on.exit(options(old)) restoration, or +# NULL when no override was applied (caller skips the on.exit in that case). +forcePakSourceIfRequested <- function(type) { + if (!identical(type, "source")) return(NULL) + options(pkg.platforms = "source") +} + pakErrorHandling <- function(err, pkg, packages, verbose = getOption("Require.verbose")) { grp <- c( .txtCntInstllDep, .txtFailedToBuildSrcPkg, .txtConflictsWith, @@ -1901,7 +1917,7 @@ pakDepsCacheInvalidate <- function(pkgsForPak, wh, repos, userPkgs = NULL) { # This replaces the pkgDep() + parsePackageFullname() + ... pipeline when usePak = TRUE. pakDepsToPkgDT <- function(packages, which, libPaths, standAlone, verbose, purge = getOption("Require.purge", FALSE), - install = TRUE) { + install = TRUE, type = getOption("pkgType")) { pakLoad <- tryCatch(loadNamespace("pak"), error = function(e) e) if (inherits(pakLoad, "error")) { @@ -1909,6 +1925,11 @@ pakDepsToPkgDT <- function(packages, which, libPaths, standAlone, verbose, conditionMessage(pakLoad), ")", call. = FALSE) } + # Honour an explicit type = "source": pin pak to source-only resolution for + # the duration of this call (covers the nested pakDepsResolve/pak::pkg_deps). + oldPlatforms <- forcePakSourceIfRequested(type) + if (!is.null(oldPlatforms)) on.exit(options(oldPlatforms), add = TRUE) + # pak spawns a subprocess that inherits .libPaths(). Set .libPaths() to match # Require's standAlone semantics before calling pak, then restore on exit. # @@ -2609,9 +2630,15 @@ pakSerialInstall <- function(pkgs, lib, repos, verbose) { # Install only the packages Require has determined need installing (needInstall == .txtInstall). # pak is called with exact version pins or any:: to avoid re-resolving deps. pakInstallFiltered <- function(pkgDT, libPaths, repos, standAlone, verbose, - forceUpgrade = FALSE) { + forceUpgrade = FALSE, type = getOption("pkgType")) { if (!requireNamespace("pak", quietly = TRUE)) stop("Please install pak") + # Honour an explicit type = "source": without this, pak's install step can + # "keep" or fetch the older platform binary even after the resolve picked the + # newer source version, producing the silent binary-lag downgrade. + oldPlatforms <- forcePakSourceIfRequested(type) + if (!is.null(oldPlatforms)) on.exit(options(oldPlatforms), add = TRUE) + # Mirror the same .libPaths() logic as pakDepsToPkgDT so the install subprocess # sees the same library set that was used for dependency resolution. pakLib <- tryCatch(dirname(find.package("pak")), error = function(e) NULL) From 8aeec57a1f3bbae8d94f76f92eb5dfcdb76b085a Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Sat, 16 May 2026 18:18:33 -0700 Subject: [PATCH 2/2] fix(pak): make dep-resolution cache key type-aware (source vs binary) The first fix threaded type into the pak path, but pakDepsCacheKey() keyed only on {pkgs, wh, repos, userPkgs}. A type="source" call landed on a stale binary-era cached pak_result (observed: "using cache (11 packages, 3.7h old)"), so pak::pkg_deps never re-ran under pkg.platforms="source": deps got rebuilt from source but reproducible itself was "kept" at the old 3.0.0 plan -> still the binary-lag downgrade, with the same misleading "could not be installed" warning. Fold a source-only discriminator into the cache key and thread type through pakDepsResolve + pakDepsCacheInvalidate so read and invalidate use the same key. Binary/both callers keep one shared key (one-time miss on upgrade); source calls now miss the binary entry and re-resolve. Co-Authored-By: Claude Opus 4.7 (1M context) --- R/Require2.R | 3 ++- R/pak.R | 27 ++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/R/Require2.R b/R/Require2.R index dbecc30d..1801ada9 100644 --- a/R/Require2.R +++ b/R/Require2.R @@ -486,7 +486,8 @@ Require <- function(packages, # call should re-resolve rather than use a stale cached result. pakDepsCacheInvalidate(pkgsForPak = trimVersionNumber(HEADtoNone(pkgDT$packageFullName)), wh = whichToDILES(doDeps), - repos = repos) + repos = repos, + type = type) ## Recovery: if pakInstallFiltered left any rows flagged ## .txtCouldNotBeInstalled, probe internet once. If missing, ## switch to offlineMode and retry those rows via the pak diff --git a/R/pak.R b/R/pak.R index cfc77dc5..3e7b995b 100644 --- a/R/pak.R +++ b/R/pak.R @@ -1629,14 +1629,22 @@ pakWhoNeeds <- function(pkg, pak_result = NULL) { # --------------------------------------------------------------------------- .pakDepsCacheTTL <- 24 * 3600 # 24 hours default -pakDepsCacheKey <- function(pkgsForPak, wh, repos, userPkgs = NULL) { +pakDepsCacheKey <- function(pkgsForPak, wh, repos, userPkgs = NULL, + type = getOption("pkgType")) { tmp <- tempfile() on.exit(unlink(tmp), add = TRUE) # coerce to character vectors: options(repos = list(...)) is a supported # pattern, and sort() errors on list input with 'x must be atomic' payload <- list(pkgs = sort(as.character(unlist(pkgsForPak, use.names = FALSE))), wh = sort(as.character(unlist(wh))), - repos = sort(as.character(unlist(repos, use.names = FALSE)))) + repos = sort(as.character(unlist(repos, use.names = FALSE))), + # Source-only resolution is a different dep-tree problem than + # binary/both: pak picks the source version (e.g. reproducible + # 3.1.0) where the binary path would "keep" the older binary + # (3.0.0). Without this, a type="source" call reuses a + # binary-era cached pak_result and the source intent is + # silently lost -- the exact binary-lag downgrade symptom. + srcOnly = isTRUE(identical(type, "source"))) # `userPkgs` (when supplied) carries the user's original version-bearing # refs, e.g. c("stringfish (<= 0.15.8)", "qs (== 0.27.3)"). pak::pkg_deps() # only sees `pkgsForPak` -- the version-stripped form -- so without folding @@ -1660,10 +1668,12 @@ pakDepsCacheDir <- function() { file.path(cacheDir(), "pak", "pkg_deps") } -pakDepsResolve <- function(pkgsForPak, wh, repos, verbose, purge, userPkgs = NULL) { +pakDepsResolve <- function(pkgsForPak, wh, repos, verbose, purge, userPkgs = NULL, + type = getOption("pkgType")) { # --- 1. Compute cache key --- - key <- pakDepsCacheKey(pkgsForPak, wh, repos, userPkgs = userPkgs) + key <- pakDepsCacheKey(pkgsForPak, wh, repos, userPkgs = userPkgs, + type = type) envKey <- paste0("pakDeps_", key) cacheDir <- pakDepsCacheDir() cacheFile <- file.path(cacheDir, paste0(key, ".rds")) @@ -1902,8 +1912,10 @@ pakDepsResolve <- function(pkgsForPak, wh, repos, verbose, purge, userPkgs = NUL # (installed state changed; cache key stays the same but should be revalidated # sooner than the normal TTL would allow). # --------------------------------------------------------------------------- -pakDepsCacheInvalidate <- function(pkgsForPak, wh, repos, userPkgs = NULL) { - key <- tryCatch(pakDepsCacheKey(pkgsForPak, wh, repos, userPkgs = userPkgs), +pakDepsCacheInvalidate <- function(pkgsForPak, wh, repos, userPkgs = NULL, + type = getOption("pkgType")) { + key <- tryCatch(pakDepsCacheKey(pkgsForPak, wh, repos, userPkgs = userPkgs, + type = type), error = function(e) NULL) if (is.null(key)) return(invisible(NULL)) envKey <- paste0("pakDeps_", key) @@ -2029,7 +2041,8 @@ pakDepsToPkgDT <- function(packages, which, libPaths, standAlone, verbose, repos = getOption("repos"), verbose = verbose, purge = purge, - userPkgs = resolvedPkgs) + userPkgs = resolvedPkgs, + type = type) if (is.null(pak_result)) { messageVerbose("pak::pkg_deps: all strategies failed; using direct package list only.",