diff --git a/local/r-minor-helpers.R b/local/r-minor-helpers.R index 65aafdf..6f01c29 100644 --- a/local/r-minor-helpers.R +++ b/local/r-minor-helpers.R @@ -31,3 +31,42 @@ parse_build_args <- function(args) { ncpus = as.integer(pos[3L]) ) } + +# Bound the {pkgcache} metadata dir, which otherwise grows without limit: the +# "patched" repo mints a new content hash on every PACKAGES change, so each build +# writes a fresh ~70 MB _metadata/pkgs-.rds (+ patched-/) that is +# never reused. Keep the `keep` newest entries by mtime; only remove entries +# older than `min_age_secs`, so a concurrent split-job's in-flight files are +# never deleted (each build uses a unique hash, so aged entries are +# unreferenced). Stable repo dirs (CRAN-*, BioC*, INLA-*) and pkg/ downloads are +# not matched and thus preserved. Returns the number of entries removed. +trim_pkgcache_metadata <- function(cache_dir = Sys.getenv("R_PKG_CACHE_DIR"), + keep = 20L, + min_age_secs = 600) { + meta <- file.path(cache_dir, "R", "pkgcache", "_metadata") + if (!nzchar(cache_dir) || !dir.exists(meta)) { + return(0L) + } + entries <- c( + Sys.glob(file.path(meta, "patched-*")), + Sys.glob(file.path(meta, "pkgs-*.rds")) + ) + if (length(entries) == 0L) { + return(0L) + } + info <- file.info(entries) + order_new_first <- order(info$mtime, decreasing = TRUE) + ranked <- entries[order_new_first] + ranked_mtime <- info$mtime[order_new_first] + if (length(ranked) <= keep) { + return(0L) + } + candidates <- ranked[(keep + 1L):length(ranked)] + candidate_age <- as.numeric(Sys.time()) - as.numeric(ranked_mtime[(keep + 1L):length(ranked)]) + removable <- candidates[candidate_age >= min_age_secs] + if (length(removable) == 0L) { + return(0L) + } + unlink(removable, recursive = TRUE, force = TRUE) + length(removable) +} diff --git a/local/tests/test-trim-pkgcache.R b/local/tests/test-trim-pkgcache.R new file mode 100644 index 0000000..1a5b418 --- /dev/null +++ b/local/tests/test-trim-pkgcache.R @@ -0,0 +1,68 @@ +source(file.path("..", "r-minor-helpers.R")) + +# Build a fake _metadata dir under a temp R_PKG_CACHE_DIR. Each entry's mtime is +# set to `age_secs` in the past so we can exercise the age gate deterministically. +make_meta <- function(patched = 0L, pkgs = 0L, keep_repos = TRUE, age_secs = 3600) { + root <- tempfile("pkgcache-") + meta <- file.path(root, "R", "pkgcache", "_metadata") + dir.create(meta, recursive = TRUE) + old <- Sys.time() - age_secs + mk_dir <- function(p) { dir.create(p); Sys.setFileTime(p, old); p } + mk_file <- function(p) { writeLines("x", p); Sys.setFileTime(p, old); p } + for (i in seq_len(patched)) mk_dir(file.path(meta, sprintf("patched-%03d", i))) + for (i in seq_len(pkgs)) mk_file(file.path(meta, sprintf("pkgs-%03d.rds", i))) + if (keep_repos) { + mk_dir(file.path(meta, "CRAN-075c426938")) + mk_dir(file.path(meta, "BioCsoft-1ac964ed6c")) + mk_file(file.path(meta, "bioc-sysreqs.dcf.gz")) + mk_dir(file.path(root, "R", "pkgcache", "pkg")) # downloads, must survive + } + root +} + +n_churn <- function(root) { + meta <- file.path(root, "R", "pkgcache", "_metadata") + length(Sys.glob(file.path(meta, "patched-*"))) + + length(Sys.glob(file.path(meta, "pkgs-*.rds"))) +} + +test_that("empty cache_dir is a no-op", { + expect_identical(trim_pkgcache_metadata("", keep = 5L, min_age_secs = 0), 0L) +}) + +test_that("missing _metadata dir is a no-op", { + expect_identical( + trim_pkgcache_metadata(tempfile("absent-"), keep = 5L, min_age_secs = 0), + 0L + ) +}) + +test_that("fewer than keep entries removes nothing", { + root <- make_meta(patched = 2L, pkgs = 2L) + expect_identical(trim_pkgcache_metadata(root, keep = 20L, min_age_secs = 0), 0L) + expect_identical(n_churn(root), 4L) +}) + +test_that("trims down to keep newest, leaving churn == keep", { + root <- make_meta(patched = 30L, pkgs = 30L) # 60 churn entries, all old + removed <- trim_pkgcache_metadata(root, keep = 20L, min_age_secs = 0) + expect_identical(removed, 40L) + expect_identical(n_churn(root), 20L) +}) + +test_that("entries younger than min_age_secs are protected", { + root <- make_meta(patched = 30L, pkgs = 0L, keep_repos = FALSE, age_secs = 60) + # keep=5 would drop 25, but all are 60s old < 600s gate -> nothing removed + expect_identical(trim_pkgcache_metadata(root, keep = 5L, min_age_secs = 600), 0L) + expect_identical(n_churn(root), 30L) +}) + +test_that("stable repo dirs and pkg downloads are never touched", { + root <- make_meta(patched = 30L, pkgs = 30L) + trim_pkgcache_metadata(root, keep = 0L, min_age_secs = 0) + meta <- file.path(root, "R", "pkgcache", "_metadata") + expect_true(dir.exists(file.path(meta, "CRAN-075c426938"))) + expect_true(dir.exists(file.path(meta, "BioCsoft-1ac964ed6c"))) + expect_true(file.exists(file.path(meta, "bioc-sysreqs.dcf.gz"))) + expect_true(dir.exists(file.path(root, "R", "pkgcache", "pkg"))) +})