From f081ce4121dfea3b97ff9e79f934889481d6256e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kirill=20M=C3=BCller?= Date: Sun, 16 Nov 2025 14:10:03 +0100 Subject: [PATCH 1/4] feat: New `install_runiverse()` --- NAMESPACE | 1 + R/install-runiverse.R | 93 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 R/install-runiverse.R diff --git a/NAMESPACE b/NAMESPACE index 57b020a5..9fac6bcb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -84,6 +84,7 @@ export(install_github) export(install_gitlab) export(install_local) export(install_remote) +export(install_runiverse) export(install_svn) export(install_url) export(install_version) diff --git a/R/install-runiverse.R b/R/install-runiverse.R new file mode 100644 index 00000000..da199622 --- /dev/null +++ b/R/install-runiverse.R @@ -0,0 +1,93 @@ +#' Install a package from R-Universe +#' +#' `install_runiverse()` retrieves the canonical universe for a CRAN package +#' using the R-Universe API, +#' then downloads and installs the package from that universe. +#' If the package has a `Remotes` field, +#' dependencies will be installed first from the appropriate remote repositories +#' using the same function. +#' +#' @param package The package name to install. +#' @param universe The R-Universe to use, infer from the package if `NULL`. +#' @return A character vector of the names of installed packages, invisibly. +#' @family package installation +#' @export +#' @examples +#' \dontrun{ +#' # From GitHub +#' install_runiverse("dplyr") +#' +#' # From GitLab +#' install_runiverse("iemiscdata") +#' +#' # From Bitbucket +#' install_runiverse("argparser") +#' } +install_runiverse <- function(package, universe = NULL, ...) { + if (is.null(universe)) { + universe <- get_runiverse_for_package(package) + } else if (length(universe) != 1 || !is.character(universe)) { + stop("'universe' must be a single string", call. = FALSE) + } + + # https://github.com/r-lib/remotes/issues/618#issuecomment-3333533114 + repo <- paste0("https://", universe, ".r-universe.dev/", package) + + tempdir <- tempfile("remotes") + dir.create(tempdir) + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + + # available.packages() does not work for the repo + download <- utils::download.packages(package, destdir = tempdir, repos = repo)[, 2] + + untar_success <- utils::untar(download, file.path(package, "DESCRIPTION"), exdir = tempdir) + if (untar_success != 0) { + stop("Failed to extract package DESCRIPTION from downloaded tarball", call. = FALSE) + } + + desc_path <- file.path(tempdir, package, "DESCRIPTION") + desc <- read_dcf(desc_path) + + installed <- character() + + if (!is.null(desc$Remotes)) { + message("Installing dependencies from Remotes field: ", desc$Remotes) + + remotes <- strsplit(desc$Remotes, "[ \n]*,[ \n]*")[[1]] + org_pkg <- re_match(remotes, "^(?:github::)?(?[^/:]+)/(?[^/@#]+)$") + + for (i in seq_len(nrow(org_pkg))) { + if (is.na(org_pkg$.match[[i]])) { + install_remote(org_pkg$.text[[i]]) + } else { + install_runiverse(org_pkg$pkg[[i]], universe = org_pkg$org[[i]], ...) + } + } + } + + # We already downloaded but can't provide a correct `type` argument + install_cran(package, repos = repo) +} + +get_runiverse_for_package <- function(package) { + # Can't use httr2, only curl + if (!requireNamespace("curl", quietly = TRUE)) { + stop("Package 'curl' is required to install from R-Universe", call. = FALSE) + } + + handle <- curl::new_handle() + curl::handle_setheaders(handle, `User-Agent` = "r-lib/remotes") + + packages_raw <- curl::curl_fetch_memory( + paste0("https://r-universe.dev/api/search?q=package:", package) + ) + + packages <- json$parse(rawToChar(packages_raw$content)) + + results <- packages$results + if (length(results) == 0) { + stop(sprintf("Package '%s' not found on R-Universe", package), call. = FALSE) + } + + packages$results[[1]]$"_user" +} From 83acc1586af7c23f24363ad9f97c3630a1a6b035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kirill=20M=C3=BCller?= Date: Sun, 16 Nov 2025 14:41:43 +0100 Subject: [PATCH 2/4] linux_distro argument --- R/install-runiverse.R | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/R/install-runiverse.R b/R/install-runiverse.R index da199622..40dc2279 100644 --- a/R/install-runiverse.R +++ b/R/install-runiverse.R @@ -9,6 +9,9 @@ #' #' @param package The package name to install. #' @param universe The R-Universe to use, infer from the package if `NULL`. +#' @param ... Additional arguments passed to `install_cran()`. +#' @param linux_distro A string specifying the Linux distribution +#' for the installation of binary packages on Linux. #' @return A character vector of the names of installed packages, invisibly. #' @family package installation #' @export @@ -23,7 +26,14 @@ #' # From Bitbucket #' install_runiverse("argparser") #' } -install_runiverse <- function(package, universe = NULL, ...) { +install_runiverse <- function(package, universe = NULL, ..., linux_distro = NULL) { + if (...length() > 0) { + stop( + "Additional arguments (...) are not supported by install_runiverse()", + call. = FALSE + ) + } + if (is.null(universe)) { universe <- get_runiverse_for_package(package) } else if (length(universe) != 1 || !is.character(universe)) { @@ -31,7 +41,16 @@ install_runiverse <- function(package, universe = NULL, ...) { } # https://github.com/r-lib/remotes/issues/618#issuecomment-3333533114 - repo <- paste0("https://", universe, ".r-universe.dev/", package) + # https://docs.r-universe.dev/install/binaries.html#how-to-install-linux-binary-packages + if (is.null(linux_distro)) { + repo <- paste0("https://", universe, ".r-universe.dev/", package) + } else { + repo <- paste0( + "https://", universe, ".r-universe.dev/", + "bin/linux/", linux_distro, "-", R.version$arch, "/", + substr(getRversion(), 1, 3), "/", package + ) + } tempdir <- tempfile("remotes") dir.create(tempdir) @@ -60,7 +79,7 @@ install_runiverse <- function(package, universe = NULL, ...) { if (is.na(org_pkg$.match[[i]])) { install_remote(org_pkg$.text[[i]]) } else { - install_runiverse(org_pkg$pkg[[i]], universe = org_pkg$org[[i]], ...) + install_runiverse(org_pkg$pkg[[i]], universe = org_pkg$org[[i]], linux_distro = linux_distro) } } } From 9cd05cb4c00c516b1043a786f1d56bcc1c9e06c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kirill=20M=C3=BCller?= Date: Sun, 16 Nov 2025 15:19:49 +0100 Subject: [PATCH 3/4] Use big repo --- R/install-runiverse.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/install-runiverse.R b/R/install-runiverse.R index 40dc2279..9fbf81a4 100644 --- a/R/install-runiverse.R +++ b/R/install-runiverse.R @@ -48,7 +48,7 @@ install_runiverse <- function(package, universe = NULL, ..., linux_distro = NULL repo <- paste0( "https://", universe, ".r-universe.dev/", "bin/linux/", linux_distro, "-", R.version$arch, "/", - substr(getRversion(), 1, 3), "/", package + substr(getRversion(), 1, 3) ) } @@ -85,7 +85,7 @@ install_runiverse <- function(package, universe = NULL, ..., linux_distro = NULL } # We already downloaded but can't provide a correct `type` argument - install_cran(package, repos = repo) + install_cran(package, repos = repo, dependencies = FALSE) } get_runiverse_for_package <- function(package) { From 195c652988eaa9459ec767854756f423d1108e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kirill=20M=C3=BCller?= Date: Sun, 16 Nov 2025 15:55:40 +0100 Subject: [PATCH 4/4] Put package in the right position --- R/install-runiverse.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/install-runiverse.R b/R/install-runiverse.R index 9fbf81a4..ad31ae89 100644 --- a/R/install-runiverse.R +++ b/R/install-runiverse.R @@ -46,8 +46,8 @@ install_runiverse <- function(package, universe = NULL, ..., linux_distro = NULL repo <- paste0("https://", universe, ".r-universe.dev/", package) } else { repo <- paste0( - "https://", universe, ".r-universe.dev/", - "bin/linux/", linux_distro, "-", R.version$arch, "/", + "https://", universe, ".r-universe.dev/", package, + "/bin/linux/", linux_distro, "-", R.version$arch, "/", substr(getRversion(), 1, 3) ) }