diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 64204cc285..406cd3fa86 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -14,7 +14,7 @@ pub async fn main( current_dir: PathBuf, process: &Process, ) -> anyhow::Result { - self_update::cleanup_self_updater(process)?; + self_update::cleanup_self_updater(&process.cargo_home()?.join("bin"))?; let _setup = job::setup(); let mut args = process.args_os().skip(1); diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 1fec1ce59d..e0a27f6bdf 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -703,7 +703,7 @@ pub async fn main( .bin("rustup") .complete(); - self_update::cleanup_self_updater(process)?; + self_update::cleanup_self_updater(&process.cargo_home()?.join("bin"))?; use clap::error::ErrorKind::*; let matches = match Rustup::try_parse_from(process.args_os()) { diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 2f8b8ca69a..0e2b70ab39 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -32,8 +32,9 @@ //! Deleting the running binary during uninstall is tricky //! and racy on Windows. +#[cfg(unix)] +use std::borrow::Cow; use std::{ - borrow::Cow, env::{self, consts::EXE_SUFFIX}, fmt, fs, io::{self, Write}, @@ -83,7 +84,7 @@ mod shell; #[cfg(unix)] mod unix; #[cfg(unix)] -use unix::{do_add_to_path, do_remove_from_path}; +use unix::{add_to_path, remove_from_path}; #[cfg(unix)] pub(crate) use unix::{run_update, self_replace}; @@ -95,8 +96,7 @@ pub use windows::complete_windows_uninstall; pub use windows::{RUSTUP_REGISTRY_TEST_ID, RegistryValueId, USER_PATH, get_path}; #[cfg(windows)] use windows::{ - add_uninstall_registry_entry, do_add_to_path, do_remove_from_path, - remove_uninstall_registry_entry, + add_to_path, add_uninstall_registry_entry, remove_from_path, remove_uninstall_registry_entry, }; #[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; @@ -147,7 +147,7 @@ impl InstallOpts<'_> { #[cfg(unix)] { - exit_code &= unix::do_anti_sudo_check(no_prompt, process)?; + exit_code &= unix::anti_sudo_check(no_prompt, process)?; } let mut term = process.stdout(); @@ -192,35 +192,30 @@ impl InstallOpts<'_> { return Ok(ExitCode::FAILURE); } - let cargo_home = canonical_cargo_home(process)?; - #[cfg(windows)] - let cargo_home = cargo_home.replace('\\', r"\\"); - #[cfg(windows)] + let cargo_home = process.cargo_home()?; + let home_dir = process.home_dir(); let msg = if no_modify_path { format!( - post_install_msg_win_no_modify_path!(), - cargo_home = cargo_home + post_install_msg_no_modify_path!(), + cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), ) } else { - format!(post_install_msg_win!(), cargo_home = cargo_home) - }; - #[cfg(not(windows))] - let source_env_lines = shell::build_source_env_lines(process); - #[cfg(not(windows))] - let msg = if no_modify_path { format!( - post_install_msg_unix_no_modify_path!(), - cargo_home = cargo_home, - source_env_lines = source_env_lines, - ) - } else { - format!( - post_install_msg_unix!(), - cargo_home = cargo_home, - source_env_lines = source_env_lines, + post_install_msg!(), + cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), ) }; md(&mut term, msg); + #[cfg(not(windows))] + md( + &mut term, + format!( + post_install_msg_unix!(), + env_dir = HomeDisplay::new(&cargo_home, home_dir.as_deref()), + source_env_lines = + shell::build_source_env_lines(process, &cargo_home, home_dir.as_deref()), + ), + ); #[cfg(unix)] warn_if_default_linker_missing(process); @@ -243,13 +238,14 @@ impl InstallOpts<'_> { quiet: bool, process: &Process, ) -> anyhow::Result<()> { - install_bins(process)?; + let cargo_bin = process.cargo_home()?.join("bin"); + install_bins(&cargo_bin, force_hard_links(process))?; #[cfg(unix)] - unix::do_write_env_files(process)?; + unix::write_env_files(process)?; if !self.no_modify_path { - do_add_to_path(process)?; + add_to_path(process)?; } #[cfg(windows)] @@ -288,7 +284,7 @@ impl InstallOpts<'_> { DistributableToolchain::install(options).await?.status }; - check_proxy_sanity(cfg.process, components, &desc)?; + check_proxy_sanity(&cargo_bin, components, &desc)?; cfg.set_default(Some(&partial_desc.into()))?; writeln!(cfg.process.stdout().lock())?; @@ -592,23 +588,40 @@ fn update_root(process: &Process) -> String { .unwrap_or_else(|_| String::from(DEFAULT_UPDATE_ROOT)) } -/// `CARGO_HOME` suitable for display, possibly with $HOME -/// substituted for the directory prefix -fn canonical_cargo_home(process: &Process) -> anyhow::Result> { - let path = process.cargo_home()?; - - let default_cargo_home = process - .home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".cargo"); - Ok(if default_cargo_home == path { - cfg_select! { - windows => r"%USERPROFILE%\.cargo".into(), - _ => "$HOME/.cargo".into(), +/// Displays an installation path with a platform-specific home abbreviation. +struct HomeDisplay<'a> { + path: &'a Path, + home_prefix: Option<&'a str>, +} + +impl<'a> HomeDisplay<'a> { + fn new(path: &'a Path, home_dir: Option<&Path>) -> Self { + match home_dir.and_then(|home| path.strip_prefix(home).ok()) { + Some(relative) => Self { + path: relative, + home_prefix: Some(cfg_select! { + windows => "%USERPROFILE%", + _ => "$HOME", + }), + }, + None => Self { + path, + home_prefix: None, + }, } - } else { - path.to_string_lossy().into_owned().into() - }) + } +} + +impl fmt::Display for HomeDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(home_prefix) = self.home_prefix { + f.write_str(home_prefix)?; + if !self.path.is_empty() { + write!(f, "{MAIN_SEPARATOR}")?; + } + } + self.path.display().fmt(f) + } } fn rustc_or_cargo_exists_in_path(process: &Process) -> anyhow::Result<()> { @@ -687,15 +700,15 @@ fn check_existence_of_settings_file(process: &Process) -> anyhow::Result<()> { fn pre_install_msg(no_modify_path: bool, process: &Process) -> anyhow::Result { let cargo_home = process.cargo_home()?; - let cargo_home_bin = cargo_home.join("bin"); + let cargo_bin_dir = cargo_home.join("bin"); let rustup_home = home::rustup_home()?; if !no_modify_path { - // Brittle code warning: some duplication in unix::do_add_to_path + // Brittle code warning: some duplication in unix::add_to_path #[cfg(not(windows))] { let rcfiles = shell::get_available_shells(process) - .flat_map(|sh| sh.update_rcs(process).into_iter()) + .flat_map(|sh| sh.rcs(process).into_iter()) .map(|rc| format!(" {}", rc.display())) .collect::>(); let plural = if rcfiles.len() > 1 { "s" } else { "" }; @@ -703,7 +716,7 @@ fn pre_install_msg(no_modify_path: bool, process: &Process) -> anyhow::Result anyhow::Result anyhow::Result<()> { - let bin_path = process.cargo_home()?.join("bin"); +fn install_bins(bin_path: &Path, force_hard_links: bool) -> anyhow::Result<()> { let this_exe_path = utils::current_exe()?; let rustup_path = bin_path.join(format!("rustup{EXE_SUFFIX}")); - utils::ensure_dir_exists("bin", &bin_path)?; + utils::ensure_dir_exists("bin", bin_path)?; // NB: Even on Linux we can't just copy the new binary over the (running) // old binary; we must unlink it first. if rustup_path.exists() { @@ -785,22 +797,22 @@ fn install_bins(process: &Process) -> anyhow::Result<()> { } utils::copy_file_symlink_to_source(&this_exe_path, &rustup_path)?; utils::make_executable(&rustup_path)?; - install_proxies(process) + install_proxies_with_opts(bin_path, force_hard_links) } pub(crate) fn install_proxies(process: &Process) -> anyhow::Result<()> { - install_proxies_with_opts( - process, - // HACK: On Windows CI machines, some Docker setups don't like symlinks, so we force hard - // links in this case. - // See: - (cfg!(windows) && process.is_ci()) - || process.var_os("RUSTUP_FORCE_HARDLINK_PROXIES").is_some(), - ) + let bin_path = process.cargo_home()?.join("bin"); + install_proxies_with_opts(&bin_path, force_hard_links(process)) } -fn install_proxies_with_opts(process: &Process, force_hard_links: bool) -> anyhow::Result<()> { - let bin_path = process.cargo_home()?.join("bin"); +fn force_hard_links(process: &Process) -> bool { + // HACK: On Windows CI machines, some Docker setups don't like symlinks, so we force hard + // links in this case. + // See: + (cfg!(windows) && process.is_ci()) || process.var_os("RUSTUP_FORCE_HARDLINK_PROXIES").is_some() +} + +fn install_proxies_with_opts(bin_path: &Path, force_hard_links: bool) -> anyhow::Result<()> { let rustup_path = bin_path.join(format!("rustup{EXE_SUFFIX}")); let rustup = Handle::from_path(&rustup_path)?; @@ -894,7 +906,7 @@ fn install_proxies_with_opts(process: &Process, force_hard_links: bool) -> anyho // This may fail for symlinks in some circumstances. let path = bin_path.join(format!("{tool}{EXE_SUFFIX}", tool = TOOLS[0])); if fs::File::open(path).is_err() { - return install_proxies_with_opts(process, true); + return install_proxies_with_opts(bin_path, true); } } @@ -902,12 +914,10 @@ fn install_proxies_with_opts(process: &Process, force_hard_links: bool) -> anyho } fn check_proxy_sanity( - process: &Process, + bin_path: &Path, components: &[&str], desc: &ToolchainDesc, ) -> anyhow::Result<()> { - let bin_path = process.cargo_home()?.join("bin"); - // Sometimes linking a proxy produces an unpredictable result, where the proxy // is in place, but manages to not call rustup correctly. One way to make sure we // don't run headfirst into the wall is to at least try and run our freshly @@ -959,10 +969,9 @@ pub(crate) fn uninstall( let msg = if no_modify_path { pre_uninstall_msg_no_modify_path!().to_owned() } else { - format!( - pre_uninstall_msg!(), - cargo_home = canonical_cargo_home(process)? - ) + let bin_home = cargo_home.join("bin"); + let cargo_bin_dir = HomeDisplay::new(&bin_home, process.home_dir().as_deref()); + format!(pre_uninstall_msg!(), cargo_bin_dir = cargo_bin_dir) }; md(&mut process.stdout(), msg); if !common::confirm("\nContinue? (y/N)", false, process)? { @@ -992,7 +1001,7 @@ pub(crate) fn uninstall( // the process exits. // see: windows::{complete_windows_uninstall,spawn_uninstall_gc} #[cfg(windows)] - windows::spawn_uninstall_gc(no_modify_path, process)?; + windows::spawn_uninstall_gc(no_modify_path, &cargo_home)?; info!("rustup is uninstalled"); @@ -1065,7 +1074,7 @@ fn clean_cargo_home( } Ok(()) if !no_modify_path => { info!("removing cargo bin directory `{cargo_bin_display}` from $PATH"); - do_remove_from_path(process)?; + remove_from_path(process)?; } Ok(()) => {} } @@ -1373,9 +1382,8 @@ pub(crate) async fn check_rustup_update(dl_cfg: &DownloadCfg<'_>) -> anyhow::Res } #[tracing::instrument(level = "trace")] -pub(crate) fn cleanup_self_updater(process: &Process) -> anyhow::Result<()> { - let cargo_home = process.cargo_home()?; - let setup = cargo_home.join(format!("bin/rustup-init{EXE_SUFFIX}")); +pub(crate) fn cleanup_self_updater(bin_path: &Path) -> anyhow::Result<()> { + let setup = bin_path.join(format!("rustup-init{EXE_SUFFIX}")); if setup.exists() { utils::remove_file("setup", &setup)?; @@ -1386,17 +1394,60 @@ pub(crate) fn cleanup_self_updater(process: &Process) -> anyhow::Result<()> { #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{collections::HashMap, path::Path}; + use super::HomeDisplay; use crate::{ cli::self_update::InstallOpts, config::Cfg, dist::{PartialToolchainDesc, Profile}, for_host, process::TestProcess, - test::{Env, test_dir, with_rustup_home}, + test::{test_dir, with_rustup_home}, }; + #[test] + fn bin_home_display() { + let home = Path::new("/home/user"); + let cargo_bin_dir = home.join(".cargo").join("bin"); + assert_eq!( + HomeDisplay::new(&cargo_bin_dir, Some(home)).to_string(), + cfg_select! { + windows => r"%USERPROFILE%\.cargo\bin", + _ => "$HOME/.cargo/bin", + } + ); + + let local_bin_dir = home.join(".local").join("bin"); + assert_eq!( + HomeDisplay::new(&local_bin_dir, Some(home)).to_string(), + cfg_select! { + windows => r"%USERPROFILE%\.local\bin", + _ => "$HOME/.local/bin", + } + ); + assert_eq!( + HomeDisplay::new(home, Some(home)).to_string(), + cfg_select! { + windows => "%USERPROFILE%", + _ => "$HOME", + } + ); + + for bin_home in ["/opt/rust/bin", "/home/username/bin", ".cargo/bin"] { + let bin_home = Path::new(bin_home); + assert_eq!( + HomeDisplay::new(bin_home, Some(home)).to_string(), + bin_home.display().to_string() + ); + } + + assert_eq!( + HomeDisplay::new(&cargo_bin_dir, None).to_string(), + cargo_bin_dir.display().to_string() + ); + } + #[test] fn default_toolchain_is_stable() { with_rustup_home(|home| { @@ -1439,10 +1490,7 @@ info: default host tuple is {0} fn install_bins_creates_cargo_home() { let root_dir = test_dir().unwrap(); let cargo_home = root_dir.path().join("cargo"); - let mut vars = HashMap::new(); - vars.env("CARGO_HOME", cargo_home.to_string_lossy().to_string()); - let tp = TestProcess::with_vars(vars); - super::install_bins(&tp.process).unwrap(); + super::install_bins(&cargo_home.join("bin"), false).unwrap(); assert!(cargo_home.exists()); } } diff --git a/src/cli/self_update/msg.rs b/src/cli/self_update/msg.rs index fecaafc695..c2d1d37c8b 100644 --- a/src/cli/self_update/msg.rs +++ b/src/cli/self_update/msg.rs @@ -26,7 +26,7 @@ This can be modified with the CARGO_HOME environment variable. The `cargo`, `rustc`, `rustup` and other commands will be added to Cargo's bin directory, located at: - {cargo_home_bin} + {cargo_bin_dir} ", $platform_msg, @@ -70,69 +70,45 @@ but will not be added automatically." }; } -#[cfg(not(windows))] -macro_rules! post_install_msg_unix { +macro_rules! post_install_msg { () => { r"# Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your `PATH` environment variable to include -Cargo's bin directory ({cargo_home}/bin). - -To configure your current shell, you need to source the -corresponding `env` file under {cargo_home}. - -Consider running the right command for your shell (note the leading DOT): -{source_env_lines}" +Cargo's bin directory (`{cargo_bin_dir}`). +" }; } -#[cfg(windows)] -macro_rules! post_install_msg_win { +macro_rules! post_install_msg_no_modify_path { () => { r"# Rust is installed now. Great! - -To get started you may need to restart your current shell. -This would reload its `PATH` environment variable to include -Cargo's bin directory ({cargo_home}\\bin). +To get started you need Cargo's bin directory (`{cargo_bin_dir}`) in your `PATH` +environment variable. This has not been done automatically. " }; } #[cfg(not(windows))] -macro_rules! post_install_msg_unix_no_modify_path { +macro_rules! post_install_msg_unix { () => { - r"# Rust is installed now. Great! - -To get started you need Cargo's bin directory ({cargo_home}/bin) in your `PATH` -environment variable. This has not been done automatically. - -To configure your current shell, you need to source -the corresponding `env` file under {cargo_home}. + r" +To configure your current shell, you need to source the +corresponding `env` file under `{env_dir}`. Consider running the right command for your shell (note the leading DOT): {source_env_lines}" }; } -#[cfg(windows)] -macro_rules! post_install_msg_win_no_modify_path { - () => { - r"# Rust is installed now. Great! - -To get started you need Cargo's bin directory ({cargo_home}\\bin) in your `PATH` -environment variable. This has not been done automatically. -" - }; -} - macro_rules! pre_uninstall_msg { () => { r"# Thanks for hacking in Rust! This will uninstall all Rust toolchains and data, and remove -`{cargo_home}/bin` from your `PATH` environment variable. +`{cargo_bin_dir}` from your `PATH` environment variable. " }; diff --git a/src/cli/self_update/shell.rs b/src/cli/self_update/shell.rs index 4e3e062923..df5cf1b511 100644 --- a/src/cli/self_update/shell.rs +++ b/src/cli/self_update/shell.rs @@ -23,15 +23,13 @@ //! 1) using a shell script that updates PATH if the path is not in PATH //! 2) sourcing this script (`. /path/to/script`) in any appropriate rc file -use std::{borrow::Cow, path::PathBuf}; +use std::path::{Path, PathBuf}; use anyhow::bail; use super::utils; use crate::process::Process; -pub(crate) type Shell = Box; - #[derive(Debug, PartialEq)] pub(crate) struct ShellScript { content: &'static str, @@ -39,46 +37,36 @@ pub(crate) struct ShellScript { } // TODO: Update into a bytestring. -fn cargo_home_str_with_home(home: &str, process: &Process) -> anyhow::Result> { - let path = process.cargo_home()?; - - let default_cargo_home = process - .home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".cargo"); - Ok(if default_cargo_home == path { - Cow::Owned(format!("{home}/.cargo")) +fn path_str_with_home( + home: &str, + path: &Path, + home_dir: Option<&Path>, + default_suffix: &str, +) -> anyhow::Result { + let default_path = home_dir + .unwrap_or_else(|| Path::new(".")) + .join(default_suffix); + Ok(if default_path == path { + format!("{home}/{default_suffix}") } else { match path.to_str() { - Some(p) => p.to_owned().into(), + Some(p) => p.to_owned(), None => bail!("Non-Unicode path!"), } }) } -// TODO: Tcsh (BSD) -// TODO?: Make a decision on Ion Shell -// Cross-platform non-POSIX shells have not been assessed for integration yet -fn enumerate_shells() -> Vec { - vec![ - Box::new(Posix), - Box::new(Bash), - Box::new(Zsh), - Box::new(Fish), - Box::new(Nu), - Box::new(Tcsh), - Box::new(Pwsh), - Box::new(Xonsh), - ] -} - /// Builds the shell source lines for the post-install message, showing only /// shells that are available on the current system. Shells sharing the same /// env file are grouped onto one line (e.g. sh/bash/zsh all use `env`). -pub(crate) fn build_source_env_lines(process: &Process) -> String { +pub(crate) fn build_source_env_lines( + process: &Process, + env_dir: &Path, + home_dir: Option<&Path>, +) -> String { let mut groups = Vec::<(_, Vec<_>)>::new(); for shell in get_available_shells(process) { - let Ok(src) = shell.source_string(process) else { + let Ok(src) = shell.source_string(env_dir, home_dir) else { continue; }; if let Some(names) = groups @@ -97,10 +85,24 @@ pub(crate) fn build_source_env_lines(process: &Process) -> String { .collect() } -pub(crate) fn get_available_shells(process: &Process) -> impl Iterator + '_ { - enumerate_shells() - .into_iter() - .filter(|sh| sh.does_exist(process)) +// TODO: Tcsh (BSD) +// TODO?: Make a decision on Ion Shell +// Cross-platform non-POSIX shells have not been assessed for integration yet +pub(crate) fn get_available_shells( + process: &Process, +) -> impl Iterator + '_ { + [ + &Posix as &dyn UnixShell, + &Bash, + &Zsh, + &Fish, + &Nu, + &Tcsh, + &Pwsh, + &Xonsh, + ] + .into_iter() + .filter(move |sh| sh.does_exist(process)) } pub(crate) trait UnixShell { @@ -113,10 +115,12 @@ pub(crate) trait UnixShell { // Gives all rcfiles of a given shell that Rustup is concerned with. // Used primarily in checking rcfiles for cleanup. - fn rcfiles(&self, process: &Process) -> Vec; + fn rc_candidates(&self, process: &Process) -> Vec; - // Gives rcs that should be written to. - fn update_rcs(&self, process: &Process) -> Vec; + // Returns rcfile paths where installation should add the source command. + // May return multiple paths, including files that do not yet exist. + // Does not modify the files. + fn rcs(&self, process: &Process) -> Vec; // Writes the relevant env file. fn env_script(&self) -> ShellScript { @@ -126,22 +130,34 @@ pub(crate) trait UnixShell { } } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { + fn home_var(&self) -> &'static str { #[cfg(windows)] let home = "%USERPROFILE%"; #[cfg(not(windows))] let home = "$HOME"; - cargo_home_str_with_home(home, process) + home } - fn source_string(&self, process: &Process) -> anyhow::Result { - Ok(format!(r#". "{}/env""#, self.cargo_home_str(process)?)) + fn env_dir_str(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { + path_str_with_home(self.home_var(), env_dir, home_dir, ".cargo") } - fn write_script(&self, script: &ShellScript, process: &Process) -> anyhow::Result<()> { - let home = process.cargo_home()?; - let cargo_bin = format!("{}/bin", self.cargo_home_str(process)?); - let env_name = home.join(script.name); + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { + Ok(format!( + r#". "{}/env""#, + self.env_dir_str(env_dir, home_dir)? + )) + } + + fn write_script( + &self, + script: &ShellScript, + env_dir: &Path, + bin_dir: &Path, + home_dir: Option<&Path>, + ) -> anyhow::Result<()> { + let cargo_bin = path_str_with_home(self.home_var(), bin_dir, home_dir, ".cargo/bin")?; + let env_name = env_dir.join(script.name); let env_file = script.content.replace("{cargo_bin}", &cargo_bin); utils::write_file(script.name, &env_name, &env_file)?; Ok(()) @@ -159,17 +175,17 @@ impl UnixShell for Posix { "sh/ash/dash/pdksh" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { match process.home_dir() { Some(dir) => vec![dir.join(".profile")], _ => vec![], } } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { // Write to .profile even if it doesn't exist. It's the only rc in the // POSIX spec so it should always be set up. - self.rcfiles(process) + self.rc_candidates(process) } } @@ -177,14 +193,14 @@ struct Bash; impl UnixShell for Bash { fn does_exist(&self, process: &Process) -> bool { - !self.update_rcs(process).is_empty() + !self.rcs(process).is_empty() } fn name(&self) -> &'static str { "bash" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { // Bash also may read .profile, however Rustup already includes handling // .profile as part of POSIX and always does setup for POSIX shells. [".bash_profile", ".bash_login", ".bashrc"] @@ -193,8 +209,8 @@ impl UnixShell for Bash { .collect() } - fn update_rcs(&self, process: &Process) -> Vec { - self.rcfiles(process) + fn rcs(&self, process: &Process) -> Vec { + self.rc_candidates(process) .into_iter() .filter(|rc| rc.is_file()) .collect() @@ -235,24 +251,24 @@ impl UnixShell for Zsh { "zsh" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { [Self::zdotdir(process).ok(), process.home_dir()] .iter() .filter_map(|dir| dir.as_ref().map(|p| p.join(".zshenv"))) .collect() } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { // zsh can change $ZDOTDIR both _before_ AND _during_ reading .zshenv, // so we: write to $ZDOTDIR/.zshenv if-exists ($ZDOTDIR changes before) // OR write to $HOME/.zshenv if it exists (change-during) // if neither exist, we create it ourselves, but using the same logic, // because we must still respond to whether $ZDOTDIR is set or unset. // In any case we only write once. - self.rcfiles(process) + self.rc_candidates(process) .into_iter() .filter(|env| env.is_file()) - .chain(self.rcfiles(process)) + .chain(self.rc_candidates(process)) .take(1) .collect() } @@ -273,7 +289,7 @@ impl UnixShell for Fish { // > "$XDG_CONFIG_HOME/fish/conf.d" (or "~/.config/fish/conf.d" if that variable is unset) for the user // from - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { let p0 = process.var("XDG_CONFIG_HOME").ok().map(|p| { let mut path = PathBuf::from(p); path.push("fish/conf.d/rustup.fish"); @@ -288,9 +304,9 @@ impl UnixShell for Fish { p0.into_iter().chain(p1).collect() } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { // The first rcfile takes precedence. - match self.rcfiles(process).into_iter().next() { + match self.rc_candidates(process).into_iter().next() { Some(path) => vec![path], None => vec![], } @@ -303,10 +319,10 @@ impl UnixShell for Fish { } } - fn source_string(&self, process: &Process) -> anyhow::Result { + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { Ok(format!( r#"source "{}/env.fish""#, - self.cargo_home_str(process)? + self.env_dir_str(env_dir, home_dir)? )) } } @@ -324,7 +340,7 @@ impl UnixShell for Nu { "nushell" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { let mut paths = vec![]; if let Ok(p) = process.var("XDG_CONFIG_HOME") { @@ -340,9 +356,9 @@ impl UnixShell for Nu { paths } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { // The first rcfile in XDG_CONFIG_HOME takes precedence. - match self.rcfiles(process).into_iter().next() { + match self.rc_candidates(process).into_iter().next() { Some(path) => vec![path], None => vec![], } @@ -355,15 +371,15 @@ impl UnixShell for Nu { } } - fn source_string(&self, process: &Process) -> anyhow::Result { + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { Ok(format!( r#"source "{}/env.nu""#, - self.cargo_home_str(process)? + self.env_dir_str(env_dir, home_dir)? )) } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { - cargo_home_str_with_home("~", process) + fn home_var(&self) -> &'static str { + "~" } } @@ -379,7 +395,7 @@ impl UnixShell for Tcsh { "tcsh" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { let mut paths = vec![]; if let Some(home) = process.home_dir() { @@ -390,8 +406,8 @@ impl UnixShell for Tcsh { paths } - fn update_rcs(&self, process: &Process) -> Vec { - for f in self.rcfiles(process) { + fn rcs(&self, process: &Process) -> Vec { + for f in self.rc_candidates(process) { if f.is_file() { return vec![f]; } @@ -412,10 +428,10 @@ impl UnixShell for Tcsh { } } - fn source_string(&self, process: &Process) -> anyhow::Result { + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { Ok(format!( r#"source "{}/env.tcsh""#, - self.cargo_home_str(process)? + self.env_dir_str(env_dir, home_dir)? )) } } @@ -432,7 +448,7 @@ impl UnixShell for Pwsh { "pwsh" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { let mut paths = vec![]; let Some(mut config_dir) = process.home_dir() else { @@ -476,7 +492,7 @@ impl UnixShell for Pwsh { paths } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { let mut paths = vec![]; // Always modify the "Current User, All Hosts" profile. let Some(mut profile) = process.home_dir() else { @@ -495,8 +511,11 @@ impl UnixShell for Pwsh { } } - fn source_string(&self, process: &Process) -> anyhow::Result { - Ok(format!(r#". "{}/env.ps1""#, self.cargo_home_str(process)?)) + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { + Ok(format!( + r#". "{}/env.ps1""#, + self.env_dir_str(env_dir, home_dir)? + )) } } @@ -511,7 +530,7 @@ impl UnixShell for Xonsh { "xonsh" } - fn rcfiles(&self, process: &Process) -> Vec { + fn rc_candidates(&self, process: &Process) -> Vec { let mut paths = vec![]; if let Ok(p) = process.var("XDG_CONFIG_HOME") { @@ -532,9 +551,9 @@ impl UnixShell for Xonsh { paths } - fn update_rcs(&self, process: &Process) -> Vec { + fn rcs(&self, process: &Process) -> Vec { // The first rcfile in XDG_CONFIG_HOME takes precedence. - match self.rcfiles(process).into_iter().next() { + match self.rc_candidates(process).into_iter().next() { Some(path) => vec![path], None => vec![], } @@ -547,26 +566,29 @@ impl UnixShell for Xonsh { } } - fn source_string(&self, process: &Process) -> anyhow::Result { + fn source_string(&self, env_dir: &Path, home_dir: Option<&Path>) -> anyhow::Result { Ok(format!( r#"source "{}/env.xsh""#, - self.cargo_home_str(process)? + self.env_dir_str(env_dir, home_dir)? )) } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { - cargo_home_str_with_home("$HOME", process) + fn home_var(&self) -> &'static str { + "$HOME" } } -pub(crate) fn legacy_paths(process: &Process) -> impl Iterator + '_ { +pub(crate) fn legacy_paths<'a>( + process: &Process, + home_dir: Option<&'a Path>, +) -> impl Iterator + 'a { let zprofiles = Zsh::zdotdir(process) .into_iter() - .chain(process.home_dir()) - .map(|d| d.join(".zprofile")); + .map(|dir| dir.join(".zprofile")) + .chain(home_dir.map(|dir| dir.join(".zprofile"))); let profiles = [".bash_profile", ".profile"] - .iter() - .filter_map(|rc| process.home_dir().map(|d| d.join(rc))); + .into_iter() + .filter_map(move |rc| home_dir.map(|dir| dir.join(rc))); profiles.chain(zprofiles) } diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 5da3d78ba0..f9378261a4 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -15,7 +15,7 @@ use crate::{process::Process, utils}; // If the user is trying to install with sudo, on some systems this will // result in writing root-owned files to the user's home directory, because // sudo is configured not to change $HOME. Don't let that bogosity happen. -pub(crate) fn do_anti_sudo_check( +pub(crate) fn anti_sudo_check( no_prompt: bool, process: &Process, ) -> anyhow::Result { @@ -53,36 +53,26 @@ pub(crate) fn do_anti_sudo_check( Ok(utils::ExitCode(0)) } -pub(crate) fn do_remove_from_path(process: &Process) -> anyhow::Result<()> { +pub(crate) fn remove_from_path(process: &Process) -> anyhow::Result<()> { + let cargo_home = process.cargo_home()?; + let home_dir = process.home_dir(); for sh in shell::get_available_shells(process) { - let source_bytes = format!("{}\n", sh.source_string(process)?).into_bytes(); - + let source_cmd = sh.source_string(&cargo_home, home_dir.as_deref())?; // Check more files for cleanup than normally are updated. - for rc in sh.rcfiles(process).iter().filter(|rc| rc.is_file()) { - let file = utils::read_file("rcfile", rc)?; - let file_bytes = file.into_bytes(); - // FIXME: This is whitespace sensitive where it should not be. - if let Some(idx) = find_exact_line(&file_bytes, &source_bytes) { - // Here we rewrite the file without the offending line. - let mut new_bytes = file_bytes[..idx].to_vec(); - new_bytes.extend(&file_bytes[idx + source_bytes.len()..]); - let new_file = String::from_utf8(new_bytes).unwrap(); - utils::write_file("rcfile", rc, &new_file)?; - } - } + remove_source_command(&source_cmd, &sh.rc_candidates(process))?; } - remove_legacy_paths(process)?; - - Ok(()) + remove_legacy_paths(process, &cargo_home, home_dir.as_deref()) } -pub(crate) fn do_add_to_path(process: &Process) -> anyhow::Result<()> { +pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { + let cargo_home = process.cargo_home()?; + let home_dir = process.home_dir(); for sh in shell::get_available_shells(process) { - let source_cmd = sh.source_string(process)?; + let source_cmd = sh.source_string(&cargo_home, home_dir.as_deref())?; let source_cmd_with_newline = format!("\n{source_cmd}"); - for rc in sh.update_rcs(process) { + for rc in sh.rcs(process) { let cmd_to_write = match utils::read_file("rcfile", &rc) { Ok(contents) if contents.contains(&source_cmd) => continue, Ok(contents) if !contents.ends_with('\n') => &source_cmd_with_newline, @@ -101,19 +91,22 @@ pub(crate) fn do_add_to_path(process: &Process) -> anyhow::Result<()> { } } - remove_legacy_paths(process)?; + remove_legacy_paths(process, &cargo_home, home_dir.as_deref())?; Ok(()) } -pub(crate) fn do_write_env_files(process: &Process) -> anyhow::Result<()> { +pub(crate) fn write_env_files(process: &Process) -> anyhow::Result<()> { + let cargo_home = process.cargo_home()?; + let bin_dir = cargo_home.join("bin"); + let home_dir = process.home_dir(); let mut written = vec![]; for sh in shell::get_available_shells(process) { let script = sh.env_script(); // Only write each possible script once. if !written.contains(&script) { - sh.write_script(&script, process)?; + sh.write_script(&script, &cargo_home, &bin_dir, home_dir.as_deref())?; written.push(script); } } @@ -140,23 +133,27 @@ pub(crate) fn run_update(setup_path: &Path, _process: &Process) -> anyhow::Resul /// `$CARGO_HOME/bin/rustup` with the running exe, and updates the /// links to it. pub(crate) fn self_replace(process: &Process) -> anyhow::Result { - install_bins(process)?; + install_bins( + &process.cargo_home()?.join("bin"), + super::force_hard_links(process), + )?; Ok(utils::ExitCode(0)) } -fn remove_legacy_source_command(source_cmd: String, process: &Process) -> anyhow::Result<()> { - let cmd_bytes = source_cmd.into_bytes(); - for rc in shell::legacy_paths(process).filter(|rc| rc.is_file()) { - let file = utils::read_file("rcfile", &rc)?; +/// Removes the first exact line matching `command` followed by a newline from each existing rcfile. +fn remove_source_command(command: &str, rcfiles: &[PathBuf]) -> anyhow::Result<()> { + let command_bytes = format!("{command}\n").into_bytes(); + for rc in rcfiles.iter().filter(|rc| rc.is_file()) { + let file = utils::read_file("rcfile", rc)?; let file_bytes = file.into_bytes(); // FIXME: This is whitespace sensitive where it should not be. - if let Some(idx) = find_exact_line(&file_bytes, &cmd_bytes) { + if let Some(idx) = find_exact_line(&file_bytes, &command_bytes) { // Here we rewrite the file without the offending line. let mut new_bytes = file_bytes[..idx].to_vec(); - new_bytes.extend(&file_bytes[idx + cmd_bytes.len()..]); + new_bytes.extend(&file_bytes[idx + command_bytes.len()..]); let new_file = String::from_utf8(new_bytes).unwrap(); - utils::write_file("rcfile", &rc, &new_file)?; + utils::write_file("rcfile", rc, &new_file)?; } } Ok(()) @@ -172,24 +169,21 @@ fn find_exact_line(file: &[u8], line: &[u8]) -> Option { }) } -fn remove_legacy_paths(process: &Process) -> anyhow::Result<()> { +fn remove_legacy_paths( + process: &Process, + cargo_home: &Path, + home_dir: Option<&Path>, +) -> anyhow::Result<()> { + let cargo_home = Posix.env_dir_str(cargo_home, home_dir)?; + let rcfiles = shell::legacy_paths(process, home_dir).collect::>(); // Before the work to support more kinds of shells, which was released in // version 1.23.0 of Rustup, we always inserted this line instead, which is // now considered legacy - remove_legacy_source_command( - format!( - "export PATH=\"{}/bin:$PATH\"\n", - Posix.cargo_home_str(process)? - ), - process, - )?; + remove_source_command(&format!("export PATH=\"{cargo_home}/bin:$PATH\""), &rcfiles)?; // Unfortunately in 1.23, we accidentally used `source` rather than `.` // which, while widely supported, isn't actually POSIX, so we also // clean that up here. This issue was filed as #2623. - remove_legacy_source_command( - format!("source \"{}/env\"\n", Posix.cargo_home_str(process)?), - process, - )?; + remove_source_command(&format!("source \"{cargo_home}/env\""), &rcfiles)?; Ok(()) } diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 0aff01b33c..bdd40e1505 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -126,7 +126,7 @@ pub(super) async fn maybe_install_msvc( opts: &InstallOpts<'_>, process: &Process, ) -> anyhow::Result<()> { - let Some(plan) = do_msvc_check(opts, process) else { + let Some(plan) = msvc_check(opts, process) else { return Ok(()); }; @@ -211,7 +211,7 @@ pub(crate) enum VsInstallPlan { // Provide guidance about setting up MSVC if it doesn't appear to be // installed -pub(crate) fn do_msvc_check(opts: &InstallOpts<'_>, process: &Process) -> Option { +pub(crate) fn msvc_check(opts: &InstallOpts<'_>, process: &Process) -> Option { // Test suite skips this since it's env dependent if process.var("RUSTUP_INIT_SKIP_MSVC_CHECK").is_ok() { return None; @@ -354,7 +354,7 @@ pub(crate) async fn try_install_msvc( // It's possible that the installer returned a non-zero exit code // even though the required components were successfully installed. // In that case we warn about the error but continue on. - let have_msvc = do_msvc_check(opts, process).is_none(); + let have_msvc = msvc_check(opts, process).is_none(); let has_libs = has_windows_sdk_libs(process); if have_msvc && has_libs { warn!("Visual Studio is installed but a problem occurred during installation"); @@ -465,7 +465,7 @@ pub(crate) fn wait_for_parent() -> anyhow::Result<()> { Ok(()) } -pub(crate) fn do_add_to_path(process: &Process) -> anyhow::Result<()> { +pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { let new_path = _with_path_cargo_home_bin(_add_to_path, process)?; _apply_new_path(new_path, process) } @@ -570,7 +570,7 @@ where Ok(windows_path.and_then(|old_path| f(old_path, HSTRING::from(path_str.as_path())))) } -pub(crate) fn do_remove_from_path(process: &Process) -> anyhow::Result<()> { +pub(crate) fn remove_from_path(process: &Process) -> anyhow::Result<()> { let new_path = _with_path_cargo_home_bin(_remove_from_path, process)?; _apply_new_path(new_path, process) } @@ -672,7 +672,10 @@ pub(crate) fn run_update(setup_path: &Path, process: &Process) -> anyhow::Result pub(crate) fn self_replace(process: &Process) -> anyhow::Result { wait_for_parent()?; - install_bins(process)?; + install_bins( + &process.cargo_home()?.join("bin"), + super::force_hard_links(process), + )?; Ok(utils::ExitCode(0)) } @@ -706,9 +709,7 @@ pub(crate) fn self_replace(process: &Process) -> anyhow::Result // // .. augmented with this SO answer // https://stackoverflow.com/questions/10319526/understanding-a-self-deleting-program-in-c -pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, process: &Process) -> anyhow::Result<()> { - // CARGO_HOME, hopefully empty except for bin/rustup.exe - let cargo_home = process.cargo_home()?; +pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, cargo_home: &Path) -> anyhow::Result<()> { // The rustup.exe bin let rustup_path = cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")); diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 07e84e98da..90bd1292b2 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -13,12 +13,19 @@ fn run_input(config: &Config, args: &[&str], input: &str) -> Assert { run_input_with_env(config, args, input, &[]) } -fn run_input_with_env(config: &Config, args: &[&str], input: &str, env: &[(&str, &str)]) -> Assert { +fn run_input_with_env( + config: &Config, + args: &[&str], + input: &str, + env: &[(&str, Option<&str>)], +) -> Assert { let mut cmd = config.cmd(args[0], &args[1..]); - config.env(&mut cmd); for (key, value) in env.iter() { - cmd.env(key, value); + match value { + Some(value) => cmd.env(key, value), + None => cmd.env_remove(key), + }; } cmd.stdin(Stdio::piped()); @@ -54,14 +61,27 @@ async fn update() { // test for the install case. #[tokio::test] async fn smoke_case_install_no_modify_path() { - let cx = CliTestContext::new(Scenario::SimpleV2).await; + let mut cx = CliTestContext::new(Scenario::SimpleV2).await; + // Keep displayed paths short and shell detection independent of the host. + cx.config.cargodir = cx.config.homedir.join(".cargo"); // During an interactive session, after "Press the Enter // key..." the UI emits a blank line, then there is a blank // line that comes from the user pressing enter, then log // output on stderr, then an explicit blank line on stdout // before printing $toolchain installed - run_input(&cx.config, &["rustup-init", "--no-modify-path"], "\n\n") - .with_stdout(snapbox::str![[r#" + run_input_with_env( + &cx.config, + &["rustup-init", "--no-modify-path"], + "\n\n", + &[ + ("PATH", Some(cx.config.exedir.to_str().unwrap())), + ("SHELL", Some("/bin/sh")), + // HACK: current XONSH detection is done via `process.var("XONSHRC").is_ok()`, + // setting this as none prevents unwanted modification + ("XONSHRC", None), + ], + ) + .with_stdout(snapbox::str![[r#" ... This path needs to be in your PATH environment variable, but will not be added automatically. @@ -88,7 +108,34 @@ Current installation options: Rust is installed now. Great! ... "#]]) - .is_ok(); + .with_stdout(cfg_select! { + windows => snapbox::str![[r#" +... +Rust is installed now. Great! + +To get started you need Cargo's bin directory (%USERPROFILE%/.cargo/bin) in[..] +your PATH +environment variable. This has not been done automatically. + +Press the Enter key to continue. + +"#]], + _ => snapbox::str![[r#" +... +Rust is installed now. Great! + +To get started you need Cargo's bin directory ($HOME/.cargo/bin) in your PATH +environment variable. This has not been done automatically. + +To configure your current shell, you need to source the +corresponding env file under $HOME/.cargo. + +Consider running the right command for your shell (note the leading DOT): +. "$HOME/.cargo/env" # For sh/ash/dash/pdksh +... +"#]], + }) + .is_ok(); if cfg!(unix) { assert!(!cx.config.homedir.join(".profile").exists()); assert!(cx.config.cargodir.join("env").exists()); @@ -97,11 +144,51 @@ Rust is installed now. Great! #[tokio::test] async fn smoke_case_install_with_path_install() { - let cx = CliTestContext::new(Scenario::SimpleV2).await; + let mut cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config.cargodir = cx.config.homedir.join(".cargo"); - run_input(&cx.config, &["rustup-init"], "\n\n") - .is_ok() - .without_stdout("This path needs to be in your PATH environment variable"); + run_input_with_env( + &cx.config, + &["rustup-init"], + "\n\n", + &[ + ("PATH", Some(cx.config.exedir.to_str().unwrap())), + ("SHELL", Some("/bin/sh")), + // HACK: current XONSH detection is done via `process.var("XONSHRC").is_ok()`, + // setting this as none prevents unwanted modification + ("XONSHRC", None), + ], + ) + .is_ok() + .without_stdout("This path needs to be in your PATH environment variable") + .with_stdout(cfg_select! { + windows => snapbox::str![[r#" +... +Rust is installed now. Great! + +To get started you may need to restart your current shell. +This would reload your PATH environment variable to include +Cargo's bin directory (%USERPROFILE%/.cargo/bin). + +Press the Enter key to continue. + +"#]], + _ => snapbox::str![[r#" +... +Rust is installed now. Great! + +To get started you may need to restart your current shell. +This would reload your PATH environment variable to include +Cargo's bin directory ($HOME/.cargo/bin). + +To configure your current shell, you need to source the +corresponding env file under $HOME/.cargo. + +Consider running the right command for your shell (note the leading DOT): +. "$HOME/.cargo/env" # For sh/ash/dash/pdksh +... +"#]], + }); } #[tokio::test]