diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b944da297..0952df50b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,13 @@ repos: additional_dependencies: - davidgohel/flextable # Error: package 'flextable' is not available - davidgohel/gdtools # for flextable + - mirai - checkmate - - future - jsonlite - lifecycle - logger - magrittr - methods - - promises - renv - rlang - shiny diff --git a/DESCRIPTION b/DESCRIPTION index f668bd7eec..0e19f7866b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -41,13 +41,10 @@ Depends: teal.slice (>= 0.5.1.9009) Imports: checkmate (>= 2.1.0), - future (>= 1.33.2), jsonlite, lifecycle (>= 0.2.0), logger (>= 0.2.0), methods, - promises (>= 1.3.0), - renv (>= 1.0.7), rlang (>= 1.0.0), shinyjs, stats, @@ -59,8 +56,10 @@ Imports: Suggests: bslib, knitr (>= 1.42), + mirai (>= 1.1.1), MultiAssayExperiment, R6, + renv (>= 1.0.7), rmarkdown (>= 2.23), rvest, shinytest2, @@ -74,8 +73,9 @@ RdMacros: lifecycle Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, insightsengineering/teal.slice, mllg/checkmate, - HenrikBengtsson/future, jeroen/jsonlite, r-lib/lifecycle, - daroczig/logger, rstudio/promises, rstudio/renv, r-lib/rlang, + jeroen/jsonlite, r-lib/lifecycle, + daroczig/logger, shikokuchuo/mirai, shikokuchuo/nanonext, + rstudio/renv, r-lib/rlang, daattali/shinyjs, insightsengineering/teal.code, insightsengineering/teal.logger, insightsengineering/teal.reporter, insightsengineering/teal.widgets, rstudio/bslib, yihui/knitr, @@ -106,6 +106,7 @@ Collate: 'module_snapshot_manager.R' 'module_teal.R' 'module_teal_data.R' + 'module_teal_lockfile.R' 'module_teal_with_splash.R' 'module_transform_data.R' 'reporter_previewer_module.R' @@ -116,7 +117,6 @@ Collate: 'teal_data_module-eval_code.R' 'teal_data_module-within.R' 'teal_data_utils.R' - 'teal_lockfile.R' 'teal_reporter.R' 'teal_slices-store.R' 'teal_slices.R' diff --git a/R/init.R b/R/init.R index 805bf1ee2e..ca5820e0a0 100644 --- a/R/init.R +++ b/R/init.R @@ -153,9 +153,6 @@ init <- function(data, # log teal.logger::log_system_info() - # invoke lockfile creation - teal_lockfile() - # argument transformations ## `modules` - landing module landing <- extract_module(modules, "teal_module_landing") diff --git a/R/module_teal.R b/R/module_teal.R index 391782cb9d..796a306966 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -138,7 +138,7 @@ ui_teal <- function(id, footer, teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), br(), - downloadLink(ns("lockFile"), "Download .lock file"), + ui_teal_lockfile(ns("lockfile")), textOutput(ns("identifier")) ) ) @@ -156,6 +156,8 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal initializing.") + srv_teal_lockfile("lockfile") + output$identifier <- renderText( paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) ) @@ -166,8 +168,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { title = "SessionInfo" ) - output$lockFile <- teal_lockfile_downloadhandler() - # `JavaScript` code run_js_files(files = "init.js") diff --git a/R/module_teal_lockfile.R b/R/module_teal_lockfile.R new file mode 100644 index 0000000000..5dd058d1f0 --- /dev/null +++ b/R/module_teal_lockfile.R @@ -0,0 +1,201 @@ +#' Generate lockfile for application's environment reproducibility +#' +#' @param lockfile_path (`character`) path to the lockfile. +#' +#' @section Different ways of creating lockfile: +#' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation. +#' +#' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses +#' `renv::dependencies()` to detect all R packages in the current project's working directory. +#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working +#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows +#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the +#' `DESCRIPTION` fields included in the lockfile. +#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set +#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. +#' +#' @section lockfile usage: +#' After creating the lockfile, you can restore the application's environment using `renv::restore()`. +#' +#' @seealso [renv::snapshot()], [renv::restore()]. +#' +#' @return `NULL` +#' +#' @name module_teal_lockfile +#' @rdname module_teal_lockfile +#' +#' @keywords internal +NULL + +#' @rdname module_teal_lockfile +ui_teal_lockfile <- function(id) { + ns <- NS(id) + shiny::tagList( + tags$span("", id = ns("lockFileStatus")), + shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile")) + ) +} + +#' @rdname module_teal_lockfile +srv_teal_lockfile <- function(id) { + moduleServer(id, function(input, output, session) { + logger::log_debug("Initialize srv_teal_lockfile.") + enable_lockfile_download <- function() { + shinyjs::html("lockFileStatus", "Application lockfile ready.") + shinyjs::hide("lockFileStatus", anim = TRUE) + shinyjs::enable("lockFileLink") + output$lockFileLink <- shiny::downloadHandler( + filename = function() { + "renv.lock" + }, + content = function(file) { + file.copy(lockfile_path, file) + file + }, + contentType = "application/json" + ) + } + disable_lockfile_download <- function() { + warning("Lockfile creation failed.", call. = FALSE) + shinyjs::html("lockFileStatus", "Lockfile creation failed.") + shinyjs::hide("lockFileLink") + } + + shiny::onStop(function() { + if (file.exists(lockfile_path) && !shiny::isRunning()) { + logger::log_debug("Removing lockfile after shutting down the app") + file.remove(lockfile_path) + } + }) + + lockfile_path <- "teal_app.lock" + mode <- getOption("teal.lockfile.mode", default = "") + + if (!(mode %in% c("auto", "enabled", "disabled"))) { + stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ") + } + + if (mode == "disabled") { + logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.") + shinyjs::hide("lockFileLink") + return(NULL) + } + + if (file.exists(lockfile_path)) { + logger::log_debug("Lockfile has already been created for this app - skipping automatic creation.") + enable_lockfile_download() + return(NULL) + } + + if (mode == "auto" && .is_disabled_lockfile_scenario()) { + logger::log_debug( + "Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()." + ) + shinyjs::hide("lockFileLink") + return(NULL) + } + + if (!.is_lockfile_deps_installed()) { + warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.") + shinyjs::hide("lockFileLink") + return(NULL) + } + + # - Will be run only if the lockfile doesn't exist (see the if-s above) + # - We render to the tempfile because the process might last after session is closed and we don't + # want to make a "teal_app.renv" then. This is why we copy only during active session. + process <- .teal_lockfile_process_invoke(lockfile_path) + observeEvent(process$status(), { + if (process$status() %in% c("initial", "running")) { + shinyjs::html("lockFileStatus", "Creating lockfile...") + } else if (process$status() == "success") { + result <- process$result() + if (any(grepl("Lockfile written to", result$out))) { + logger::log_debug("Lockfile containing { length(result$res$Packages) } packages created.") + if (any(grepl("(WARNING|ERROR):", result$out))) { + warning("Lockfile created with warning(s) or error(s):", call. = FALSE) + for (i in result$out) { + warning(i, call. = FALSE) + } + } + enable_lockfile_download() + } else { + disable_lockfile_download() + } + } else if (process$status() == "error") { + disable_lockfile_download() + } + }) + + NULL + }) +} + +utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call +#' @rdname module_teal_lockfile +.teal_lockfile_process_invoke <- function(lockfile_path) { + mirai_obj <- NULL + process <- shiny::ExtendedTask$new(function() { + m <- mirai::mirai( + { + options(opts) + do.call(Sys.setenv, sysenv) + .libPaths(libpaths) + setwd(wd) + run(lockfile_path = lockfile_path) + }, + run = .renv_snapshot, + lockfile_path = lockfile_path, + opts = options(), + libpaths = .libPaths(), + sysenv = as.list(Sys.getenv()), + wd = getwd() + ) + mirai_obj <<- m + m + }) + + shiny::onStop(function() { + if (mirai::unresolved(mirai_obj)) { + logger::log_debug("Terminating a running lockfile process...") + mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed + } + }) + + suppressWarnings({ # 'package:stats' may not be available when loading + process$invoke() + }) + + logger::log_debug("Lockfile creation started based on { getwd() }.") + + process +} + +#' @rdname module_teal_lockfile +.renv_snapshot <- function(lockfile_path) { + out <- utils::capture.output( + res <- renv::snapshot( + lockfile = lockfile_path, + prompt = FALSE, + force = TRUE, + type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here + ) + ) + + list(out = out, res = res) +} + +#' @rdname module_teal_lockfile +.is_lockfile_deps_installed <- function() { + requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE) +} + +#' @rdname module_teal_lockfile +.is_disabled_lockfile_scenario <- function() { + identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process + identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test + !identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process + ( + ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) + ) # inside R CMD CHECK +} diff --git a/R/teal_lockfile.R b/R/teal_lockfile.R deleted file mode 100644 index 5d253f8e9c..0000000000 --- a/R/teal_lockfile.R +++ /dev/null @@ -1,114 +0,0 @@ -#' Generate lockfile for application reproducibility -#' -#' This function is invoked during [teal::init] to create `renv`-compatible lockfile for use within the application. -#' -#' The function leverages [renv::snapshot()], which offers multiple methods for lockfile creation. -#' -#' - User-specified: -#' - **Pre-computed lockfile**: Users can provide their own pre-computed lockfile by specifying the path via -#' `teal.renv.lockfile` option. Automatic lockfile computation is skipped in such case. -#' - Automatically computed: -#' - **Working directory lockfile**: If `teal.renv.lockfile` is not set, `teal` will, by default, create an -#' `implicit` type lockfile that uses `renv::dependencies()` to detect all R packages in the current project's -#' working directory. -#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working -#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows -#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the -#' `DESCRIPTION` fields included in the lockfile. -#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set -#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. -#' -#' @section lockfile usage: -#' After creating the lockfile, you can restore the application environment using `renv::restore()`. -#' -#' @seealso [renv::snapshot()], [renv::restore()]. -#' -#' @return Nothing. This function is executed for its side effect of creating a lockfile used in the `teal` application. -#' -#' @keywords internal -teal_lockfile <- function() { - lockfile_path <- "teal_app.lock" - # If user has setup the file, there is no need to compute a new one. - user_lockfile <- getOption("teal.renv.lockfile", "") - if (!identical(user_lockfile, "")) { - if (file.exists(user_lockfile)) { - file.copy(user_lockfile, lockfile_path) - return(invisible(NULL)) - } else { - stop("lockfile provided through options('teal.renv.lockfile') does not exist.") - } - } - - if (!(is_in_test() || is_r_cmd_check())) { - old_plan <- future::plan() - # If there is already a parallel (non-sequential) backend, reuse it. - if (inherits(old_plan, "sequential")) { - future::plan(future::multisession, workers = 2) - } - - lockfile_task <- ExtendedTask$new(create_renv_lockfile) - lockfile_task$invoke(close = inherits(old_plan, "sequential"), lockfile_path) - logger::log_debug("lockfile creation invoked.") - } -} - -create_renv_lockfile <- function(close = FALSE, lockfile_path = NULL) { - checkmate::assert_flag(close) - checkmate::assert_string(lockfile_path, na.ok = TRUE) - promise <- promises::future_promise({ - # Below we can not use a file created in tempdir() directory. - # If a file is created in tempdir(), it gets deleted on 'then(onFulfilled' part. - shiny::onStop(function() file.remove(lockfile_path)) - - renv_logs <- utils::capture.output( - renv::snapshot( - lockfile = lockfile_path, - prompt = FALSE, - force = TRUE - # type = is taken from renv::settings$snapshot.type() - ) - ) - if (any(grepl("Lockfile written", renv_logs))) { - logger::log_debug("lockfile created successfully.") - } else { - logger::log_debug("lockfile created with issues.") - } - - lockfile_path - }) - if (close) { - # If the initial backend was only sequential, bring it back. - promises::then(promise, onFulfilled = function() { - future::plan(future::sequential) - }) - } - promise -} - -teal_lockfile_downloadhandler <- function() { - downloadHandler( - filename = function() { - "renv.lock" - }, - content = function(file) { - teal_lockfile <- "teal_app.lock" - iter <- 1 - while (!file.exists(teal_lockfile) && iter <= 100) { - logger::log_debug("lockfile not created yet, retrying...") - Sys.sleep(0.25) - iter <- iter + 1 # max wait time is 25 seconds - } - file.copy(teal_lockfile, file) - file - }, - contentType = "application/json" - ) -} - -is_r_cmd_check <- function() { - ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) -} - -is_in_test <- function() { - identical(Sys.getenv("TESTTHAT"), "true") -} diff --git a/R/zzz.R b/R/zzz.R index 817f9bae4b..a991d041f2 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,13 +1,16 @@ .onLoad <- function(libname, pkgname) { # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R - teal_default_options <- list(teal.show_js_log = FALSE) + + teal_default_options <- list( + teal.show_js_log = FALSE, + teal.lockfile.mode = "auto", + shiny.sanitize.errors = FALSE + ) op <- options() toset <- !(names(teal_default_options) %in% names(op)) if (any(toset)) options(teal_default_options[toset]) - options("shiny.sanitize.errors" = FALSE) - # Set up the teal logger instance teal.logger::register_logger("teal") teal.logger::register_handlers("teal") diff --git a/man/module_teal_lockfile.Rd b/man/module_teal_lockfile.Rd new file mode 100644 index 0000000000..c4f7170dfd --- /dev/null +++ b/man/module_teal_lockfile.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_teal_lockfile.R +\name{module_teal_lockfile} +\alias{module_teal_lockfile} +\alias{ui_teal_lockfile} +\alias{srv_teal_lockfile} +\alias{.teal_lockfile_process_invoke} +\alias{.renv_snapshot} +\alias{.is_lockfile_deps_installed} +\alias{.is_disabled_lockfile_scenario} +\title{Generate lockfile for application's environment reproducibility} +\usage{ +ui_teal_lockfile(id) + +srv_teal_lockfile(id) + +.teal_lockfile_process_invoke(lockfile_path) + +.renv_snapshot(lockfile_path) + +.is_lockfile_deps_installed() + +.is_disabled_lockfile_scenario() +} +\arguments{ +\item{lockfile_path}{(\code{character}) path to the lockfile.} +} +\value{ +\code{NULL} +} +\description{ +Generate lockfile for application's environment reproducibility +} +\section{Different ways of creating lockfile}{ + +\code{teal} leverages \code{\link[renv:snapshot]{renv::snapshot()}}, which offers multiple methods for lockfile creation. +\itemize{ +\item \strong{Working directory lockfile}: \code{teal}, by default, will create an \code{implicit} type lockfile that uses +\code{renv::dependencies()} to detect all R packages in the current project's working directory. +\item \strong{\code{DESCRIPTION}-based lockfile}: To generate a lockfile based on a \code{DESCRIPTION} file in your working +directory, set \code{renv::settings$snapshot.type("explicit")}. The naming convention for \code{type} follows +\code{renv::snapshot()}. For the \code{"explicit"} type, refer to \code{renv::settings$package.dependency.fields()} for the +\code{DESCRIPTION} fields included in the lockfile. +\item \strong{Custom files-based lockfile}: To specify custom files as the basis for the lockfile, set +\code{renv::settings$snapshot.type("custom")} and configure the \code{renv.snapshot.filter} option. +} +} + +\section{lockfile usage}{ + +After creating the lockfile, you can restore the application's environment using \code{renv::restore()}. +} + +\seealso{ +\code{\link[renv:snapshot]{renv::snapshot()}}, \code{\link[renv:restore]{renv::restore()}}. +} +\keyword{internal} diff --git a/man/teal_lockfile.Rd b/man/teal_lockfile.Rd deleted file mode 100644 index dc83582c6f..0000000000 --- a/man/teal_lockfile.Rd +++ /dev/null @@ -1,45 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/teal_lockfile.R -\name{teal_lockfile} -\alias{teal_lockfile} -\title{Generate lockfile for application reproducibility} -\usage{ -teal_lockfile() -} -\value{ -Nothing. This function is executed for its side effect of creating a lockfile used in the \code{teal} application. -} -\description{ -This function is invoked during \link{init} to create \code{renv}-compatible lockfile for use within the application. -} -\details{ -The function leverages \code{\link[renv:snapshot]{renv::snapshot()}}, which offers multiple methods for lockfile creation. -\itemize{ -\item User-specified: -\itemize{ -\item \strong{Pre-computed lockfile}: Users can provide their own pre-computed lockfile by specifying the path via -\code{teal.renv.lockfile} option. Automatic lockfile computation is skipped in such case. -} -\item Automatically computed: -\itemize{ -\item \strong{Working directory lockfile}: If \code{teal.renv.lockfile} is not set, \code{teal} will, by default, create an -\code{implicit} type lockfile that uses \code{renv::dependencies()} to detect all R packages in the current project's -working directory. -\item \strong{\code{DESCRIPTION}-based lockfile}: To generate a lockfile based on a \code{DESCRIPTION} file in your working -directory, set \code{renv::settings$snapshot.type("explicit")}. The naming convention for \code{type} follows -\code{renv::snapshot()}. For the \code{"explicit"} type, refer to \code{renv::settings$package.dependency.fields()} for the -\code{DESCRIPTION} fields included in the lockfile. -\item \strong{Custom files-based lockfile}: To specify custom files as the basis for the lockfile, set -\code{renv::settings$snapshot.type("custom")} and configure the \code{renv.snapshot.filter} option. -} -} -} -\section{lockfile usage}{ - -After creating the lockfile, you can restore the application environment using \code{renv::restore()}. -} - -\seealso{ -\code{\link[renv:snapshot]{renv::snapshot()}}, \code{\link[renv:restore]{renv::restore()}}. -} -\keyword{internal} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index b1162c9924..41f057c2e0 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -65,6 +65,56 @@ transform_list <<- list( ) ) +testthat::describe("srv_teal lockfile", { + testthat::it(paste0( + "creation process is invoked for teal.lockfile.mode = \"enabled\" ", + "and snapshot is copied to teal_app.lock and removed after session ended" + ), { + withr::with_options( + list(teal.lockfile.mode = "enabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = teal.data::teal_data(iris = iris), + modules = modules(example_module()) + ), + expr = { + iter <- 1 + while (!file.exists(renv_filename) && iter <= 100) { + Sys.sleep(0.25) + iter <- iter + 1 # max wait time is 25 seconds + } + testthat::expect_true(file.exists(renv_filename)) + } + ) + testthat::expect_false(file.exists(renv_filename)) + } + ) + }) + testthat::it("creation process is not invoked for teal.lockfile.mode = \"disabled\"", { + withr::with_options( + list(teal.lockfile.mode = "disabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = teal.data::teal_data(iris = iris), + modules = modules(example_module()) + ), + expr = { + testthat::expect_false(file.exists(renv_filename)) + } + ) + } + ) + }) +}) + testthat::describe("srv_teal arguments", { testthat::it("accepts data to be teal_data", { testthat::expect_no_error( diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 2905288579..4904c46461 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -176,31 +176,3 @@ testthat::test_that("defunction recursively goes down a list", { y ) }) - -testthat::test_that("create_renv_lockfile creates a lock file during the execution", { - old_plan <- future::plan(future::sequential) - withr::defer(future::plan(old_plan)) - - renv_file_name <- "teal_app.lock" - withr::defer(file.remove(renv_file_name)) - promise <- create_renv_lockfile(TRUE, renv_file_name) - - testthat::expect_true(file.exists(renv_file_name)) -}) - -testthat::test_that("check_modules_datanames message is the same in html tags and in string", { - testthat::skip_if_not_installed("rvest") - modules <- module(datanames = c("iris", "mtcars"), ui = function(id) NULL, server = function(id, data) NULL) - - message <- check_modules_datanames(modules, "missing") - - # Compares 2 strings (removes quotations and empty space surrounding tags) - testthat::expect_identical( - gsub("\"", "", message$string), - trimws( - rvest::html_text2( - rvest::read_html(as.character(message$html(with_module_name = TRUE))) - ) - ) - ) -}) diff --git a/vignettes/teal-options.Rmd b/vignettes/teal-options.Rmd index 99accfa5a6..3ab894456e 100644 --- a/vignettes/teal-options.Rmd +++ b/vignettes/teal-options.Rmd @@ -84,9 +84,19 @@ This indicates whether to print the `JavaScript` console logs to the `R` console Default: `FALSE`. -### `teal.renv.lockfile` (`character`) +### `teal.lockfile.mode` (`character`) -The path to the pre-computed `renv` lockfile that will be shared through teal app. To read more about lockfile usage creation check `?teal::teal_lockfile`. +This enables to compute `renv` lockfile and shows a button to `"download lockfile"` in the footer. + +Values: + +* `"auto"` - auto detect whether to compute `lockfile` +* `"enabled"` - compute `lockfile` and show `"download lockfile"` in the footer +* `"disabled"` - do not compute `lockfile` and do not show `"download lockfile"` in the footer + +Default: `"auto"`. + +To read more about lockfile usage creation check `?teal::module_teal_lockfile`. # Deprecated options