diff --git a/CLAUDE.md b/CLAUDE.md index ad4beb1..9a3cd31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ Contents: - `bonsai` — parsnip bridge for the `lightgbm` and `catboost` engines - `catboost` (v1.2.10, pinned) — not on CRAN; installed from its `linux-x86_64` GitHub release binary via `remotes::install_url` with `--no-test-load` (so the end-to-end fit smoke test is the real verification). amd64-only image, so the x86_64 binary suffices. - `brms` — Bayesian regression via Stan. Needs `BH`/`RcppEigen`/`RcppParallel` installed explicitly: rstan lists them as `LinkingTo`, but installing rstan as a *binary* skips them, yet Stan *model* compilation at runtime needs them. The brms smoke test compiles a model to verify this end to end. +- An **inference-reporting + presentation set**: `gt` (tables), `marginaleffects` (post-estimation predictions/comparisons/slopes), `patchwork` (ggplot composition), `easystats` (parameters/performance/effectsize meta-package). All four are used pervasively in both PPBDS/primer's `book/` and its tutorials (audited 2026-07) but were previously either absent (only `Suggests` in primer.tutorials, which pak doesn't install) or present only transitively (`gt` via primer.tutorials' Imports). General-purpose, hence base rather than student. - Headless-container workarounds applicable to any consumer (Codespaces, local Docker, Gateway): - `/usr/local/bin/xdg-open` replaced with a no-op stub so tools that try to open a browser (e.g., `quarto publish`) do not error - `BROWSER=/usr/bin/true` set as `ENV` for tools that respect `$BROWSER` @@ -129,7 +130,7 @@ The steps to take when changing this infrastructure. `devcontainers` (this repo) 2. Make the change. Update the matching **smoke test** and **this CLAUDE.md** if the package/lib set changed. 3. **Validate before merging:** `gh workflow run build.yml --ref `, then watch it green. This catches build + sysreqs failures *before* a release tag exists. The `student` build is heavy (source-builds `primer.*`, ~4.6 GB) — allow ~20 min. To introspect a published image cheaply (e.g. "is X actually installed?"), push a throwaway **push-triggered** workflow that does `docker run …` or `FROM … RUN …`, read the log, then delete the branch. (We did exactly this to root-cause the `libnode-dev` phantom.) 4. PR → `gh pr merge --merge --delete-branch` → sync `main`. -5. Pick the version (semver): **patch** `Z` for fixes / no new content; **minor** `Y` for new packages or features; **major** for a breaking `base` change. `main` is allowed to sit ahead of the latest tag — pure doc/no-op changes can ride with the next real release rather than forcing one. +5. Pick the version: **increment only the last digit** (`Z`), whatever the change — new packages included (David's rule, 2026-07: e.g. 0.9.0 → 0.9.1). Bump `Y` or the major **only when David explicitly asks for it**. `main` is allowed to sit ahead of the latest tag — pure doc/no-op changes can ride with the next real release rather than forcing one. 6. `gh release create vX.Y.Z --target main` → this triggers `build.yml` on the tag, publishing `base`/`dev`/`student:X.Y.Z` and moving `:X.Y`. 7. Watch the tag build green and **confirm `student:X.Y.Z` is on GHCR** before repinning (query the GHCR manifest, don't assume). diff --git a/base/Dockerfile b/base/Dockerfile index 0fa61bf..a77c9a7 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -23,6 +23,11 @@ ARG CODEX_VERSION=0.141.0 # - Geospatial: sf, terra, etc. # - text shaping libs: ragg / textshaping (modern ggplot2 graphics) # - cmake: build tool for source packages like fs (pulled in by primer.tutorials) +# - libuv1-dev: fs >= 2.1.0 declares libuv as a sysreq. The P3M binary works +# without it, but pak prints a phantom "✖ Missing 1 system package: +# libuv1-dev" on every install that touches fs (i.e. nearly all of them) — +# same annoyance class as the libnode-dev story below, solved here by just +# baking the real (small) lib. # NOTE: libv8/libnode-dev is deliberately NOT installed here. The V8 R package # binary from Posit Package Manager is statically linked (bundled libv8) and # needs no system library, AND NodeSource's nodejs (installed below) ships @@ -38,6 +43,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libharfbuzz-dev \ libfribidi-dev \ cmake \ + libuv1-dev \ && rm -rf /var/lib/apt/lists/* # GitHub CLI. Official install method per the gh docs. @@ -193,6 +199,21 @@ RUN R -q -e 'install.packages("pak", repos = sprintf("https://r-lib.github.io/p/ RUN R -q -e 'options(repos = c(P3M = "https://packagemanager.posit.co/cran/__linux__/noble/latest")); pak::pkg_install(c("tidymodels", "bonsai", "remotes", "xgboost", "lightgbm", "randomForest", "ranger", "glmnet", "brms", "BH", "RcppEigen", "RcppParallel"))' \ && R -q -e 'remotes::install_url("https://github.com/catboost/catboost/releases/download/v1.2.10/catboost-R-linux-x86_64-1.2.10.tgz", INSTALL_opts = c("--no-multiarch", "--no-test-load", "--no-staged-install"))' +# Inference-reporting + presentation packages, shared by dev and student. +# These are used pervasively in BOTH PPBDS/primer's book/ and its tutorials +# (audited 2026-07), but pak never installed them because primer.tutorials +# only lists them as Suggests — students hit an install wall at the first +# library() call. General-purpose (not course-specific), so they belong here: +# - gt : presentation-quality tables (1,100+ gt:: calls in the +# book; was previously present only transitively via +# primer.tutorials' Imports — now a deliberate choice) +# - marginaleffects : predictions/comparisons/slopes from fitted models — +# the book's standard post-estimation workflow +# - patchwork : ggplot composition ("p1 + p2") +# - easystats : meta-package (parameters/performance/effectsize/see/…) +# used across the tutorials +RUN R -q -e 'options(repos = c(P3M = "https://packagemanager.posit.co/cran/__linux__/noble/latest")); pak::pkg_install(c("gt", "marginaleffects", "patchwork", "easystats"))' + # httpgd (graphics device for the VS Code R extension) is intentionally # NOT installed here: the rocker devcontainer base already bakes it in via # its r-packages feature ("packages": "httpgd"), so a second install was @@ -213,7 +234,19 @@ ENV BROWSER=/usr/bin/true # of group-writable site-library so the runtime user (rstudio, in staff # group) can install/update packages — and so package-load paths that # touch lock files or caches don't trip permission errors. -RUN chmod -R g+w /usr/local/lib/R/site-library +# +# Two hardenings, both for the same failure class (a root pak run poisons +# the library for later non-root installs; see the student Dockerfile's +# rstudio-user rationale): +# - rm -rf _cache: pkgdepends locks site-library/_cache/.lock during +# installs. Locks created by ROOT here are group-unreadable (root's +# umask), and a downstream rstudio-user pak run then dies with +# "filelock::lock(): Cannot open lock file: Permission denied" — this +# broke the student build (2026-07). The dir is pure scratch; delete it. +# - g+rwX (not just g+w): write alone leaves group-READ missing on any +# file root created without it, and no traverse on such dirs. +RUN rm -rf /usr/local/lib/R/site-library/_cache \ + && chmod -R g+rwX /usr/local/lib/R/site-library # R smoke test: every baked-in package must actually load. Catches # binary-ABI mismatches at build time instead of at Codespace-launch time. @@ -223,6 +256,9 @@ RUN R --vanilla -e 'for (p in c("pak", "httpgd")) if (!requireNamespace(p, quiet # Modeling smoke test 1: tidymodels, bonsai, every engine, and brms must load. RUN R --vanilla -e 'for (p in c("tidymodels", "bonsai", "xgboost", "lightgbm", "randomForest", "ranger", "glmnet", "brms", "catboost")) if (!requireNamespace(p, quietly = TRUE)) stop("modeling smoke test failed to load: ", p)' +# Inference/presentation smoke test: the primer book+tutorial packages must load. +RUN R --vanilla -e 'for (p in c("gt", "marginaleffects", "patchwork", "easystats")) if (!requireNamespace(p, quietly = TRUE)) stop("inference/presentation smoke test failed to load: ", p)' + # Modeling smoke test 2: CatBoost end-to-end THROUGH tidymodels/bonsai — fit a # tiny model and predict, not just load the package. Since catboost is installed # with --no-test-load, this is the real proof the binary + engine binding work.