diff --git a/.crow/build-all-versions-install-deps.yaml b/.crow/build-all-versions-install-deps.yaml index e0cca86..8649ca0 100644 --- a/.crow/build-all-versions-install-deps.yaml +++ b/.crow/build-all-versions-install-deps.yaml @@ -4,7 +4,7 @@ # they are merged with build-all-versions' identical declarations. variables: target_arch: - description: 'Architecture to build.' + description: "Architecture to build." options: - amd64 - arm64 @@ -27,7 +27,7 @@ variables: - "noble" default: "3.24" R_VERSION: - description: 'Primary R version under /opt/R.' + description: "Primary R version under /opt/R." options: - 4.5.3 - 4.4.3 @@ -74,10 +74,6 @@ steps: commands: # one-time full wipe to fix corrupted .so files from previous failed builds # - rm -rf /mnt/cache/R-pkgs - # Clear churny pkgcache metadata left by a prior crashed run (the "patched" - # repo mints a new hash per PACKAGES change -> unbounded pkgs-*.rds/patched-*). - # Keep pkg/ downloads and the stable CRAN/BioC/INLA repo dirs. - - rm -rf /mnt/cache/pkgcache/R/pkgcache/_metadata/patched-* /mnt/cache/pkgcache/R/pkgcache/_metadata/pkgs-*.rds || true - mkdir -p /mnt/cache/pkgcache /mnt/cache/R-pkgs /mnt/cache/ccache /mnt/cache/packages - git clone -q https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git . # Pin the same bincraft version the build steps use, so the precomputed diff --git a/.crow/build-all-versions.yaml b/.crow/build-all-versions.yaml index 3932811..615d273 100644 --- a/.crow/build-all-versions.yaml +++ b/.crow/build-all-versions.yaml @@ -5,7 +5,7 @@ # Skip list lives in local/excluded-packages.json (read by local/build-all.R). variables: target_arch: - description: 'Architecture to build.' + description: "Architecture to build." options: - amd64 - arm64 @@ -31,7 +31,7 @@ variables: - "resolute" default: "3.24" R_VERSION: - description: 'Primary R version under /opt/R.' + description: "Primary R version under /opt/R." options: - 4.5.3 - 4.4.3 @@ -116,11 +116,6 @@ steps: - ${ARCH}-binaries-r-dep-cache-${OS}-${OS_VERSION//./}:/mnt/cache commands: - git clone -q https://pat-s:$$REPO_RO_TOKEN@git.devxy.io/devxy/build-cran-binaries.git . - # Clear churny pkgcache metadata left by a prior crashed run (the "patched" - # repo mints a new hash per PACKAGES change -> unbounded pkgs-*.rds/patched-*). - # Keep pkg/ downloads and the stable CRAN/BioC/INLA repo dirs. Within-run - # growth is bounded separately by trim_pkgcache_metadata() in build-all.R. - - rm -rf /mnt/cache/pkgcache/R/pkgcache/_metadata/patched-* /mnt/cache/pkgcache/R/pkgcache/_metadata/pkgs-*.rds || true - mkdir -p /mnt/cache/pkgcache /mnt/cache/R-pkgs /mnt/cache/ccache /mnt/cache/packages # The primary pass must not rely on build-all-versions-install-deps having # run on *this* agent: depends_on only orders the steps, but the cache diff --git a/docker/buildkitd.toml b/docker/buildkitd.toml deleted file mode 100644 index ebbeb38..0000000 --- a/docker/buildkitd.toml +++ /dev/null @@ -1,41 +0,0 @@ -# BuildKit GC config for the remote buildx builders (artemis/amd64, gaia/arm64). -# -# Apply when creating the docker-container builders: -# docker buildx create --name artemis --driver docker-container \ -# --config docker/buildkitd.toml ssh:// -# docker buildx create --name gaia --driver docker-container \ -# --config docker/buildkitd.toml ssh:// -# -# Why: BuildKit's default GC caps the ephemeral cache tier — RUN -# --mount=type=cache mounts, local build context, git checkouts — at a -# hardcoded 512 MB (shown as "488.3 MiB" in `buildx inspect`). Across our -# 7-distro build matrix that fills instantly and forces re-downloads of -# system + R packages every rebuild. The first rule below raises that tier. -# -# Limits are absolute (not %) on purpose: artemis and gaia have very -# different free space (Hetzner ~42 GiB free vs Mac mini ~279 GiB), so a -# percentage would mean wildly different real budgets. minFreeSpace = 20 GB -# keeps the tight Hetzner host safe while staying modest on the Mac mini. - -[worker.oci] - gc = true - - # Tier 1 — ephemeral caches (cache mounts, local context, git checkouts). - # Raised from the 512 MB default to 8 GB, retained for 7 days so weekly - # rebuilds reuse downloaded packages instead of re-fetching them. - [[worker.oci.gcpolicy]] - filters = [ - "type==source.local", - "type==exec.cachemount", - "type==source.git.checkout", - ] - keepDuration = "168h" - maxUsedSpace = "8GB" - - # Tier 2 — everything else (image layers, RUN exec results). Bounds the - # whole buildkit cache and always leaves 20 GB free on the host disk. - [[worker.oci.gcpolicy]] - all = true - reservedSpace = "2GB" - maxUsedSpace = "40GB" - minFreeSpace = "20GB" diff --git a/justfile b/justfile index a9a705e..2d9388d 100644 --- a/justfile +++ b/justfile @@ -10,10 +10,8 @@ # - buildx builders named `artemis` (amd64) and `gaia` (arm64), created with the # docker-container driver (runs BuildKit on the remote host's docker daemon over # SSH). The default `remote` driver does NOT work with an ssh:// docker host. -# Pass --config docker/buildkitd.toml so BuildKit's GC keeps a usable cache -# (the default caps the cache-mount tier at 512 MB, forcing re-downloads). -# docker buildx create --name artemis --driver docker-container --config docker/buildkitd.toml ssh:// -# docker buildx create --name gaia --driver docker-container --config docker/buildkitd.toml ssh:// +# docker buildx create --name artemis --driver docker-container ssh:// +# docker buildx create --name gaia --driver docker-container ssh:// # - exported secrets: B2_S3_ACCESS_KEY, B2_S3_SECRET_KEY, PGPASS (GITHUB_PAT optional) # # Overridable (env or `just VAR=… rebuild …`): diff --git a/local/build-all.R b/local/build-all.R index 1df1f83..c0fe7f7 100644 --- a/local/build-all.R +++ b/local/build-all.R @@ -64,13 +64,10 @@ sprintf("# of package versions for this job: %s", nrow(chunk)) exclude <- jsonlite::fromJSON("local/excluded-packages.json")[["package"]] chunk <- chunk[!chunk$Package %in% exclude, ] -# Skip package versions already attempted in a previous run (built or errored). +# Skip package versions already built in a previous run. # pkgs_to_build.rds is a static snapshot from the install-deps step, so on a # restart it still lists everything an interrupted run already produced. The # metadata DB reflects that progress, so we re-derive the remaining set here. -# We exclude *all* attempted versions, not just successful ones: a previously -# errored version is skipped by build_binary_package() anyway, so leaving it in -# the chunk only makes the job cycle through it one-by-one for no benefit. # Derive platform + arch from the running container, mirroring the codename -> # platform mapping bincraft uses internally. The OS/OS_VERSION selectors are # workflow-level CI variables that are not injected into the container @@ -107,13 +104,13 @@ con <- DBI::dbConnect( ) built <- DBI::dbGetQuery( con, - "SELECT name, tag FROM single_builds WHERE platform = $1 AND arch = $2", + "SELECT name, tag FROM single_builds WHERE platform = $1 AND arch = $2 AND error_occurred = FALSE", params = list(platform, arch) ) DBI::dbDisconnect(con) before <- nrow(chunk) chunk <- chunk[!paste(chunk$Package, chunk$Version) %in% paste(built$name, built$tag), ] -sprintf("Skipped %d already-attempted package versions; %d remaining for this job", before - nrow(chunk), nrow(chunk)) +sprintf("Skipped %d already-built package versions; %d remaining for this job", before - nrow(chunk), nrow(chunk)) # Read pre-computed S3 listing from install-deps step # This avoids loading s3fs/reticulate/Python in the build container, @@ -122,10 +119,6 @@ s3_cache <- readRDS("/mnt/cache/packages/s3_cache.rds") sprintf("S3 cache: %s files", length(s3_cache)) n <- nrow(chunk) -# Every `trim_every` packages, bound the pkgcache _metadata dir so a full-platform -# run does not accumulate thousands of ~70 MB snapshots and fill the host disk. -# No-op on amd64 (R_PKG_CACHE_DIR is empty / cache not persisted). -trim_every <- 25L mapply( function(pkg, ver, sens, i) { cat(sprintf("[%d/%d] %s_%s (r_minor_sensitive=%s)\n", i, n, pkg, ver, sens)) @@ -151,12 +144,6 @@ mapply( upload = TRUE, store_build_metadata = TRUE ) - if (i %% trim_every == 0L) { - removed <- trim_pkgcache_metadata() - if (removed > 0L) { - cat(sprintf(" [pkgcache trim] removed %d stale _metadata entries\n", removed)) - } - } }, chunk$Package, chunk$Version, diff --git a/local/patches/fs/force-vendored-libuv.patch b/local/patches/fs/force-vendored-libuv.patch deleted file mode 100644 index c69e8b9..0000000 --- a/local/patches/fs/force-vendored-libuv.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/configure b/configure ---- a/configure -+++ b/configure -@@ -11,6 +11,15 @@ - PKG_TEST_HEADER="" - PKG_LIBS="-luv" - -+# bincraft patch: force the vendored static libuv so the resulting binary -+# is self-contained. fs configure otherwise links system libuv whenever -+# pkg-config finds libuv-devel (installed as a build-time sysreq), yielding -+# an fs.so with NEEDED libuv.so.1 that fails to dyn.load on machines lacking -+# runtime libuv (install.packages/renv do not install SystemRequirements). -+echo "Building static libuv (bincraft: forced vendored)" 1>&2 -+cp -f src/Makevars.vendor src/Makevars -+exit 0 -+ - # Use pkg-config if available - if [ `command -v pkg-config` ]; then - PKGCONFIG_CFLAGS=`pkg-config --cflags --silence-errors ${PKG_CONFIG_NAME}` diff --git a/local/patches/registry.json b/local/patches/registry.json index e53f1c9..b1a472d 100644 --- a/local/patches/registry.json +++ b/local/patches/registry.json @@ -9,16 +9,6 @@ "patch": "RcppParallel/disable-tbb.patch", "reason": "bundled Intel TBB build hangs/fails on musl (Alpine) and newer toolchains (g++ 15 on ubuntu-2604); patch unsets USE_TBB and forces -DRCPP_PARALLEL_USE_TBB=0 so RcppParallel skips the bundled build and uses the TinyThread backend" }, - { - "package": "fs", - "versions": "*", - "platforms": ["*"], - "env": {}, - "configure_args": [], - "makevars": {}, - "patch": "fs/force-vendored-libuv.patch", - "reason": "fs 2.x configure links system libuv whenever pkg-config finds libuv-devel (installed as a build-time sysreq), producing an fs.so with NEEDED libuv.so.1. That binary fails to dyn.load on consumer machines lacking runtime libuv, because install.packages/renv do not install SystemRequirements (only pak does, and only in the build container). The patch short-circuits configure to copy src/Makevars.vendor and build the bundled static libuv (needs cmake) so the binary is self-contained on every platform. An env/pkg-config override was tried first but the rebuilt binary still linked libuv.so.1, so a source patch is used instead." - }, { "package": "rstan", "versions": "*", diff --git a/local/r-minor-helpers.R b/local/r-minor-helpers.R index 6f01c29..65aafdf 100644 --- a/local/r-minor-helpers.R +++ b/local/r-minor-helpers.R @@ -31,42 +31,3 @@ 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 deleted file mode 100644 index 1a5b418..0000000 --- a/local/tests/test-trim-pkgcache.R +++ /dev/null @@ -1,68 +0,0 @@ -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"))) -})