diff --git a/crates/ark/src/modules/positron/repos.R b/crates/ark/src/modules/positron/repos.R index 989a38341..2a5d28f28 100644 --- a/crates/ark/src/modules/positron/repos.R +++ b/crates/ark/src/modules/positron/repos.R @@ -19,10 +19,13 @@ apply_repo_defaults <- function( repos <- defaults } else { if ("CRAN" %in% names(repos) && "CRAN" %in% names(defaults)) { - # If a CRAN repository is set to @CRAN@, and a default provides a - # specific URL, override it. This is the only instance in which we - # replace an already-set repository option with a default. - if (identical(repos[["CRAN"]], "@CRAN@")) { + # If a CRAN repository is set to @CRAN@ or is marked as having been + # updated by "the IDE" *and* a default provides a specific URL, + # override it. + # + # This is the only instance in which we replace an already-set + # repository option with a default. + if (identical(repos[["CRAN"]], "@CRAN@") || isTRUE(attr(repos, "IDE"))) { repos[["CRAN"]] <- defaults[["CRAN"]] # Flag this CRAN repository as being set by the IDE. This flag is @@ -39,3 +42,25 @@ apply_repo_defaults <- function( } options(repos = repos) } + +#' Set the Posit Package Manager repository +#' +#' Sets the Posit Package Manager repository URL for the current session. The +#' URL will be processed to point to a Linux distribution-specific binary URL if +#' applicable. +#' +#' This function only overrides the CRAN repository when Ark has previously set +#' it or when it uses placeholder `"@CRAN@"`. +#' +#' @param url A PPM repository URL. Must be in the form +#' `"https://host/repo/snapshot"`, e.g., +#' `"https://packagemanager.posit.co/cran/latest"`. +#' +#' @return `NULL`, invisibly. +#' +#' @export +.ps.set_ppm_repo <- function(url) { + # Use Ark's built-in PPM binary URL detection. + url <- .ps.Call("ps_get_ppm_binary_url", url) + apply_repo_defaults(c(CRAN = url)) +} diff --git a/crates/ark/src/repos.rs b/crates/ark/src/repos.rs index 83ce87b9d..2b50e63fb 100644 --- a/crates/ark/src/repos.rs +++ b/crates/ark/src/repos.rs @@ -13,9 +13,11 @@ use std::io::BufRead; use std::io::BufReader; use std::path::PathBuf; +use anyhow::Context; use harp::exec::RFunction; use harp::exec::RFunctionExt; use harp::RObject; +use libr::SEXP; use crate::modules::ARK_ENVS; @@ -346,6 +348,28 @@ fn get_ppm_binary_package_repo(repo_url: Option) -> String { } } +#[harp::register] +pub extern "C-unwind" fn ps_get_ppm_binary_url(url: SEXP) -> anyhow::Result { + let url_string = unsafe { RObject::view(url).to::().context("`url` must be a string")? }; + if url_string.is_empty() { + return Err(anyhow::anyhow!("Empty Package Manager URL provided")); + } + + // Validate the PPM URL structure. + let parsed = url::Url::parse(&url_string).context("Invalid URL format")?; + let segments = parsed + .path_segments() + .ok_or_else(|| anyhow::anyhow!("Package Manager URL must have a path"))?; + if segments.count() != 2 { + return Err(anyhow::anyhow!( + "Package Manager URL must have exactly 2 path segments (e.g., /cran/latest)" + )); + } + + let final_url = get_ppm_binary_package_repo(Some(parsed)); + Ok(RObject::from(final_url).sexp) +} + #[cfg(test)] mod tests { use super::*;