Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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 <branch>`, 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 <image> …` or `FROM <image> … 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).

Expand Down
38 changes: 37 additions & 1 deletion base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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/<pkg>.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.
Expand All @@ -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.
Expand Down