Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a3fc3d9
New rig config subcommand
gaborcsardi May 5, 2026
6403f83
Machinery to use alternative binary dir
gaborcsardi May 5, 2026
e49bedc
check_local_bin_path() helper
gaborcsardi May 5, 2026
fdee58c
Configurable binary dir
gaborcsardi May 5, 2026
a2c581a
Some test cases for the new code
gaborcsardi May 5, 2026
41b7369
Fix failing test cases
gaborcsardi May 5, 2026
ced18a4
Only escalate for rig system make-links if needed
gaborcsardi May 5, 2026
a21644e
Fix compilation on Linux
gaborcsardi May 5, 2026
f1f93e0
Improve ~/local/bin setup
gaborcsardi May 6, 2026
6d311db
Set admin/user mode via option/envvar/config
gaborcsardi May 6, 2026
f61e585
Eliminate hard-coded install paths on macOS
gaborcsardi May 6, 2026
7c8c3ac
User mode versions of macos commands
gaborcsardi May 6, 2026
205c097
PoC user mode installation
gaborcsardi May 7, 2026
50b111e
More user mode fixes
gaborcsardi May 7, 2026
a259307
Fix compilation on Windows
gaborcsardi May 7, 2026
b31deed
Merge branch 'main' into feature/user-mode-macos
gaborcsardi May 7, 2026
2295bdd
Fix fontconfig in macos user mode installs
gaborcsardi May 7, 2026
e252cd9
Run fc-cache after installation
gaborcsardi May 7, 2026
af32ea4
Fix user mode install of older R versions
gaborcsardi May 9, 2026
9086b8e
Fix binary path in allow_debugger
gaborcsardi May 9, 2026
a37a6e8
Fix macos user mode install on R 3.6 and older
gaborcsardi May 9, 2026
3f90066
GHA: update action versions
gaborcsardi May 28, 2026
840550a
macOS user mode: need Resources/bin/R path for Positron
gaborcsardi Jun 12, 2026
44acbd7
Fix rig rstudio <version> with user mode install
gaborcsardi Jun 12, 2026
b3eb08a
macOS user mode: RSTUDIO_WHICH_R must be set
gaborcsardi Jun 12, 2026
e95dfbe
Register user mode rig R versions in positron
gaborcsardi Jun 12, 2026
d3dfefc
Only install plist for rstudio if rstudio is installed
gaborcsardi Jun 12, 2026
abdc436
Fix test cases
gaborcsardi Jun 12, 2026
0ff9fea
Improve messaging for rstudio/positron setup
gaborcsardi Jun 12, 2026
dbcf069
Need brew trust r-lib/rig on macOS
gaborcsardi Jun 12, 2026
df26679
Merge branch 'main' into feature/user-mode-macos
gaborcsardi Jun 13, 2026
dff8e3d
Merge branch 'main' into feature/user-mode-macos
gaborcsardi Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ whoami = "1.4.1"
assert_cmd = "2.0.8"
insta = "1.35"
predicates = "2.1.5"
tempfile = "3"
wiremock = "0.6"
20 changes: 13 additions & 7 deletions src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use crate::linux::*;

use crate::escalate::*;
use crate::output::OUTPUT;
#[cfg(target_os = "macos")]
use crate::utils::{check_local_bin_path, get_binary_dir};

