From dde8441388aa8e956f6c833a7809cd439999d6f9 Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Tue, 12 May 2026 08:28:45 +0200 Subject: [PATCH 1/2] chore: keep Colin Fay as the package maintainer Revert the maintainer (`cre`) swap from #111: Colin Fay (`contact@colinfay.me`) stays the `cre`, Vincent Guyader goes back to `aut`. Both remain listed as authors. No "New maintainer" note will be emitted by CRAN's submission system, so the corresponding section of `cran-comments.md` is removed. --- DESCRIPTION | 4 ++-- cran-comments.md | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c28e1b9..5caac57 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -2,9 +2,9 @@ Package: dockerfiler Title: Easy Dockerfile Creation from R Version: 0.3.0 Authors@R: c( - person("Colin", "Fay", , "contact@colinfay.me", role = "aut", + person("Colin", "Fay", , "contact@colinfay.me", role = c("cre", "aut"), comment = c(ORCID = "0000-0001-7343-1846")), - person("Vincent", "Guyader", , "vincent@thinkr.fr", role = c("cre", "aut"), + person("Vincent", "Guyader", , "vincent@thinkr.fr", role = "aut", comment = c(ORCID = "0000-0003-0671-9270")), person("Josiah", "Parry", , "josiah.parry@gmail.com", role = "aut", comment = c(ORCID = "0000-0001-9910-865X")), diff --git a/cran-comments.md b/cran-comments.md index 148f560..4e2d146 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -20,15 +20,6 @@ clock skew on the build VM; the package itself is unaffected.) CRAN submission; the win-builder result will follow by email and will be forwarded to CRAN if it surfaces anything new. -## Maintainer change - -This release changes the maintainer from Colin Fay -(`contact@colinfay.me`) to Vincent Guyader -(`vincent@thinkr.fr`). Both remain listed as authors. The -previous maintainer (Colin Fay) is aware of this submission and -will confirm the maintainer change by replying to the automated -email from CRAN's submission system. - ## Major changes since 0.2.6 A focused 0.3.0 release. Headline bullets (full details in From 437c48a486ac0445cfb5dca6104c1cead1a23992 Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Tue, 12 May 2026 09:02:10 +0200 Subject: [PATCH 2/2] docs+chore: pre-CRAN polish for 0.3.0 Bundled pre-submission cleanup, the result of a dedicated documentation review plus a coverage pass. dock_from_desc(): flip the default `repos` to `c(CRAN = "https://p3m.dev/cran/latest")`, matching dock_from_renv(), and wire `.patch_rprofile_for_ppm()` into the generated Rprofile.site RUN so the build pulls pre-compiled Linux binaries (the `__linux__/$VERSION_CODENAME/` rewrite + the strict `HTTPUserAgent` PPM requires). Pass `repos = c(CRAN = "https://cran.rstudio.com/")` to opt out. Breaking change (behaviour of the generated Dockerfile); NEWS updated. Tests added (red-first): the default `repos` formal is the PPM URL, and a default `dock_from_desc()` invocation produces the rewritten Rprofile line. Documentation: - README: the renv.lock section now shows the bare `dock_from_renv(lockfile = ...)` call and spells out the 0.3.0 defaults (multi-arch `rocker/r-ver`, p3m.dev binaries, `rstudio` user) and how to opt out; a `parse_dockerfile()` section is added; the README links to `vignette("dockerfiler")`. - Vignette: adds `dock_from_renv()` and `parse_dockerfile()` sections, fixes the `r(install.packages(..., repo = ...))` typo (`repos =`), and notes the generators' default `FROM` / `repos`. - `dock_from_desc()` roxygen: add `@examples` and `@details` (explaining the `build_from_source` / `update_tar_gz` interplay), document the new `repos` default + PPM rewrite, fix the `AS` "Default it NULL" typo, clarify `AS` is a build-stage name. - `Dockerfile$new()` roxygen: `@param FROM` now states the default (`"rocker/r-base"`, and that the high-level generators differ); `@param AS` describes it as a build-stage name. - `Dockerfile$ENV()` roxygen: `@param key,value` said "label" (copy-paste from `LABEL()`); now "environment variable". - `r()` roxygen: clearer `@description` / `@param` / `@return` (it captures an R *expression* unevaluated and returns a shell-quoted `R -e '...'` string), fix the example's `repo =` typo. - `get_sysreqs()` roxygen: add `@examples`. - NEWS: split the dense "Small polish bundle" bullet into readable, user-facing lines; widen the FROM/repos breaking-change bullets to cover `dock_from_desc()`. - regenerated `man/*.Rd` (`devtools::document()`; bumps `RoxygenNote` 7.3.2 -> 7.3.3 to match the installed roxygen2). Coverage: - `tests/testthat/test-utils.R`: direct tests for the type/length/NA/format guards of every `.validate_*` helper. These guards are part of the security contract (the argument is interpolated into a generated Dockerfile directive) but were only exercised via the format-regex branch. Package coverage is back to 99.65% (`R/utils.R` 99.10%, was 80.18%); `cran-comments.md` updated with the real figure. Full test suite: 0 failures. R CMD check --as-cran: 0/0/0 (one transient "unable to verify current time" host NOTE, unrelated). --- DESCRIPTION | 2 +- NEWS.md | 22 ++++++----- R/dock_from_desc.R | 59 ++++++++++++++++++++++++---- R/dockerfile.R | 12 ++++-- R/get_sysreqs.R | 6 +++ R/rthis.R | 13 ++++-- README.Rmd | 30 +++++++++++--- README.md | 32 ++++++++++++--- cran-comments.md | 9 ++--- man/Dockerfile.Rd | 12 ++++-- man/dockerfiles.Rd | 57 +++++++++++++++++++++++---- man/get_sysreqs.Rd | 6 +++ man/r.Rd | 13 +++--- tests/testthat/test-dock_from_desc.R | 30 ++++++++++++++ tests/testthat/test-utils.R | 48 ++++++++++++++++++++++ vignettes/dockerfiler.Rmd | 43 ++++++++++++++++++-- 16 files changed, 332 insertions(+), 62 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5caac57..f269ea1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,4 +44,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 diff --git a/NEWS.md b/NEWS.md index 926a790..8b77f32 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,8 +29,9 @@ appended at codegen time (e.g. `rocker/r-ver:4.5.0`). Apple Silicon and ARM Linux hosts (Ampere, AWS Graviton) now build natively without Rosetta. Pass the legacy `FROM = "rocker/r-base"` - to opt out. Closes #47. -- `dock_from_renv()` default `repos` flips from + to opt out. `dock_from_desc()`'s default `FROM` was already + `rocker/r-ver:` and is unchanged. Closes #47. +- `dock_from_renv()` and `dock_from_desc()` default `repos` flips from `"https://cran.rstudio.com/"` (source-only CRAN mirror) to `"https://p3m.dev/cran/latest"` (Posit Public Package Manager), with automatic rewrite to the `__linux__/$VERSION_CODENAME/` shape @@ -151,13 +152,16 @@ function arguments, so the branch was unreachable; the success path always ran when `build()` returned. The branch is removed; failures of `pkgbuild::build()` propagate normally via `stop()`. Closes #98. -- Small polish bundle (no behavioural changes for end users): fix two - `length(x > 0)` typos in `dock_from_desc()` (intent was - `length(x) > 0`); drop a duplicate `@export` tag in `dockerignore.R`; - use the new `dock$ARG(name, default = ...)` form internally instead - of inlining the `=`; tighten a previously brittle regression test - that checked for any occurrence of `"remotes"` in the generated - Dockerfile. +- `dock_from_desc()`: fixed two `length(x > 0)` typos in the dependency + handling (the intent was `length(x) > 0`); the conditions now behave + as documented. +- The `_*.tar.gz` cleanup glob in `dock_from_desc(build_from_source = FALSE)` + is now built with `glob2rx()`, so a dot in a package name (e.g. + `R.utils`) is matched literally and a sibling package's tarball is no + longer swept up. +- Internal tidy-ups with no user-visible effect: dropped a duplicate + `@export` in `dockerignore.R`; the codegen now uses the new + `dock$ARG(name, default = ...)` form instead of inlining the `=`. # dockerfiler 0.2.6 diff --git a/R/dock_from_desc.R b/R/dock_from_desc.R index a832c89..7b4400f 100644 --- a/R/dock_from_desc.R +++ b/R/dock_from_desc.R @@ -47,15 +47,24 @@ quote_not_na <- function(x){ #' dash, underscore, optional `:tag` and / or `@sha256:`); other #' values raise an error to prevent shell-metacharacter injection #' into the generated FROM directive. -#' @param AS The AS of the Dockerfile. Default it NULL. When non-NULL, -#' validated as a simple build-stage name (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`). +#' @param AS The build-stage name of the Dockerfile (`FROM ... AS `). +#' Default is `NULL` (no `AS`). When non-`NULL`, validated as a simple +#' build-stage name (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`). #' @param sysreqs boolean. If TRUE, the Dockerfile will contain sysreq installation. #' @param repos character. The URL(s) of the repositories to use for -#' `options("repos")`. Each value must look like an http(s) URL (no -#' quotes, spaces or newlines); each name (when set) must be a simple -#' identifier (`^[A-Za-z][A-Za-z0-9._-]*$`). Other values raise an -#' error to prevent injection into the generated `echo "options(...)"` -#' shell command. +#' `options("repos")`. Default is `c(CRAN = "https://p3m.dev/cran/latest")` +#' (Posit Public Package Manager). When `repos` is a single `CRAN`-keyed +#' PPM URL (`packagemanager.posit.co`, `packagemanager.rstudio.com`, or +#' `p3m.dev`), the codegen rewrites it to the +#' `__linux__/$VERSION_CODENAME/` shape (codename resolved from +#' `/etc/os-release` at image build time) and adds the strict +#' `HTTPUserAgent` PPM requires, so the build pulls pre-compiled Linux +#' binaries instead of compiling from source. Pass the legacy +#' `c(CRAN = "https://cran.rstudio.com/")` to opt out. Each value must +#' look like an http(s) URL (no quotes, spaces or newlines); each name +#' (when set) must be a simple identifier (`^[A-Za-z][A-Za-z0-9._-]*$`). +#' Other values raise an error to prevent injection into the generated +#' `echo "options(...)"` shell command. #' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line. #' @param build_from_source boolean. If `TRUE` no tar.gz is created and #' the Dockerfile directly mount the source folder. @@ -86,9 +95,38 @@ quote_not_na <- function(x){ #' Must be a single scalar logical; `NA`, character, numeric, #' `NULL` and length-2+ vectors are rejected with an error. #' +#' @details +#' Two install strategies are available for the package itself: +#' * `build_from_source = TRUE` (the default): the generated Dockerfile +#' mounts the source folder and installs from it directly. `update_tar_gz` +#' is ignored. +#' * `build_from_source = FALSE`: a source tarball (`_.tar.gz`) +#' is `COPY`'d into the image and installed with +#' `remotes::install_local()`. When `update_tar_gz = TRUE`, a fresh +#' tarball is built with `pkgbuild::build()` first (and any stale +#' `_*.tar.gz` in the current directory is removed); when +#' `update_tar_gz = FALSE`, an already-built tarball is expected +#' alongside the `DESCRIPTION`. +#' +#' The package name and its dependency-field names are read from the +#' `DESCRIPTION` and validated against the CRAN package-name grammar +#' before being interpolated into the generated directives. +#' #' @export #' @rdname dockerfiles #' +#' @examples +#' \dontrun{ +#' # From the DESCRIPTION of the package in the working directory: +#' dock <- dock_from_desc("DESCRIPTION") +#' dock +#' +#' # Pull source packages from the classic CRAN mirror instead of PPM: +#' dock_from_desc( +#' "DESCRIPTION", +#' repos = c(CRAN = "https://cran.rstudio.com/") +#' ) +#' } #' @importFrom utils installed.packages packageVersion glob2rx #' @importFrom remotes dev_package_deps #' @importFrom desc desc_get_deps desc_get @@ -106,7 +144,7 @@ dock_from_desc <- function( ), AS = NULL, sysreqs = TRUE, - repos = c(CRAN = "https://cran.rstudio.com/"), + repos = c(CRAN = "https://p3m.dev/cran/latest"), expand = FALSE, update_tar_gz = TRUE, build_from_source = TRUE, @@ -244,6 +282,11 @@ dock_from_desc <- function( repos_as_character ) ) + # When `repos` is a single CRAN-keyed Posit Package Manager URL, + # rewrite the Rprofile.site line so the build pulls pre-compiled + # Linux binaries (the `__linux__/$VERSION_CODENAME/` shape + the + # strict `HTTPUserAgent` PPM requires). No-op for non-PPM repos. + .patch_rprofile_for_ppm(dock, repos) diff --git a/R/dockerfile.R b/R/dockerfile.R index 575ab88..e9b674b 100644 --- a/R/dockerfile.R +++ b/R/dockerfile.R @@ -11,8 +11,12 @@ public = list( Dockerfile = character(), #' @description #' Create a new Dockerfile object. -#' @param FROM The base image. -#' @param AS The name of the image. +#' @param FROM The base image. Default `"rocker/r-base"`. (Note: the +#' high-level generators [dock_from_desc()] and [dock_from_renv()] +#' use a different default, `rocker/r-ver` tagged with your R +#' version.) +#' @param AS Optional build-stage name (`FROM ... AS `). Default +#' `NULL` (no `AS`). #' @return A Dockerfile object. initialize = function(FROM = "rocker/r-base", AS = NULL) { @@ -80,8 +84,8 @@ LABEL = function(key, value) { self$Dockerfile <- c(self$Dockerfile, add_label(key, value)) }, #' @description -#' Add a ENV command. -#' @param key,value The key and value of the label. +#' Add an ENV command. +#' @param key,value The key and value of the environment variable. #' @return the Dockerfile object, invisibly. ENV = function(key, value) { self$Dockerfile <- c(self$Dockerfile, add_env(key, value)) diff --git a/R/get_sysreqs.R b/R/get_sysreqs.R index c34bb33..ba79a80 100644 --- a/R/get_sysreqs.R +++ b/R/get_sysreqs.R @@ -14,6 +14,12 @@ #' @export #' #' @return A vector of system requirements. +#' +#' @examples +#' \dontrun{ +#' get_sysreqs("glue") +#' get_sysreqs(c("curl", "xml2")) +#' } get_sysreqs <- function( packages, quiet = TRUE, diff --git a/R/rthis.R b/R/rthis.R index 3959f11..2f2f4d9 100644 --- a/R/rthis.R +++ b/R/rthis.R @@ -1,13 +1,18 @@ -#' Turn an R call into an Unix call +#' Turn an R expression into a shell `R -e '...'` call #' -#' @param code the function to call +#' Captures an R expression unevaluated and renders it as a single +#' shell-quoted `R -e '...'` string, suitable for a [Dockerfile] +#' `$RUN()` directive. #' -#' @return an unix R call +#' @param code an R expression (captured unevaluated) to wrap. +#' +#' @return a length-1 character string of the form `R -e '...'`, +#' shell-quoted with [base::shQuote()]. #' @export #' #' @examples #' r(print("yeay")) -#' r(install.packages("plumber", repo = "http://cran.irsn.fr/")) +#' r(install.packages("plumber", repos = "http://cran.irsn.fr/")) r <- function(code) { code <- paste(trimws(deparse(substitute(code))), collapse = " ") glue("R -e {shQuote(code, type = 'sh')}") diff --git a/README.Rmd b/README.Rmd index 2678f20..db6d4c6 100644 --- a/README.Rmd +++ b/README.Rmd @@ -47,9 +47,13 @@ Or from CRAN with : install.packages("dockerfiler") ``` +See `vignette("dockerfiler")` for a longer walkthrough. + ## Basic workflow -By default, Dockerfiles are created with `FROM "rocker/r-base"`. +By default, `Dockerfile$new()` creates a Dockerfile with `FROM "rocker/r-base"`. +(The high-level generators `dock_from_desc()` and `dock_from_renv()` use a +different default: `rocker/r-ver` tagged with your R version; see below.) You can set another FROM in `new()` @@ -201,10 +205,26 @@ renv::snapshot( - Build Dockerfile ```{r} -my_dock <- dock_from_renv( - lockfile = the_lockfile, - FROM = "rocker/verse" -) +my_dock <- dock_from_renv(lockfile = the_lockfile) +my_dock +``` + +By default the generated Dockerfile is `FROM rocker/r-ver:` +(multi-arch: linux/amd64 + linux/arm64), pulls pre-compiled Linux binaries +from Posit Public Package Manager (`https://p3m.dev/cran/latest`, rewritten +to the `__linux__/$VERSION_CODENAME/` shape at build time), and runs the +container as the non-root `rstudio` user. Pass `FROM = "rocker/r-base"`, +`repos = c(CRAN = "https://cran.rstudio.com/")`, or `user = NULL` to restore +the previous behaviour. `dock_from_desc()` uses the same `FROM` / `repos` +defaults. + +## Parse an existing Dockerfile + +Already have a Dockerfile? `parse_dockerfile()` reads it back into a +`Dockerfile` object you can edit and re-`$write()`. + +```{r} +my_dock <- parse_dockerfile("Dockerfile") my_dock ``` diff --git a/README.md b/README.md index e2bfccf..ce8dbe2 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,14 @@ Or from CRAN with : install.packages("dockerfiler") ``` +See `vignette("dockerfiler")` for a longer walkthrough. + ## Basic workflow -By default, Dockerfiles are created with `FROM "rocker/r-base"`. +By default, `Dockerfile$new()` creates a Dockerfile with +`FROM "rocker/r-base"`. (The high-level generators `dock_from_desc()` +and `dock_from_renv()` use a different default: `rocker/r-ver` tagged +with your R version; see below.) You can set another FROM in `new()` @@ -195,10 +200,27 @@ renv::snapshot( - Build Dockerfile ``` r -my_dock <- dock_from_renv( - lockfile = the_lockfile, - FROM = "rocker/verse" -) +my_dock <- dock_from_renv(lockfile = the_lockfile) +my_dock +``` + +By default the generated Dockerfile is +`FROM rocker/r-ver:` (multi-arch: linux/amd64 + +linux/arm64), pulls pre-compiled Linux binaries from Posit Public +Package Manager (`https://p3m.dev/cran/latest`, rewritten to the +`__linux__/$VERSION_CODENAME/` shape at build time), and runs the +container as the non-root `rstudio` user. Pass `FROM = "rocker/r-base"`, +`repos = c(CRAN = "https://cran.rstudio.com/")`, or `user = NULL` to +restore the previous behaviour. `dock_from_desc()` uses the same `FROM` +/ `repos` defaults. + +## Parse an existing Dockerfile + +Already have a Dockerfile? `parse_dockerfile()` reads it back into a +`Dockerfile` object you can edit and re-`$write()`. + +``` r +my_dock <- parse_dockerfile("Dockerfile") my_dock ``` diff --git a/cran-comments.md b/cran-comments.md index 4e2d146..f4036e7 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -129,10 +129,9 @@ its summary table. ## Other notes -* Test coverage stands at 99.84% (320+ tests). The single - uncovered line is a defensive `stop()` guard in - `dock_from_desc()` that fires only when `{pkgbuild}` is not - installed; since `{pkgbuild}` is in `Imports`, the guard is - unreachable in normal package use. +* Test coverage stands at 99.65% (`covr::package_coverage()`). The + handful of uncovered lines are defensive `stop()` guards that are + unreachable in normal package use; chiefly the `{pkgbuild}`-not- + installed guard in `dock_from_desc()` (`{pkgbuild}` is in `Imports`). * No URLs in the package metadata or vignettes 404 (verified with `urlchecker::url_check()` prior to submission). diff --git a/man/Dockerfile.Rd b/man/Dockerfile.Rd index d04dcac..9c99e88 100644 --- a/man/Dockerfile.Rd +++ b/man/Dockerfile.Rd @@ -61,9 +61,13 @@ Create a new Dockerfile object. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{FROM}}{The base image.} +\item{\code{FROM}}{The base image. Default \code{"rocker/r-base"}. (Note: the +high-level generators \code{\link[=dock_from_desc]{dock_from_desc()}} and \code{\link[=dock_from_renv]{dock_from_renv()}} +use a different default, \code{rocker/r-ver} tagged with your R +version.)} -\item{\code{AS}}{The name of the image.} +\item{\code{AS}}{Optional build-stage name (\verb{FROM ... AS }). Default +\code{NULL} (no \code{AS}).} } \if{html}{\out{
}} } @@ -245,7 +249,7 @@ the Dockerfile object, invisibly. \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Dockerfile-ENV}{}}} \subsection{Method \code{ENV()}}{ -Add a ENV command. +Add an ENV command. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Dockerfile$ENV(key, value)}\if{html}{\out{
}} } @@ -253,7 +257,7 @@ Add a ENV command. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{key, value}}{The key and value of the label.} +\item{\code{key, value}}{The key and value of the environment variable.} } \if{html}{\out{
}} } diff --git a/man/dockerfiles.Rd b/man/dockerfiles.Rd index 1627a35..366ed3d 100644 --- a/man/dockerfiles.Rd +++ b/man/dockerfiles.Rd @@ -9,7 +9,7 @@ dock_from_desc( FROM = paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor), AS = NULL, sysreqs = TRUE, - repos = c(CRAN = "https://cran.rstudio.com/"), + repos = c(CRAN = "https://p3m.dev/cran/latest"), expand = FALSE, update_tar_gz = TRUE, build_from_source = TRUE, @@ -28,17 +28,26 @@ dash, underscore, optional \verb{:tag} and / or \verb{@sha256:}); other values raise an error to prevent shell-metacharacter injection into the generated FROM directive.} -\item{AS}{The AS of the Dockerfile. Default it NULL. When non-NULL, -validated as a simple build-stage name (\verb{^[a-zA-Z0-9][a-zA-Z0-9._-]*$}).} +\item{AS}{The build-stage name of the Dockerfile (\verb{FROM ... AS }). +Default is \code{NULL} (no \code{AS}). When non-\code{NULL}, validated as a simple +build-stage name (\verb{^[a-zA-Z0-9][a-zA-Z0-9._-]*$}).} \item{sysreqs}{boolean. If TRUE, the Dockerfile will contain sysreq installation.} \item{repos}{character. The URL(s) of the repositories to use for -\code{options("repos")}. Each value must look like an http(s) URL (no -quotes, spaces or newlines); each name (when set) must be a simple -identifier (\verb{^[A-Za-z][A-Za-z0-9._-]*$}). Other values raise an -error to prevent injection into the generated \verb{echo "options(...)"} -shell command.} +\code{options("repos")}. Default is \code{c(CRAN = "https://p3m.dev/cran/latest")} +(Posit Public Package Manager). When \code{repos} is a single \code{CRAN}-keyed +PPM URL (\code{packagemanager.posit.co}, \code{packagemanager.rstudio.com}, or +\code{p3m.dev}), the codegen rewrites it to the +\verb{__linux__/$VERSION_CODENAME/} shape (codename resolved from +\verb{/etc/os-release} at image build time) and adds the strict +\code{HTTPUserAgent} PPM requires, so the build pulls pre-compiled Linux +binaries instead of compiling from source. Pass the legacy +\code{c(CRAN = "https://cran.rstudio.com/")} to opt out. Each value must +look like an http(s) URL (no quotes, spaces or newlines); each name +(when set) must be a simple identifier (\verb{^[A-Za-z][A-Za-z0-9._-]*$}). +Other values raise an error to prevent injection into the generated +\verb{echo "options(...)"} shell command.} \item{expand}{boolean. If \code{TRUE} each system requirement will have its own \code{RUN} line.} @@ -81,3 +90,35 @@ Dockerfile \description{ Create a Dockerfile from a DESCRIPTION } +\details{ +Two install strategies are available for the package itself: +\itemize{ +\item \code{build_from_source = TRUE} (the default): the generated Dockerfile +mounts the source folder and installs from it directly. \code{update_tar_gz} +is ignored. +\item \code{build_from_source = FALSE}: a source tarball (\verb{_.tar.gz}) +is \code{COPY}'d into the image and installed with +\code{remotes::install_local()}. When \code{update_tar_gz = TRUE}, a fresh +tarball is built with \code{pkgbuild::build()} first (and any stale +\verb{_*.tar.gz} in the current directory is removed); when +\code{update_tar_gz = FALSE}, an already-built tarball is expected +alongside the \code{DESCRIPTION}. +} + +The package name and its dependency-field names are read from the +\code{DESCRIPTION} and validated against the CRAN package-name grammar +before being interpolated into the generated directives. +} +\examples{ +\dontrun{ +# From the DESCRIPTION of the package in the working directory: +dock <- dock_from_desc("DESCRIPTION") +dock + +# Pull source packages from the classic CRAN mirror instead of PPM: +dock_from_desc( + "DESCRIPTION", + repos = c(CRAN = "https://cran.rstudio.com/") +) +} +} diff --git a/man/get_sysreqs.Rd b/man/get_sysreqs.Rd index d76ad08..9936ec6 100644 --- a/man/get_sysreqs.Rd +++ b/man/get_sysreqs.Rd @@ -20,3 +20,9 @@ A vector of system requirements. This function retrieves information about the system requirements using the \code{pak::pkg_sysreqs()}. } +\examples{ +\dontrun{ +get_sysreqs("glue") +get_sysreqs(c("curl", "xml2")) +} +} diff --git a/man/r.Rd b/man/r.Rd index 1ab79fa..c313716 100644 --- a/man/r.Rd +++ b/man/r.Rd @@ -2,20 +2,23 @@ % Please edit documentation in R/rthis.R \name{r} \alias{r} -\title{Turn an R call into an Unix call} +\title{Turn an R expression into a shell \verb{R -e '...'} call} \usage{ r(code) } \arguments{ -\item{code}{the function to call} +\item{code}{an R expression (captured unevaluated) to wrap.} } \value{ -an unix R call +a length-1 character string of the form \verb{R -e '...'}, +shell-quoted with \code{\link[base:shQuote]{base::shQuote()}}. } \description{ -Turn an R call into an Unix call +Captures an R expression unevaluated and renders it as a single +shell-quoted \verb{R -e '...'} string, suitable for a \link{Dockerfile} +\verb{$RUN()} directive. } \examples{ r(print("yeay")) -r(install.packages("plumber", repo = "http://cran.irsn.fr/")) +r(install.packages("plumber", repos = "http://cran.irsn.fr/")) } diff --git a/tests/testthat/test-dock_from_desc.R b/tests/testthat/test-dock_from_desc.R index faf0288..5afed95 100644 --- a/tests/testthat/test-dock_from_desc.R +++ b/tests/testthat/test-dock_from_desc.R @@ -120,6 +120,36 @@ withr::with_dir( }) + test_that("dock_from_desc default repos is p3m.dev/cran/latest", { + fmls <- formals(dock_from_desc) + expect_equal( + deparse(fmls$repos), + 'c(CRAN = "https://p3m.dev/cran/latest")' + ) + }) + + test_that("dock_from_desc with the default repos triggers the PPM binary rewrite (codename + UA + os-release prefix)", { + skip_if(is_rdevel, "skip on R-devel") + out <- dock_from_desc( + file.path(".", "DESCRIPTION__"), + sysreqs = FALSE + ) + df <- paste(out$Dockerfile, collapse = "\n") + # The default p3m.dev URL must be rewritten to the + # `__linux__/$VERSION_CODENAME/` shape so the build pulls Linux + # binaries instead of compiling from source. + expect_match( + df, + "https://p3m\\.dev/cran/__linux__/\\$VERSION_CODENAME/latest" + ) + # The HTTPUserAgent line must be emitted (PPM serves source if it + # is missing, even with a `__linux__/` URL). + expect_match(df, "HTTPUserAgent = sprintf\\('R \\(") + # The RUN that uses $VERSION_CODENAME must be prefixed so the var + # is resolved from /etc/os-release at build time. + expect_match(df, "\\. /etc/os-release && ") + }) + test_that("dock_from_desc(strict_install = TRUE) prepends options(warn = 2) to every install RUN", { skip_if(is_rdevel, "skip on R-devel") out <- dock_from_desc( diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 2ba27af..a739281 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -27,3 +27,51 @@ test_that("utils works", { ) ) }) + +test_that(".validate_* helpers reject malformed inputs (type / length / NA guards)", { + # Each `.validate_*` interpolates its argument into a generated + # Dockerfile directive, so the type/length/NA guards are part of the + # security contract -- they must fire, not just the format-regex + # branch (which is exercised elsewhere via the dock_from_* tests). + + expect_error(dockerfiler:::.validate_FROM(123L), "FROM") + expect_error(dockerfiler:::.validate_FROM(c("a", "b")), "FROM") + expect_error(dockerfiler:::.validate_r_version(123L), "single string") + expect_error(dockerfiler:::.validate_r_version(c("4.5", "4.6")), "single string") + expect_error(dockerfiler:::.validate_AS(123L), "AS") + expect_error(dockerfiler:::.validate_AS(c("a", "b")), "AS") + expect_error(dockerfiler:::.validate_renv_version(123L), "renv_version") + expect_error(dockerfiler:::.validate_renv_version(c("1.0", "2.0")), "renv_version") + expect_error(dockerfiler:::.validate_lockfile(123L), "lockfile") + expect_error(dockerfiler:::.validate_lockfile(c("a", "b")), "lockfile") + expect_error(dockerfiler:::.validate_pkg_name(123L), "package name") + expect_error(dockerfiler:::.validate_pkg_name(c("a", "b")), "package name") + expect_error(dockerfiler:::.validate_pkg_name(NA_character_), "package name") + expect_error(dockerfiler:::.validate_renv_paths_cache(123L), "renv_paths_cache") + expect_error(dockerfiler:::.validate_renv_paths_cache(c("/a", "/b")), "renv_paths_cache") + + # `NULL` is accepted by the validators that document a `NULL` default. + expect_silent(dockerfiler:::.validate_AS(NULL)) + expect_silent(dockerfiler:::.validate_renv_version(NULL)) + expect_silent(dockerfiler:::.validate_renv_paths_cache(NULL)) + + # Vector validators: a non-character input must raise; an empty vector + # is the legitimate "no dependencies" case and must be silent. + expect_error(dockerfiler:::.validate_repos(123L), "character vector") + expect_error(dockerfiler:::.validate_extra_sysreqs(123L), "character vector") + expect_error(dockerfiler:::.validate_pkg_names(123L), "character vector") + expect_silent(dockerfiler:::.validate_extra_sysreqs(NULL)) + expect_silent(dockerfiler:::.validate_pkg_names(character(0))) + + # Scalar-logical validator: anything that is not a single TRUE/FALSE. + expect_error(dockerfiler:::.validate_scalar_logical(1L, "x"), "x") + expect_error(dockerfiler:::.validate_scalar_logical(c(TRUE, FALSE), "x"), "x") + expect_error(dockerfiler:::.validate_scalar_logical(NA, "x"), "x") + expect_error(dockerfiler:::.validate_scalar_logical("TRUE", "x"), "x") + + # Format-regex branch of the validators that the dock_from_* tests do + # not already cover directly. + expect_error(dockerfiler:::.validate_renv_paths_cache("relative/path"), "renv_paths_cache") + expect_error(dockerfiler:::.validate_pkg_names(c("ok", "1bad")), "package name") + expect_silent(dockerfiler:::.validate_pkg_names(c("cli", "glue", "R.utils"))) +}) diff --git a/vignettes/dockerfiler.Rmd b/vignettes/dockerfiler.Rmd index b145a43..834dc2f 100644 --- a/vignettes/dockerfiler.Rmd +++ b/vignettes/dockerfiler.Rmd @@ -37,7 +37,9 @@ install.packages("dockerfiler") ## Basic workflow -By default, Dockerfiles are created with `FROM "rocker/r-base"`. +By default, `Dockerfile$new()` creates a Dockerfile with `FROM "rocker/r-base"`. +(The high-level generators `dock_from_desc()` and `dock_from_renv()` use a +different default: `rocker/r-ver` tagged with your R version; see below.) You can set another FROM in `new()` @@ -51,7 +53,7 @@ my_dock$MAINTAINER("Colin FAY", "contact@colinfay.me") Wrap your raw R Code inside the `r()` function to turn it into a bash command with `R -e`. ```{r} -my_dock$RUN(r(install.packages("attempt", repo = "http://cran.irsn.fr/"))) +my_dock$RUN(r(install.packages("attempt", repos = "http://cran.irsn.fr/"))) ``` Classical Docker stuffs: @@ -109,7 +111,7 @@ Save your Dockerfile: my_dock$write() ``` -## Create a Dockerfile from a DESCRIPTION +## Create a Dockerfile from a DESCRIPTION You can use a DESCRIPTION file to create a Dockerfile that installs the dependencies and the package. @@ -119,8 +121,41 @@ my_dock <- dock_from_desc("DESCRIPTION") my_dock$CMD(r(library(dockerfiler))) my_dock$add_after( - cmd = "RUN R -e 'remotes::install_cran(\"rlang\")'", + cmd = "RUN R -e 'remotes::install_cran(\"rlang\")'", after = 3 ) ``` +`dock_from_desc()` defaults to `FROM rocker/r-ver:` and +pulls Linux binaries from Posit Public Package Manager +(`https://p3m.dev/cran/latest`). Pass `FROM = "rocker/r-base"` or +`repos = c(CRAN = "https://cran.rstudio.com/")` to opt out. + +## Create a Dockerfile from an renv.lock + +If your project uses `{renv}`, `dock_from_renv()` turns the `renv.lock` +into a Dockerfile that restores the exact pinned versions. + +```{r, eval = FALSE} +my_dock <- dock_from_renv(lockfile = "renv.lock") +my_dock +``` + +By default the generated Dockerfile is `FROM rocker/r-ver:` +(multi-arch amd64 + arm64), pulls Linux binaries from Posit Public Package +Manager, and runs the container as the non-root `rstudio` user. Pass +`FROM = "rocker/r-base"`, `repos = c(CRAN = "https://cran.rstudio.com/")`, +or `user = NULL` to restore the previous behaviour. + +## Parse an existing Dockerfile + +Already have a Dockerfile? `parse_dockerfile()` reads it back into a +`Dockerfile` object you can edit and re-`$write()`. + +```{r, eval = FALSE} +my_dock <- parse_dockerfile("Dockerfile") +my_dock +my_dock$RUN(r(library(dockerfiler))) +my_dock$write("Dockerfile") +``` +