From 62c319c8a5aa8ed7daa1f88c5b94221e50de364e Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Wed, 7 May 2025 09:38:34 +0000 Subject: [PATCH] RC 0.2.5 --- DESCRIPTION | 2 +- NEWS.md | 9 +- R/dock_from_renv.R | 1 - R/gen_base_image.R | 1 - README.Rmd | 31 +- README.md | 39 +- dev/config_fusen.yaml | 16 - dev/flat_dock_from_renv.Rmd | 515 ------------------------ tests/testthat/renv_Dockerfile | 2 +- tests/testthat/test-dock_from_renv.R | 13 +- vignettes/dockerfile-from-renv-lock.Rmd | 1 - 11 files changed, 66 insertions(+), 564 deletions(-) delete mode 100644 dev/config_fusen.yaml delete mode 100644 dev/flat_dock_from_renv.Rmd diff --git a/DESCRIPTION b/DESCRIPTION index 72dbdfc..2fac6cc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: dockerfiler Title: Easy Dockerfile Creation from R -Version: 0.2.4.9000 +Version: 0.2.5 Authors@R: c( person("Colin", "Fay", , "contact@colinfay.me", role = c("cre", "aut"), comment = c(ORCID = "0000-0001-7343-1846")), diff --git a/NEWS.md b/NEWS.md index 28ce5d8..f173fee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,10 @@ -# dockerfiler 0.2.4.9xxx +# dockerfiler 0.2.5 + +- feat: allow multistage dockerfile creation +- feat: COPY function can now specify a stage to copy from. +- feat: add dedicated cache for `renv::restore` +- feat: add COMMENT function to add comment in Dockerfile thanks to @jcrodriguez1989 -- allow multistage dockerfile creation -- add dedicated cache for `renv::restore` # dockerfiler 0.2.4 diff --git a/R/dock_from_renv.R b/R/dock_from_renv.R index c2cfebf..a0d863d 100644 --- a/R/dock_from_renv.R +++ b/R/dock_from_renv.R @@ -1,4 +1,3 @@ -# WARNING - Generated by {fusen} from dev/flat_dock_from_renv.Rmd: do not edit by hand #' @importFrom memoise memoise #' @noRd diff --git a/R/gen_base_image.R b/R/gen_base_image.R index 72e32df..d1704fe 100644 --- a/R/gen_base_image.R +++ b/R/gen_base_image.R @@ -1,4 +1,3 @@ -# WARNING - Generated by {fusen} from dev/flat_dock_from_renv.Rmd: do not edit by hand #' Generate base image name #' diff --git a/README.Rmd b/README.Rmd index 3de4b0f..a2c123b 100644 --- a/README.Rmd +++ b/README.Rmd @@ -32,12 +32,6 @@ You're reading the doc about version : desc::desc_get_version() ``` -The check results are: - -```{r eval = TRUE} -devtools::check(quiet = TRUE) -``` - ## Installation You can install dockerfiler from GitHub with: @@ -135,6 +129,31 @@ Save your Dockerfile: my_dock$write() ``` + +## Multi-stage dockerfile + +Here is an example of generating a multi-stage Dockerfile directly from R: we create two Dockerfile objects, one for the build stage (builder) and one for the final stage (final), and then merge them into a single file. + +```{r} +stage_1 <- Dockerfile$new( + FROM = "alpine",AS ="builder" +) +stage_1$RUN('echo "Hi from builder" > /coucou.txt') + +stage_2 <- Dockerfile$new( + FROM = "ubuntu", AS = "final" +) +stage_2$COMMENT("copy /coucou.txt from builder to /truc.txt in final") +stage_2$COPY(from = "/coucou",to = "/truc.txt",force = TRUE, stage ="builder") +stage_2$RUN( "cat /truc.txt") + +stage_1$write() +stage_2$write(append = TRUE) +#file.edit("Dockerfile") +``` + + + ## Create a Dockerfile from a DESCRIPTION You can use a DESCRIPTION file to create a Dockerfile that installs the dependencies and the package. diff --git a/README.md b/README.md index d6c693d..fd6e06b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ + [![R-CMD-check](https://github.com/ThinkR-open/dockerfiler/workflows/R-CMD-check/badge.svg)](https://github.com/ThinkR-open/dockerfiler/actions) @@ -18,18 +19,7 @@ You’re reading the doc about version : ``` r desc::desc_get_version() -#> [1] '0.2.3' -``` - -The check results are: - -``` r -devtools::check(quiet = TRUE) -#> ℹ Loading dockerfiler -#> ── R CMD check results ────────────────────────────────── dockerfiler 0.2.3 ──── -#> Duration: 1m 31.3s -#> -#> 0 errors ✔ | 0 warnings ✔ | 0 notes ✔ +#> [1] '0.2.5' ``` ## Installation @@ -131,6 +121,31 @@ Save your Dockerfile: my_dock$write() ``` +## Multi-stage dockerfile + +Here is an example of generating a multi-stage Dockerfile directly from +R: we create two Dockerfile objects, one for the build stage (builder) +and one for the final stage (final), and then merge them into a single +file. + +``` r +stage_1 <- Dockerfile$new( + FROM = "alpine",AS ="builder" +) +stage_1$RUN('echo "Hi from builder" > /coucou.txt') + +stage_2 <- Dockerfile$new( + FROM = "ubuntu", AS = "final" +) +stage_2$COMMENT("copy /coucou.txt from builder to /truc.txt in final") +stage_2$COPY(from = "/coucou",to = "/truc.txt",force = TRUE, stage ="builder") +stage_2$RUN( "cat /truc.txt") + +stage_1$write() +stage_2$write(append = TRUE) +#file.edit("Dockerfile") +``` + ## Create a Dockerfile from a DESCRIPTION You can use a DESCRIPTION file to create a Dockerfile that installs the diff --git a/dev/config_fusen.yaml b/dev/config_fusen.yaml deleted file mode 100644 index af07a3b..0000000 --- a/dev/config_fusen.yaml +++ /dev/null @@ -1,16 +0,0 @@ -flat_dock_from_renv.Rmd: - path: dev/flat_dock_from_renv.Rmd - state: active - R: - - R/dock_from_renv.R - - R/gen_base_image.R - tests: tests/testthat/test-dock_from_renv.R - vignettes: vignettes/dockerfile-from-renv-lock.Rmd - inflate: - flat_file: dev/flat_dock_from_renv.Rmd - vignette_name: Dockerfile from renv.lock - open_vignette: false - check: false - document: false - overwrite: 'yes' - clean: ask diff --git a/dev/flat_dock_from_renv.Rmd b/dev/flat_dock_from_renv.Rmd deleted file mode 100644 index 6c4d18a..0000000 --- a/dev/flat_dock_from_renv.Rmd +++ /dev/null @@ -1,515 +0,0 @@ ---- -title: "flat_dock_from_renv.Rmd empty" -output: html_document -editor_options: - chunk_output_type: console ---- - -```{r development, include=FALSE} -library(testthat) -``` - - - -# gen_base_image - -```{r function-gen_base_image} -#' Generate base image name -#' -#' Creates the base image name from the provided distro name and the R version found in the `renv.lock` file. -#' -#' @keywords internal -gen_base_image <- function( - distro = NULL, - r_version = "4.0", - FROM = "rstudio/r-base" -) { - -if (!is.null(distro)){ - warning("the `distro` parameter is not used anymore, only debian/ubuntu based images are supported") -} - - glue::glue("{FROM}:{r_version}") - -} -``` - - - - -# Create a Dockerfile from a renv.lock file - - - -```{r function-dock_from_renv} -#' @importFrom memoise memoise -#' @noRd -pkg_sysreqs_mem <- memoise::memoise( - pak::pkg_sysreqs -) - - -#' Create a Dockerfile from an `renv.lock` file -#' -#' @param lockfile Path to an `renv.lock` file to use as an input.. -#' @param FROM Docker image to start FROM Default is FROM rocker/r-base -#' @param AS The AS of the Dockerfile. Default it `NULL`. -#' @param distro - deprecated - only debian/ubuntu based images are supported -#' @param sysreqs boolean. If `TRUE`, the Dockerfile will contain sysreq installation. -#' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line. -#' @param repos character. The URL(s) of the repositories to use for `options("repos")`. -#' @param extra_sysreqs character vector. Extra debian system requirements. -#' Will be installed with apt-get install. -#' @param renv_version character. The renv version to use in the generated Dockerfile. By default, it is set to the version specified in the `renv.lock` file. -#' If the `renv.lock` file does not specify a renv version, -#' the version of renv bundled with dockerfiler, -#' specifically `r dockerfiler::renv$initialize();toString(dockerfiler::renv$the$metadata$version)`, will be used. -#' If you set it to `NULL`, the latest available version of renv will be used. -#' @param use_pak boolean. If `TRUE` use pak to deal with dependencies during `renv::restore()`. FALSE by default -#' @param user Name of the user to specify in the Dockerfile with the USER instruction. Default is `NULL`, in which case the user from the FROM image is used. -#' @param dependencies What kinds of dependencies to install. Most commonly -#' one of the following values: -#' - `NA`: only required (hard) dependencies, -#' - `TRUE`: required dependencies plus optional and development -#' dependencies, -#' - `FALSE`: do not install any dependencies. (You might end up with a -#' non-working package, and/or the installation might fail.) -#' @param sysreqs_platform System requirements platform.`ubuntu` by default. If `NULL`, then the current platform is used. Can be : "ubuntu-22.04" if needed to fit with the `FROM` Operating System. Only debian or ubuntu based images are supported -#' @importFrom utils getFromNamespace -#' @return A R6 object of class `Dockerfile`. -#' @details -#' -#' System requirements for packages are provided -#' through RStudio Package Manager via the pak -#' package. The install commands provided from pak -#' are added as `RUN` directives within the `Dockerfile`. -#' -#' The R version is taken from the `renv.lock` file. -#' Packages are installed using `renv::restore()` which ensures -#' that the proper package version and source is used when installed. -#' -#' @importFrom attempt map_try_catch -#' @importFrom glue glue -#' @importFrom pak pkg_sysreqs -#' @importFrom purrr keep_at pluck - -#' @export -dock_from_renv <- function( - lockfile = "renv.lock", - distro = NULL, - FROM = "rocker/r-base", - AS = NULL, - sysreqs = TRUE, - repos = c(CRAN = "https://cran.rstudio.com/"), - expand = FALSE, - extra_sysreqs = NULL, - use_pak = FALSE, - user = NULL, - dependencies = NA, - sysreqs_platform = "ubuntu", - renv_version -) { - try(dockerfiler::renv$initialize(),silent=TRUE) - lock <- dockerfiler::renv$lockfile_read(file = lockfile) # using vendored renv - # https://rstudio.github.io/renv/reference/vendor.html?q=vendor#null - - # start the dockerfile - R_major_minor <- lock$R$Version - dock <- Dockerfile$new( - FROM = gen_base_image( - r_version = R_major_minor, - FROM = FROM - ), - AS = AS - ) - if (!is.null(user)) { - dock$USER(user) - } - # get renv version - - if (missing(renv_version)) { - if (!is.null(lock$Packages$renv$Version)) { - renv_version <- lock$Packages$renv$Version - } else { - renv_version <- dockerfiler::renv$the$metadata$version - } - } - - message("renv version = ", - ifelse(!is.null(renv_version),renv_version,"the must up to date in the repos") - ) - - - distro_args <- list(sysreqs_platform = sysreqs_platform) - - install_cmd <- "apt-get install -y" - update_cmd <-"apt-get update -y" - clean_cmd <- "rm -rf /var/lib/apt/lists/*" - - pkgs <- names(lock$Packages) - - if (sysreqs) { - - # please wait during system requirement calculation - cat_bullet( - "Please wait while we compute system requirements...", - bullet = "info", - bullet_col = "green" - ) - - message( - sprintf( - "Fetching system dependencies for %s package(s) records.", - length(pkgs) - ) - ) - - pkg_os <- lapply( - pkgs, - FUN = function(x) { - c( - list(pkg = x, - dependencies = dependencies), - distro_args - ) - } - ) - - - pkg_sysreqs <- unlist(attempt::map_try_catch( - pkg_os, - function(x) { - keep_at( - pluck( - do.call(pkg_sysreqs_mem, x), - "packages" - ), - "system_packages" - ) - }, - .e = ~ character(0) - )) - - - - - - pkg_installs <- - lapply( - X = unique(pkg_sysreqs), - FUN = function(.x) { - paste0(install_cmd, " ", .x) - } - ) - - if (length(unlist(pkg_installs)) == 0) { - cat_bullet( - "No sysreqs required", - bullet = "info", - bullet_col = "green" - ) - } - - cat_green_tick("Done") # TODO animated version ? - } else { - pkg_installs <- NULL - } - - # extra_sysreqs - - - - - if (length(extra_sysreqs) > 0) { - extra <- paste( - install_cmd, - extra_sysreqs - ) - pkg_installs <- unique(c(pkg_installs, extra)) - } - - - - - - # compact - if (!expand) { - # we compact sysreqs - pkg_installs <- compact_sysreqs( - pkg_installs, - update_cmd = update_cmd, - install_cmd = install_cmd, - clean_cmd = clean_cmd - ) - - } else { - dock$RUN(update_cmd) - } - - do.call(dock$RUN, list(pkg_installs)) - - if (expand) { - dock$RUN(clean_cmd) - } - - repos_as_character <- repos_as_character(repos) - dock$RUN("mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/") - - dock$RUN( - sprintf( - "echo \"options(renv.config.pak.enabled = %s, repos = %s, download.file.method = 'libcurl', Ncpus = 4)\" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site", - use_pak, - repos_as_character - ) - ) - - - if (!is.null(renv_version)){ - dock$RUN("R -e 'install.packages(\"remotes\")'") - install_renv_string <- paste0( - "R -e 'remotes::install_version(\"renv\", version = \"", - renv_version, - "\")'" - ) - dock$RUN(install_renv_string) - - } else { - dock$RUN("R -e 'install.packages(c(\"renv\",\"remotes\"))'") - } - - dock$COPY(basename(lockfile), "renv.lock") - dock$RUN("R -e 'renv::restore()'") - - dock -} - -``` - -```{r example-dock_from_renv} -#' \dontrun{ -#' dock <- dock_from_renv("renv.lock", distro = "xenial") -#' dock$write("Dockerfile") -#' } -``` - - -```{r test-dock_from_renv, eval=FALSE} -# A temporary directory -dir_build <- tempfile(pattern = "renv") -dir.create(dir_build) - -# Create a lockfile -the_lockfile <- file.path(dir_build, "renv.lock") - -custom_packages <- c( - # attachment::att_from_description(), - "cli", - "glue", # "golem", - "shiny", - "stats", - "utils", - "testthat", - "knitr" -) -try(dockerfiler::renv$initialize(),silent=TRUE) -if ( !testthat:::on_cran()){ -dockerfiler::renv$snapshot( - packages = custom_packages, - lockfile = the_lockfile, - prompt = FALSE -) } else { - file.copy(from = system.file("renv.lock",package = "dockerfiler"),to = the_lockfile) -} - -# Modify R version for tests -renv_file <- readLines(file.path(dir_build, "renv.lock")) -renv_file[grep("Version", renv_file)[1]] <- ' "Version": "4.1.2",' -writeLines(renv_file, file.path(dir_build, "renv.lock")) - - - -# dock_from_renv ---- -test_that("dock_from_renv works", { - - # testthat::skip_on_cran() - # skip_if_not(interactive()) - # Create Dockerfile - skip_if(is_rdevel, "skip on R-devel") - - testthat::with_mocked_bindings(code = { - out <- dock_from_renv( - lockfile = the_lockfile, - FROM = "rocker/verse", - renv_version = "0.0.0" - ) - }, - compact_sysreqs = function(...) "fake sys reqs", - repos_as_character = function(...) "fake repos" - ) - - expect_s3_class( - out, - "Dockerfile" - ) - expect_s3_class( - out, - "R6" - ) - - # read Dockerfile - out$write( - file.path( - dir_build, - "Dockerfile" - ) - ) - - dock_created <- readLines( - file.path( - dir_build, - "Dockerfile" - ) - ) - - dock_expected <- readLines( - testthat::test_path("renv_Dockerfile") - ) - - expect_equal(dock_created, dock_expected) - - skip_if(is_rdevel, "Skip R-devel") - #python3 is not a direct dependencies from custom_packages - expect_false( any(grepl("python3",out$Dockerfile))) - -}) -# rstudioapi::navigateToFile(file.path(dir_build, "Dockerfile")) - -test_that("dock_from_renv works with full dependencies", { - # testthat::skip_on_cran() - # skip_if_not(interactive()) - # Create Dockerfile -skip_if(is_rdevel, "skip on R-devel") - out <- dock_from_renv( - dependencies = TRUE, - lockfile = the_lockfile, - FROM = "rocker/verse", - ) - expect_s3_class( - out, - "Dockerfile" - ) - expect_s3_class( - out, - "R6" - ) - skip_if(is_rdevel, "Skip R-devel") - #python3 is a un-direct dependencies from custom_packages - expect_true( any(grepl("python3",out$Dockerfile))) -}) -# rstudioapi::navigateToFile(file.path(dir_build, "Dockerfile")) - - - -# repos_as_character ---- -test_that("repos_as_character works", { - out <- dockerfiler:::repos_as_character( - repos = c( - RSPM = paste0("https://packagemanager.rstudio.com/all/__linux__/focal/latest"), - CRAN = "https://cran.rstudio.com/" - ) - ) - expect_equal( - out, - "c(RSPM = 'https://packagemanager.rstudio.com/all/__linux__/focal/latest', CRAN = 'https://cran.rstudio.com/')" - ) -}) - -# gen_base_image ---- -test_that("gen_base_image works", { - out <- dockerfiler:::gen_base_image( - r_version = "4.0", - FROM = "rstudio/r-base" - ) - expect_equal(out, "rstudio/r-base:4.0") - - out <- dockerfiler:::gen_base_image( - r_version = "4.0", - FROM = "rocker/verse" - ) - expect_equal(out, "rocker/verse:4.0") -}) - - - - - -test_that("dock_from_renv works with specific renv", { - - skip_if(is_rdevel, "skip on R-devel") - # testthat::skip_on_cran() -the_lockfile1.0.0 <- system.file("renv_with_1.0.0.lock",package = "dockerfiler") - -for (lf in list(the_lockfile,the_lockfile1.0.0)){ -for (renv_version in list(NULL,"banana","missing")){ - - - if (!is.null(renv_version) && renv_version == "missing") { - out <- dock_from_renv(lockfile = lf, - FROM = "rocker/verse") - } else{ - out <- dock_from_renv( - lockfile = lf, - FROM = "rocker/verse", - renv_version = renv_version - ) - - } -socle_install_version <- "remotes::install_version\\(\"renv\", version = \"" - if (lf == the_lockfile & is.null(renv_version)) { - test_string <- 'install.packages\\(c\\(\"renv\",\"remotes\"))' - } else if (lf == the_lockfile1.0.0 & is.null(renv_version)) { - test_string <- 'install.packages\\(c\\(\"renv\",\"remotes\"))' - } else if (lf == the_lockfile & renv_version == "banana") { - test_string <- paste0(socle_install_version,"banana" ,"\"\\)") - } else if (lf == the_lockfile1.0.0 & renv_version == "banana") { - test_string <- paste0(socle_install_version,"banana","\"\\)") - } else if (lf == the_lockfile & renv_version == "missing") { - test_string <- - paste0( - socle_install_version,dockerfiler::renv$the$metadata$version,"\"\\)" - ) - } else if (lf == the_lockfile1.0.0 & renv_version == "missing") { - test_string <-paste0(socle_install_version,"1.0.0","\"\\)") - } - - expect_true( any( grepl(test_string , out$Dockerfile) ), - info = paste(lf," & ",renv_version)) - - -}} - - - - -}) - -unlink(dir_build, recursive = TRUE) - -``` - - - - -```{r development-inflate, eval=FALSE} -# Run but keep eval=FALSE to avoid infinite loop -# Execute in the console directly -fusen::inflate(flat_file = "dev/flat_dock_from_renv.Rmd", vignette_name = "Dockerfile from renv.lock",check=FALSE,open_vignette = FALSE,overwrite = TRUE, - document = FALSE - # ,pkg_ignore = "renv" - ) -# attention si la version de renv vendor change il faut éditer la doc - - -``` diff --git a/tests/testthat/renv_Dockerfile b/tests/testthat/renv_Dockerfile index da26f76..1f1ba0d 100644 --- a/tests/testthat/renv_Dockerfile +++ b/tests/testthat/renv_Dockerfile @@ -5,4 +5,4 @@ RUN echo "options(renv.config.pak.enabled = FALSE, repos = fake repos, download. RUN R -e 'install.packages("remotes")' RUN R -e 'remotes::install_version("renv", version = "0.0.0")' COPY renv.lock renv.lock -RUN R -e 'renv::restore()' +RUN --mount=type=cache,id=renv-cache,target=/root/.cache/R/renv R -e 'renv::restore()' diff --git a/tests/testthat/test-dock_from_renv.R b/tests/testthat/test-dock_from_renv.R index f2316e4..09efe71 100644 --- a/tests/testthat/test-dock_from_renv.R +++ b/tests/testthat/test-dock_from_renv.R @@ -1,4 +1,3 @@ -# WARNING - Generated by {fusen} from dev/flat_dock_from_renv.Rmd: do not edit by hand # A temporary directory dir_build <- tempfile(pattern = "renv") @@ -89,7 +88,7 @@ test_that("dock_from_renv works", { }) # rstudioapi::navigateToFile(file.path(dir_build, "Dockerfile")) - +# test_that("dock_from_renv works with full dependencies", { # testthat::skip_on_cran() # skip_if_not(interactive()) @@ -150,7 +149,7 @@ test_that("gen_base_image works", { test_that("dock_from_renv works with specific renv", { - + skip_if(is_rdevel, "skip on R-devel") # testthat::skip_on_cran() the_lockfile1.0.0 <- system.file("renv_with_1.0.0.lock",package = "dockerfiler") @@ -192,10 +191,10 @@ socle_install_version <- "remotes::install_version\\(\"renv\", version = \"" info = paste(lf," & ",renv_version)) -}} - - - +}} + + + }) diff --git a/vignettes/dockerfile-from-renv-lock.Rmd b/vignettes/dockerfile-from-renv-lock.Rmd index 661f4bc..388774b 100644 --- a/vignettes/dockerfile-from-renv-lock.Rmd +++ b/vignettes/dockerfile-from-renv-lock.Rmd @@ -18,7 +18,6 @@ knitr::opts_chunk$set( library(dockerfiler) ``` -