diff --git a/DESCRIPTION b/DESCRIPTION index a64825f8..d63fc9b1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: golem Title: A Framework for Robust Shiny Applications -Version: 0.3.2 +Version: 0.3.3 Authors@R: c(person(given = "Colin", family = "Fay", @@ -68,7 +68,7 @@ Suggests: rlang, covr, devtools, - dockerfiler (>= 0.1.4), + dockerfiler (>= 0.2.0), knitr, pkgbuild, pkgdown, @@ -82,11 +82,11 @@ Suggests: testthat, tools, withr, - attachment + attachment (>= 0.2.5) VignetteBuilder: knitr Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.0 diff --git a/NAMESPACE b/NAMESPACE index 7f527830..f31af341 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,9 @@ export(add_css_file) export(add_dockerfile) export(add_dockerfile_heroku) export(add_dockerfile_shinyproxy) +export(add_dockerfile_with_renv) +export(add_dockerfile_with_renv_heroku) +export(add_dockerfile_with_renv_shinyproxy) export(add_fct) export(add_html_template) export(add_js_file) diff --git a/NEWS.md b/NEWS.md index 5d483926..6d0f6614 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,15 @@ > Notes: the # between parenthesis referes to the related issue on GitHub, and the @ refers to an external contributor solving this issue. +# golem 0.3.3 + +## New functions + ++ `add_dockerfile_with_renv()`, `add_dockerfile_with_renv_heroku()` and `add_dockerfile_with_renv_shinyproxy()` build Dockerfiles that rely on `{renv}` + +### Soft deprecated + ++ `add_dockerfile`, `add_dockerfile_shinyproxy()` and `add_dockerfile_heroku()` now recommend to switch to their `_with_renv_` counterpart + # golem 0.3.2 ### Soft deprecated diff --git a/R/add_dockerfiles.R b/R/add_dockerfiles.R index 6bd0dc07..df3bb111 100644 --- a/R/add_dockerfiles.R +++ b/R/add_dockerfiles.R @@ -1,7 +1,18 @@ +talk_once <- function(.f, msg = "") { + talk <- TRUE + function(...) { + if (talk) { + talk <<- FALSE + cat_red_bullet(msg) + } + .f(...) + } +} + #' Create a Dockerfile for your App #' -#' Build a container containing your Shiny App. `add_dockerfile()` creates -#' a generic Dockerfile, while `add_dockerfile_shinyproxy()` and +#' Build a container containing your Shiny App. `add_dockerfile()` and `add_dockerfile_with_renv()` creates +#' a generic Dockerfile, while `add_dockerfile_shinyproxy()`, `add_dockerfile_with_renv_shinyproxy()` and #' `add_dockerfile_heroku()` creates platform specific Dockerfile. #' #' @inheritParams add_module @@ -9,7 +20,12 @@ #' @param path path to the DESCRIPTION file to use as an input. #' @param output name of the Dockerfile output. #' @param from The FROM of the Dockerfile. Default is -#' FROM rocker/r-ver:`R.Version()$major`.`R.Version()$minor`. +#' +#' FROM rocker/verse +#' +#' without renv.lock file passed +#' `R.Version()$major`.`R.Version()$minor` is used as tag +#' #' @param as The AS of the Dockerfile. Default it NULL. #' @param port The `options('shiny.port')` on which to run the App. #' Default is 80. @@ -18,13 +34,12 @@ #' @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")`. #' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line. -#' @param open boolean. Should the Dockerfile be open after creation? Default is `TRUE`. +#' @param open boolean. Should the Dockerfile/README be open after creation? Default is `TRUE`. #' @param build_golem_from_source boolean. If `TRUE` no tar.gz is created and #' the Dockerfile directly mount the source folder. #' @param update_tar_gz boolean. If `TRUE` and `build_golem_from_source` is also `TRUE`, #' an updated tar.gz is created. #' @param extra_sysreqs character vector. Extra debian system requirements. -#' Will be installed with apt-get install. #' #' @export #' @rdname dockerfiles @@ -40,10 +55,28 @@ #' if (interactive()) { #' add_dockerfile() #' } +#' # Crete a 'deploy' folder containing everything needed to deploy +#' # the golem using docker based on {renv} +#' if (interactive()) { +#' add_dockerfile_with_renv( +#' # lockfile = "renv.lock", # uncomment to use existing renv.lock file +#' output_dir = "deploy" +#' ) +#' } #' # Add a Dockerfile for ShinyProxy #' if (interactive()) { #' add_dockerfile_shinyproxy() #' } +#' +#' # Crete a 'deploy' folder containing everything needed to deploy +#' # the golem with ShinyProxy using docker based on {renv} +#' if (interactive()) { +#' add_dockerfile_with_renv( +#' # lockfile = "renv.lock",# uncomment to use existing renv.lock file +#' output_dir = "deploy" +#' ) +#' } +#' #' # Add a Dockerfile for Heroku #' if (interactive()) { #' add_dockerfile_heroku() @@ -55,7 +88,7 @@ add_dockerfile <- function( output = "Dockerfile", pkg = get_golem_wd(), from = paste0( - "rocker/r-ver:", + "rocker/verse:", R.Version()$major, ".", R.Version()$minor @@ -71,54 +104,96 @@ add_dockerfile <- function( build_golem_from_source = TRUE, extra_sysreqs = NULL ) { - check_is_installed("dockerfiler") - - required_version("dockerfiler", "0.1.4") - - where <- path(pkg, output) - - usethis::use_build_ignore(path_file(where)) - - dock <- dockerfiler::dock_from_desc( + add_dockerfile_( path = path, - FROM = from, - AS = as, + output = output, + pkg = pkg, + from = from, + as = as, + port = port, + host = host, sysreqs = sysreqs, repos = repos, expand = expand, - build_from_source = build_golem_from_source, + open = open, update_tar_gz = update_tar_gz, + build_golem_from_source = build_golem_from_source, extra_sysreqs = extra_sysreqs ) +} - dock$EXPOSE(port) +add_dockerfile_ <- talk_once( + function( + path = "DESCRIPTION", + output = "Dockerfile", + pkg = get_golem_wd(), + from = paste0( + "rocker/verse:", + R.Version()$major, + ".", + R.Version()$minor + ), + as = NULL, + port = 80, + host = "0.0.0.0", + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + open = TRUE, + update_tar_gz = TRUE, + build_golem_from_source = TRUE, + extra_sysreqs = NULL + ) { + check_is_installed("dockerfiler") - dock$CMD( - sprintf( - "R -e \"options('shiny.port'=%s,shiny.host='%s');%s::run_app()\"", - port, - host, - read.dcf(path)[1] + required_version("dockerfiler", "0.1.4") + + where <- path(pkg, output) + + usethis::use_build_ignore(path_file(where)) + + dock <- dockerfiler::dock_from_desc( + path = path, + FROM = from, + AS = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + build_from_source = build_golem_from_source, + update_tar_gz = update_tar_gz, + extra_sysreqs = extra_sysreqs ) - ) - dock$write(output) + dock$EXPOSE(port) - if (open) { - if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { - rstudioapi::navigateToFile(output) - } else { - try(file.edit(output)) + dock$CMD( + sprintf( + "R -e \"options('shiny.port'=%s,shiny.host='%s');%s::run_app()\"", + port, + host, + read.dcf(path)[1] + ) + ) + + dock$write(output) + + if (open) { + if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { + rstudioapi::navigateToFile(output) + } else { + try(file.edit(output)) + } } - } - alert_build( - path = path, - output = output, - build_golem_from_source = build_golem_from_source - ) + alert_build( + path = path, + output = output, + build_golem_from_source = build_golem_from_source + ) - return(invisible(dock)) -} + return(invisible(dock)) + }, + "golem::add_dockerfile() is not recommended anymore.\nPlease use golem::add_dockerfile_with_renv() instead." +) #' @export #' @rdname dockerfiles @@ -128,7 +203,7 @@ add_dockerfile_shinyproxy <- function( output = "Dockerfile", pkg = get_golem_wd(), from = paste0( - "rocker/r-ver:", + "rocker/verse:", R.Version()$major, ".", R.Version()$minor @@ -142,47 +217,84 @@ add_dockerfile_shinyproxy <- function( build_golem_from_source = TRUE, extra_sysreqs = NULL ) { - check_is_installed("dockerfiler") - required_version("dockerfiler", "0.1.4") - - where <- path(pkg, output) - - usethis::use_build_ignore(output) - - dock <- dockerfiler::dock_from_desc( + add_dockerfile_shinyproxy_( path = path, - FROM = from, - AS = as, + output = output, + pkg = pkg, + from = from, + as = as, sysreqs = sysreqs, repos = repos, expand = expand, - build_from_source = build_golem_from_source, + open = open, update_tar_gz = update_tar_gz, + build_golem_from_source = build_golem_from_source, extra_sysreqs = extra_sysreqs ) +} + +add_dockerfile_shinyproxy_ <- talk_once( + function( + path = "DESCRIPTION", + output = "Dockerfile", + pkg = get_golem_wd(), + from = paste0( + "rocker/verse:", + R.Version()$major, + ".", + R.Version()$minor + ), + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + open = TRUE, + update_tar_gz = TRUE, + build_golem_from_source = TRUE, + extra_sysreqs = NULL + ) { + check_is_installed("dockerfiler") + required_version("dockerfiler", "0.1.4") + where <- path(pkg, output) - dock$EXPOSE(3838) - dock$CMD(sprintf( - " [\"R\", \"-e\", \"options('shiny.port'=3838,shiny.host='0.0.0.0');%s::run_app()\"]", - read.dcf(path)[1] - )) - dock$write(output) + usethis::use_build_ignore(output) - if (open) { - if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { - rstudioapi::navigateToFile(output) - } else { - try(file.edit(output)) + dock <- dockerfiler::dock_from_desc( + path = path, + FROM = from, + AS = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + build_from_source = build_golem_from_source, + update_tar_gz = update_tar_gz, + extra_sysreqs = extra_sysreqs + ) + + dock$EXPOSE(3838) + dock$CMD(sprintf( + " [\"R\", \"-e\", \"options('shiny.port'=3838,shiny.host='0.0.0.0');%s::run_app()\"]", + read.dcf(path)[1] + )) + dock$write(output) + + if (open) { + if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { + rstudioapi::navigateToFile(output) + } else { + try(file.edit(output)) + } } - } - alert_build( - path, - output, - build_golem_from_source = build_golem_from_source - ) + alert_build( + path, + output, + build_golem_from_source = build_golem_from_source + ) - return(invisible(dock)) -} + return(invisible(dock)) + }, + "golem::add_dockerfile_shinyproxy() is not recommended anymore.\nPlease use golem::add_dockerfile_with_renv_shinyproxy() instead." +) #' @export #' @rdname dockerfiles @@ -192,7 +304,7 @@ add_dockerfile_heroku <- function( output = "Dockerfile", pkg = get_golem_wd(), from = paste0( - "rocker/r-ver:", + "rocker/verse:", R.Version()$major, ".", R.Version()$minor @@ -206,77 +318,116 @@ add_dockerfile_heroku <- function( build_golem_from_source = TRUE, extra_sysreqs = NULL ) { - check_is_installed("dockerfiler") - required_version("dockerfiler", "0.1.4") - - where <- path(pkg, output) - - usethis::use_build_ignore(output) - - dock <- dockerfiler::dock_from_desc( + add_dockerfile_heroku_( path = path, - FROM = from, - AS = as, + output = output, + pkg = pkg, + from = from, + as = as, sysreqs = sysreqs, repos = repos, expand = expand, - build_from_source = build_golem_from_source, + open = open, update_tar_gz = update_tar_gz, + build_golem_from_source = build_golem_from_source, extra_sysreqs = extra_sysreqs ) +} - dock$CMD( - sprintf( - "R -e \"options('shiny.port'=$PORT,shiny.host='0.0.0.0');%s::run_app()\"", - read.dcf(path)[1] +add_dockerfile_heroku_ <- talk_once( + function( + path = "DESCRIPTION", + output = "Dockerfile", + pkg = get_golem_wd(), + from = paste0( + "rocker/verse:", + R.Version()$major, + ".", + R.Version()$minor + ), + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + open = TRUE, + update_tar_gz = TRUE, + build_golem_from_source = TRUE, + extra_sysreqs = NULL + ) { + check_is_installed("dockerfiler") + required_version("dockerfiler", "0.1.4") + where <- path(pkg, output) + + usethis::use_build_ignore(output) + + dock <- dockerfiler::dock_from_desc( + path = path, + FROM = from, + AS = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + build_from_source = build_golem_from_source, + update_tar_gz = update_tar_gz, + extra_sysreqs = extra_sysreqs ) - ) - dock$write(output) - alert_build( - path = path, - output = output, - build_golem_from_source = build_golem_from_source - ) + dock$CMD( + sprintf( + "R -e \"options('shiny.port'=$PORT,shiny.host='0.0.0.0');%s::run_app()\"", + read.dcf(path)[1] + ) + ) + dock$write(output) - apps_h <- gsub( - "\\.", - "-", - sprintf( - "%s-%s", - read.dcf(path)[1], - read.dcf(path)[1, ][["Version"]] + alert_build( + path = path, + output = output, + build_golem_from_source = build_golem_from_source ) - ) - cat_rule("From your command line, run:") - cat_line("heroku container:login") - cat_line( - sprintf("heroku create %s", apps_h) - ) - cat_line( - sprintf("heroku container:push web --app %s", apps_h) - ) - cat_line( - sprintf("heroku container:release web --app %s", apps_h) - ) - cat_line( - sprintf("heroku open --app %s", apps_h) - ) - cat_red_bullet("Be sure to have the heroku CLI installed.") - cat_red_bullet( - sprintf("You can replace %s with another app name.", apps_h) - ) - if (open) { - if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { - rstudioapi::navigateToFile(output) - } else { - try(file.edit(output)) + apps_h <- gsub( + "\\.", + "-", + sprintf( + "%s-%s", + read.dcf(path)[1], + read.dcf(path)[1, ][["Version"]] + ) + ) + + cat_rule("From your command line, run:") + cat_line("heroku container:login") + cat_line( + sprintf("heroku create %s", apps_h) + ) + cat_line( + sprintf("heroku container:push web --app %s", apps_h) + ) + cat_line( + sprintf("heroku container:release web --app %s", apps_h) + ) + cat_line( + sprintf("heroku open --app %s", apps_h) + ) + cat_red_bullet("Be sure to have the heroku CLI installed.") + cat_red_bullet( + sprintf("You can replace %s with another app name.", apps_h) + ) + if (open) { + if (rstudioapi::isAvailable() & rstudioapi::hasFun("navigateToFile")) { + rstudioapi::navigateToFile(output) + } else { + try(file.edit(output)) + } } - } - usethis::use_build_ignore(files = output) - return(invisible(dock)) -} + usethis::use_build_ignore(files = output) + return(invisible(dock)) + }, + " +golem::add_dockerfile_heroku() is not recommended anymore.\nPlease use golem::add_dockerfile_with_renv_heroku() instead. +" +) alert_build <- function( path, @@ -294,4 +445,4 @@ alert_build <- function( ) ) } -} \ No newline at end of file +} diff --git a/R/add_dockerfiles_renv.R b/R/add_dockerfiles_renv.R new file mode 100644 index 00000000..80fb9d61 --- /dev/null +++ b/R/add_dockerfiles_renv.R @@ -0,0 +1,331 @@ +add_dockerfile_with_renv_ <- function( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + FROM = "rocker/verse", + AS = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + extra_sysreqs = NULL, + update_tar_gz = TRUE + # build_golem_from_source = TRUE, +) { + check_is_installed("renv") + check_is_installed("dockerfiler") + required_version("dockerfiler", "0.2.0") + check_is_installed("attachment") + + # Small hack to prevent warning from rlang::lang() in tests + # This should be managed in {attempt} later on + x <- suppressWarnings({ + rlang::lang(print) + }) + + dir.create(output_dir) + + # add output_dir in Rbuildignore if the output is inside the golem + if (normalizePath(dirname(output_dir)) == normalizePath(source_folder)) { + usethis::use_build_ignore(output_dir) + } + + if (is.null(lockfile)) { + lockfile <- attachment::create_renv_for_prod(path = source_folder, output = file.path(output_dir, "renv.lock.prod")) + } + + file.copy(from = lockfile, to = output_dir) + + socle <- dockerfiler::dock_from_renv( + lockfile = lockfile, + distro = distro, + FROM = FROM, + repos = repos, + AS = AS, + sysreqs = sysreqs, + expand = expand, + extra_sysreqs = extra_sysreqs + ) + + socle$write(as = file.path(output_dir, "Dockerfile_base")) + + my_dock <- dockerfiler::Dockerfile$new(FROM = paste0(golem::get_golem_name(), "_base")) + + my_dock$COPY("renv.lock.prod", "renv.lock") + + my_dock$RUN("R -e 'renv::restore()'") + + if (update_tar_gz) { + old_version <- list.files(path = output_dir, pattern = paste0(golem::get_golem_name(), "_*.*.tar.gz"), full.names = TRUE) + # file.remove(old_version) + if (length(old_version) > 0) { + lapply(old_version, file.remove) + lapply(old_version, unlink, force = TRUE) + cat_red_bullet( + sprintf( + "%s were removed from folder", + paste( + old_version, + collapse = ", " + ) + ) + ) + } + + if ( + isTRUE( + requireNamespace( + "pkgbuild", + quietly = TRUE + ) + ) + ) { + out <- pkgbuild::build( + path = ".", + dest_path = output_dir, + vignettes = FALSE + ) + if (missing(out)) { + cat_red_bullet("Error during tar.gz building") + } else { + cat_green_tick( + sprintf( + " %s created.", + out + ) + ) + } + } else { + stop("please install {pkgbuild}") + } + } + + # we use an already built tar.gz file + my_dock$COPY( + from = + paste0(golem::get_golem_name(), "_*.tar.gz"), + to = "/app.tar.gz" + ) + my_dock$RUN("R -e 'remotes::install_local(\"/app.tar.gz\",upgrade=\"never\")'") + my_dock$RUN("rm /app.tar.gz") + my_dock +} + +#' @param source_folder path to the Package/golem source folder to deploy. +#' default is current folder '.' +#' @param lockfile path to the renv.lock file to use. default is `NULL` +#' @param output_dir folder to export everything deployment related. +#' @param distro One of "focal", "bionic", "xenial", "centos7", or "centos8". +#' See available distributions at https://hub.docker.com/r/rstudio/r-base/. +#' @param dockerfile_cmd What is the CMD to add to the Dockerfile. If NULL, the default, +#' the CMD will be `R -e "options('shiny.port'={port},shiny.host='{host}');{appname}::run_app()\` +#' @inheritParams add_dockerfile +#' @rdname dockerfiles +#' @export +add_dockerfile_with_renv <- function( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + port = 80, + host = "0.0.0.0", + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + open = TRUE, + extra_sysreqs = NULL, + update_tar_gz = TRUE, + dockerfile_cmd = NULL +) { + base_dock <- add_dockerfile_with_renv_( + source_folder = source_folder, + lockfile = lockfile, + output_dir = output_dir, + distro = distro, + FROM = from, + AS = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + extra_sysreqs = extra_sysreqs, + update_tar_gz = update_tar_gz + ) + if (!is.null(port)) { + base_dock$EXPOSE(port) + } + if (is.null(dockerfile_cmd)) { + dockerfile_cmd <- sprintf( + "R -e \"options('shiny.port'=%s,shiny.host='%s');%s::run_app()\"", + port, + host, + golem::get_golem_name() + ) + } + base_dock$CMD( + dockerfile_cmd + ) + base_dock + base_dock$write(as = file.path(output_dir, "Dockerfile")) + + out <- sprintf( + "docker build -f Dockerfile_base --progress=plain -t %s . +docker build -f Dockerfile --progress=plain -t %s . +docker run -p %s:%s %s +# then go to 127.0.0.1:%s", + paste0(golem::get_golem_name(), "_base"), + paste0(golem::get_golem_name(), ":latest"), + port, + port, + paste0(golem::get_golem_name(), ":latest"), + port + ) + + cat(out, file = file.path(output_dir, "README")) + + open_or_go_to( + where = file.path(output_dir, "README"), + open_file = open + ) +} + +#' @inheritParams add_dockerfile +#' @rdname dockerfiles +#' @export +#' @export +add_dockerfile_with_renv_shinyproxy <- function( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + extra_sysreqs = NULL, + open = TRUE, + update_tar_gz = TRUE +) { + add_dockerfile_with_renv( + source_folder = source_folder, + lockfile = lockfile, + output_dir = output_dir, + distro = distro, + from = from, + as = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + port = 3838, + host = "0.0.0.0", + extra_sysreqs = extra_sysreqs, + update_tar_gz = update_tar_gz, + open = open, + dockerfile_cmd = sprintf( + "R -e \"options('shiny.port'=3838,shiny.host='0.0.0.0');%s::run_app()\"", + golem::get_golem_name() + ) + ) +} + +#' @inheritParams add_dockerfile +#' @rdname dockerfiles +#' @export +#' @export +add_dockerfile_with_renv_heroku <- function( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + extra_sysreqs = NULL, + open = TRUE, + update_tar_gz = TRUE +) { + add_dockerfile_with_renv( + source_folder = source_folder, + lockfile = lockfile, + output_dir = output_dir, + distro = distro, + from = from, + as = as, + sysreqs = sysreqs, + repos = repos, + expand = expand, + port = NULL, + host = "0.0.0.0", + extra_sysreqs = extra_sysreqs, + update_tar_gz = update_tar_gz, + open = FALSE, + dockerfile_cmd = sprintf( + "R -e \"options('shiny.port'=$PORT,shiny.host='0.0.0.0');%s::run_app()\"", + golem::get_golem_name() + ) + ) + + apps_h <- gsub( + "\\.", + "-", + sprintf( + "%s-%s", + golem::get_golem_name(), + golem::get_golem_version() + ) + ) + + readme_output <- file.path(output_dir, "README") + + write_there <- function(...) { + write(..., file = readme_output, append = TRUE) + } + + write_there("From your command line, run:\n") + + write_there( + sprintf( + "docker build -f Dockerfile_base --progress=plain -t %s .", + paste0(golem::get_golem_name(), "_base") + ) + ) + + write_there( + sprintf( + "docker build -f Dockerfile --progress=plain -t %s .\n", + paste0(golem::get_golem_name(), ":latest") + ) + ) + + write_there("Then, to push on heroku:\n") + + write_there("heroku container:login") + write_there( + sprintf("heroku create %s", apps_h) + ) + write_there( + sprintf("heroku container:push web --app %s", apps_h) + ) + write_there( + sprintf("heroku container:release web --app %s", apps_h) + ) + write_there( + sprintf("heroku open --app %s\n", apps_h) + ) + write_there("> Be sure to have the heroku CLI installed.") + + write_there( + sprintf("> You can replace %s with another app name.", apps_h) + ) + + # The open is deported here just to be sure + # That we open the README once it has been populated + open_or_go_to( + where = readme_output, + open_file = open + ) +} diff --git a/R/utils.R b/R/utils.R index e95d3b2d..ae3200ea 100644 --- a/R/utils.R +++ b/R/utils.R @@ -17,8 +17,8 @@ darkgrey <- function(x) { } #' @importFrom fs dir_exists file_exists -dir_not_exist <- Negate(dir_exists) -file_not_exist <- Negate(file_exists) +dir_not_exist <- Negate(fs::dir_exists) +file_not_exist <- Negate(fs::file_exists) #' @importFrom fs dir_create file_create create_if_needed <- function( diff --git a/README.Rmd b/README.Rmd index c06f5466..ad948985 100644 --- a/README.Rmd +++ b/README.Rmd @@ -88,7 +88,7 @@ This package is part of a series of tools for Shiny, which includes: These are examples from the community. Please note that they may not necessarily be written in a canonical fashion and may have been written with different versions of `{golem}` or `{shiny}`. - -- +- - - diff --git a/README.md b/README.md index 8ceefc47..19bddce1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ - [![Lifecycle: @@ -24,33 +23,33 @@ You’re reading the doc about version : ``` r desc::desc_get_version() -#> [1] '0.3.2' +#> [1] '0.3.3' ``` ## Tool series This package is part of a series of tools for Shiny, which includes: - - `{golem}` - - - `{shinipsum}` - - - `{fakir}` - - - `{shinysnippets}` - +- `{golem}` - +- `{shinipsum}` - +- `{fakir}` - +- `{shinysnippets}` - ## Resources ### The Book : - - - - [paper version of the book “Engineering Production-Grade Shiny +- +- [paper version of the book “Engineering Production-Grade Shiny Apps”](https://www.routledge.com/Engineering-Production-Grade-Shiny-Apps/Fay-Rochette-Guyader-Girard/p/book/9780367466022) ### Blog posts : *Building Big Shiny Apps* - - Part 1: +- Part 1: - - Part 2: +- Part 2: [*Make a Fitness App from @@ -58,34 +57,34 @@ scratch*](https://towardsdatascience.com/production-grade-r-shiny-with-golem-pro ### Slide decks - - useR\! 2019 : [A Framework for Building Robust & Production Ready +- useR! 2019 : [A Framework for Building Robust & Production Ready Shiny Apps](https://github.com/VincentGuyader/user2019/raw/master/golem_Vincent_Guyader_USER!2019.pdf) - - ThinkR x RStudio Roadshow,Paris : [Production-grade Shiny Apps with +- ThinkR x RStudio Roadshow,Paris : [Production-grade Shiny Apps with {golem}](https://speakerdeck.com/colinfay/production-grade-shiny-apps-with-golem) - - rstudio::conf(2020) : [Production-grade Shiny Apps with +- rstudio::conf(2020) : [Production-grade Shiny Apps with golem](https://speakerdeck.com/colinfay/rstudio-conf-2020-production-grade-shiny-apps-with-golem) - - barcelonar (2019-12-03) : [Engineering Production-Grade Shiny Apps +- barcelonar (2019-12-03) : [Engineering Production-Grade Shiny Apps with {golem}](https://www.barcelonar.org/presentations/BarcelonaR_Building_Production_Grade_Shiny_Apps_with_golem.pdf) ### Video - - [{golem} and Effective Shiny Development +- [{golem} and Effective Shiny Development Methods](https://www.youtube.com/watch?v=OU1-CkSVdTI) - - [Hands-on demonstration of +- [Hands-on demonstration of {golem}](https://www.youtube.com/watch?v=3-p9XLvoJV0) - - useR\! 2019 : [A Framework for Building Robust & Production Ready +- useR! 2019 : [A Framework for Building Robust & Production Ready Shiny Apps](https://youtu.be/tCAan6smrjs) - - 🇫🇷 [Introduction to {golem}](https://youtu.be/6qI4NzxlAFU) - - rstudio::conf(2020) : [Production-grade Shiny Apps with +- 🇫🇷 [Introduction to {golem}](https://youtu.be/6qI4NzxlAFU) +- rstudio::conf(2020) : [Production-grade Shiny Apps with golem](https://www.rstudio.com/resources/rstudioconf-2020/production-grade-shiny-apps-with-golem/) - - 🇫🇷 Rencontres R 2021 : [Conception d’applications Shiny avec +- 🇫🇷 Rencontres R 2021 : [Conception d’applications Shiny avec {golem}](https://www.youtube.com/watch?v=0f5Me1PFGDs) ### Cheatsheet - - [{golem} cheatsheet](https://thinkr.fr/golem_cheatsheet_v0.1.pdf) +- [{golem} cheatsheet](https://thinkr.fr/golem_cheatsheet_v0.1.pdf) ### Examples apps @@ -93,31 +92,27 @@ These are examples from the community. Please note that they may not necessarily be written in a canonical fashion and may have been written with different versions of `{golem}` or `{shiny}`. - - - - - - - - +- +- +- +- You can also find apps at: - - - - +- +- ## Installation - - You can install the stable version from CRAN with: - - +- You can install the stable version from CRAN with: ``` r install.packages("golem") ``` - - You can install the development version from +- You can install the development version from [GitHub](https://github.com/Thinkr-open/golem) with: - - ``` r # install.packages("remotes") remotes::install_github("Thinkr-open/golem") @@ -125,8 +120,7 @@ remotes::install_github("Thinkr-open/golem") ## Launch the project -Create a new package with the project -template: +Create a new package with the project template: diff --git a/inst/shinyexample/dev/02_dev.R b/inst/shinyexample/dev/02_dev.R index 82478726..67699ea7 100644 --- a/inst/shinyexample/dev/02_dev.R +++ b/inst/shinyexample/dev/02_dev.R @@ -15,6 +15,7 @@ ## Dependencies ---- ## Amend DESCRIPTION with dependencies read from package code parsing +## install.package('attachment') # if needed. attachment::att_amend_desc() ## Add modules ---- diff --git a/inst/shinyexample/dev/03_deploy.R b/inst/shinyexample/dev/03_deploy.R index 464d59fd..2f9595ef 100644 --- a/inst/shinyexample/dev/03_deploy.R +++ b/inst/shinyexample/dev/03_deploy.R @@ -33,10 +33,8 @@ golem::add_shinyserver_file() ## Docker ---- ## If you want to deploy via a generic Dockerfile -golem::add_dockerfile() +golem::add_dockerfile_with_renv() ## If you want to deploy to ShinyProxy -golem::add_dockerfile_shinyproxy() +golem::add_dockerfile_with_renv_shinyproxy() -## If you want to deploy to Heroku -golem::add_dockerfile_heroku() diff --git a/man/dockerfiles.Rd b/man/dockerfiles.Rd index bbeb97b4..53129521 100644 --- a/man/dockerfiles.Rd +++ b/man/dockerfiles.Rd @@ -1,16 +1,19 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/add_dockerfiles.R +% Please edit documentation in R/add_dockerfiles.R, R/add_dockerfiles_renv.R \name{add_dockerfile} \alias{add_dockerfile} \alias{add_dockerfile_shinyproxy} \alias{add_dockerfile_heroku} +\alias{add_dockerfile_with_renv} +\alias{add_dockerfile_with_renv_shinyproxy} +\alias{add_dockerfile_with_renv_heroku} \title{Create a Dockerfile for your App} \usage{ add_dockerfile( path = "DESCRIPTION", output = "Dockerfile", pkg = get_golem_wd(), - from = paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor), + from = paste0("rocker/verse:", R.Version()$major, ".", R.Version()$minor), as = NULL, port = 80, host = "0.0.0.0", @@ -27,7 +30,7 @@ add_dockerfile_shinyproxy( path = "DESCRIPTION", output = "Dockerfile", pkg = get_golem_wd(), - from = paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor), + from = paste0("rocker/verse:", R.Version()$major, ".", R.Version()$minor), as = NULL, sysreqs = TRUE, repos = c(CRAN = "https://cran.rstudio.com/"), @@ -42,7 +45,7 @@ add_dockerfile_heroku( path = "DESCRIPTION", output = "Dockerfile", pkg = get_golem_wd(), - from = paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor), + from = paste0("rocker/verse:", R.Version()$major, ".", R.Version()$minor), as = NULL, sysreqs = TRUE, repos = c(CRAN = "https://cran.rstudio.com/"), @@ -52,6 +55,54 @@ add_dockerfile_heroku( build_golem_from_source = TRUE, extra_sysreqs = NULL ) + +add_dockerfile_with_renv( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + port = 80, + host = "0.0.0.0", + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + open = TRUE, + extra_sysreqs = NULL, + update_tar_gz = TRUE, + dockerfile_cmd = NULL +) + +add_dockerfile_with_renv_shinyproxy( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + extra_sysreqs = NULL, + open = TRUE, + update_tar_gz = TRUE +) + +add_dockerfile_with_renv_heroku( + source_folder = ".", + lockfile = NULL, + output_dir = fs::path(tempdir(), "deploy"), + distro = "focal", + from = "rocker/verse", + as = NULL, + sysreqs = TRUE, + repos = c(CRAN = "https://cran.rstudio.com/"), + expand = FALSE, + extra_sysreqs = NULL, + open = TRUE, + update_tar_gz = TRUE +) } \arguments{ \item{path}{path to the DESCRIPTION file to use as an input.} @@ -61,7 +112,12 @@ add_dockerfile_heroku( \item{pkg}{Path to the root of the package. Default is \code{get_golem_wd()}.} \item{from}{The FROM of the Dockerfile. Default is -FROM rocker/r-ver:\code{R.Version()$major}.\code{R.Version()$minor}.} + +\if{html}{\out{
}}\preformatted{FROM rocker/verse + +without renv.lock file passed +`R.Version()$major`.`R.Version()$minor` is used as tag +}\if{html}{\out{
}}} \item{as}{The AS of the Dockerfile. Default it NULL.} @@ -77,7 +133,7 @@ Default is 0.0.0.0.} \item{expand}{boolean. If \code{TRUE} each system requirement will have its own \code{RUN} line.} -\item{open}{boolean. Should the Dockerfile be open after creation? Default is \code{TRUE}.} +\item{open}{boolean. Should the Dockerfile/README be open after creation? Default is \code{TRUE}.} \item{update_tar_gz}{boolean. If \code{TRUE} and \code{build_golem_from_source} is also \code{TRUE}, an updated tar.gz is created.} @@ -85,15 +141,27 @@ an updated tar.gz is created.} \item{build_golem_from_source}{boolean. If \code{TRUE} no tar.gz is created and the Dockerfile directly mount the source folder.} -\item{extra_sysreqs}{character vector. Extra debian system requirements. -Will be installed with apt-get install.} +\item{extra_sysreqs}{character vector. Extra debian system requirements.} + +\item{source_folder}{path to the Package/golem source folder to deploy. +default is current folder '.'} + +\item{lockfile}{path to the renv.lock file to use. default is \code{NULL}} + +\item{output_dir}{folder to export everything deployment related.} + +\item{distro}{One of "focal", "bionic", "xenial", "centos7", or "centos8". +See available distributions at https://hub.docker.com/r/rstudio/r-base/.} + +\item{dockerfile_cmd}{What is the CMD to add to the Dockerfile. If NULL, the default, +the CMD will be \verb{R -e "options('shiny.port'=\{port\},shiny.host='\{host\}');\{appname\}::run_app()\\}} } \value{ The \code{{dockerfiler}} object, invisibly. } \description{ -Build a container containing your Shiny App. \code{add_dockerfile()} creates -a generic Dockerfile, while \code{add_dockerfile_shinyproxy()} and +Build a container containing your Shiny App. \code{add_dockerfile()} and \code{add_dockerfile_with_renv()} creates +a generic Dockerfile, while \code{add_dockerfile_shinyproxy()}, \code{add_dockerfile_with_renv_shinyproxy()} and \code{add_dockerfile_heroku()} creates platform specific Dockerfile. } \examples{ @@ -102,10 +170,28 @@ a generic Dockerfile, while \code{add_dockerfile_shinyproxy()} and if (interactive()) { add_dockerfile() } +# Crete a 'deploy' folder containing everything needed to deploy +# the golem using docker based on {renv} +if (interactive()) { + add_dockerfile_with_renv( + # lockfile = "renv.lock", # uncomment to use existing renv.lock file + output_dir = "deploy" + ) +} # Add a Dockerfile for ShinyProxy if (interactive()) { add_dockerfile_shinyproxy() } + +# Crete a 'deploy' folder containing everything needed to deploy +# the golem with ShinyProxy using docker based on {renv} +if (interactive()) { + add_dockerfile_with_renv( + # lockfile = "renv.lock",# uncomment to use existing renv.lock file + output_dir = "deploy" + ) +} + # Add a Dockerfile for Heroku if (interactive()) { add_dockerfile_heroku() diff --git a/man/document_and_reload.Rd b/man/document_and_reload.Rd index 8d0c84ec..95b60561 100644 --- a/man/document_and_reload.Rd +++ b/man/document_and_reload.Rd @@ -24,7 +24,7 @@ which defaults to \code{c("collate", "namespace", "rd")}.} \item{load_code}{A function used to load all the R code in the package directory. The default, \code{NULL}, uses the strategy defined by -the \code{load} roxygen option, which defaults to \code{\link[roxygen2:load]{load_pkgload()}}. +the \code{load} roxygen option, which defaults to \code{\link[roxygen2:load_pkgload]{load_pkgload()}}. See \link[roxygen2]{load} for more details.} \item{clean}{If \code{TRUE}, roxygen will delete all files previously diff --git a/man/figures/logo.png b/man/figures/logo.png deleted file mode 100644 index a149d478..00000000 Binary files a/man/figures/logo.png and /dev/null differ diff --git a/tests/testthat/helper-config.R b/tests/testthat/helper-config.R index d9af3ab9..9273c45d 100644 --- a/tests/testthat/helper-config.R +++ b/tests/testthat/helper-config.R @@ -3,6 +3,12 @@ library(golem) library(withr) old_usethis.quiet <- getOption("usethis.quiet") options("usethis.quiet" = TRUE) +# Small hack to prevent warning from rlang::lang() in tests +# This should be managed in {attempt} later on +x <- suppressWarnings({ + rlang::lang(print) +}) + ### Funs remove_file <- function(path) { if (file.exists(path)) unlink(path, force = TRUE) @@ -56,13 +62,16 @@ fakename <- sprintf( gsub("[ :-]", "", Sys.time()) ) + +## random dir +randir <- paste0(sample(safe_let(), 10, TRUE), collapse = "") + tpdir <- normalizePath(tempdir()) unlink(file.path(tpdir, fakename), recursive = TRUE) create_golem(file.path(tpdir, fakename), open = FALSE) pkg <- file.path(tpdir, fakename) -## random dir -randir <- paste0(sample(safe_let(), 10, TRUE), collapse = "") + fp <- file.path("inst/app", randir) dir.create(file.path(pkg, fp), recursive = TRUE) diff --git a/tests/testthat/test-add_deploy_helpers.R b/tests/testthat/test-add_deploy_helpers.R index 8aef7553..7057e65a 100644 --- a/tests/testthat/test-add_deploy_helpers.R +++ b/tests/testthat/test-add_deploy_helpers.R @@ -76,60 +76,8 @@ test_that("add_dockerfiles repos variation", { } }) }) -test_that("add_dockerfiles multi repos", { - skip_if_not_installed("dockerfiler", "0.1.4") - - repos <- c( - bioc1 = "https://bioconductor.org/packages/3.10/data/annotation", - bioc2 = "https://bioconductor.org/packages/3.10/data/experiment", - CRAN = "https://cran.rstudio.com" - ) - - - with_dir(pkg, { - for (fun in list( - add_dockerfile, - add_dockerfile_heroku, - add_dockerfile_shinyproxy - )) { - burn_after_reading( - "Dockerfile", - { - output <- testthat::capture_output( - fun( - pkg = pkg, - sysreqs = FALSE, - open = FALSE, - repos = repos, - output = "Dockerfile" - ) - ) - - expect_exists("Dockerfile") - - - test <- stringr::str_detect( - output, - "Dockerfile created at Dockerfile" - ) - expect_true(test) - to_find <- "RUN echo \"options(repos = c(bioc1 = 'https://bioconductor.org/packages/3.10/data/annotation', bioc2 = 'https://bioconductor.org/packages/3.10/data/experiment', CRAN = 'https://cran.rstudio.com'), download.file.method = 'libcurl', Ncpus = 4)\" >> /usr/local/lib/R/etc/Rprofile.site" - # for R <= 3.4 - to_find_old <- "RUN echo \"options(repos = structure(c('https://bioconductor.org/packages/3.10/data/annotation', 'https://bioconductor.org/packages/3.10/data/experiment', 'https://cran.rstudio.com'), .Names = c('bioc1', 'bioc2', 'CRAN')), download.file.method = 'libcurl', Ncpus = 4)\" >> /usr/local/lib/R/etc/Rprofile.site" - - expect_true( - sum( - readLines(con = "Dockerfile") %in% c(to_find, to_find_old) - ) == 1 - ) - } - ) - } - }) -}) - test_that("add_rstudio_files", { with_dir(pkg, { for (fun in list( diff --git a/tests/testthat/test-renv_stuff.R b/tests/testthat/test-renv_stuff.R new file mode 100644 index 00000000..43d74c15 --- /dev/null +++ b/tests/testthat/test-renv_stuff.R @@ -0,0 +1,33 @@ +test_that("add_dockerfiles_renv and add_dockerfile_with_renv_shinyproxy all output file are present", { + skip_if_not_installed("dockerfiler", "0.2.0") + + with_dir(pkg, { + for (fun in list( + add_dockerfile_with_renv, + add_dockerfile_with_renv_shinyproxy, + add_dockerfile_with_renv_heroku + )) { + deploy_folder <- file.path( + tempdir(), + make.names( + paste0( + "deploy", + round( + runif(1, min = 0, max = 99999) + ) + ) + ) + ) + + fun(output_dir = deploy_folder, open = FALSE) + + expect_exists(file.path(deploy_folder, "Dockerfile")) + expect_exists(file.path(deploy_folder, "Dockerfile_base")) + expect_exists(file.path(deploy_folder, "README")) + expect_exists(file.path(deploy_folder, "renv.lock.prod")) + + expect_length(list.files(path = deploy_folder, pattern = "tar.gz$"), 1) + unlink(deploy_folder, force = TRUE, recursive = TRUE) + } + }) +}) diff --git a/vignettes/a_start.Rmd b/vignettes/a_start.Rmd index 002671ec..3d93116e 100644 --- a/vignettes/a_start.Rmd +++ b/vignettes/a_start.Rmd @@ -122,7 +122,7 @@ golem::fill_desc( ) ``` -About [the DESCRIPTION file](https://r-pkgs.org/description.html). +About [the DESCRIPTION file](https://r-pkgs.org/Metadata.html#sec-description). ### Add `{golem}` options @@ -155,7 +155,7 @@ Create a template for tests: golem::use_recommended_tests() ``` -About [tests in a package](https://r-pkgs.org/tests.html). +About [tests in a package](https://r-pkgs.org/testing-basics.html). ### Use Recommended Packages diff --git a/vignettes/b_dev.Rmd b/vignettes/b_dev.Rmd index aaa3dd82..052bbf95 100644 --- a/vignettes/b_dev.Rmd +++ b/vignettes/b_dev.Rmd @@ -45,7 +45,7 @@ Note that the `{attachment}` package should be installed on your machine. attachment::att_amend_desc() ``` -About [package dependencies](https://r-pkgs.org/namespace.html). +About [package dependencies](https://r-pkgs.org/Metadata.html#sec-namespace). ### Add modules @@ -139,7 +139,7 @@ Add more tests to your application: usethis::use_test("app") ``` -About [testing a package](https://r-pkgs.org/tests.html). +About [testing a package](https://r-pkgs.org/testing-basics.html). ## Documentation diff --git a/vignettes/c_deploy.Rmd b/vignettes/c_deploy.Rmd index 4021cd03..cf434e2f 100644 --- a/vignettes/c_deploy.Rmd +++ b/vignettes/c_deploy.Rmd @@ -69,6 +69,10 @@ golem::add_shinyserver_file() ### Docker +#### without using {renv} + + + ```{r} # If you want to deploy via a generic Dockerfile golem::add_dockerfile() @@ -80,3 +84,75 @@ golem::add_dockerfile_shinyproxy() golem::add_dockerfile_heroku() ``` +#### using {renv} + + +#### CASE 1 : you didn't use renv during developpment process + + +> this functions will create a "deploy" folder containing : + +```{txt} +deploy/ ++-- Dockerfile ++-- Dockerfile_base ++-- yourgolem_0.0.0.9000.tar.gz ++-- README +\-- renv.lock.prod +``` + +then follow the README file + + +```{r} +# If you want to deploy via a generic Dockerfile +golem::add_dockerfile_with_renv(output_dir = "deploy") + +# If you want to deploy to ShinyProxy +golem::add_dockerfile_with_renv_shinyproxy(output_dir = "deploy") + +``` + +If you would like to use {renv} during developpement, you can init a renv.lock file with + +```{r} +attachment::create_renv_for_dev(dev_pkg = c("renv", "devtools", "roxygen2", + "usethis", "pkgload", "testthat", "remotes", "covr", "attachment", + "pak", "dockerfiler","golem")) +``` +an activate {renv} with + +```{r} +renv::activate() +``` + + + + + +#### CASE 2 : you already have a renv.lock file for your project + + +```{r} + +# If you want to deploy via a generic Dockerfile +golem::add_dockerfile_with_renv(output_dir = "deploy",lockfile = "renv.lock") + +# If you want to deploy to ShinyProxy +golem::add_dockerfile_with_renv_shinyproxy(output_dir = "deploy",lockfile = "renv.lock") + + +``` + +> this functions will create a "deploy" folder containing : + +```{txt} +deploy/ ++-- Dockerfile ++-- Dockerfile_base ++-- yourgolem_0.0.0.9000.tar.gz ++-- README +\-- renv.lock.prod +``` + +then follow the README file \ No newline at end of file