diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 3c18270..a0d151f 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -1,11 +1,9 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples on: push: - branches: - - master - - test-ci + branches: [main, master] pull_request: - branches: - - master + branches: [main, master] name: R-CMD-check @@ -19,74 +17,30 @@ jobs: fail-fast: false matrix: config: - # - {os: windows-latest, r: 'release', port: '8786', url: 'http://127.0.0.1'} - # - {os: macOS-latest, r: 'release', port: '8787', url: 'localhost'} - - {os: ubuntu-18.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/bionic/latest", port: '8787', url: 'localhost'} - - {os: ubuntu-18.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/bionic/latest", port: '8789', url: 'localhost'} + - {os: ubuntu-latest, r: 'release'} env: - R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - RSPM: ${{ matrix.config.rspm }} - PORT: ${{ matrix.config.port }} - URL: ${{ matrix.config.url }} + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - # In this step, this action saves a list of existing images, - # the cache is created without them in the post run. - # It also restores the cache if it exists. - - uses: satackey/action-docker-layer-caching@v0.0.8 - # Ignore the failure of a step and avoid terminating the job. - continue-on-error: true + - uses: r-lib/actions/setup-pandoc@v2 - - uses: r-lib/actions/setup-r@master + - uses: r-lib/actions/setup-r@v2 with: r-version: ${{ matrix.config.r }} + use-public-rspm: true - - uses: r-lib/actions/setup-pandoc@master - - - name: Query dependencies - run: | - install.packages('remotes') - saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) - writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") - shell: Rscript {0} - - - name: Cache R packages - if: runner.os != 'Windows' - uses: actions/cache@v1 + - uses: r-lib/actions/setup-r-dependencies@v2 with: - path: ${{ env.R_LIBS_USER }} - key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} - restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- - - - name: Install system dependencies - if: runner.os == 'Linux' - run: | - while read -r cmd - do - eval sudo $cmd - done < <(Rscript -e 'cat(remotes::system_requirements("ubuntu", "18.04"), sep = "\n")') - - - name: Install dependencies - run: | - remotes::install_deps(dependencies = TRUE) - remotes::install_cran("rcmdcheck") - remotes::install_cran("devtools") - shell: Rscript {0} - - - name: Check - env: - _R_CHECK_CRAN_INCOMING_REMOTE_: false - run: | - devtools::test() - rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") - shell: Rscript {0} + extra-packages: any::rcmdcheck + needs: check - - name: Upload check results - if: failure() - uses: actions/upload-artifact@main + - uses: r-lib/actions/check-r-package@v2 with: - name: ${{ runner.os }}-r${{ matrix.config.r }}-results - path: check + upload-snapshots: true + args: 'c("--no-manual", "--as-cran", "--no-vignettes")' + build_args: 'c("--no-manual", "--no-build-vignettes")' + error-on: '"error"' diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 09af3c8..bee07ad 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -12,11 +12,11 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: r-lib/actions/setup-r@v1 + - uses: r-lib/actions/setup-r@v2 - - uses: r-lib/actions/setup-pandoc@v1 + - uses: r-lib/actions/setup-pandoc@v2 - name: Query dependencies run: | @@ -26,7 +26,7 @@ jobs: shell: Rscript {0} - name: Cache R packages - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ env.R_LIBS_USER }} key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} diff --git a/DESCRIPTION b/DESCRIPTION index 1afb010..f7058e9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,5 +28,5 @@ VignetteBuilder: Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 +RoxygenNote: 7.3.3 SystemRequirements: Docker diff --git a/NAMESPACE b/NAMESPACE index d2edf57..c0d8ff0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(build_volume_arg) export(launch_proj_docker) export(stop_proj_docker) export(update_renv_help) diff --git a/R/launch-stop-docker.R b/R/launch-stop-docker.R index 5a37155..2184205 100644 --- a/R/launch-stop-docker.R +++ b/R/launch-stop-docker.R @@ -20,6 +20,11 @@ #' @param url Localhost url. Default to "http://127.0.0.1". "localhost" can be a good alternative. #' @param password String. Default to NULL. If not NULL, `password` will be used as a password to open RStudio server. #' Useful in case you're using `{devindocker}` on a remote server. +#' @param mount_mode Optional bind-mount consistency mode passed to every `-v` +#' flag in the `docker run` command. Recommended values on macOS are +#' `"delegated"` (best for high-I/O volumes such as the renv cache and the +#' RStudio config directory) or `"cached"`. Defaults to `NULL` (no mode +#' suffix), which matches the behaviour before this option was introduced. #' #' @importFrom utils browseURL #' @@ -78,7 +83,8 @@ launch_proj_docker <- function(path = ".", volumes, open_url = TRUE, url = "http://127.0.0.1", - password = NULL + password = NULL, + mount_mode = NULL # vbox = FALSE ) { # @param vbox Logical. If Docker run on windows in a virtual box, paths need to be changed @@ -97,7 +103,7 @@ launch_proj_docker <- function(path = ".", volumes[,"local"] <- normalizePath(as.character(volumes[,"local"])) add_volumes <- paste( - apply(volumes, 1, function(x) paste0(" -v '", x["local"], ":", x["docker"], "'")), + apply(volumes, 1, function(x) build_volume_arg(x["local"], x["docker"], mode = mount_mode)), collapse = "" ) } else { @@ -220,15 +226,15 @@ launch_proj_docker <- function(path = ".", # {renv} # _Global renv cache ifelse(!is.null(renv_cache), paste0(" -e RENV_PATHS_CACHE=", RENV_PATHS_CACHE_CONTAINER), ""), - ifelse(!is.null(renv_cache), paste0(" -v ", RENV_PATHS_CACHE_HOST, ":", RENV_PATHS_CACHE_CONTAINER), ""), + ifelse(!is.null(renv_cache), build_volume_arg(RENV_PATHS_CACHE_HOST, RENV_PATHS_CACHE_CONTAINER, mode = mount_mode), ""), # _Project renv library ifelse(isTRUE(renv_out), paste0(" -e RENV_PATHS_LIBRARY_ROOT=", RENV_PATHS_LIBRARY_ROOT_CONTAINER), ""), - ifelse(isTRUE(renv_out), paste0(" -v ", RENV_PATHS_LIBRARY_ROOT_HOST, ":", RENV_PATHS_LIBRARY_ROOT_CONTAINER), ""), + ifelse(isTRUE(renv_out), build_volume_arg(RENV_PATHS_LIBRARY_ROOT_HOST, RENV_PATHS_LIBRARY_ROOT_CONTAINER, mode = mount_mode), ""), # Rstudio server - " -v '", path, ":/home/rstudio/", projectname, "'", - " -v '", normalizePath(file.path(path, "rstudio_dotconfig"), mustWork = TRUE), ":/home/rstudio/.config'", #/rstudio - " -v '", normalizePath(file.path(path, "rstudio_dotrstudio"), mustWork = TRUE), ":/home/rstudio/.rstudio'", + build_volume_arg(path, paste0("/home/rstudio/", projectname), mode = mount_mode), + build_volume_arg(normalizePath(file.path(path, "rstudio_dotconfig"), mustWork = TRUE), "/home/rstudio/.config", mode = mount_mode), + build_volume_arg(normalizePath(file.path(path, "rstudio_dotrstudio"), mustWork = TRUE), "/home/rstudio/.rstudio", mode = mount_mode), # Addtional volumes ifelse(!is.null(add_volumes), add_volumes, ""), @@ -297,3 +303,36 @@ clean_project_name <- function(x) { clean_dir_name <- function(x) { gsub(" ", "\\", x) } + +#' Render a docker `-v` volume argument, optionally with a mount mode +#' +#' On macOS, high-I/O bind mounts (e.g. the renv cache or RStudio's config +#' directory) benefit from the `delegated` or `cached` consistency mode to +#' reduce FS latency. This helper is used by [launch_proj_docker()] to build +#' every `-v` flag. +#' +#' @param local host path to mount. +#' @param container container mount point. +#' @param mode one of `NULL` / `NA` (no mode suffix), `"delegated"`, +#' `"cached"` or `"consistent"`. See +#' for details. +#' +#' @return a single string like `" -v '/host:/container[:mode]'"`, including +#' a leading space so the result can be concatenated into the `docker run` +#' command as-is. +#' @export +build_volume_arg <- function(local, container, mode = NULL) { + if (!is.null(mode) && !is.na(mode)) { + allowed <- c("delegated", "cached", "consistent") + if (!mode %in% allowed) { + stop( + "`mode` must be one of: ", paste(allowed, collapse = ", "), + " (got '", mode, "')", + call. = FALSE + ) + } + sprintf(" -v '%s:%s:%s'", local, container, mode) + } else { + sprintf(" -v '%s:%s'", local, container) + } +} diff --git a/man/build_volume_arg.Rd b/man/build_volume_arg.Rd new file mode 100644 index 0000000..5769135 --- /dev/null +++ b/man/build_volume_arg.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/launch-stop-docker.R +\name{build_volume_arg} +\alias{build_volume_arg} +\title{Render a docker \code{-v} volume argument, optionally with a mount mode} +\usage{ +build_volume_arg(local, container, mode = NULL) +} +\arguments{ +\item{local}{host path to mount.} + +\item{container}{container mount point.} + +\item{mode}{one of \code{NULL} / \code{NA} (no mode suffix), \code{"delegated"}, +\code{"cached"} or \code{"consistent"}. See +\url{https://docs.docker.com/engine/storage/bind-mounts/} for details.} +} +\value{ +a single string like \code{" -v '/host:/container[:mode]'"}, including +a leading space so the result can be concatenated into the \verb{docker run} +command as-is. +} +\description{ +On macOS, high-I/O bind mounts (e.g. the renv cache or RStudio's config +directory) benefit from the \code{delegated} or \code{cached} consistency mode to +reduce FS latency. This helper is used by \code{\link[=launch_proj_docker]{launch_proj_docker()}} to build +every \code{-v} flag. +} diff --git a/man/devindocker-package.Rd b/man/devindocker-package.Rd index 91c872c..9459975 100644 --- a/man/devindocker-package.Rd +++ b/man/devindocker-package.Rd @@ -6,9 +6,17 @@ \alias{devindocker-package} \title{devindocker: Help Develop a R Project Inside a Docker Container} \description{ -\if{html}{\figure{logo.png}{options: align='right' alt='logo' width='120'}} +\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} Help develop a R project inside a Docker container. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://thinkr-open.github.io/devindocker/} + \item \url{https://github.com/Thinkr-open/devindocker} +} + } \author{ \strong{Maintainer}: Sébastien Rochette \email{sebastien@thinkr.fr} (\href{https://orcid.org/0000-0002-1565-9313}{ORCID}) diff --git a/man/launch_proj_docker.Rd b/man/launch_proj_docker.Rd index 0e90bf4..dada67b 100644 --- a/man/launch_proj_docker.Rd +++ b/man/launch_proj_docker.Rd @@ -19,7 +19,8 @@ launch_proj_docker( volumes, open_url = TRUE, url = "http://127.0.0.1", - password = NULL + password = NULL, + mount_mode = NULL ) stop_proj_docker(path, sleep = 10, network_name = "r-db", stop_network = TRUE) @@ -59,6 +60,12 @@ Can be useful if you want to simulate your CI behaviour in the Terminal using "\ \item{password}{String. Default to NULL. If not NULL, \code{password} will be used as a password to open RStudio server. Useful in case you're using \code{{devindocker}} on a remote server.} +\item{mount_mode}{Optional bind-mount consistency mode passed to every \code{-v} +flag in the \verb{docker run} command. Recommended values on macOS are +\code{"delegated"} (best for high-I/O volumes such as the renv cache and the +RStudio config directory) or \code{"cached"}. Defaults to \code{NULL} (no mode +suffix), which matches the behaviour before this option was introduced.} + \item{sleep}{Numeric. Number of seconds to wait for user to correctly stop Rstudio Server} \item{stop_network}{Logical. Whether to stop Docker network.} diff --git a/tests/testthat/test-build_volume_arg.R b/tests/testthat/test-build_volume_arg.R new file mode 100644 index 0000000..4c88d33 --- /dev/null +++ b/tests/testthat/test-build_volume_arg.R @@ -0,0 +1,39 @@ +testthat::context("build_volume_arg (#9)") + +test_that("build_volume_arg renders a plain -v pair", { + testthat::expect_equal( + build_volume_arg("/host/path", "/container/path"), + " -v '/host/path:/container/path'" + ) +}) + +test_that("build_volume_arg appends :delegated when requested (#9)", { + testthat::expect_equal( + build_volume_arg("/host", "/container", mode = "delegated"), + " -v '/host:/container:delegated'" + ) +}) + +test_that("build_volume_arg accepts :cached and :consistent", { + testthat::expect_equal( + build_volume_arg("/a", "/b", mode = "cached"), + " -v '/a:/b:cached'" + ) + testthat::expect_equal( + build_volume_arg("/a", "/b", mode = "consistent"), + " -v '/a:/b:consistent'" + ) +}) + +test_that("build_volume_arg rejects unknown modes", { + testthat::expect_error( + build_volume_arg("/a", "/b", mode = "bogus"), + "mode" + ) +}) + +test_that("NULL / NA mode is equivalent to omitting the mode", { + plain <- build_volume_arg("/a", "/b") + testthat::expect_equal(build_volume_arg("/a", "/b", mode = NULL), plain) + testthat::expect_equal(build_volume_arg("/a", "/b", mode = NA), plain) +})