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

148 lines
4.6 KiB
Markdown

### 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**
```r
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`)**
```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**
```bash
git add R/patches.R tests/testthat/test-patches.R
git commit -m "feat(patches): cache key, version resolution, and patch description"
```
---