@@ -37,9 +37,11 @@ pub enum DefaultRepos {
3737 /// Set the repository to the default CRAN repository, `cran.rstudio.com`
3838 RStudio ,
3939
40- /// Use Posit's Public Package Manager; this is a Posit-hosted service that hosts built
41- /// binaries for many operating systems.
42- PositPPM ,
40+ /// Use a Posit Package Manager instance with this URL. When the URL is
41+ /// `None`, default to the latest CRAN repository on Posit Public Package
42+ /// Manager, a Posit-hosted service that hosts built binaries for many
43+ /// operating systems.
44+ PositPackageManager ( Option < url:: Url > ) ,
4345
4446 /// Use the repositories specified in the given configuration file.
4547 ConfFile ( PathBuf ) ,
@@ -80,10 +82,19 @@ pub fn apply_default_repos(repos: DefaultRepos) -> anyhow::Result<()> {
8082 apply_default_repos ( DefaultRepos :: Auto )
8183 }
8284 } ,
83- DefaultRepos :: PositPPM => {
85+ DefaultRepos :: PositPackageManager ( None ) => {
8486 log:: info!( "Setting default repositories to Posit's Public Package Manager" ) ;
8587 let mut repos = HashMap :: new ( ) ;
86- repos. insert ( "CRAN" . to_string ( ) , get_p3m_binary_package_repo ( ) ) ;
88+ repos. insert ( "CRAN" . to_string ( ) , get_ppm_binary_package_repo ( None ) ) ;
89+ apply_repos ( repos)
90+ } ,
91+ DefaultRepos :: PositPackageManager ( Some ( url) ) => {
92+ log:: info!(
93+ "Setting default repositories to custom Package Manager repo: {}" ,
94+ url
95+ ) ;
96+ let mut repos = HashMap :: new ( ) ;
97+ repos. insert ( "CRAN" . to_string ( ) , get_ppm_binary_package_repo ( Some ( url) ) ) ;
8798 apply_repos ( repos)
8899 } ,
89100 }
@@ -198,7 +209,12 @@ pub fn apply_repos_conf(path: PathBuf) -> anyhow::Result<()> {
198209
199210/// Checks the Linux distribution name and version to determine the appropriate P3M repository URL.
200211#[ cfg( target_os = "linux" ) ]
201- fn get_p3m_linux_repo ( linux_name : String ) -> String {
212+ fn get_ppm_linux_repo ( repo_url : Option < url:: Url > , linux_name : String ) -> anyhow:: Result < String > {
213+ let generic_url = match repo_url {
214+ Some ( url) => url,
215+ None => url:: Url :: parse ( GENERIC_P3M_REPO ) . unwrap ( ) ,
216+ } ;
217+
202218 // The following Linux names have 1:1 mappings to a P3M repository URL
203219 let repo_names = [
204220 String :: from ( "bookworm" ) ,
@@ -211,26 +227,28 @@ fn get_p3m_linux_repo(linux_name: String) -> String {
211227 String :: from ( "rhel9" ) ,
212228 ] ;
213229
214- // First check for an empty name, and default to the generic P3M repo in that case.
215- // Then handle Linux names with a 1:1 mapping to a P3M repo.
216- // Then handle the special cases which map to different P3M repos.
217- // Otherwise, default to the generic P3M repo.
218- if linux_name. is_empty ( ) {
219- return GENERIC_P3M_REPO . to_string ( ) ;
220- } else if repo_names. contains ( & linux_name) {
221- return format ! (
222- "https://packagemanager.posit.co/cran/__linux__/{}/latest" ,
223- linux_name
224- ) ;
230+ // Handle special cases which map to different P3M repos.
231+ let distro = if repo_names. contains ( & linux_name) {
232+ & linux_name
225233 } else if linux_name == "rhel8" {
226- return "https://packagemanager.posit.co/cran/__linux__/ centos8/latest" . to_string ( ) ;
234+ " centos8"
227235 } else if linux_name == "sles155" {
228- return "https://packagemanager.posit.co/cran/__linux__/ opensuse155/latest" . to_string ( ) ;
236+ " opensuse155"
229237 } else if linux_name == "sles156" {
230- return "https://packagemanager.posit.co/cran/__linux__/ opensuse156/latest" . to_string ( ) ;
238+ " opensuse156"
231239 } else {
232- return GENERIC_P3M_REPO . to_string ( ) ;
240+ return Ok ( generic_url. to_string ( ) ) ;
241+ } ;
242+
243+ let mut distro_url = generic_url. clone ( ) ;
244+ if let Some ( segments) = distro_url. path_segments ( ) {
245+ let parts: Vec < & str > = segments. collect ( ) ;
246+ if parts. len ( ) == 2 {
247+ distro_url. set_path ( & format ! ( "{}/__linux__/{}/{}" , parts[ 0 ] , distro, parts[ 1 ] ) ) ;
248+ return Ok ( distro_url. to_string ( ) ) ;
249+ }
233250 }
251+ anyhow:: bail!( "Invalid Package Manager repository URL: {}" , distro_url) ;
234252}
235253
236254#[ cfg( target_os = "linux" ) ]
@@ -262,7 +280,12 @@ fn get_p3m_linux_codename(id: String, version: String, version_codename: String)
262280 }
263281}
264282
265- fn get_p3m_binary_package_repo ( ) -> String {
283+ fn get_ppm_binary_package_repo ( repo_url : Option < url:: Url > ) -> String {
284+ let generic_url = match repo_url {
285+ Some ( ref url) => url. clone ( ) . to_string ( ) ,
286+ None => GENERIC_P3M_REPO . to_string ( ) ,
287+ } ;
288+
266289 #[ cfg( target_os = "linux" ) ]
267290 {
268291 // For Linux, we want a distro-specific URL if possible
@@ -285,14 +308,140 @@ fn get_p3m_binary_package_repo() -> String {
285308 version = line[ version_id_key. len ( ) ..] . to_string ( ) ;
286309 }
287310 }
311+ } else {
312+ log:: error!(
313+ "Error opening /etc/os-release, falling back to generic URL: {generic_url}" ,
314+ ) ;
315+ return generic_url;
288316 }
289317
290- get_p3m_linux_repo ( get_p3m_linux_codename ( id, version, version_codename) )
318+ let codename = get_p3m_linux_codename ( id, version, version_codename) ;
319+ match get_ppm_linux_repo ( repo_url, codename) {
320+ Ok ( url) => url,
321+ Err ( e) => {
322+ log:: error!(
323+ "Error determining Linux binary repository URL, falling back to generic URL '{generic_url}': {e}" ,
324+ ) ;
325+ generic_url
326+ } ,
327+ }
291328 }
292329
293330 #[ cfg( not( target_os = "linux" ) ) ]
294331 {
295- // For non-Linux, we can use the generic P3M URL
296- GENERIC_P3M_REPO . to_string ( )
332+ // For non-Linux, we can use the generic URL
333+ generic_url
334+ }
335+ }
336+
337+ #[ cfg( test) ]
338+ mod tests {
339+ use super :: * ;
340+
341+ #[ test]
342+ #[ cfg( target_os = "linux" ) ]
343+ fn test_get_ppm_linux_repo ( ) {
344+ let test_cases = vec ! [
345+ // Supported distros.
346+ (
347+ "bookworm" ,
348+ "https://packagemanager.posit.co/cran/__linux__/bookworm/latest" ,
349+ ) ,
350+ (
351+ "bullseye" ,
352+ "https://packagemanager.posit.co/cran/__linux__/bullseye/latest" ,
353+ ) ,
354+ (
355+ "focal" ,
356+ "https://packagemanager.posit.co/cran/__linux__/focal/latest" ,
357+ ) ,
358+ (
359+ "jammy" ,
360+ "https://packagemanager.posit.co/cran/__linux__/jammy/latest" ,
361+ ) ,
362+ (
363+ "noble" ,
364+ "https://packagemanager.posit.co/cran/__linux__/noble/latest" ,
365+ ) ,
366+ (
367+ "opensuse155" ,
368+ "https://packagemanager.posit.co/cran/__linux__/opensuse155/latest" ,
369+ ) ,
370+ (
371+ "opensuse156" ,
372+ "https://packagemanager.posit.co/cran/__linux__/opensuse156/latest" ,
373+ ) ,
374+ (
375+ "rhel9" ,
376+ "https://packagemanager.posit.co/cran/__linux__/rhel9/latest" ,
377+ ) ,
378+ // Special cases.
379+ (
380+ "rhel8" ,
381+ "https://packagemanager.posit.co/cran/__linux__/centos8/latest" ,
382+ ) ,
383+ (
384+ "sles155" ,
385+ "https://packagemanager.posit.co/cran/__linux__/opensuse155/latest" ,
386+ ) ,
387+ (
388+ "sles156" ,
389+ "https://packagemanager.posit.co/cran/__linux__/opensuse156/latest" ,
390+ ) ,
391+ // Unsupported distros fall back to the generic URL.
392+ ( "centos7" , GENERIC_P3M_REPO ) ,
393+ ( "arch" , GENERIC_P3M_REPO ) ,
394+ ( "" , GENERIC_P3M_REPO ) ,
395+ ] ;
396+
397+ for ( distro, expected) in test_cases {
398+ let result = get_ppm_linux_repo ( None , distro. to_string ( ) ) . unwrap ( ) ;
399+ assert_eq ! ( result, expected) ;
400+ }
401+ }
402+
403+ #[ test]
404+ #[ cfg( target_os = "linux" ) ]
405+ fn test_get_custom_ppm_linux_repo ( ) {
406+ let test_cases = vec ! [
407+ (
408+ "jammy" ,
409+ "https://ppm.internal/approved/__linux__/jammy/2025-03-02" ,
410+ ) ,
411+ (
412+ "rhel8" ,
413+ "https://ppm.internal/approved/__linux__/centos8/2025-03-02" ,
414+ ) ,
415+ ( "arch" , "https://ppm.internal/approved/2025-03-02" ) ,
416+ ( "" , "https://ppm.internal/approved/2025-03-02" ) ,
417+ ] ;
418+
419+ let custom_url = url:: Url :: parse ( "https://ppm.internal/approved/2025-03-02" ) . unwrap ( ) ;
420+ for ( distro, expected) in test_cases {
421+ let result = get_ppm_linux_repo ( Some ( custom_url. clone ( ) ) , distro. to_string ( ) ) . unwrap ( ) ;
422+ assert_eq ! ( result, expected) ;
423+ }
424+ }
425+
426+ #[ test]
427+ #[ cfg( target_os = "linux" ) ]
428+ fn test_invalid_ppm_url ( ) {
429+ let custom_url = url:: Url :: parse ( "https://ppm.internal/not/a/repo" ) . unwrap ( ) ;
430+ let result = get_ppm_linux_repo ( Some ( custom_url) , "jammy" . to_string ( ) ) ;
431+ assert ! ( result. is_err( ) ) ;
432+ }
433+
434+ #[ test]
435+ #[ cfg( not( target_os = "linux" ) ) ]
436+ fn test_custom_ppm_url_for_non_linux ( ) {
437+ let custom_url = url:: Url :: parse ( "https://ppm.internal/approved/2025-03-02" ) . unwrap ( ) ;
438+ let result = get_ppm_binary_package_repo ( Some ( custom_url) ) ;
439+ assert_eq ! ( result, "https://ppm.internal/approved/2025-03-02" ) ;
440+ }
441+
442+ #[ test]
443+ #[ cfg( not( target_os = "linux" ) ) ]
444+ fn test_generic_ppm_url_for_non_linux ( ) {
445+ assert_eq ! ( get_ppm_binary_package_repo( None ) , GENERIC_P3M_REPO ) ;
297446 }
298447}
0 commit comments