Skip to content

Commit 1f6375d

Browse files
feat(dock_from_desc): add strict_install parameter (default TRUE) (#105)
Closes #9. The previous behaviour of `R -e 'remotes::install_*(...)'` lets a docker build succeed even when an install emitted a warning that silently masked a real failure: "package was archived from CRAN", "partial download, retrying", "no installation candidate". The docker user only finds out the package is missing at container runtime. Add a new `strict_install` parameter, default `TRUE`. When `TRUE`, every install RUN is prefixed with `options(warn = 2);` so any R warning during install becomes a hard error and aborts the build. The opt-out (`strict_install = FALSE`) restores the previous tolerant behaviour for build environments that emit benign warnings (locale defaulting, NTP time-verification, ABI-version notices) the caller does not want to fail the build. The injection covers the five install RUN sites in `dock_from_desc()`: - `R -e 'install.packages("remotes")'` - `Rscript -e 'remotes::install_version(...)'` per CRAN dep - `Rscript -e 'remotes::install_github(...)'` per non-CRAN dep - `R -e 'remotes::install_local("/app.tar.gz")'` (build_from_source = FALSE) - `R -e 'remotes::install_local()'` (build_from_source = TRUE) `dock_from_renv()` is not covered by this PR; it has its own install RUN family (`renv::restore()`, `install.packages("renv")`, `install_version("renv", ...)`) and a separate POC will extend the same `options(warn = 2);` injection there. R CMD check --as-cran: 0 errors / 0 warnings / 0 notes.
1 parent f0624ff commit 1f6375d

5 files changed

Lines changed: 177 additions & 12 deletions

File tree

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@
4343
cache location at image build time with
4444
`--build-arg RENV_PATHS_CACHE=...` without regenerating the
4545
Dockerfile.
46+
- `dock_from_desc()` gains a `strict_install` parameter (default
47+
`TRUE`). When `TRUE`, every install RUN in the generated Dockerfile
48+
is prefixed with `options(warn = 2);` so any R warning during
49+
install (missing CRAN package, partial download, archived package,
50+
404 on a remote) becomes a hard error and aborts the docker build.
51+
This is a behaviour change for users regenerating their Dockerfile:
52+
install RUNs now refuse to silently swallow warnings. Pass
53+
`strict_install = FALSE` if your build environment routinely
54+
emits benign warnings (locale defaulting, NTP time-verification,
55+
ABI-version notices) that you do not want to fail the build.
56+
Closes #9.
4657

4758
## Bug fixes
4859

R/dock_from_desc.R

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ quote_not_na <- function(x){
6363
#' RUN; the PAT is never persisted in the image; requires BuildKit, so
6464
#' pass with
6565
#' `DOCKER_BUILDKIT=1 docker build --secret id=github_pat,env=GITHUB_PAT ...`).
66+
#' @param strict_install boolean. When `TRUE` (the default), every
67+
#' install RUN in the generated Dockerfile is prefixed with
68+
#' `options(warn = 2);` so that any R warning during install
69+
#' (missing CRAN package, partial download, archived package,
70+
#' 404 on a remote) becomes a hard error and aborts the docker
71+
#' build. Set to `FALSE` if your build environment routinely emits
72+
#' benign warnings (locale defaulting, NTP time-verification,
73+
#' ABI-version notices) that you do not want to fail the build.
74+
#' Must be a single scalar logical; `NA`, character, numeric,
75+
#' `NULL` and length-2+ vectors are rejected with an error.
6676
#'
6777
#' @export
6878
#' @rdname dockerfiles
@@ -89,9 +99,20 @@ dock_from_desc <- function(
8999
update_tar_gz = TRUE,
90100
build_from_source = TRUE,
91101
extra_sysreqs = NULL,
92-
github_pat = c("none", "build_arg", "secret")
102+
github_pat = c("none", "build_arg", "secret"),
103+
strict_install = TRUE
93104
) {
94105
github_pat <- match.arg(github_pat)
106+
if (
107+
!is.logical(strict_install) ||
108+
length(strict_install) != 1L ||
109+
is.na(strict_install)
110+
) {
111+
stop(
112+
"`strict_install` must be a single `TRUE` or `FALSE`, got: ",
113+
deparse(strict_install)
114+
)
115+
}
95116
path <- fs::path_abs(path)
96117

97118
packages <- desc_get_deps(path)$package
@@ -207,17 +228,28 @@ dock_from_desc <- function(
207228

208229

209230

210-
dock$RUN("R -e 'install.packages(\"remotes\")'")
231+
strict_prefix <- .r_strict_prefix(strict_install)
232+
233+
dock$RUN(
234+
sprintf(
235+
"R -e '%sinstall.packages(\"remotes\")'",
236+
strict_prefix
237+
)
238+
)
211239

212240
if (length(packages_on_cran) > 0) {
213241
ping <- mapply(
214-
function(dock, ver, nm) {
215-
res <- dock$RUN(sprintf("Rscript -e 'remotes::install_version(\"%s\",upgrade=\"never\", version = %s)'",
216-
nm, ver))
242+
function(dock, ver, nm, strict_prefix) {
243+
res <- dock$RUN(sprintf(
244+
"Rscript -e '%sremotes::install_version(\"%s\",upgrade=\"never\", version = %s)'",
245+
strict_prefix,
246+
nm,
247+
ver
248+
))
217249
},
218250
ver = quote_not_na(packages_on_cran),
219251
nm = names(packages_on_cran),
220-
MoreArgs = list(dock = dock)
252+
MoreArgs = list(dock = dock, strict_prefix = strict_prefix)
221253
)
222254
}
223255

@@ -243,17 +275,18 @@ dock_from_desc <- function(
243275

244276

245277
pong <- mapply(
246-
function(dock, ver, nm) {
278+
function(dock, ver, strict_prefix) {
247279
res <- dock$RUN(
248280
sprintf(
249-
"%sRscript -e 'remotes::install_github(\"%s\")'",
281+
"%sRscript -e '%sremotes::install_github(\"%s\")'",
250282
.github_pat_run_prefix(github_pat),
283+
strict_prefix,
251284
ver
252285
)
253286
)
254287
},
255288
ver = nn,
256-
MoreArgs = list(dock = dock)
289+
MoreArgs = list(dock = dock, strict_prefix = strict_prefix)
257290
)
258291
}
259292

@@ -313,7 +346,9 @@ dock_from_desc <- function(
313346
dock$RUN(
314347
paste0(
315348
.github_pat_run_prefix(github_pat),
316-
"R -e 'remotes::install_local(\"/app.tar.gz\",upgrade=\"never\")'"
349+
"R -e '",
350+
strict_prefix,
351+
"remotes::install_local(\"/app.tar.gz\",upgrade=\"never\")'"
317352
)
318353
)
319354
dock$RUN("rm /app.tar.gz")
@@ -324,7 +359,9 @@ dock_from_desc <- function(
324359
dock$RUN(
325360
paste0(
326361
.github_pat_run_prefix(github_pat),
327-
"R -e 'remotes::install_local(upgrade=\"never\")'"
362+
"R -e '",
363+
strict_prefix,
364+
"remotes::install_local(upgrade=\"never\")'"
328365
)
329366
)
330367
dock$RUN("rm -rf /build_zone")

R/utils.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ cat_info <- function(...) {
7272
}
7373
}
7474

75+
#' Strict-install prefix for an R / Rscript invocation in a Dockerfile RUN.
76+
#'
77+
#' When `strict_install = TRUE`, returns `"options(warn = 2); "` so
78+
#' that warnings emitted during the install (e.g. a missing CRAN
79+
#' package, a 404 on a remote, a partial install) become hard
80+
#' errors and the docker build aborts. Otherwise an empty string,
81+
#' which preserves the legacy behavior where install warnings did
82+
#' not fail the build.
83+
#' @noRd
84+
.r_strict_prefix <- function(strict_install) {
85+
# Caller must pass a single TRUE / FALSE; `dock_from_desc()` validates.
86+
if (strict_install) {
87+
"options(warn = 2); "
88+
} else {
89+
""
90+
}
91+
}
92+
7593
#' Emit a one-shot reminder describing how the PAT must be supplied at
7694
#' `docker build` time. No-op when mode is `"none"`.
7795
#' @noRd

man/dockerfiles.Rd

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-dock_from_desc.R

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,93 @@ withr::with_dir(
120120

121121
})
122122

123+
test_that("dock_from_desc(strict_install = TRUE) prepends options(warn = 2) to every install RUN", {
124+
skip_if(is_rdevel, "skip on R-devel")
125+
out <- dock_from_desc(
126+
file.path(".", "DESCRIPTION__"),
127+
sysreqs = FALSE,
128+
strict_install = TRUE
129+
)
130+
install_lines <- grep(
131+
"(R|Rscript) -e '.*install",
132+
out$Dockerfile,
133+
value = TRUE
134+
)
135+
expect_gt(length(install_lines), 0L)
136+
for (line in install_lines) {
137+
expect_match(
138+
line,
139+
"options\\(warn = 2\\);",
140+
info = sprintf(
141+
"install RUN must carry options(warn = 2): %s",
142+
line
143+
)
144+
)
145+
}
146+
})
147+
148+
test_that("dock_from_desc(strict_install = FALSE) does not prepend options(warn = 2)", {
149+
skip_if(is_rdevel, "skip on R-devel")
150+
out <- dock_from_desc(
151+
file.path(".", "DESCRIPTION__"),
152+
sysreqs = FALSE,
153+
strict_install = FALSE
154+
)
155+
df <- paste(out$Dockerfile, collapse = "\n")
156+
expect_false(grepl("options\\(warn = 2\\)", df))
157+
})
158+
159+
test_that("dock_from_desc default is strict_install = TRUE so install warnings fail the build", {
160+
fmls <- formals(dock_from_desc)
161+
expect_true("strict_install" %in% names(fmls))
162+
expect_true(fmls$strict_install)
163+
})
164+
165+
test_that("dock_from_desc rejects non-scalar / NA / non-logical strict_install", {
166+
skip_if(is_rdevel, "skip on R-devel")
167+
168+
expect_error(
169+
dock_from_desc(
170+
file.path(".", "DESCRIPTION__"),
171+
sysreqs = FALSE,
172+
strict_install = NA
173+
),
174+
"single `TRUE` or `FALSE`"
175+
)
176+
expect_error(
177+
dock_from_desc(
178+
file.path(".", "DESCRIPTION__"),
179+
sysreqs = FALSE,
180+
strict_install = c(TRUE, FALSE)
181+
),
182+
"single `TRUE` or `FALSE`"
183+
)
184+
expect_error(
185+
dock_from_desc(
186+
file.path(".", "DESCRIPTION__"),
187+
sysreqs = FALSE,
188+
strict_install = "TRUE"
189+
),
190+
"single `TRUE` or `FALSE`"
191+
)
192+
expect_error(
193+
dock_from_desc(
194+
file.path(".", "DESCRIPTION__"),
195+
sysreqs = FALSE,
196+
strict_install = 1
197+
),
198+
"single `TRUE` or `FALSE`"
199+
)
200+
expect_error(
201+
dock_from_desc(
202+
file.path(".", "DESCRIPTION__"),
203+
sysreqs = FALSE,
204+
strict_install = NULL
205+
),
206+
"single `TRUE` or `FALSE`"
207+
)
208+
})
209+
123210
test_that("dock_from_desc emits no GITHUB_PAT plumbing by default", {
124211
skip_if(is_rdevel, "skip on R-devel")
125212
my_dock <- dock_from_desc(file.path(".", "DESCRIPTION__"))

0 commit comments

Comments
 (0)