### Task A5: Orchestrate the local patched repo (cache + index) **Files:** - Modify: `R/patches.R` - Test: `tests/testthat/test-patches.R` **Interfaces:** - Consumes: all helpers above. - Produces: `prepare_patched_repo(patches_dir, platform, arch, r_minor, cache_dir, repo_dir)` → path to a `src/contrib`-style dir containing patched binaries + a `PACKAGES` index, or `NULL` when nothing matched/built. On a cache hit it copies the cached tarball into `repo_dir`; on a miss it builds, then writes the result into `cache_dir`. - [ ] **Step 1: Write the failing test** ```r test_that("prepare_patched_repo returns NULL when no entries match", { dir <- withr::local_tempdir() jsonlite::write_json( list(list(package = "A", versions = "*", platforms = list("redhat"), reason = "r")), file.path(dir, "registry.json"), auto_unbox = TRUE ) expect_null( prepare_patched_repo(dir, "ubuntu-2604", "amd64", "4.5", cache_dir = withr::local_tempdir(), repo_dir = withr::local_tempdir()) ) }) test_that("prepare_patched_repo serves a cached binary and writes an index", { dir <- withr::local_tempdir() jsonlite::write_json( list(list(package = "glue", versions = "*", platforms = list("*"), env = list(A = "1"), reason = "r")), file.path(dir, "registry.json"), auto_unbox = TRUE ) cache <- withr::local_tempdir() repo <- withr::local_tempdir() local_mocked_bindings( resolve_patch_version = function(entry) "1.0.0", build_patched_binary = function(entry, version, dest_dir) { f <- file.path(dest_dir, sprintf("%s_%s.tar.gz", entry$package, version)) writeLines("fake binary", f) f } ) out <- prepare_patched_repo(dir, "ubuntu-2604", "amd64", "4.5", cache_dir = cache, repo_dir = repo) expect_identical(out, repo) expect_true(file.exists(file.path(repo, "glue_1.0.0.tar.gz"))) expect_true(file.exists(file.path(repo, "PACKAGES"))) # The build result was cached under the key. expect_length(list.files(cache, pattern = "^glue_1.0.0_.*\\.tar\\.gz$"), 1L) }) ``` - [ ] **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 prepare_patched_repo". - [ ] **Step 3: Write minimal implementation (append to `R/patches.R`)** ```r #' Prepare a local repo of patched binaries for the current build #' #' For each registry entry matching the current platform, ensures a patched #' binary is present in `repo_dir` (from `cache_dir` if available, else built #' and then cached) and writes a `PACKAGES` index over them. #' #' @param patches_dir Directory with `registry.json`, or `NULL`. #' @param platform Build platform, e.g. `"ubuntu-2604"`. #' @param arch Build arch, e.g. `"amd64"`. #' @param r_minor R `"major.minor"` string, e.g. `"4.5"`. #' @param cache_dir Persistent cache for patched binaries. #' @param repo_dir Directory to assemble the local repo in. #' @return `repo_dir` if at least one patched binary was produced, else `NULL`. #' @keywords internal prepare_patched_repo <- function( patches_dir, platform, arch, r_minor, cache_dir = file.path("/mnt", "cache", "patched-binaries"), repo_dir = tempfile("patched_repo_") ) { entries <- match_patch_entries( load_patch_registry(patches_dir), platform, arch ) if (length(entries) == 0L) { return(NULL) } dir.create(repo_dir, recursive = TRUE, showWarnings = FALSE) dir.create(cache_dir, recursive = TRUE, showWarnings = FALSE) produced <- 0L for (entry in entries) { version <- resolve_patch_version(entry) if (is.na(version)) { log_warn(sprintf( "No CRAN version of {.pkg %s} satisfies '%s'; patch skipped.", entry$package, entry$versions )) next } key <- patch_cache_key(entry, version, platform, arch, r_minor) cached <- file.path(cache_dir, sprintf("%s.tar.gz", key)) target <- file.path( repo_dir, sprintf("%s_%s.tar.gz", entry$package, version) ) if (file.exists(cached)) { log_info(sprintf( "Using cached patched binary for {.pkg %s} %s.", entry$package, version )) file.copy(cached, target, overwrite = TRUE) } else { log_info(sprintf( "Applying patch to {.pkg %s} %s [%s]: %s", entry$package, version, describe_patch(entry), entry$reason )) built <- build_patched_binary(entry, version, repo_dir) if (is.null(built)) { next } if (!identical(normalizePath(built), normalizePath(target))) { file.copy(built, target, overwrite = TRUE) } file.copy(target, cached, overwrite = TRUE) } produced <- produced + 1L } if (produced == 0L) { return(NULL) } cranlike::add_PACKAGES( list.files(repo_dir, pattern = "\\.tar\\.gz$"), repo_dir ) repo_dir } ``` - [ ] **Step 4: Run test to verify it passes** Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'` Expected: PASS. - [ ] **Step 5: Update imports and document** Add to `DESCRIPTION` `Imports:` (alphabetical) `jsonlite` if not present, and ensure `pkgsearch`, `pkgbuild`, `cranlike`, `withr` are listed (they are). Then: ```bash Rscript -e 'devtools::document()' git add R/patches.R tests/testthat/test-patches.R DESCRIPTION NAMESPACE git commit -m "feat(patches): orchestrate local patched-binary repo with caching" ``` ---