diff --git a/NEWS.md b/NEWS.md index 85bedeb..79d5053 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,9 @@ * rig now prints output from `apt`, etc. like regular logging output on Linux. +* rig now supports the alises `oldrel`, `release`, `next`, `devel` in + `rig default`, `rig rm`, `rig rstudio`, etc. (#108). + # rig 0.5.0 * rig can now open renv projects in RStudio, with the correct R version. diff --git a/src/alias.rs b/src/alias.rs new file mode 100644 index 0000000..ae3b01b --- /dev/null +++ b/src/alias.rs @@ -0,0 +1,201 @@ + +use std::error::Error; +#[cfg(target_os = "windows")] +use std::io::Write; +#[cfg(target_os = "windows")] +use std::fs::File; +use std::path::Path; + +#[cfg(any(target_os = "macos", target_os = "linux"))] +use std::os::unix::fs::symlink; + +use clap::ArgMatches; +#[cfg(any(target_os = "macos", target_os = "linux"))] +use simple_error::*; +use simplelog::*; + +#[cfg(target_os = "macos")] +use crate::macos::*; + +#[cfg(target_os = "windows")] +use crate::windows::*; + +#[cfg(target_os = "linux")] +use crate::linux::*; + +use crate::escalate::*; + +#[cfg(target_os = "macos")] +pub fn get_alias(args: &ArgMatches) -> Option { + match args.value_of("str") { + None => None, + Some(str) => { + match str { + "oldrel" | "oldrel/1" => Some("oldrel".to_string()), + "release" | "devel" | "next" => Some(str.to_string()), + _ => None + } + } + } +} + +#[cfg(target_os = "linux")] +pub fn get_alias(args: &ArgMatches) -> Option { + match args.value_of("str") { + None => None, + Some(str) => { + match str { + "oldrel" | "oldrel/1" => Some("oldrel".to_string()), + "release" => Some(str.to_string()), + _ => None + } + } + } +} + +#[cfg(target_os = "windows")] +pub fn get_alias(args: &ArgMatches) -> Option { + match args.value_of("str") { + None => None, + Some(str) => { + match str { + "oldrel" | "oldrel/1" => Some("oldrel".to_string()), + "release" | "next" => Some(str.to_string()), + _ => None + } + } + } +} + +#[cfg(target_os = "macos")] +pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box> { + let msg = "Adding R-".to_string() + alias + " alias"; + escalate(&msg)?; + + info!("Adding R-{} alias to R {}", alias, ver); + + let base = Path::new(R_ROOT); + let target = base.join(ver).join("Resources/bin/R"); + let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias); + + // If it exists then we check that it points to the right place + // Cannot use .exists(), because it follows symlinks + let meta = std::fs::symlink_metadata(&linkfile); + if meta.is_ok() { + match std::fs::read_link(&linkfile) { + Err(_) => bail!("{} is not a symlink, aborting", linkfile.display()), + Ok(xtarget) => { + if xtarget == target { + return Ok(()) + } else { + debug!("{} is wrong, updating", linkfile.display()); + match std::fs::remove_file(&linkfile) { + Err(err) => { + bail!( + "Failed to delete {}, cannot update alias: {}", + linkfile.display(), + err.to_string() + ); + }, + _ => {} + } + } + } + } + } + + // If we are still here, then we need to create the link + debug!("Adding {} -> {}", linkfile.display(), target.display()); + match symlink(&target, &linkfile) { + Err(err) => bail!( + "Cannot create alias {}: {}", + linkfile.display(), + err.to_string() + ), + _ => {} + }; + + Ok(()) +} + +#[cfg(target_os = "windows")] +pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box> { + let msg = "Adding R-".to_string() + alias + " alias"; + escalate(&msg)?; + let base = Path::new(R_ROOT); + let bin = base.join("bin"); + + // should exist at this point, but make sure + std::fs::create_dir_all(&bin)?; + + let filename = "R-".to_string() + alias + ".bat"; + let linkfile = bin.join(&filename); + + let cnt = "@\"C:\\Program Files\\R\\R-".to_string() + &ver + "\\bin\\R\" %*\n"; + let op; + if linkfile.exists() { + op = "Updating"; + let orig = std::fs::read_to_string(&linkfile)?; + if orig == cnt { + return Ok(()); + } + } else { + op = "Adding"; + }; + info!("{} R-{} -> {} alias", op, alias, ver); + let mut file = File::create(&linkfile)?; + file.write_all(cnt.as_bytes())?; + + Ok(()) +} + +#[cfg(target_os = "linux")] +pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box> { + let msg = "Adding R-".to_string() + alias + " alias"; + escalate(&msg)?; + + info!("Adding R-{} alias to R {}", alias, ver); + + let base = Path::new(R_ROOT); + let target = base.join(ver).join("bin/R"); + let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias); + + // If it exists then we check that it points to the right place + // Cannot use .exists(), because it follows symlinks + let meta = std::fs::symlink_metadata(&linkfile); + if meta.is_ok() { + match std::fs::read_link(&linkfile) { + Err(_) => bail!("{} is not a symlink, aborting", linkfile.display()), + Ok(xtarget) => { + if xtarget == target { + return Ok(()) + } else { + debug!("{} is wrong, updating", linkfile.display()); + match std::fs::remove_file(&linkfile) { + Err(err) => { + bail!( + "Failed to delete {}, cannot update alias: {}", + linkfile.display(), + err.to_string() + ); + }, + _ => {} + } + } + } + } + } + + // If we are still here, then we need to create the link + debug!("Adding {} -> {}", linkfile.display(), target.display()); + match symlink(&target, &linkfile) { + Err(err) => bail!( + "Cannot create alias {}: {}", + linkfile.display(), + err.to_string() + ), + _ => {} + }; + + Ok(()) +} diff --git a/src/common.rs b/src/common.rs index 94689cd..e5e6543 100644 --- a/src/common.rs +++ b/src/common.rs @@ -23,12 +23,20 @@ use crate::rversion::*; use crate::run::*; use crate::utils::*; -pub fn check_installed(ver: &String) -> Result> { - let inst = sc_get_list()?; - if !inst.contains(&ver) { - bail!("R version {} is not installed", &ver); +pub fn check_installed(x: &String) -> Result> { + let inst = sc_get_list_details()?; + + for ver in inst { + if &ver.name == x { + return Ok(ver.name); + } + if ver.aliases.contains(x) { + debug!("Alias {} is resolved to version {}", x, ver.name); + return Ok(ver.name); + } } - Ok(true) + + bail!("R version {} is not installed", &x); } // -- rig default --------------------------------------------------------- @@ -55,6 +63,7 @@ pub fn set_default_if_none(ver: String) -> Result<(), Box> { pub fn sc_get_list_details() -> Result, Box> { let names = sc_get_list()?; + let aliases = find_aliases()?; let mut res: Vec = vec![]; let re = Regex::new("^Version:[ ]?")?; @@ -74,11 +83,18 @@ pub fn sc_get_list_details() -> Result, Box> { }; let path = Path::new(R_ROOT).join(R_VERSIONDIR.replace("{}", &name)); let binary = Path::new(R_ROOT).join(R_BINPATH.replace("{}", &name)); + let mut myaliases: Vec = vec![]; + for a in &aliases { + if a.version == name { + myaliases.push(a.alias.to_owned()); + } + } res.push(InstalledVersion { name: name.to_string(), version: version, path: path.to_str().and_then(|x| Some(x.to_string())), - binary: binary.to_str().and_then(|x| Some(x.to_string())) + binary: binary.to_str().and_then(|x| Some(x.to_string())), + aliases: myaliases }); } @@ -98,7 +114,7 @@ pub fn system_add_pak( }; for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; if update { info!("Installing pak for R {}", ver); } else { diff --git a/src/config.rs b/src/config.rs index df9c525..1321fcf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,7 @@ fn empty_stringmap() -> HashMap { fn rig_config_file() -> Result> { let proj_dirs = match ProjectDirs::from("com", "gaborcsardi", "rig") { Some(x) => x, - None => bail!("Config file if not supported on this system"), + None => bail!("Config file is not supported on this system"), }; let config_dir = proj_dirs.data_dir(); let config_file = config_dir.join("config.json"); diff --git a/src/download.rs b/src/download.rs index 62043a6..4aa2300 100644 --- a/src/download.rs +++ b/src/download.rs @@ -2,6 +2,7 @@ use futures::future; use futures_util::StreamExt; use std::error::Error; use std::ffi::OsStr; +#[cfg(any(target_os = "macos", target_os = "windows"))] use std::ffi::OsString; use std::fs::File; use std::io::Write; @@ -11,10 +12,12 @@ use std::path::Path; use clap::ArgMatches; use simple_error::bail; +#[cfg(any(target_os = "macos", target_os = "windows"))] use simplelog::info; #[cfg(target_os = "windows")] use crate::rversion::Rversion; +#[cfg(any(target_os = "macos", target_os = "windows"))] use crate::utils::*; #[cfg(target_os = "windows")] use crate::windows::*; diff --git a/src/lib.rs b/src/lib.rs index a1c8425..e4b9948 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use lazy_static::lazy_static; use libc; use simple_error::bail; +mod alias; mod args; mod common; mod config; diff --git a/src/linux.rs b/src/linux.rs index 667daf9..32a42a4 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -11,11 +11,12 @@ use std::{file, line}; use clap::ArgMatches; use simple_error::*; -use simplelog::{debug, info, warn}; +use simplelog::{trace,debug, info, warn}; use crate::resolve::resolve_versions; use crate::rversion::*; +use crate::alias::*; use crate::common::*; use crate::download::*; use crate::escalate::*; @@ -76,6 +77,7 @@ pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { let linux = detect_linux()?; let version = get_resolve(args)?; + let alias = get_alias(args); let ver = version.version.to_owned(); let verstr = match ver { Some(ref x) => x, @@ -110,6 +112,10 @@ pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { library_update_rprofile(&dirname.to_string())?; sc_system_make_links()?; + match alias { + Some(alias) => add_alias(&dirname, &alias)?, + None => { } + }; if !args.is_present("without-cran-mirror") { set_cloud_mirror(Some(vec![dirname.to_string()]))?; @@ -186,9 +192,9 @@ pub fn sc_rm(args: &ArgMatches) -> Result<(), Box> { let vers = require_with!(vers, "clap error"); for ver in vers { - check_installed(&ver.to_string())?; + let ver = check_installed(&ver.to_string())?; - let pkgname = "r-".to_string() + ver; + let pkgname = "r-".to_string() + &ver; let out = try_with!( Command::new("dpkg").args(["-s", &pkgname]).output(), "Failed to run dpkg -s {} @{}:{}", @@ -247,7 +253,7 @@ pub fn sc_system_make_links() -> Result<(), Box> { // Remove dangling links let paths = std::fs::read_dir("/usr/local/bin")?; - let re = Regex::new("^R-[0-9]+[.][0-9]+")?; + let re = Regex::new("^R-([0-9]+[.][0-9]+[.][0-9]+|oldrel|next|release|devel)$")?; for file in paths { let path = file?.path(); // If no path name, then path ends with ..., so we can skip @@ -280,6 +286,76 @@ pub fn sc_system_make_links() -> Result<(), Box> { Ok(()) } +pub fn re_alias() -> Regex { + let re= Regex::new("^R-(release|oldrel)$").unwrap(); + re +} + +pub fn find_aliases() -> Result, Box> { + debug!("Finding existing aliases"); + + let paths = std::fs::read_dir("/usr/local/bin")?; + let re = re_alias(); + let mut result: Vec = vec![]; + + for file in paths { + let path = file?.path(); + // If no path name, then path ends with ..., so we can skip + let fnamestr = match path.file_name() { + Some(x) => x, + None => continue, + }; + // If the path is not UTF-8, we'll skip it, this should not happen + let fnamestr = match fnamestr.to_str() { + Some(x) => x, + None => continue, + }; + if re.is_match(&fnamestr) { + trace!("Checking {}", path.display()); + match std::fs::read_link(&path) { + Err(_) => debug!("{} is not a symlink", path.display()), + Ok(target) => { + if !target.exists() { + debug!("Target does not exist at {}", target.display()); + + } else { + let version = version_from_link(target); + match version { + None => continue, + Some(version) => { + trace!("{} -> {}", fnamestr, version); + let als = Alias { + alias: fnamestr[2..].to_string(), + version: version.to_string() + }; + result.push(als); + } + }; + } + } + }; + } + } + + Ok(result) +} + +fn version_from_link(pb: PathBuf) -> Option { + let osver = match pb.parent() + .and_then(|x| x.parent()) + .and_then(|x| x.file_name()) { + None => None, + Some(s) => Some(s.to_os_string()) + }; + + let s = match osver { + None => None, + Some(os) => os.into_string().ok() + }; + + s +} + pub fn get_resolve(args: &ArgMatches) -> Result> { let str = args .value_of("str") @@ -328,7 +404,7 @@ pub fn sc_get_list() -> Result, Box> { pub fn sc_set_default(ver: &str) -> Result<(), Box> { escalate("setting the default R version")?; - check_installed(&ver.to_string())?; + let ver = check_installed(&ver.to_string())?; // Remove current link if Path::new(R_CUR).exists() { @@ -375,7 +451,7 @@ fn set_cloud_mirror(vers: Option>) -> Result<(), Box> { info!("Setting default CRAN mirror"); for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join(ver.as_str()); let profile = path.join("lib/R/library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -420,7 +496,7 @@ options(HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion( let rcode = rcode.to_string().replace("%url%", &linux.rspm_url); for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join(ver.as_str()); let profile = path.join("lib/R/library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -453,7 +529,7 @@ Sys.setenv(PKG_SYSREQS = "true") "#; for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join(ver.as_str()); let profile = path.join("lib/R/library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -650,7 +726,7 @@ pub fn sc_rstudio_(version: Option<&str>, project: Option<&str>, arg: Option<&Os let mut envname = "dummy"; let mut path = "".to_string(); if let Some(ver) = version { - check_installed(&ver.to_string())?; + let ver = check_installed(&ver.to_string())?; envname = "RSTUDIO_WHICH_R"; path = R_ROOT.to_string() + "/" + &ver + "/bin/R" }; diff --git a/src/macos.rs b/src/macos.rs index 327a981..2e988a8 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -14,6 +14,7 @@ use semver::Version; use simple_error::*; use simplelog::{debug, info, warn}; +use crate::alias::*; use crate::common::*; use crate::download::*; use crate::escalate::*; @@ -32,6 +33,7 @@ const R_CUR: &str = "/Library/Frameworks/R.framework/Versions/Current"; pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { escalate("adding new R versions")?; let mut version = get_resolve(args)?; + let alias = get_alias(args); let ver = version.version.to_owned(); let verstr = match ver { Some(ref x) => x, @@ -91,6 +93,10 @@ pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { system_fix_permissions(None)?; library_update_rprofile(&dirname.to_string())?; sc_system_make_links()?; + match alias { + Some(alias) => add_alias(dirname, &alias)?, + None => { } + }; if !args.is_present("without-cran-mirror") { set_cloud_mirror(Some(vec![dirname.to_string()]))?; @@ -239,10 +245,11 @@ pub fn sc_rm(args: &ArgMatches) -> Result<(), Box> { let default = sc_get_default()?; for ver in vers { - check_installed(&ver.to_string())?; + + let ver = check_installed(&ver.to_string())?; if let Some(ref default) = default { - if default == ver { + if default == &ver { warn!("Removing default version, set new default with \ rig default "); } @@ -268,18 +275,14 @@ pub fn sc_system_make_links() -> Result<(), Box> { let vers = sc_get_list()?; let base = Path::new(R_ROOT); - info!("Adding R-* quick links (if needed)"); + info!("Updating R-* quick links (as needed)"); // Create new links for ver in vers { let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + &ver); let target = base.join(&ver).join("Resources/bin/R"); if !linkfile.exists() { - debug!( - "[DEBUG] Adding {} -> {}", - linkfile.display(), - target.display() - ); + debug!("Adding {} -> {}", linkfile.display(), target.display()); match symlink(&target, &linkfile) { Err(err) => bail!( "Cannot create symlink {}: {}", @@ -291,9 +294,10 @@ pub fn sc_system_make_links() -> Result<(), Box> { } } - // Remove danglink links + // Remove dangling links let paths = std::fs::read_dir("/usr/local/bin")?; let re = Regex::new("^R-[0-9]+[.][0-9]+")?; + let re2 = re_alias(); for file in paths { let path = file?.path(); // If no path name, then path ends with ..., so we can skip @@ -306,12 +310,12 @@ pub fn sc_system_make_links() -> Result<(), Box> { Some(x) => x, None => continue, }; - if re.is_match(&fnamestr) { + if re.is_match(&fnamestr) || re2.is_match(&fnamestr) { match std::fs::read_link(&path) { - Err(_) => debug!("[DEBUG] {} is not a symlink", path.display()), + Err(_) => debug!("{} is not a symlink", path.display()), Ok(target) => { if !target.exists() { - debug!("[DEBUG] Cleaning up {}", target.display()); + debug!("Cleaning up {}", target.display()); match std::fs::remove_file(&path) { Err(err) => { warn!("Failed to remove {}: {}", path.display(), err.to_string()) @@ -327,6 +331,77 @@ pub fn sc_system_make_links() -> Result<(), Box> { Ok(()) } +pub fn re_alias() -> Regex { + let re= Regex::new("^R-(next|devel|release|oldrel)$").unwrap(); + re +} + +pub fn find_aliases() -> Result, Box> { + debug!("Finding existing aliaes"); + + let paths = std::fs::read_dir("/usr/local/bin")?; + let re = re_alias(); + let mut result: Vec = vec![]; + + for file in paths { + let path = file?.path(); + // If no path name, then path ends with ..., so we can skip + let fnamestr = match path.file_name() { + Some(x) => x, + None => continue, + }; + // If the path is not UTF-8, we'll skip it, this should not happen + let fnamestr = match fnamestr.to_str() { + Some(x) => x, + None => continue, + }; + if re.is_match(&fnamestr) { + match std::fs::read_link(&path) { + Err(_) => debug!("{} is not a symlink", path.display()), + Ok(target) => { + if !target.exists() { + debug!("Target does not exist at {}", target.display()); + + } else { + let version = version_from_link(target); + match version { + None => continue, + Some(version) => { + let als = Alias { + alias: fnamestr[2..].to_string(), + version: version.to_string() + }; + result.push(als); + } + }; + } + } + }; + } + } + + Ok(result) +} + +// /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/bin/R -> +// 4.2-arm64 +fn version_from_link(pb: PathBuf) -> Option { + let osver = match pb.parent() + .and_then(|x| x.parent()) + .and_then(|x| x.parent()) + .and_then(|x| x.file_name()) { + None => None, + Some(s) => Some(s.to_os_string()) + }; + + let s = match osver { + None => None, + Some(os) => os.into_string().ok() + }; + + s +} + pub fn sc_system_allow_core_dumps(args: &ArgMatches) -> Result<(), Box> { escalate("updating code signature of R and /cores permissions")?; sc_system_allow_debugger(args)?; @@ -351,7 +426,7 @@ pub fn sc_system_allow_debugger(args: &ArgMatches) -> Result<(), Box> }; for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = PathBuf::new() .join(R_ROOT) .join(ver.as_str()) @@ -487,7 +562,7 @@ fn system_make_orthogonal(vers: Option>) -> Result<(), Box>) -> Result<(), Box>) -> Result<(), Box Result<(), Box> { // TODO: this can fail, but if it fails it will still have exit // status 0, so we would need to check stderr to see if it failed. for line in output.lines() { - debug!("[DEBUG] Calling pkgutil --forget {}", line.trim()); + debug!("Calling pkgutil --forget {}", line.trim()); Command::new("pkgutil") .args(["--forget", line.trim()]) .output()?; @@ -672,7 +747,7 @@ fn system_no_openmp(vers: Option>) -> Result<(), Box> { let re = Regex::new("[-]fopenmp")?; for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join(ver.as_str()); let makevars = path.join("Resources/etc/Makeconf".to_string()); if !makevars.exists() { @@ -699,7 +774,7 @@ fn set_cloud_mirror(vers: Option>) -> Result<(), Box> { info!("Setting default CRAN mirror"); for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join(ver.as_str()); let profile = path.join("Resources/library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -741,8 +816,8 @@ pub fn sc_rstudio_(version: Option<&str>, let path; if let Some(ver) = version { - check_installed(&ver.to_string())?; - if !is_orthogonal(ver)? { + let ver = check_installed(&ver.to_string())?; + if !is_orthogonal(&ver)? { bail!("R {} is not orthogonal, it cannot run as a non-default. \ Run `rig system make-orthogonal`.", ver) } @@ -777,7 +852,7 @@ pub fn check_has_pak(ver: &String) -> Result> { } pub fn sc_set_default(ver: &str) -> Result<(), Box> { - check_installed(&ver.to_string())?; + let ver = check_installed(&ver.to_string())?; // Maybe it does not exist, ignore error here match std::fs::remove_file(R_CUR) { _ => {} diff --git a/src/main.rs b/src/main.rs index e044871..ddcaada 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod linux; #[cfg(target_os = "linux")] use linux::*; +mod alias; mod library; mod common; mod config; @@ -185,10 +186,15 @@ fn sc_list(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box = ver.aliases.iter() + .map(|v| "\"".to_string() + v + "\"") + .collect(); + let als = "[".to_string() + &alsq.join(", ") + "]"; println!(" {{"); println!(" \"name\": \"{}\",", ver.name); println!(" \"default\": {},", dflt); println!(" \"version\": \"{}\",", or_null(&ver.version)); + println!(" \"aliases\": {},", als); println!(" \"path\": \"{}\",", or_null(&ver.path)); println!(" \"binary\": \"{}\"", or_null(&ver.binary)); println!(" }}{}", if idx == num - 1 { "" } else { "," }); @@ -196,20 +202,23 @@ fn sc_list(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box " (broken?)".to_string(), + None => "(broken?)".to_string(), Some(v) => { if v != ver.name { - format!(" (R {})", v) + format!("(R {})", v) } else { "".to_string() } } }; - tab.add_row(row!(dflt, ver.name, note)); + let als = ver.aliases.join(", "); + tab.add_row(row!(dflt, ver.name, note, als)); } print!("{}", tab); diff --git a/src/renv.rs b/src/renv.rs index e35253f..0f23074 100644 --- a/src/renv.rs +++ b/src/renv.rs @@ -35,7 +35,11 @@ fn filter_ok_versions(all: Vec) for ver in all.iter() { match ver { InstalledVersion { - name: n, version: Some(v), path: Some(p), binary: Some(b) + name: n, + version: Some(v), + path: Some(p), + binary: Some(b), + aliases: _ } => { if let Ok(sv) = semver::Version::parse(v) { ok.push(OKInstalledVersion { diff --git a/src/rversion.rs b/src/rversion.rs index 55ac1bc..538bc2a 100644 --- a/src/rversion.rs +++ b/src/rversion.rs @@ -16,7 +16,8 @@ pub struct InstalledVersion { pub name: String, pub version: Option, pub path: Option, - pub binary: Option + pub binary: Option, + pub aliases: Vec, } #[derive(Debug, Clone)] @@ -73,3 +74,9 @@ pub struct User { pub dir: OsString, pub sudo: bool, } + +#[derive(Default, Debug)] +pub struct Alias { + pub alias: String, + pub version: String, +} diff --git a/src/utils.rs b/src/utils.rs index f9edaf6..8f47b39 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -23,6 +23,7 @@ pub fn os(x: &str) -> OsString { ostr } +#[cfg(any(target_os = "macos", target_os = "windows"))] pub fn osjoin(x: Vec, sep: &str) -> String { let mut buffer = String::new(); diff --git a/src/windows.rs b/src/windows.rs index 7f314f5..1e1f1c9 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -14,16 +14,17 @@ use std::{thread, time}; use clap::ArgMatches; use remove_dir_all::remove_dir_all; use simple_error::{bail, SimpleError}; -use simplelog::{debug, info, warn}; +use simplelog::*; use winreg::enums::*; use winreg::RegKey; +use crate::alias::*; use crate::common::*; use crate::download::*; use crate::escalate::*; use crate::library::*; use crate::resolve::resolve_versions; -use crate::rversion::Rversion; +use crate::rversion::*; use crate::run::*; use crate::utils::*; @@ -35,6 +36,7 @@ pub const R_BINPATH: &str = "R-{}\\bin\\R.exe"; #[warn(unused_variables)] pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { escalate("adding new R version")?; + let alias = get_alias(args); sc_clean_registry()?; let str = args .value_of("str") @@ -75,6 +77,15 @@ pub fn sc_add(args: &ArgMatches) -> Result<(), Box> { } }; sc_system_make_links()?; + match dirname { + None => {}, + Some(ref dirname) => { + match alias { + Some(alias) => add_alias(&dirname, &alias)?, + None => { } + } + } + }; patch_for_rtools()?; maybe_update_registry_default()?; @@ -252,7 +263,7 @@ fn set_cloud_mirror(vers: Option>) -> Result<(), Box> { info!("Setting default CRAN mirror"); for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join("R-".to_string() + ver.as_str()); let profile = path.join("library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -285,7 +296,7 @@ options(repos = c(RSPM="https://packagemanager.rstudio.com/all/latest", getOptio "#; for ver in vers { - check_installed(&ver)?; + let ver = check_installed(&ver)?; let path = Path::new(R_ROOT).join("R-".to_string() + ver.as_str()); let profile = path.join("library/base/R/Rprofile".to_string()); if !profile.exists() { @@ -313,10 +324,10 @@ pub fn sc_rm(args: &ArgMatches) -> Result<(), Box> { rm_rtools(verstr)?; continue; } - check_installed(&verstr)?; + let ver = check_installed(&verstr)?; if let Some(ref default) = default { - if default == ver { + if default == &ver { warn!("Removing default version, set new default with \ rig default "); match unset_default() { @@ -326,7 +337,7 @@ pub fn sc_rm(args: &ArgMatches) -> Result<(), Box> { } } - let ver = "R-".to_string() + ver; + let ver = "R-".to_string() + &ver; let dir = Path::new(R_ROOT); let dir = dir.join(ver); info!("Removing {}", dir.display()); @@ -400,6 +411,7 @@ pub fn sc_system_make_links() -> Result<(), Box> { } // Delete the ones we don't need + let re_als = re_alias(); let old_links = std::fs::read_dir(base.join("bin"))?; for path in old_links { let path = path?; @@ -412,6 +424,13 @@ pub fn sc_system_make_links() -> Result<(), Box> { if !filename.starts_with("R-") { continue; } + if re_als.is_match(&filename) { + let rver = find_r_version_in_link(&path.path())?; + let realname = "R-".to_string() + &rver + ".bat"; + if new_links.contains(&realname) { + continue; + } + } if !new_links.contains(&filename) { info!("Deleting unused {}", filename); match std::fs::remove_file(path.path()) { @@ -428,6 +447,67 @@ pub fn sc_system_make_links() -> Result<(), Box> { Ok(()) } +fn re_alias() -> Regex { + let re = Regex::new("^R-(oldrel|release|next)[.]bat$").unwrap(); + re +} + +pub fn find_aliases() -> Result, Box> { + debug!("Finding existing aliases"); + + let mut result: Vec = vec![]; + let bin = Path::new(R_ROOT).join("bin"); + + if !bin.exists() { + return Ok(result); + } + + let paths = std::fs::read_dir(bin)?; + let re = re_alias(); + + for file in paths { + let path = file?.path(); + // If no path name, then path ends with ..., so we can skip + let fnamestr = match path.file_name() { + Some(x) => x, + None => continue, + }; + // If the path is not UTF-8, we'll skip it, this should not happen + let fnamestr = match fnamestr.to_str() { + Some(x) => x, + None => continue, + }; + if re.is_match(&fnamestr) { + trace!("Checking {}", path.display()); + let rver = find_r_version_in_link(&path)?; + let als = Alias { + alias: fnamestr[2..fnamestr.len()-4].to_string(), + version: rver + }; + result.push(als); + } + } + + Ok(result) +} + +fn find_r_version_in_link(path: &PathBuf) -> Result> { + let lines = read_lines(path)?; + if lines.len() == 0 { + bail!("Invalid R link file: {}", path.display()); + } + let split = lines[0].split("\\").collect::>(); + for s in split { + if s == "R-devel" { + return Ok("devel".to_string()); + } + if s.starts_with("R-") { + return Ok(s[2..].to_string()); + } + } + bail!("Cannot extract R version from {}, invalid R link file?", path.display()); +} + pub fn sc_system_allow_core_dumps(_args: &ArgMatches) -> Result<(), Box> { // Nothing to do on Windows Ok(()) @@ -507,7 +587,7 @@ pub fn sc_get_list() -> Result, Box> { } pub fn sc_set_default(ver: &str) -> Result<(), Box> { - check_installed(&ver.to_string())?; + let ver = check_installed(&ver.to_string())?; escalate("setting the default R version")?; let base = Path::new(R_ROOT); let bin = base.join("bin"); @@ -782,7 +862,7 @@ pub fn sc_rstudio_(version: Option<&str>, project: Option<&str>, arg: Option<&Os if let Some(version) = version { let ver = version.to_string(); - check_installed(&ver)?; + let ver = check_installed(&ver)?; update_registry_default_to(&ver)?; }