Skip to content

Commit 0db31cd

Browse files
authored
Merge pull request #2387 from workingjubilee/impl-shell
Implement shell PATH fixes via UnixShell trait
2 parents ac1a8c4 + cc10149 commit 0db31cd

File tree

8 files changed

+656
-374
lines changed

8 files changed

+656
-374
lines changed

src/cli/self_update.rs

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ use crate::utils::Notification;
5151
use crate::{Cfg, UpdateStatus};
5252
use crate::{DUP_TOOLS, TOOLS};
5353

54-
mod path_update;
55-
use path_update::PathUpdateMethod;
54+
#[cfg(unix)]
55+
mod shell;
5656
#[cfg(unix)]
5757
mod unix;
5858
#[cfg(windows)]
@@ -122,6 +122,7 @@ these changes will be reverted.
122122
};
123123
}
124124

125+
#[cfg(unix)]
125126
macro_rules! pre_install_msg_unix {
126127
() => {
127128
pre_install_msg_template!(
@@ -133,6 +134,7 @@ modifying the profile file{plural} located at:
133134
};
134135
}
135136

137+
#[cfg(windows)]
136138
macro_rules! pre_install_msg_win {
137139
() => {
138140
pre_install_msg_template!(
@@ -322,7 +324,7 @@ pub fn install(
322324
let install_res: Result<utils::ExitCode> = (|| {
323325
install_bins()?;
324326
if !opts.no_modify_path {
325-
do_add_to_path(&get_add_path_methods())?;
327+
do_add_to_path()?;
326328
}
327329
utils::create_rustup_home()?;
328330
maybe_install_rust(
@@ -336,9 +338,6 @@ pub fn install(
336338
quiet,
337339
)?;
338340

339-
#[cfg(unix)]
340-
write_env()?;
341-
342341
Ok(utils::ExitCode(0))
343342
})();
344343

@@ -514,23 +513,14 @@ fn pre_install_msg(no_modify_path: bool) -> Result<String> {
514513
let rustup_home = utils::rustup_home()?;
515514

516515
if !no_modify_path {
517-
if cfg!(unix) {
518-
let add_path_methods = get_add_path_methods();
519-
let rcfiles = add_path_methods
520-
.into_iter()
521-
.filter_map(|m| {
522-
if let PathUpdateMethod::RcFile(path) = m {
523-
Some(format!("{}", path.display()))
524-
} else {
525-
None
526-
}
527-
})
516+
// Brittle code warning: some duplication in unix::do_add_to_path
517+
#[cfg(not(windows))]
518+
{
519+
let rcfiles = shell::get_available_shells()
520+
.flat_map(|sh| sh.update_rcs().into_iter())
521+
.map(|rc| format!(" {}", rc.display()))
528522
.collect::<Vec<_>>();
529523
let plural = if rcfiles.len() > 1 { "s" } else { "" };
530-
let rcfiles = rcfiles
531-
.into_iter()
532-
.map(|f| format!(" {}", f))
533-
.collect::<Vec<_>>();
534524
let rcfiles = rcfiles.join("\n");
535525
Ok(format!(
536526
pre_install_msg_unix!(),
@@ -540,14 +530,14 @@ fn pre_install_msg(no_modify_path: bool) -> Result<String> {
540530
rcfiles = rcfiles,
541531
rustup_home = rustup_home.display(),
542532
))
543-
} else {
544-
Ok(format!(
545-
pre_install_msg_win!(),
546-
cargo_home = cargo_home.display(),
547-
cargo_home_bin = cargo_home_bin.display(),
548-
rustup_home = rustup_home.display(),
549-
))
550533
}
534+
#[cfg(windows)]
535+
Ok(format!(
536+
pre_install_msg_win!(),
537+
cargo_home = cargo_home.display(),
538+
cargo_home_bin = cargo_home_bin.display(),
539+
rustup_home = rustup_home.display(),
540+
))
551541
} else {
552542
Ok(format!(
553543
pre_install_msg_no_modify_path!(),
@@ -846,8 +836,7 @@ pub fn uninstall(no_prompt: bool) -> Result<utils::ExitCode> {
846836
info!("removing cargo home");
847837

848838
// Remove CARGO_HOME/bin from PATH
849-
let remove_path_methods = get_remove_path_methods()?;
850-
do_remove_from_path(&remove_path_methods)?;
839+
do_remove_from_path()?;
851840

852841
// Delete everything in CARGO_HOME *except* the rustup bin
853842

src/cli/self_update/env.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
# rustup shell setup
3+
# affix colons on either side of $PATH to simplify matching
4+
case ":${PATH}:" in
5+
*:"{cargo_bin}":*)
6+
;;
7+
*)
8+
# Prepending path in case a system-installed rustc must be overwritten
9+
export PATH="{cargo_bin}:$PATH"
10+
;;
11+
esac

src/cli/self_update/path_update.rs

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/cli/self_update/shell.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//! Paths and Unix shells
2+
//!
3+
//! MacOS, Linux, FreeBSD, and many other OS model their design on Unix,
4+
//! so handling them is relatively consistent. But only relatively.
5+
//! POSIX postdates Unix by 20 years, and each "Unix-like" shell develops
6+
//! unique quirks over time.
7+
//!
8+
//!
9+
//! Windowing Managers, Desktop Environments, GUI Terminals, and PATHs
10+
//!
11+
//! Duplicating paths in PATH can cause performance issues when the OS searches
12+
//! the same place multiple times. Traditionally, Unix configurations have
13+
//! resolved this by setting up PATHs in the shell's login profile.
14+
//!
15+
//! This has its own issues. Login profiles are only intended to run once, but
16+
//! changing the PATH is common enough that people may run it twice. Desktop
17+
//! environments often choose to NOT start login shells in GUI terminals. Thus,
18+
//! a trend has emerged to place PATH updates in other run-commands (rc) files,
19+
//! leaving Rustup with few assumptions to build on for fulfilling its promise
20+
//! to set up PATH appropriately.
21+
//!
22+
//! Rustup addresses this by:
23+
//! 1) using a shell script that updates PATH if the path is not in PATH
24+
//! 2) sourcing this script in any known and appropriate rc file
25+
26+
use std::path::PathBuf;
27+
28+
use error_chain::bail;
29+
30+
use super::*;
31+
use crate::process;
32+
33+
pub type Shell = Box<dyn UnixShell>;
34+
35+
#[derive(Debug, PartialEq)]
36+
pub struct ShellScript {
37+
content: &'static str,
38+
name: &'static str,
39+
}
40+
41+
impl ShellScript {
42+
pub fn write(&self) -> Result<()> {
43+
let home = utils::cargo_home()?;
44+
let cargo_bin = format!("{}/bin", cargo_home_str()?);
45+
let env_name = home.join(self.name);
46+
let env_file = self.content.replace("{cargo_bin}", &cargo_bin);
47+
utils::write_file(self.name, &env_name, &env_file)?;
48+
Ok(())
49+
}
50+
}
51+
52+
// TODO: Update into a bytestring.
53+
pub fn cargo_home_str() -> Result<Cow<'static, str>> {
54+
let path = utils::cargo_home()?;
55+
56+
let default_cargo_home = utils::home_dir()
57+
.unwrap_or_else(|| PathBuf::from("."))
58+
.join(".cargo");
59+
Ok(if default_cargo_home == path {
60+
"$HOME/.cargo".into()
61+
} else {
62+
match path.to_str() {
63+
Some(p) => p.to_owned().into(),
64+
None => bail!("Non-Unicode path!"),
65+
}
66+
})
67+
}
68+
69+
// TODO: Tcsh (BSD)
70+
// TODO?: Make a decision on Ion Shell, Power Shell, Nushell
71+
// Cross-platform non-POSIX shells have not been assessed for integration yet
72+
fn enumerate_shells() -> Vec<Shell> {
73+
vec![Box::new(Posix), Box::new(Bash), Box::new(Zsh)]
74+
}
75+
76+
pub fn get_available_shells() -> impl Iterator<Item = Shell> {
77+
enumerate_shells().into_iter().filter(|sh| sh.does_exist())
78+
}
79+
80+
pub trait UnixShell {
81+
// Detects if a shell "exists". Users have multiple shells, so an "eager"
82+
// heuristic should be used, assuming shells exist if any traces do.
83+
fn does_exist(&self) -> bool;
84+
85+
// Gives all rcfiles of a given shell that rustup is concerned with.
86+
// Used primarily in checking rcfiles for cleanup.
87+
fn rcfiles(&self) -> Vec<PathBuf>;
88+
89+
// Gives rcs that should be written to.
90+
fn update_rcs(&self) -> Vec<PathBuf>;
91+
92+
// Writes the relevant env file.
93+
fn env_script(&self) -> ShellScript {
94+
ShellScript {
95+
name: "env",
96+
content: include_str!("env.sh"),
97+
}
98+
}
99+
100+
fn source_string(&self) -> Result<String> {
101+
Ok(format!(r#"source "{}/env""#, cargo_home_str()?))
102+
}
103+
}
104+
105+
struct Posix;
106+
impl UnixShell for Posix {
107+
fn does_exist(&self) -> bool {
108+
true
109+
}
110+
111+
fn rcfiles(&self) -> Vec<PathBuf> {
112+
match utils::home_dir() {
113+
Some(dir) => vec![dir.join(".profile")],
114+
_ => vec![],
115+
}
116+
}
117+
118+
fn update_rcs(&self) -> Vec<PathBuf> {
119+
// Write to .profile even if it doesn't exist. It's the only rc in the
120+
// POSIX spec so it should always be set up.
121+
self.rcfiles()
122+
}
123+
}
124+
125+
struct Bash;
126+
127+
impl UnixShell for Bash {
128+
fn does_exist(&self) -> bool {
129+
self.update_rcs().len() > 0
130+
}
131+
132+
fn rcfiles(&self) -> Vec<PathBuf> {
133+
// Bash also may read .profile, however Rustup already includes handling
134+
// .profile as part of POSIX and always does setup for POSIX shells.
135+
[".bash_profile", ".bash_login", ".bashrc"]
136+
.iter()
137+
.filter_map(|rc| utils::home_dir().map(|dir| dir.join(rc)))
138+
.collect()
139+
}
140+
141+
fn update_rcs(&self) -> Vec<PathBuf> {
142+
self.rcfiles()
143+
.into_iter()
144+
.filter(|rc| rc.is_file())
145+
.collect()
146+
}
147+
}
148+
149+
struct Zsh;
150+
151+
impl Zsh {
152+
fn zdotdir() -> Result<PathBuf> {
153+
use std::ffi::OsStr;
154+
use std::os::unix::ffi::OsStrExt;
155+
156+
if matches!(process().var("SHELL"), Ok(sh) if sh.contains("zsh")) {
157+
match process().var("ZDOTDIR") {
158+
Ok(dir) if dir.len() > 0 => Ok(PathBuf::from(dir)),
159+
_ => bail!("Zsh setup failed."),
160+
}
161+
} else {
162+
match std::process::Command::new("zsh")
163+
.args(&["-c", "'echo $ZDOTDIR'"])
164+
.output()
165+
{
166+
Ok(io) if io.stdout.len() > 0 => Ok(PathBuf::from(OsStr::from_bytes(&io.stdout))),
167+
_ => bail!("Zsh setup failed."),
168+
}
169+
}
170+
}
171+
}
172+
173+
impl UnixShell for Zsh {
174+
fn does_exist(&self) -> bool {
175+
// zsh has to either be the shell or be callable for zsh setup.
176+
matches!(process().var("SHELL"), Ok(sh) if sh.contains("zsh"))
177+
|| matches!(utils::find_cmd(&["zsh"]), Some(_))
178+
}
179+
180+
fn rcfiles(&self) -> Vec<PathBuf> {
181+
[Zsh::zdotdir().ok(), utils::home_dir()]
182+
.iter()
183+
.filter_map(|dir| dir.as_ref().map(|p| p.join(".zshenv")))
184+
.collect()
185+
}
186+
187+
fn update_rcs(&self) -> Vec<PathBuf> {
188+
// zsh can change $ZDOTDIR both _before_ AND _during_ reading .zshenv,
189+
// so we: write to $ZDOTDIR/.zshenv if-exists ($ZDOTDIR changes before)
190+
// OR write to $HOME/.zshenv if it exists (change-during)
191+
// if neither exist, we create it ourselves, but using the same logic,
192+
// because we must still respond to whether $ZDOTDIR is set or unset.
193+
// In any case we only write once.
194+
self.rcfiles()
195+
.into_iter()
196+
.filter(|env| env.is_file())
197+
.chain(self.rcfiles().into_iter())
198+
.take(1)
199+
.collect()
200+
}
201+
}
202+
203+
pub fn legacy_paths() -> impl Iterator<Item = PathBuf> {
204+
let zprofiles = Zsh::zdotdir()
205+
.into_iter()
206+
.chain(utils::home_dir())
207+
.map(|d| d.join(".zprofile"));
208+
let profiles = [".bash_profile", ".profile"]
209+
.iter()
210+
.filter_map(|rc| utils::home_dir().map(|d| d.join(rc)));
211+
212+
profiles.chain(zprofiles)
213+
}

0 commit comments

Comments
 (0)