-
Notifications
You must be signed in to change notification settings - Fork 2
feat: experimental uvr backend (shiny2docker_uvr) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
cb64451
e11ea24
50e6cfe
9df9837
8570e52
0a7b2e5
6b295bc
8e31564
d821744
a83445e
93cac67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #' shiny2docker_uvr | ||
| #' | ||
| #' Generate a Dockerfile for a Shiny application using | ||
| #' [uvr](https://github.com/nbafrank/uvr) instead of `renv`. The R version is | ||
| #' installed inside the container by `uvr` (no `rocker/r-ver` base image | ||
| #' required) and packages are restored from `uvr.lock` via `uvr sync`. | ||
| #' | ||
| #' Lifecycle: experimental. Requires the project to already be uvr-managed | ||
| #' (`uvr.toml`, `uvr.lock`, and `.r-version` present at the project root). | ||
| #' | ||
| #' Platform constraints: the generated Dockerfile uses `apt-get` and Debian | ||
| #' package names, and downloads the `x86_64-unknown-linux-gnu` uvr binary, so | ||
| #' `base_image` must be a Debian/Ubuntu-derived image on amd64/glibc. | ||
| #' | ||
| #' @param path Character. Path to the folder containing the Shiny application. | ||
| #' @param output Character. Path to the generated Dockerfile. | ||
| #' @param uvr_toml Path to the `uvr.toml` manifest. Defaults to `<path>/uvr.toml`. | ||
| #' @param uvr_lock Path to the `uvr.lock` lockfile. Defaults to `<path>/uvr.lock`. | ||
| #' @param r_version_file Path to the R version pin. Defaults to `<path>/.r-version`. | ||
| #' @param base_image Character. Docker base image. Defaults to `"debian:stable-slim"`. | ||
| #' Must be a Debian/Ubuntu-based amd64 image (see "Platform constraints" above). | ||
| #' @param uvr_version Character. `"latest"` or a semver tag like `"v0.3.1"`. | ||
| #' Pinning a tag is recommended for reproducible builds. | ||
| #' @param port Integer in `[1, 65535]`. Shiny port to expose. Default `3838`. | ||
| #' @param host Character. Shiny host. Default `"0.0.0.0"`. Single value, no | ||
| #' shell metacharacters; validated before being interpolated into the image. | ||
| #' @param extra_sysreqs Character vector of extra debian package names to | ||
| #' install before `uvr sync` (escape hatch for system dependencies that uvr | ||
| #' does not detect on its own). Each entry must match | ||
| #' `[A-Za-z0-9.+:-]+` to avoid shell injection. | ||
| #' @param write Logical. Whether to write the Dockerfile and `.dockerignore` | ||
| #' to disk. When `FALSE`, the function only returns the in-memory Dockerfile | ||
| #' object and does not touch the filesystem. Default `TRUE`. | ||
| #' | ||
| #' @return Invisibly, an R6 `dockerfiler::Dockerfile` object that can be further | ||
| #' customised before writing. | ||
| #' | ||
| #' @export | ||
| #' | ||
| #' @importFrom dockerfiler Dockerfile | ||
| shiny2docker_uvr <- function(path = ".", | ||
| output = file.path(path, "Dockerfile"), | ||
| uvr_toml = file.path(path, "uvr.toml"), | ||
| uvr_lock = file.path(path, "uvr.lock"), | ||
| r_version_file = file.path(path, ".r-version"), | ||
| base_image = "debian:stable-slim", | ||
| uvr_version = "latest", | ||
| port = 3838, | ||
| host = "0.0.0.0", | ||
| extra_sysreqs = NULL, | ||
| write = TRUE) { | ||
|
|
||
| uvr_assert_state( | ||
| uvr_toml = uvr_toml, | ||
| uvr_lock = uvr_lock, | ||
| r_version_file = r_version_file | ||
| ) | ||
|
|
||
| uvr_validate_host_port(host = host, port = port) | ||
| uvr_validate_sysreqs(extra_sysreqs) | ||
|
|
||
| dock <- uvr_build_dockerfile( | ||
| base_image = base_image, | ||
| uvr_version = uvr_version, | ||
| port = port, | ||
| host = host, | ||
| extra_sysreqs = extra_sysreqs | ||
| ) | ||
|
|
||
| if (isTRUE(write)) { | ||
| dockerignore <- file.path(dirname(output), ".dockerignore") | ||
| if (!file.exists(dockerignore)) { | ||
| create_dockerignore_uvr(path = dockerignore) | ||
| } | ||
| dock$write(output) | ||
| } | ||
|
|
||
| invisible(dock) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,251 @@ | ||||||||
| #' Write a uvr-aware .dockerignore | ||||||||
| #' | ||||||||
| #' Adds the entries that are specific to a uvr-managed project (`.uvr/`, | ||||||||
| #' `.Rprofile` written by `uvr init`, leftover `renv/` artefacts) on top of | ||||||||
| #' the defaults used by the `renv` backend. | ||||||||
| #' | ||||||||
| #' @param path Path to the `.dockerignore` to write. | ||||||||
| #' @noRd | ||||||||
| create_dockerignore_uvr <- function(path = ".dockerignore") { | ||||||||
| entries <- c( | ||||||||
| ".Rhistory", | ||||||||
| ".git", | ||||||||
| ".gitignore", | ||||||||
| ".Rproj.user", | ||||||||
| "manifest.json", | ||||||||
| "rsconnect/", | ||||||||
| ".uvr/", | ||||||||
| ".Rprofile", | ||||||||
| "renv/", | ||||||||
| "renv.lock" | ||||||||
| ) | ||||||||
|
VincentGuyader marked this conversation as resolved.
|
||||||||
| writeLines(entries, con = path) | ||||||||
| } | ||||||||
|
|
||||||||
| #' Validate user-supplied host and port | ||||||||
| #' | ||||||||
| #' Both values are interpolated into shell commands inside the Dockerfile, so | ||||||||
| #' we reject anything that could break the build or inject extra tokens. | ||||||||
| #' | ||||||||
| #' @param host Character of length 1. | ||||||||
| #' @param port Numeric of length 1, integer in [1, 65535]. | ||||||||
| #' @return Invisibly `TRUE` on success; raises an error otherwise. | ||||||||
| #' @noRd | ||||||||
| uvr_validate_host_port <- function(host, port) { | ||||||||
| if (!is.character(host) || length(host) != 1L || is.na(host) || | ||||||||
| !grepl("^[A-Za-z0-9._-]+$", host)) { | ||||||||
| stop("`host` must be a single non-NA character matching ", | ||||||||
| "[A-Za-z0-9._-]+ (e.g. \"0.0.0.0\").", call. = FALSE) | ||||||||
| } | ||||||||
| if (!is.numeric(port) || length(port) != 1L || is.na(port) || | ||||||||
| port != as.integer(port) || port < 1L || port > 65535L) { | ||||||||
| stop("`port` must be a single integer in [1, 65535].", call. = FALSE) | ||||||||
| } | ||||||||
| invisible(TRUE) | ||||||||
| } | ||||||||
|
|
||||||||
| #' Validate user-supplied extra system requirements | ||||||||
| #' | ||||||||
| #' Each entry is concatenated into an `apt-get install` command in the | ||||||||
| #' Dockerfile. We restrict to a conservative Debian package-name pattern to | ||||||||
| #' avoid shell injection. | ||||||||
| #' | ||||||||
| #' @param sysreqs `NULL` or a character vector. | ||||||||
| #' @return Invisibly `TRUE` on success; raises an error otherwise. | ||||||||
| #' @noRd | ||||||||
| uvr_validate_sysreqs <- function(sysreqs) { | ||||||||
| if (is.null(sysreqs) || length(sysreqs) == 0L) return(invisible(TRUE)) | ||||||||
| if (!is.character(sysreqs) || any(is.na(sysreqs))) { | ||||||||
| stop("`extra_sysreqs` must be a character vector with no NA.", call. = FALSE) | ||||||||
| } | ||||||||
| bad <- !grepl("^[A-Za-z0-9.+:-]+$", sysreqs) | ||||||||
| if (any(bad)) { | ||||||||
| stop("Invalid package name(s) in `extra_sysreqs`: ", | ||||||||
| paste(shQuote(sysreqs[bad]), collapse = ", "), | ||||||||
| ". Allowed characters: A-Z a-z 0-9 . + : -", call. = FALSE) | ||||||||
| } | ||||||||
| invisible(TRUE) | ||||||||
| } | ||||||||
|
|
||||||||
| #' Assert that the project is uvr-ready | ||||||||
| #' | ||||||||
| #' Checks that `uvr.toml`, `uvr.lock` and a R version pin file all exist, and | ||||||||
| #' that `uvr.lock` is at least as recent as `uvr.toml`. Stops with an actionable | ||||||||
| #' message otherwise. | ||||||||
| #' | ||||||||
| #' @param uvr_toml Path to `uvr.toml`. | ||||||||
| #' @param uvr_lock Path to `uvr.lock`. | ||||||||
| #' @param r_version_file Path to `.r-version`. | ||||||||
| #' | ||||||||
| #' @return Invisibly `TRUE` on success. | ||||||||
| #' @noRd | ||||||||
| uvr_assert_state <- function(uvr_toml, uvr_lock, r_version_file) { | ||||||||
|
|
||||||||
| if (!file.exists(uvr_toml)) { | ||||||||
| stop( | ||||||||
| "uvr.toml not found at '", uvr_toml, "'.\n", | ||||||||
| "Run `uvr init` (or `uvr import` from an existing renv.lock), then `uvr lock`.", | ||||||||
| call. = FALSE | ||||||||
| ) | ||||||||
| } | ||||||||
| if (!file.exists(uvr_lock)) { | ||||||||
| stop( | ||||||||
| "uvr.lock not found at '", uvr_lock, "'.\n", | ||||||||
| "Run `uvr lock` to generate it.", | ||||||||
| call. = FALSE | ||||||||
| ) | ||||||||
| } | ||||||||
| if (file.info(uvr_lock)$mtime < file.info(uvr_toml)$mtime) { | ||||||||
| stop( | ||||||||
| "uvr.lock is older than uvr.toml — manifest changed since last lock.\n", | ||||||||
| "Run `uvr lock` to refresh it.", | ||||||||
| call. = FALSE | ||||||||
| ) | ||||||||
| } | ||||||||
| if (!file.exists(r_version_file)) { | ||||||||
| stop( | ||||||||
| ".r-version not found at '", r_version_file, "'.\n", | ||||||||
| "Run `uvr r pin <version>` to pin the R version for the project.", | ||||||||
| call. = FALSE | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| invisible(TRUE) | ||||||||
| } | ||||||||
|
VincentGuyader marked this conversation as resolved.
VincentGuyader marked this conversation as resolved.
|
||||||||
|
|
||||||||
| #' Build the uvr release download URL | ||||||||
| #' | ||||||||
| #' @param uvr_version Either `"latest"` or a tag like `"v0.3.1"` / `"0.3.1"`. | ||||||||
| #' @return A list with `url` and `arg_value`. `arg_value` is `NA` when | ||||||||
| #' `uvr_version = "latest"` (no `ARG UVR_VERSION` injected). | ||||||||
| #' @noRd | ||||||||
| uvr_release_url <- function(uvr_version = "latest") { | ||||||||
| asset <- "uvr-x86_64-unknown-linux-gnu.tar.gz" | ||||||||
| base <- "https://github.com/nbafrank/uvr/releases" | ||||||||
|
|
||||||||
| if (identical(uvr_version, "latest")) { | ||||||||
| return(list( | ||||||||
| url = sprintf("%s/latest/download/%s", base, asset), | ||||||||
| arg_value = NA_character_ | ||||||||
| )) | ||||||||
| } | ||||||||
|
|
||||||||
| tag <- if (startsWith(uvr_version, "v")) uvr_version else paste0("v", uvr_version) | ||||||||
| if (!grepl("^v[0-9]+\\.[0-9]+\\.[0-9]+$", tag)) { | ||||||||
| stop( | ||||||||
| "uvr_version must be 'latest' or a semver tag like 'v0.3.1' / '0.3.1'. ", | ||||||||
| "Got: '", uvr_version, "'", | ||||||||
| call. = FALSE | ||||||||
| ) | ||||||||
| } | ||||||||
|
VincentGuyader marked this conversation as resolved.
|
||||||||
| list( | ||||||||
| url = sprintf("%s/download/%s/%s", base, tag, asset), | ||||||||
| arg_value = tag | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| #' Build the Dockerfile object for the uvr backend | ||||||||
| #' | ||||||||
| #' @param base_image Base OS image (e.g. `"debian:stable-slim"`). | ||||||||
| #' @param uvr_version `"latest"` or a tag (`"v0.3.1"`). | ||||||||
| #' @param port Shiny port. | ||||||||
| #' @param host Shiny host. | ||||||||
| #' @param extra_sysreqs Optional character vector of extra debian packages to | ||||||||
| #' `apt-get install` before `uvr sync`. | ||||||||
| #' | ||||||||
| #' @return An R6 `dockerfiler::Dockerfile` object. | ||||||||
| #' @noRd | ||||||||
| uvr_build_dockerfile <- function(base_image = "debian:stable-slim", | ||||||||
| uvr_version = "latest", | ||||||||
| port = 3838, | ||||||||
| host = "0.0.0.0", | ||||||||
| extra_sysreqs = NULL) { | ||||||||
|
|
||||||||
| release <- uvr_release_url(uvr_version) | ||||||||
|
|
||||||||
| dock <- dockerfiler::Dockerfile$new(FROM = base_image) | ||||||||
|
|
||||||||
| dock$ENV("DEBIAN_FRONTEND", "noninteractive") | ||||||||
| dock$ENV("LANG", "en_US.UTF-8") | ||||||||
| dock$ENV("LC_ALL", "en_US.UTF-8") | ||||||||
|
|
||||||||
| dock$RUN(paste( | ||||||||
| "apt-get update && apt-get install -y --no-install-recommends", | ||||||||
| # base tooling | ||||||||
| "ca-certificates curl locales tzdata", | ||||||||
| # binutils provides `ar`, used by `uvr r install` to extract the Posit .deb | ||||||||
| "binutils", | ||||||||
| # runtime + build deps required by Posit's R .deb | ||||||||
| "g++ gcc gfortran make zip unzip ucf", | ||||||||
| "libbz2-dev libc6 libcairo2 libcurl4-openssl-dev libdeflate-dev", | ||||||||
| "libglib2.0-0 libgomp1 libicu-dev liblzma-dev libopenblas-dev", | ||||||||
| "libpango-1.0-0 libpangocairo-1.0-0 libpaper-utils libpcre2-dev", | ||||||||
| "libpng16-16 libreadline8 libtcl8.6 libtiff6 libtirpc-dev libtk8.6", | ||||||||
| "libx11-6 libxt6 zlib1g-dev", | ||||||||
| # extra deps commonly needed when uvr falls back to source compilation | ||||||||
| # (P3M binaries are not always available on debian targets as of uvr 0.2.15) | ||||||||
| "libuv1-dev libxml2-dev libssl-dev libsodium-dev", | ||||||||
| "&& sed -i '/en_US.UTF-8/s/^# //' /etc/locale.gen && locale-gen", | ||||||||
| "&& rm -rf /var/lib/apt/lists/*" | ||||||||
| )) | ||||||||
|
|
||||||||
| if (!is.na(release$arg_value)) { | ||||||||
| dock$ARG(paste0("UVR_VERSION=", release$arg_value)) | ||||||||
| } | ||||||||
|
|
||||||||
|
Comment on lines
+483
to
+486
|
||||||||
| if (!is.na(release$arg_value)) { | |
| dock$ARG(paste0("UVR_VERSION=", release$arg_value)) | |
| } |
Uh oh!
There was an error while loading. Please reload this page.