#[cfg(target_os = "macos")]
pub fn get_alias(args: &ArgMatches) -> Option<String> {
Expand Down Expand Up @@ -65,15 +67,19 @@ pub fn get_alias(args: &ArgMatches) -> Option<String> {
#[cfg(target_os = "macos")]
pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
let msg = "Adding R-".to_string() + alias + " alias";
escalate(&msg)?;
if crate::utils::get_mode()? == crate::utils::Mode::Admin {
escalate(&msg)?;
}

check_local_bin_path()?;

OUTPUT.status(&format!("Adding R-{} alias to R {}", alias, ver));
info!("Adding R-{} alias to R {}", alias, ver);

let rroot = get_r_root();
let base = Path::new(&rroot);
let target = base.join(ver).join("Resources/bin/R");
let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias);
let binding = get_r_binpath()?.replace("{}", ver);
let target = Path::new(&get_r_root()?).join(&binding);
let binary_dir = get_binary_dir()?;
let linkfile = Path::new(&binary_dir).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
Expand Down Expand Up @@ -148,7 +154,7 @@ pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
let msg = "Adding R-".to_string() + alias + " alias";
escalate(&msg)?;
let rroot = get_r_root_for(ver);
let rroot = get_r_root_for(ver)?;
let base = version_dir_key(ver);
let linkdir = Path::new(RIG_LINKS_DIR);

Expand Down Expand Up @@ -185,7 +191,7 @@ pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
OUTPUT.status(&format!("Adding R-{} alias to R {}", alias, ver));
info!("Adding R-{} alias to R {}", alias, ver);

let rroot = get_r_root();
let rroot = get_r_root()?;
let base = Path::new(&rroot);
let target = base.join(ver).join("bin/R");
let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias);
Expand Down
73 changes: 72 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,55 @@ pub fn rig_app() -> Command {
),
);

#[cfg(target_os = "macos")]
{
let cmd_config = Command::new("config")
.about("Manage rig configuration")
.display_order(0)
.arg_required_else_help(true)
.subcommand(
Command::new("config-file-path")
.about("Print the path to the rig config file")
.display_order(0),
)
.subcommand(
Command::new("list")
.about("List the names of all config entries")
.display_order(0)
.arg(
Arg::new("json")
.help("JSON output")
.long("json")
.num_args(0)
.required(false),
),
)
.subcommand(
Command::new("get")
.about("Get a config entry")
.display_order(0)
.arg(Arg::new("key").help("config key to get").required(true))
.arg(
Arg::new("json")
.help("JSON output")
.long("json")
.num_args(0)
.required(false),
),
)
.subcommand(
Command::new("set")
.about("Set a config entry")
.display_order(0)
.arg(
Arg::new("keyvalue")
.help("key=value pair to set")
.required(true),
),
);
rig = rig.subcommand(cmd_config);
}

#[cfg(target_os = "macos")]
{
let cmd_sysreqs = Command::new("sysreqs")
Expand Down Expand Up @@ -1288,7 +1337,29 @@ pub fn rig_app() -> Command {
.long("json")
.num_args(0)
.required(false),
)
);

#[cfg(any(target_os = "macos", target_os = "linux"))]
{
rig = rig
.arg(
Arg::new("user")
.help("Run in user mode (overrides RIG_MODE and config)")
.long("user")
.global(true)
.action(clap::ArgAction::SetTrue)
.conflicts_with("admin"),
)
.arg(
Arg::new("admin")
.help("Run in admin mode (overrides RIG_MODE and config)")
.long("admin")
.global(true)
.action(clap::ArgAction::SetTrue),
);
}

rig = rig
.subcommand(cmd_default)
.subcommand(cmd_list)
.subcommand(cmd_add)
Expand Down
20 changes: 9 additions & 11 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ pub fn check_installed(x: &String) -> Result<String, Box<dyn Error>> {
bail!("R version {} is not installed", &x);
}

pub fn get_r_base_profile(ver: &str) -> String {
R_BASE_PROFILE.replace("{}", &version_dir_key(ver))
}

// -- rig default ---------------------------------------------------------

// Fail if no default is set
Expand All @@ -69,8 +65,10 @@ pub fn sc_get_default_or_fail() -> Result<String, Box<dyn Error>> {
}

