From 1d5a9d1befcf3b259156f26d5ebd94ea277c9f66 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:33:22 +0800 Subject: [PATCH 1/8] refactor(self-update): clarify helper names --- src/cli/self_update.rs | 17 +++++----- src/cli/self_update/shell.rs | 58 ++++++++++++++++++---------------- src/cli/self_update/unix.rs | 12 +++---- src/cli/self_update/windows.rs | 10 +++--- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 2f8b8ca69a..755e1fac98 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -83,7 +83,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 +95,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 +146,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(); @@ -246,10 +245,10 @@ impl InstallOpts<'_> { install_bins(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)] @@ -691,11 +690,11 @@ fn pre_install_msg(no_modify_path: bool, process: &Process) -> anyhow::Result>(); let plural = if rcfiles.len() > 1 { "s" } else { "" }; @@ -1065,7 +1064,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(()) => {} } diff --git a/src/cli/self_update/shell.rs b/src/cli/self_update/shell.rs index 4e3e062923..38b037d5ec 100644 --- a/src/cli/self_update/shell.rs +++ b/src/cli/self_update/shell.rs @@ -113,10 +113,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 { @@ -159,17 +161,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 +179,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 +195,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 +237,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 +275,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 +290,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![], } @@ -324,7 +326,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 +342,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![], } @@ -379,7 +381,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 +392,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]; } @@ -432,7 +434,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 +478,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 { @@ -511,7 +513,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 +534,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![], } diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 5da3d78ba0..2ac6ddabcc 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,12 +53,12 @@ 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<()> { for sh in shell::get_available_shells(process) { let source_bytes = format!("{}\n", sh.source_string(process)?).into_bytes(); // Check more files for cleanup than normally are updated. - for rc in sh.rcfiles(process).iter().filter(|rc| rc.is_file()) { + for rc in sh.rc_candidates(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. @@ -77,12 +77,12 @@ pub(crate) fn do_remove_from_path(process: &Process) -> anyhow::Result<()> { Ok(()) } -pub(crate) fn do_add_to_path(process: &Process) -> anyhow::Result<()> { +pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { for sh in shell::get_available_shells(process) { let source_cmd = sh.source_string(process)?; 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, @@ -106,7 +106,7 @@ pub(crate) fn do_add_to_path(process: &Process) -> anyhow::Result<()> { Ok(()) } -pub(crate) fn do_write_env_files(process: &Process) -> anyhow::Result<()> { +pub(crate) fn write_env_files(process: &Process) -> anyhow::Result<()> { let mut written = vec![]; for sh in shell::get_available_shells(process) { diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 0aff01b33c..230e267c9a 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) } From df487bb626f94986d174238882b6e62191c1e5d5 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:33:32 +0800 Subject: [PATCH 2/8] refactor(shell): simplify shell representations --- src/cli/self_update/shell.rs | 54 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/cli/self_update/shell.rs b/src/cli/self_update/shell.rs index 38b037d5ec..3e2aaf9421 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::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,7 +37,7 @@ pub(crate) struct ShellScript { } // TODO: Update into a bytestring. -fn cargo_home_str_with_home(home: &str, process: &Process) -> anyhow::Result> { +fn cargo_home_str_with_home(home: &str, process: &Process) -> anyhow::Result { let path = process.cargo_home()?; let default_cargo_home = process @@ -47,31 +45,15 @@ fn cargo_home_str_with_home(home: &str, process: &Process) -> anyhow::Result 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`). @@ -97,10 +79,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 { @@ -128,7 +124,7 @@ pub(crate) trait UnixShell { } } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { + fn cargo_home_str(&self, process: &Process) -> anyhow::Result { #[cfg(windows)] let home = "%USERPROFILE%"; #[cfg(not(windows))] @@ -364,7 +360,7 @@ impl UnixShell for Nu { )) } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { + fn cargo_home_str(&self, process: &Process) -> anyhow::Result { cargo_home_str_with_home("~", process) } } @@ -556,7 +552,7 @@ impl UnixShell for Xonsh { )) } - fn cargo_home_str(&self, process: &Process) -> anyhow::Result> { + fn cargo_home_str(&self, process: &Process) -> anyhow::Result { cargo_home_str_with_home("$HOME", process) } } From deb1edcf476b45f2b3a364d3bbcf7d2b02e4ae62 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:34:52 +0800 Subject: [PATCH 3/8] refactor(install): accept explicit installation paths Pass bin directories to binary, proxy, and updater helpers, and pass the resolved Cargo home to Windows uninstall GC. Give shell script helpers separate env and bin directory inputs while preserving legacy home-variable spelling. --- src/cli/proxy_mode.rs | 2 +- src/cli/rustup_mode.rs | 2 +- src/cli/self_update.rs | 52 +++++++++----------- src/cli/self_update/shell.rs | 87 +++++++++++++++++++++------------- src/cli/self_update/unix.rs | 26 +++++++--- src/cli/self_update/windows.rs | 9 ++-- 6 files changed, 104 insertions(+), 74 deletions(-) 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 755e1fac98..38fa209062 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -242,7 +242,8 @@ 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::write_env_files(process)?; @@ -287,7 +288,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())?; @@ -771,12 +772,11 @@ fn warn_if_default_linker_missing(process: &Process) { } } -fn install_bins(process: &Process) -> 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() { @@ -784,22 +784,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)?; @@ -893,7 +893,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); } } @@ -901,12 +901,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 @@ -991,7 +989,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"); @@ -1372,9 +1370,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)?; @@ -1393,7 +1390,7 @@ mod tests { dist::{PartialToolchainDesc, Profile}, for_host, process::TestProcess, - test::{Env, test_dir, with_rustup_home}, + test::{test_dir, with_rustup_home}, }; #[test] @@ -1438,10 +1435,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/shell.rs b/src/cli/self_update/shell.rs index 3e2aaf9421..470612bc4b 100644 --- a/src/cli/self_update/shell.rs +++ b/src/cli/self_update/shell.rs @@ -23,7 +23,7 @@ //! 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::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::bail; @@ -37,15 +37,17 @@ 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 { - 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(), @@ -58,9 +60,13 @@ fn cargo_home_str_with_home(home: &str, process: &Process) -> anyhow::Result String { + let Ok(cargo_home) = process.cargo_home() else { + return String::new(); + }; + let home_dir = process.home_dir(); 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(&cargo_home, home_dir.as_deref()) else { continue; }; if let Some(names) = groups @@ -124,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(()) @@ -301,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)? )) } } @@ -353,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 { + "~" } } @@ -410,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)? )) } } @@ -493,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)? + )) } } @@ -545,15 +566,15 @@ 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" } } diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 2ac6ddabcc..365ade57c6 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -54,8 +54,11 @@ pub(crate) fn anti_sudo_check( } 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_bytes = + format!("{}\n", sh.source_string(&cargo_home, home_dir.as_deref())?).into_bytes(); // Check more files for cleanup than normally are updated. for rc in sh.rc_candidates(process).iter().filter(|rc| rc.is_file()) { @@ -78,8 +81,10 @@ pub(crate) fn remove_from_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.rcs(process) { @@ -107,13 +112,16 @@ pub(crate) fn add_to_path(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,7 +148,10 @@ 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)) } @@ -179,7 +190,7 @@ fn remove_legacy_paths(process: &Process) -> anyhow::Result<()> { remove_legacy_source_command( format!( "export PATH=\"{}/bin:$PATH\"\n", - Posix.cargo_home_str(process)? + Posix.env_dir_str(&process.cargo_home()?, process.home_dir().as_deref())? ), process, )?; @@ -187,7 +198,10 @@ fn remove_legacy_paths(process: &Process) -> anyhow::Result<()> { // 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)?), + format!( + "source \"{}/env\"\n", + Posix.env_dir_str(&process.cargo_home()?, process.home_dir().as_deref())? + ), process, )?; diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 230e267c9a..bdd40e1505 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -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}")); From 9c4f6cb4d8db299f21487765b3e0b7db89d5ab28 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:35:49 +0800 Subject: [PATCH 4/8] refactor(shell): reuse resolved paths during setup and cleanup Pass resolved env and user-home paths into source-line rendering. Share exact source-line removal and reuse the current operation paths and legacy rcfile candidates during cleanup. --- src/cli/self_update.rs | 6 +++- src/cli/self_update/shell.rs | 25 ++++++++------- src/cli/self_update/unix.rs | 62 ++++++++++++------------------------ 3 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 38fa209062..54afc1b676 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -204,7 +204,11 @@ impl InstallOpts<'_> { format!(post_install_msg_win!(), cargo_home = cargo_home) }; #[cfg(not(windows))] - let source_env_lines = shell::build_source_env_lines(process); + let source_env_lines = { + let env_dir = process.cargo_home()?; + let home_dir = process.home_dir(); + shell::build_source_env_lines(process, &env_dir, home_dir.as_deref()) + }; #[cfg(not(windows))] let msg = if no_modify_path { format!( diff --git a/src/cli/self_update/shell.rs b/src/cli/self_update/shell.rs index 470612bc4b..df5cf1b511 100644 --- a/src/cli/self_update/shell.rs +++ b/src/cli/self_update/shell.rs @@ -59,14 +59,14 @@ fn path_str_with_home( /// 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 { - let Ok(cargo_home) = process.cargo_home() else { - return String::new(); - }; - let home_dir = process.home_dir(); +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(&cargo_home, home_dir.as_deref()) else { + let Ok(src) = shell.source_string(env_dir, home_dir) else { continue; }; if let Some(names) = groups @@ -578,14 +578,17 @@ impl UnixShell for Xonsh { } } -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 365ade57c6..f9378261a4 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -57,27 +57,12 @@ 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(&cargo_home, home_dir.as_deref())?).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.rc_candidates(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 add_to_path(process: &Process) -> anyhow::Result<()> { @@ -106,7 +91,7 @@ pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { } } - remove_legacy_paths(process)?; + remove_legacy_paths(process, &cargo_home, home_dir.as_deref())?; Ok(()) } @@ -156,18 +141,19 @@ pub(crate) fn self_replace(process: &Process) -> anyhow::Result 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(()) @@ -183,27 +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.env_dir_str(&process.cargo_home()?, process.home_dir().as_deref())? - ), - 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.env_dir_str(&process.cargo_home()?, process.home_dir().as_deref())? - ), - process, - )?; + remove_source_command(&format!("source \"{cargo_home}/env\""), &rcfiles)?; Ok(()) } From 892cac775f0014fdbbf59e7dcdf8af435c10cedb Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:51:55 +0800 Subject: [PATCH 5/8] test(cli): remove redundant environment setup --- tests/suite/cli_inst_interactive.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 07e84e98da..290c5cacd0 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -15,7 +15,6 @@ fn run_input(config: &Config, args: &[&str], input: &str) -> Assert { fn run_input_with_env(config: &Config, args: &[&str], input: &str, env: &[(&str, &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); From d5d78636f556d836e532c8377c021d5f3be91ad0 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:01:12 +0800 Subject: [PATCH 6/8] test(install): cover post-install messages --- tests/suite/cli_inst_interactive.rs | 108 +++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 290c5cacd0..90bd1292b2 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -13,11 +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..]); 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()); @@ -53,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. @@ -87,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()); @@ -96,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] From 6a35093201bc7778c8b6f3e8eba63c4992ff37cd Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:36:37 +0800 Subject: [PATCH 7/8] refactor(install): unify message path inputs across platforms Keep canonical_cargo_home and accept resolved paths without renaming it. Pass complete bin and env directory text to the templates, share post-install wording across platforms, and reuse the resolved homes for source commands. Keep display formatting separate from filesystem paths and use native separators in the uninstall message. --- src/cli/self_update.rs | 70 +++++++++++++++----------------------- src/cli/self_update/msg.rs | 48 +++++++------------------- 2 files changed, 39 insertions(+), 79 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 54afc1b676..792faf77a7 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -191,39 +191,29 @@ 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 env_dir = canonical_cargo_home(&cargo_home, home_dir.as_deref()); + let cargo_bin_dir = format!("{env_dir}{MAIN_SEPARATOR}bin"); 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 = cargo_bin_dir ) } else { - format!(post_install_msg_win!(), cargo_home = cargo_home) - }; - #[cfg(not(windows))] - let source_env_lines = { - let env_dir = process.cargo_home()?; - let home_dir = process.home_dir(); - shell::build_source_env_lines(process, &env_dir, home_dir.as_deref()) + format!(post_install_msg!(), cargo_bin_dir = cargo_bin_dir) }; + md(&mut term, msg); #[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 { + md( + &mut term, format!( post_install_msg_unix!(), - cargo_home = cargo_home, - source_env_lines = source_env_lines, - ) - }; - md(&mut term, msg); + env_dir = env_dir, + source_env_lines = + shell::build_source_env_lines(process, &cargo_home, home_dir.as_deref()), + ), + ); #[cfg(unix)] warn_if_default_linker_missing(process); @@ -598,21 +588,16 @@ fn update_root(process: &Process) -> String { /// `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 { +fn canonical_cargo_home(cargo_home: &Path, home_dir: Option<&Path>) -> Cow<'static, str> { + let default_cargo_home = home_dir.unwrap_or_else(|| Path::new(".")).join(".cargo"); + if default_cargo_home == cargo_home { cfg_select! { windows => r"%USERPROFILE%\.cargo".into(), _ => "$HOME/.cargo".into(), } } else { - path.to_string_lossy().into_owned().into() - }) + cargo_home.to_string_lossy().into_owned().into() + } } fn rustc_or_cargo_exists_in_path(process: &Process) -> anyhow::Result<()> { @@ -691,7 +676,7 @@ 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 { @@ -707,7 +692,7 @@ fn pre_install_msg(no_modify_path: bool, process: &Process) -> anyhow::Result anyhow::Result { 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. " }; From faa3ae16871e2a1d7a43f38e08cc7f0e7f754430 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:18:21 +0800 Subject: [PATCH 8/8] refactor(install): replace canonical_cargo_home with HomeDisplay --- src/cli/self_update.rs | 105 +++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 19 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 792faf77a7..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}, @@ -193,15 +194,16 @@ impl InstallOpts<'_> { let cargo_home = process.cargo_home()?; let home_dir = process.home_dir(); - let env_dir = canonical_cargo_home(&cargo_home, home_dir.as_deref()); - let cargo_bin_dir = format!("{env_dir}{MAIN_SEPARATOR}bin"); let msg = if no_modify_path { format!( post_install_msg_no_modify_path!(), - cargo_bin_dir = cargo_bin_dir + cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), ) } else { - format!(post_install_msg!(), cargo_bin_dir = cargo_bin_dir) + format!( + post_install_msg!(), + cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), + ) }; md(&mut term, msg); #[cfg(not(windows))] @@ -209,7 +211,7 @@ impl InstallOpts<'_> { &mut term, format!( post_install_msg_unix!(), - env_dir = env_dir, + 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()), ), @@ -586,17 +588,39 @@ 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(cargo_home: &Path, home_dir: Option<&Path>) -> Cow<'static, str> { - let default_cargo_home = home_dir.unwrap_or_else(|| Path::new(".")).join(".cargo"); - if default_cargo_home == cargo_home { - 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 { - cargo_home.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) } } @@ -945,8 +969,8 @@ pub(crate) fn uninstall( let msg = if no_modify_path { pre_uninstall_msg_no_modify_path!().to_owned() } else { - let cargo_home = canonical_cargo_home(&cargo_home, process.home_dir().as_deref()); - let cargo_bin_dir = format!("{cargo_home}{MAIN_SEPARATOR}bin"); + 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); @@ -1370,8 +1394,9 @@ pub(crate) fn cleanup_self_updater(bin_path: &Path) -> 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, @@ -1381,6 +1406,48 @@ mod tests { 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| {