Skip to content

Commit 65dea1d

Browse files
authored
Merge pull request #333 from r-lib/feature/user-mode-macos
User mode on macOS
2 parents 45073ab + dff8e3d commit 65dea1d

19 files changed

Lines changed: 1841 additions & 161 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,5 @@ whoami = "1.4.1"
120120
assert_cmd = "2.0.8"
121121
insta = "1.35"
122122
predicates = "2.1.5"
123+
tempfile = "3"
123124
wiremock = "0.6"

src/alias.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use crate::linux::*;
2424

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

2830
#[cfg(target_os = "macos")]
2931
pub fn get_alias(args: &ArgMatches) -> Option<String> {
@@ -65,15 +67,19 @@ pub fn get_alias(args: &ArgMatches) -> Option<String> {
6567
#[cfg(target_os = "macos")]
6668
pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
6769
let msg = "Adding R-".to_string() + alias + " alias";
68-
escalate(&msg)?;
70+
if crate::utils::get_mode()? == crate::utils::Mode::Admin {
71+
escalate(&msg)?;
72+
}
73+
74+
check_local_bin_path()?;
6975

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

73-
let rroot = get_r_root();
74-
let base = Path::new(&rroot);
75-
let target = base.join(ver).join("Resources/bin/R");
76-
let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias);
79+
let binding = get_r_binpath()?.replace("{}", ver);
80+
let target = Path::new(&get_r_root()?).join(&binding);
81+
let binary_dir = get_binary_dir()?;
82+
let linkfile = Path::new(&binary_dir).join("R-".to_string() + alias);
7783

7884
// If it exists then we check that it points to the right place
7985
// Cannot use .exists(), because it follows symlinks
@@ -148,7 +154,7 @@ pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
148154
pub fn add_alias(ver: &str, alias: &str) -> Result<(), Box<dyn Error>> {
149155
let msg = "Adding R-".to_string() + alias + " alias";
150156
escalate(&msg)?;
151-
let rroot = get_r_root_for(ver);
157+
let rroot = get_r_root_for(ver)?;
152158
let base = version_dir_key(ver);
153159
let linkdir = Path::new(RIG_LINKS_DIR);
154160

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

188-
let rroot = get_r_root();
194+
let rroot = get_r_root()?;
189195
let base = Path::new(&rroot);
190196
let target = base.join(ver).join("bin/R");
191197
let linkfile = Path::new("/usr/local/bin/").join("R-".to_string() + alias);

src/args.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,55 @@ pub fn rig_app() -> Command {
752752
),
753753
);
754754

755+
#[cfg(target_os = "macos")]
756+
{
757+
let cmd_config = Command::new("config")
758+
.about("Manage rig configuration")
759+
.display_order(0)
760+
.arg_required_else_help(true)
761+
.subcommand(
762+
Command::new("config-file-path")
763+
.about("Print the path to the rig config file")
764+
.display_order(0),
765+
)
766+
.subcommand(
767+
Command::new("list")
768+
.about("List the names of all config entries")
769+
.display_order(0)
770+
.arg(
771+
Arg::new("json")
772+
.help("JSON output")
773+
.long("json")
774+
.num_args(0)
775+
.required(false),
776+
),
777+
)
778+
.subcommand(
779+
Command::new("get")
780+
.about("Get a config entry")
781+
.display_order(0)
782+
.arg(Arg::new("key").help("config key to get").required(true))
783+
.arg(
784+
Arg::new("json")
785+
.help("JSON output")
786+
.long("json")
787+
.num_args(0)
788+
.required(false),
789+
),
790+
)
791+
.subcommand(
792+
Command::new("set")
793+
.about("Set a config entry")
794+
.display_order(0)
795+
.arg(
796+
Arg::new("keyvalue")
797+
.help("key=value pair to set")
798+
.required(true),
799+
),
800+
);
801+
rig = rig.subcommand(cmd_config);
802+
}
803+
755804
#[cfg(target_os = "macos")]
756805
{
757806
let cmd_sysreqs = Command::new("sysreqs")
@@ -1288,7 +1337,29 @@ pub fn rig_app() -> Command {
12881337
.long("json")
12891338
.num_args(0)
12901339
.required(false),
1291-
)
1340+
);
1341+
1342+
#[cfg(any(target_os = "macos", target_os = "linux"))]
1343+
{
1344+
rig = rig
1345+
.arg(
1346+
Arg::new("user")
1347+
.help("Run in user mode (overrides RIG_MODE and config)")
1348+
.long("user")
1349+
.global(true)
1350+
.action(clap::ArgAction::SetTrue)
1351+
.conflicts_with("admin"),
1352+
)
1353+
.arg(
1354+
Arg::new("admin")
1355+
.help("Run in admin mode (overrides RIG_MODE and config)")
1356+
.long("admin")
1357+
.global(true)
1358+
.action(clap::ArgAction::SetTrue),
1359+
);
1360+
}
1361+
1362+
rig = rig
12921363
.subcommand(cmd_default)
12931364
.subcommand(cmd_list)
12941365
.subcommand(cmd_add)

src/common.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ pub fn check_installed(x: &String) -> Result<String, Box<dyn Error>> {
4848
bail!("R version {} is not installed", &x);
4949
}
5050

51-
pub fn get_r_base_profile(ver: &str) -> String {
52-
R_BASE_PROFILE.replace("{}", &version_dir_key(ver))
53-
}
54-
5551
// -- rig default ---------------------------------------------------------
5652

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

7167
pub fn set_default_if_none(ver: String) -> Result<(), Box<dyn Error>> {
68+
debug!("Checking if a default R version is set");
7269
let cur = sc_get_default()?;
7370
if cur.is_none() {
71+
debug!("No default R version is set, setting it to {}", ver);
7472
sc_set_default(&ver)?;
7573
}
7674
Ok(())
@@ -83,8 +81,8 @@ pub fn get_default_r_version() -> Result<Option<String>, Box<dyn Error>> {
8381
None => Ok(None),
8482
Some(d) => {
8583
let name = check_installed(&d)?;
86-
let desc = Path::new(&get_r_root_for(&name))
87-
.join(R_SYSLIBPATH.replace("{}", &version_dir_key(&name)))
84+
let desc = Path::new(&get_r_root_for(&name)?)
85+
.join(get_r_syslibpath()?.replace("{}", &version_dir_key(&name)))
8886
.join("base/DESCRIPTION");
8987
let lines = match read_lines(&desc) {
9088
Ok(x) => x,
@@ -106,8 +104,8 @@ pub fn get_default_r_version() -> Result<Option<String>, Box<dyn Error>> {
106104

107105
pub fn get_r_version_data_version(name: &str) -> Result<String, Box<dyn Error>> {
108106
let re = Regex::new("^Version:[ ]?").expect("Invalid regex pattern");
109-
let desc = Path::new(&get_r_root_for(name))
110-
.join(R_SYSLIBPATH.replace("{}", &version_dir_key(name)))
107+
let desc = Path::new(&get_r_root_for(name)?)
108+
.join(get_r_syslibpath()?.replace("{}", &version_dir_key(name)))
111109
.join("base/DESCRIPTION");
112110
let lines = match read_lines(&desc) {
113111
Ok(x) => x,
@@ -137,8 +135,8 @@ pub fn get_r_version_data(
137135
aliases: &[Alias],
138136
) -> Result<InstalledVersion, Box<dyn Error>> {
139137
let version = Some(get_r_version_data_version(name)?);
140-
let path = Path::new(&get_r_root_for(name)).join(R_VERSIONDIR.replace("{}", &version_dir_key(name)));
141-
let binary = Path::new(&get_r_root_for(name)).join(R_BINPATH.replace("{}", &version_dir_key(name)));
138+
let path = Path::new(&get_r_root_for(name)?).join(R_VERSIONDIR.replace("{}", &version_dir_key(name)));
139+
let binary = Path::new(&get_r_root_for(name)?).join(get_r_binpath()?.replace("{}", &version_dir_key(name)));
142140
let mut myaliases: Vec<String> = vec![];
143141
for a in aliases {
144142
if a.version == name {
@@ -159,7 +157,7 @@ pub fn sc_get_list_details() -> Result<Vec<InstalledVersion>, Box<dyn Error>> {
159157
let aliases = find_aliases()?;
160158
let mut res: Vec<InstalledVersion> = vec![];
161159

162-
for name in names {
160+
for name in &names {
163161
res.push(get_r_version_data(&name, &aliases)?);
164162
}
165163

src/config.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::collections::HashMap;
22
use std::error::Error;
33
use std::path::PathBuf;
44

5+
use clap::ArgMatches;
6+
57
use simple_error::{bail, SimpleError};
68

79
use serde_derive::Deserialize;
@@ -14,6 +16,8 @@ use crate::utils::*;
1416
struct Config {
1517
#[serde(default = "empty_stringmap")]
1618
userlibrary: HashMap<String, String>,
19+
#[serde(flatten)]
20+
extra: HashMap<String, serde_json::Value>,
1721
}
1822

1923
fn empty_stringmap() -> HashMap<String, String> {
@@ -86,3 +90,137 @@ pub fn get_config(rver: &str, key: &str) -> Result<Option<String>, Box<dyn Error
8690
_ => bail!("Unknown config key: {}, internal error", key),
8791
}
8892
}
93+
94+
#[cfg(any(target_os = "windows", target_os = "linux"))]
95+
pub fn sc_config(_args: &ArgMatches, _mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
96+
// Cannot be called
97+
Ok(())
98+
}
99+
100+
#[cfg(target_os = "macos")]
101+
pub fn sc_config(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
102+
match args.subcommand() {
103+
Some(("config-file-path", _)) => sc_config_config_file_path(),
104+
Some(("list", s)) => sc_config_list(s, mainargs),
105+
Some(("get", s)) => sc_config_get(s, mainargs),
106+
Some(("set", s)) => sc_config_set(s),
107+
_ => Ok(()),
108+
}
109+
}
110+
111+
#[cfg(target_os = "macos")]
112+
fn sc_config_config_file_path() -> Result<(), Box<dyn Error>> {
113+
let path = rig_config_file()?;
114+
println!("{}", path.display());
115+
Ok(())
116+
}
117+
118+
#[cfg(target_os = "macos")]
119+
fn sc_config_get(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
120+
let key = args.get_one::<String>("key").unwrap();
121+
let json = args.get_flag("json") || mainargs.get_flag("json");
122+
123+
let config_file = rig_config_file()?;
124+
let root: serde_json::Value = if config_file.exists() {
125+
let contents = read_file_string(&config_file)?;
126+
serde_json::from_str(&contents)?
127+
} else {
128+
serde_json::Value::Object(serde_json::Map::new())
129+
};
130+
131+
let value = &root[key.as_str()];
132+
match value {
133+
serde_json::Value::Null => {
134+
if json {
135+
println!("null");
136+
}
137+
}
138+
serde_json::Value::String(s) => {
139+
if json {
140+
println!("{}", serde_json::to_string(s)?);
141+
} else {
142+
println!("{}", s);
143+
}
144+
}
145+
scalar @ (serde_json::Value::Bool(_) | serde_json::Value::Number(_)) => {
146+
println!("{}", scalar);
147+
}
148+
complex => {
149+
println!("{}", serde_json::to_string_pretty(complex)?);
150+
}
151+
}
152+
Ok(())
153+
}
154+
155+
#[cfg(any(target_os = "macos", target_os = "linux"))]
156+
fn load_raw_config() -> Result<serde_json::Map<String, serde_json::Value>, Box<dyn Error>> {
157+
let config_file = rig_config_file()?;
158+
if config_file.exists() {
159+
let contents = read_file_string(&config_file)?;
160+
let value: serde_json::Value = serde_json::from_str(&contents)?;
161+
match value {
162+
serde_json::Value::Object(map) => Ok(map),
163+
_ => bail!("Config file is not a JSON object"),
164+
}
165+
} else {
166+
Ok(serde_json::Map::new())
167+
}
168+
}
169+
170+
#[cfg(target_os = "macos")]
171+
fn save_raw_config(map: &serde_json::Map<String, serde_json::Value>) -> Result<(), Box<dyn Error>> {
172+
let config_file = rig_config_file()?;
173+
let parent = config_file
174+
.parent()
175+
.ok_or(SimpleError::new("Invalid config file directory"))?;
176+
std::fs::create_dir_all(parent)?;
177+
std::fs::write(config_file, serde_json::to_string_pretty(map)?)?;
178+
Ok(())
179+
}
180+
181+
#[cfg(any(target_os = "macos", target_os = "linux"))]
182+
pub fn get_global_config_value(key: &str) -> Result<Option<String>, Box<dyn Error>> {
183+
let map = load_raw_config()?;
184+
match map.get(key) {
185+
Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
186+
_ => Ok(None),
187+
}
188+
}
189+
190+
#[cfg(target_os = "macos")]
191+
fn sc_config_set(args: &ArgMatches) -> Result<(), Box<dyn Error>> {
192+
let keyvalue = args.get_one::<String>("keyvalue").unwrap();
193+
let (key, value) = keyvalue
194+
.split_once('=')
195+
.ok_or_else(|| SimpleError::new(format!("Invalid key=value format: '{}'", keyvalue)))?;
196+
let mut map = load_raw_config()?;
197+
map.insert(key.to_string(), serde_json::Value::String(value.to_string()));
198+
save_raw_config(&map)
199+
}
200+
201+
#[cfg(target_os = "macos")]
202+
fn sc_config_list(args: &ArgMatches, mainargs: &ArgMatches) -> Result<(), Box<dyn Error>> {
203+
let config_file = rig_config_file()?;
204+
let keys: Vec<String> = if config_file.exists() {
205+
let contents = read_file_string(&config_file)?;
206+
let value: serde_json::Value = serde_json::from_str(&contents)?;
207+
match value.as_object() {
208+
Some(obj) => obj.keys().cloned().collect(),
209+
None => vec![],
210+
}
211+
} else {
212+
vec![]
213+
};
214+
215+
if args.get_flag("json") || mainargs.get_flag("json") {
216+
#[derive(serde::Serialize)]
217+
struct Entry { key: String }
218+
let entries: Vec<Entry> = keys.into_iter().map(|k| Entry { key: k }).collect();
219+
println!("{}", serde_json::to_string_pretty(&entries)?);
220+
} else {
221+
for key in keys {
222+
println!("{}", key);
223+
}
224+
}
225+
Ok(())
226+
}

src/escalate.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub fn escalate(task: &str) -> Result<(), Box<dyn Error>> {
3636
);
3737
with_env(&[
3838
"RIG_HOME",
39+
"RIG_BINARY_DIR",
40+
"RIG_MODE",
41+
"RIG_R_INSTALL_DIR",
3942
"RUST_BACKTRACE",
4043
"http_proxy",
4144
"https_proxy",

src/help-macos.in

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ const HELP_SYSTEM_ORTHO: &str = "
186186

187187
const HELP_SYSTEM_LINKS: &str = "
188188
\x1b[4m\x1b[1mDescription:\x1b[22m\x1b[24m
189-
Create quick links in `/usr/local/bin` for the current R installations.
190-
These let you directly run a specific R version. E.g. `R-4.1` will start
191-
R 4.1.x.
189+
Create quick links in the rig binary directory (`/usr/local/bin` by
190+
default) for the current R installations. These let you directly run a
191+
specific R version. E.g. `R-4.1` will start R 4.1.x.
192192
193193
`rig add` runs `rig system make-links`, so if you only use rig to
194194
install R, then you do not need to run it manually.

0 commit comments

Comments
 (0)