pub fn set_default_if_none(ver: String) -> Result<(), Box<dyn Error>> {
debug!("Checking if a default R version is set");
let cur = sc_get_default()?;
if cur.is_none() {
debug!("No default R version is set, setting it to {}", ver);
sc_set_default(&ver)?;
}
Ok(())
Expand All @@ -83,8 +81,8 @@ pub fn get_default_r_version() -> Result<Option<String>, Box<dyn Error>> {
None => Ok(None),
Some(d) => {
let name = check_installed(&d)?;
let desc = Path::new(&get_r_root_for(&name))
.join(R_SYSLIBPATH.replace("{}", &version_dir_key(&name)))
let desc = Path::new(&get_r_root_for(&name)?)
.join(get_r_syslibpath()?.replace("{}", &version_dir_key(&name)))
.join("base/DESCRIPTION");
let lines = match read_lines(&desc) {
Ok(x) => x,
Expand All @@ -106,8 +104,8 @@ pub fn get_default_r_version() -> Result<Option<String>, Box<dyn Error>> {

pub fn get_r_version_data_version(name: &str) -> Result<String, Box<dyn Error>> {
let re = Regex::new("^Version:[ ]?").expect("Invalid regex pattern");
let desc = Path::new(&get_r_root_for(name))
.join(R_SYSLIBPATH.replace("{}", &version_dir_key(name)))
let desc = Path::new(&get_r_root_for(name)?)
.join(get_r_syslibpath()?.replace("{}", &version_dir_key(name)))
.join("base/DESCRIPTION");
let lines = match read_lines(&desc) {
Ok(x) => x,
Expand Down Expand Up @@ -137,8 +135,8 @@ pub fn get_r_version_data(
aliases: &[Alias],
) -> Result<InstalledVersion, Box<dyn Error>> {
let version = Some(get_r_version_data_version(name)?);
let path = Path::new(&get_r_root_for(name)).join(R_VERSIONDIR.replace("{}", &version_dir_key(name)));
let binary = Path::new(&get_r_root_for(name)).join(R_BINPATH.replace("{}", &version_dir_key(name)));
let path = Path::new(&get_r_root_for(name)?).join(R_VERSIONDIR.replace("{}", &version_dir_key(name)));
let binary = Path::new(&get_r_root_for(name)?).join(get_r_binpath()?.replace("{}", &version_dir_key(name)));
let mut myaliases: Vec<String> = vec![];
for a in aliases {
if a.version == name {
Expand All @@ -159,7 +157,7 @@ pub fn sc_get_list_details() -> Result<Vec<InstalledVersion>, Box<dyn Error>> {
let aliases = find_aliases()?;
let mut res: Vec<InstalledVersion> = vec![];

for name in names {
for name in &names {
res.push(get_r_version_data(&name, &aliases)?);
}

Expand Down
138 changes: 138 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;

use clap::ArgMatches;

use simple_error::{bail, SimpleError};

use serde_derive::Deserialize;
Expand All @@ -14,6 +16,8 @@ use crate::utils::*;
struct Config {
#[serde(default = "empty_stringmap")]
userlibrary: HashMap<String, String>,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}

fn empty_stringmap() -> HashMap<String, String> {
Expand Down Expand Up @@ -86,3 +90,137 @@ pub fn get_config(rver: &str, key: &str) -> Result<Option<String>, Box<dyn Error
_ => bail!("Unknown config key: {}, internal error", key),
}
}

#[cfg(any(target_os = "windows", target_os = "linux"))]
pub fn sc_config(_args: &ArgMatches, _mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
// Cannot be called
Ok(())
}

#[cfg(target_os = "macos")]
pub fn sc_config(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
match args.subcommand() {
Some(("config-file-path", _)) => sc_config_config_file_path(),
Some(("list", s)) => sc_config_list(s, mainargs),
Some(("get", s)) => sc_config_get(s, mainargs),
Some(("set", s)) => sc_config_set(s),
_ => Ok(()),
}
}

#[cfg(target_os = "macos")]
fn sc_config_config_file_path() -> Result<(), Box<dyn Error>> {
let path = rig_config_file()?;
println!("{}", path.display());
Ok(())
}

#[cfg(target_os = "macos")]
fn sc_config_get(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
let key = args.get_one::<String>("key").unwrap();
let json = args.get_flag("json") || mainargs.get_flag("json");

let config_file = rig_config_file()?;
let root: serde_json::Value = if config_file.exists() {
let contents = read_file_string(&config_file)?;
serde_json::from_str(&contents)?
} else {
serde_json::Value::Object(serde_json::Map::new())
};

let value = &root[key.as_str()];
match value {
serde_json::Value::Null => {
if json {
println!("null");
}
}
serde_json::Value::String(s) => {
if json {
println!("{}", serde_json::to_string(s)?);
} else {
println!("{}", s);
}
}
scalar @ (serde_json::Value::Bool(_) | serde_json::Value::Number(_)) => {
println!("{}", scalar);
}
complex => {
println!("{}", serde_json::to_string_pretty(complex)?);
}
}
Ok(())
}

#[cfg(any(target_os = "macos", target_os = "linux"))]
fn load_raw_config() -> Result<serde_json::Map<String, serde_json::Value>, Box<dyn Error>> {
let config_file = rig_config_file()?;
if config_file.exists() {
let contents = read_file_string(&config_file)?;
let value: serde_json::Value = serde_json::from_str(&contents)?;
match value {
serde_json::Value::Object(map) => Ok(map),
_ => bail!("Config file is not a JSON object"),
}
} else {
Ok(serde_json::Map::new())
}
}

#[cfg(target_os = "macos")]
fn save_raw_config(map: &serde_json::Map<String, serde_json::Value>) -> Result<(), Box<dyn Error>> {
let config_file = rig_config_file()?;
let parent = config_file
.parent()
.ok_or(SimpleError::new("Invalid config file directory"))?;
std::fs::create_dir_all(parent)?;
std::fs::write(config_file, serde_json::to_string_pretty(map)?)?;
Ok(())
}

#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn get_global_config_value(key: &str) -> Result<Option<String>, Box<dyn Error>> {
let map = load_raw_config()?;
match map.get(key) {
Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
_ => Ok(None),
}
}

#[cfg(target_os = "macos")]
fn sc_config_set(args: &ArgMatches) -> Result<(), Box<dyn Error>> {
let keyvalue = args.get_one::<String>("keyvalue").unwrap();
let (key, value) = keyvalue
.split_once('=')
.ok_or_else(|| SimpleError::new(format!("Invalid key=value format: '{}'", keyvalue)))?;
let mut map = load_raw_config()?;
map.insert(key.to_string(), serde_json::Value::String(value.to_string()));
save_raw_config(&map)
}

#[cfg(target_os = "macos")]
fn sc_config_list(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
let config_file = rig_config_file()?;
let keys: Vec<String> = if config_file.exists() {
let contents = read_file_string(&config_file)?;
let value: serde_json::Value = serde_json::from_str(&contents)?;
match value.as_object() {
Some(obj) => obj.keys().cloned().collect(),
None => vec![],
}
} else {
vec![]
};

if args.get_flag("json") || mainargs.get_flag("json") {
#[derive(serde::Serialize)]
struct Entry { key: String }
let entries: Vec<Entry> = keys.into_iter().map(|k| Entry { key: k }).collect();
println!("{}", serde_json::to_string_pretty(&entries)?);
} else {
for key in keys {
println!("{}", key);
}
}
Ok(())
}
3 changes: 3 additions & 0 deletions src/escalate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub fn escalate(task: &str) -> Result<(), Box<dyn Error>> {
);
with_env(&[
"RIG_HOME",
"RIG_BINARY_DIR",
"RIG_MODE",
"RIG_R_INSTALL_DIR",
"RUST_BACKTRACE",
"http_proxy",
"https_proxy",
Expand Down
6 changes: 3 additions & 3 deletions src/help-macos.in
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ const HELP_SYSTEM_ORTHO: &str = "

const HELP_SYSTEM_LINKS: &str = "
\x1b[4m\x1b[1mDescription:\x1b[22m\x1b[24m
Create quick links in `/usr/local/bin` for the current R installations.
These let you directly run a specific R version. E.g. `R-4.1` will start
R 4.1.x.
Create quick links in the rig binary directory (`/usr/local/bin` by
default) for the current R installations. These let you directly run a
specific R version. E.g. `R-4.1` will start R 4.1.x.

`rig add` runs `rig system make-links`, so if you only use rig to
install R, then you do not need to run it manually.
Expand Down
Loading
Loading