build-cran-binaries/.superpowers/handoff/task-A3-brief.md

4.6 KiB

Task A3: Cache key and version resolution

Files:

  • Modify: R/patches.R
  • Test: tests/testthat/test-patches.R

Interfaces:

  • Consumes: normalized entries.

  • Produces: patch_cache_key(entry, version, platform, arch, r_minor) → string "<pkg>_<ver>_<platform>_<arch>_<rminor>_<hash12>", where hash12 covers env/configure_args/makevars/patch bytes; resolve_patch_version(entry) → latest CRAN version satisfying entry$versions, or NA_character_; describe_patch(entry) → short human label.

  • Step 1: Write the failing test

test_that("patch_cache_key is stable and sensitive to env/patch changes", {
  e1 <- list(package = "P", env = list(A = "1"),
             configure_args = character(0L), makevars = list(),
             patch_path = NULL)
  e2 <- e1; e2$env <- list(A = "2")

  k1 <- patch_cache_key(e1, "1.0", "alpine-324", "amd64", "4.5")
  expect_identical(k1, patch_cache_key(e1, "1.0", "alpine-324", "amd64", "4.5"))
  expect_false(identical(
    k1, patch_cache_key(e2, "1.0", "alpine-324", "amd64", "4.5")
  ))
  expect_match(k1, "^P_1.0_alpine-324_amd64_4.5_[0-9a-f]{12}$")
})

test_that("resolve_patch_version returns latest for wildcard, NA when unmet", {
  local_mocked_bindings(
    cran_package = function(pkg) list(Version = "5.1.12"),
    .package = "pkgsearch"
  )
  expect_identical(
    resolve_patch_version(list(package = "RcppParallel", versions = "*")),
    "5.1.12"
  )
  expect_identical(
    resolve_patch_version(list(package = "RcppParallel", versions = ">=9.0")),
    NA_character_
  )
})

test_that("describe_patch summarizes the active overrides", {
  expect_match(
    describe_patch(list(env = list(RCPP_PARALLEL_USE_TBB = "0"),
                        configure_args = character(0L), makevars = list(),
                        patch_path = NULL)),
    "env: RCPP_PARALLEL_USE_TBB=0"
  )
  expect_match(
    describe_patch(list(env = list(), configure_args = character(0L),
                        makevars = list(), patch_path = "/x/fix.patch")),
    "source patch"
  )
})
  • Step 2: Run test to verify it fails

Run: Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")' Expected: FAIL with "could not find function patch_cache_key".

  • Step 3: Write minimal implementation (append to R/patches.R)
#' Compute the cache key for a patched binary
#' @keywords internal
patch_cache_key <- function(entry, version, platform, arch, r_minor) {
  payload <- list(
    env = entry$env,
    configure_args = entry$configure_args,
    makevars = entry$makevars,
    patch = if (!is.null(entry$patch_path)) {
      readBin(entry$patch_path, "raw", file.size(entry$patch_path))
    } else {
      raw(0L)
    }
  )
  tmp <- tempfile()
  on.exit(unlink(tmp), add = TRUE)
  saveRDS(payload, tmp)
  hash <- substr(unname(tools::md5sum(tmp)), 1L, 12L)
  sprintf(
    "%s_%s_%s_%s_%s_%s",
    entry$package, version, platform, arch, r_minor, hash
  )
}

#' Resolve the CRAN version to build for a patch entry
#'
#' Returns the latest CRAN version satisfying the entry's `versions` constraint,
#' or `NA_character_` when CRAN's latest does not satisfy it or lookup fails.
#' @keywords internal
resolve_patch_version <- function(entry) {
  latest <- tryCatch(
    pkgsearch::cran_package(entry$package)$Version,
    error = function(e) NA_character_
  )
  if (is.na(latest)) {
    return(NA_character_)
  }
  if (identical(entry$versions, "*") || version_satisfies(latest, entry$versions)) {
    return(latest)
  }
  NA_character_
}

#' Short human label describing a patch entry's overrides
#' @keywords internal
describe_patch <- function(entry) {
  bits <- character(0L)
  if (length(entry$env) > 0L) {
    bits <- c(bits, sprintf(
      "env: %s",
      paste(
        names(entry$env),
        unlist(entry$env),
        sep = "=", collapse = ","
      )
    ))
  }
  if (length(entry$configure_args) > 0L) {
    bits <- c(bits, sprintf("configure: %s", toString(entry$configure_args)))
  }
  if (length(entry$makevars) > 0L) {
    bits <- c(bits, "makevars")
  }
  if (!is.null(entry$patch_path)) {
    bits <- c(bits, "source patch")
  }
  if (length(bits) == 0L) "no-op" else paste(bits, collapse = "; ")
}
  • Step 4: Run test to verify it passes

Run: Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")' Expected: PASS. (Note: local_mocked_bindings requires testthat >= 3.1.7; bincraft uses 3e.)

  • Step 5: Commit
git add R/patches.R tests/testthat/test-patches.R
git commit -m "feat(patches): cache key, version resolution, and patch description"