From acc938f4956eda009e869eef5a9bdd77dcda9da0 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:22:34 +0800 Subject: [PATCH 1/9] chore: re-organize imports --- src/cli/self_update/unix.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 05c598f551..a715d6e24a 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -4,8 +4,10 @@ use std::process::Command; use anyhow::{Context, Result, bail}; use tracing::{error, warn}; -use super::install_bins; -use super::shell::{self, Posix, UnixShell}; +use super::{ + install_bins, + shell::{self, Posix, UnixShell}, +}; use crate::process::Process; use crate::utils; From 14fb229ef2904e79ebe10acf4027b5d67894080a Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Sat, 23 May 2026 02:24:18 +0800 Subject: [PATCH 2/9] refactor: rename delete_rustup_and_cargo_home to clean_cargo_bin --- src/cli/self_update.rs | 6 +++--- src/cli/self_update/unix.rs | 2 +- src/cli/self_update/windows.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 564fe080d8..7f85023463 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -80,7 +80,7 @@ mod shell; #[cfg(unix)] mod unix; #[cfg(unix)] -use unix::{delete_rustup_and_cargo_home, do_add_to_path, do_remove_from_path}; +use unix::{clean_cargo_bin, do_add_to_path, do_remove_from_path}; #[cfg(unix)] pub(crate) use unix::{run_update, self_replace}; @@ -91,7 +91,7 @@ pub use windows::complete_windows_uninstall; #[cfg(all(windows, feature = "test"))] pub use windows::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; #[cfg(windows)] -use windows::{delete_rustup_and_cargo_home, do_add_to_path, do_remove_from_path}; +use windows::{clean_cargo_bin, do_add_to_path, do_remove_from_path}; #[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; @@ -1151,7 +1151,7 @@ pub(crate) fn uninstall( // Delete rustup. This is tricky because this is *probably* // the running executable and on Windows can't be unlinked until // the process exits. - delete_rustup_and_cargo_home(process)?; + clean_cargo_bin(process)?; info!("rustup is uninstalled"); diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index a715d6e24a..507de60eb0 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -49,7 +49,7 @@ pub(crate) fn do_anti_sudo_check(no_prompt: bool, process: &Process) -> Result Result<()> { +pub(crate) fn clean_cargo_bin(process: &Process) -> Result<()> { let cargo_home = process.cargo_home()?; utils::remove_dir("cargo_home", &cargo_home) } diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 07578e3ec1..417371e2be 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -674,7 +674,7 @@ pub(crate) fn self_replace(process: &Process) -> Result { // // .. augmented with this SO answer // https://stackoverflow.com/questions/10319526/understanding-a-self-deleting-program-in-c -pub(crate) fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> { +pub(crate) fn clean_cargo_bin(process: &Process) -> Result<()> { use std::io; use std::ptr; use std::thread; From fa2f7ed8bd081db71df73f9841352676c323d667 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Sun, 31 May 2026 01:02:44 +0800 Subject: [PATCH 3/9] refactor: move PATH cleanup into clean_cargo_bin --- src/cli/self_update.rs | 7 +------ src/cli/self_update/unix.rs | 6 +++++- src/cli/self_update/windows.rs | 6 +++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 7f85023463..2dad4eb8ac 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -1092,11 +1092,6 @@ pub(crate) fn uninstall( info!("removing cargo home"); - // Remove CARGO_HOME/bin from PATH - if !no_modify_path { - do_remove_from_path(process)?; - } - // Delete everything in CARGO_HOME *except* the rustup bin // First everything except the bin directory @@ -1151,7 +1146,7 @@ pub(crate) fn uninstall( // Delete rustup. This is tricky because this is *probably* // the running executable and on Windows can't be unlinked until // the process exits. - clean_cargo_bin(process)?; + clean_cargo_bin(no_modify_path, process)?; info!("rustup is uninstalled"); diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 507de60eb0..67d58a0343 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -49,8 +49,12 @@ pub(crate) fn do_anti_sudo_check(no_prompt: bool, process: &Process) -> Result Result<()> { +pub(crate) fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result<()> { let cargo_home = process.cargo_home()?; + if !no_modify_path { + do_remove_from_path(process)?; + } + utils::remove_dir("cargo_home", &cargo_home) } diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 417371e2be..ae076866e8 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -674,7 +674,7 @@ pub(crate) fn self_replace(process: &Process) -> Result { // // .. augmented with this SO answer // https://stackoverflow.com/questions/10319526/understanding-a-self-deleting-program-in-c -pub(crate) fn clean_cargo_bin(process: &Process) -> Result<()> { +pub(crate) fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result<()> { use std::io; use std::ptr; use std::thread; @@ -687,6 +687,10 @@ pub(crate) fn clean_cargo_bin(process: &Process) -> Result<()> { // CARGO_HOME, hopefully empty except for bin/rustup.exe let cargo_home = process.cargo_home()?; + if !no_modify_path { + do_remove_from_path(process)?; + } + // The rustup.exe bin let rustup_path = cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")); From d756beaf828d237bac8cab7f52d8a3b5e6c4de41 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:56:06 +0800 Subject: [PATCH 4/9] refactor: share cargo bin cleanup helper --- src/cli/self_update.rs | 43 ++++++++++++++++++++++++++++++++++--- src/cli/self_update/unix.rs | 9 -------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 2dad4eb8ac..a42335c09b 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -80,7 +80,7 @@ mod shell; #[cfg(unix)] mod unix; #[cfg(unix)] -use unix::{clean_cargo_bin, do_add_to_path, do_remove_from_path}; +use unix::{do_add_to_path, do_remove_from_path}; #[cfg(unix)] pub(crate) use unix::{run_update, self_replace}; @@ -88,11 +88,11 @@ pub(crate) use unix::{run_update, self_replace}; mod windows; #[cfg(windows)] pub use windows::complete_windows_uninstall; +#[cfg(windows)] +use windows::do_add_to_path; #[cfg(all(windows, feature = "test"))] pub use windows::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; #[cfg(windows)] -use windows::{clean_cargo_bin, do_add_to_path, do_remove_from_path}; -#[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; pub(crate) struct InstallOpts<'a> { @@ -1146,6 +1146,9 @@ pub(crate) fn uninstall( // Delete rustup. This is tricky because this is *probably* // the running executable and on Windows can't be unlinked until // the process exits. + #[cfg(windows)] + windows::clean_cargo_bin(no_modify_path, process)?; + #[cfg(unix)] clean_cargo_bin(no_modify_path, process)?; info!("rustup is uninstalled"); @@ -1153,6 +1156,40 @@ pub(crate) fn uninstall( Ok(ExitCode::SUCCESS) } +/// Remove the `$CARGO_HOME/bin` directory if it's empty. +/// On success, remove it from `$PATH` unless `no_modify_path` is set. +/// If the directory is not empty, emit a warning and return success. +#[cfg(unix)] +fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result<()> { + // Remove rustup binary + let cargo_bin_path = process.cargo_home()?.join("bin"); + let rustup_path = cargo_bin_path.join(format!("rustup{EXE_SUFFIX}")); + + utils::remove_file("rustup_bin", &rustup_path)?; + + // Remove $CARGO_HOME/bin + let cargo_bin_path_display = cargo_bin_path.display(); + info!("removing empty cargo bin directory `{cargo_bin_path_display}`"); + + let Err(e) = fs::remove_dir(&cargo_bin_path) else { + if !no_modify_path { + info!("removing cargo bin directory `{cargo_bin_path_display}` from $PATH"); + do_remove_from_path(process)?; + } + + return Ok(()); + }; + + if e.kind() == io::ErrorKind::DirectoryNotEmpty { + warn!("keeping non-empty cargo bin directory `{cargo_bin_path_display}`"); + + return Ok(()); + } + + Err(e) + .with_context(|| format!("failed to remove cargo bin directory `{cargo_bin_path_display}`")) +} + #[derive(Clone, Copy, Debug)] pub(crate) enum SelfUpdatePermission { HardFail, diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index 67d58a0343..16773359f6 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -49,15 +49,6 @@ pub(crate) fn do_anti_sudo_check(no_prompt: bool, process: &Process) -> Result Result<()> { - let cargo_home = process.cargo_home()?; - if !no_modify_path { - do_remove_from_path(process)?; - } - - utils::remove_dir("cargo_home", &cargo_home) -} - pub(crate) fn do_remove_from_path(process: &Process) -> Result<()> { for sh in shell::get_available_shells(process) { let source_bytes = format!("{}\n", sh.source_string(process)?).into_bytes(); From 5e3c9de90ace76585052546183c3d8730bdf6c02 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Sun, 31 May 2026 22:14:53 +0800 Subject: [PATCH 5/9] fix: remove rustup proxies during uninstall --- src/cli/self_update.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index a42335c09b..8a432243f1 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -47,7 +47,7 @@ use clap::ValueEnum; use clap::builder::PossibleValue; use clap_cargo::style::{GOOD, WARN}; use itertools::Itertools; -use same_file::Handle; +use same_file::{Handle, is_same_file}; use serde::{Deserialize, Serialize}; use tracing::{error, info, trace, warn}; @@ -1061,7 +1061,8 @@ pub(crate) fn uninstall( let cargo_home = process.cargo_home()?; - if !cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")).exists() { + let rustup_path = cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")); + if !rustup_path.exists() { return Err(CliError::NotSelfInstalled { p: cargo_home }.into()); } @@ -1090,6 +1091,19 @@ pub(crate) fn uninstall( utils::remove_dir("rustup_home", &rustup_dir)?; } + // Clean up rustup tool links + let cargo_bin_path = cargo_home.join("bin"); + let proxy_paths = TOOLS + .iter() + .chain(DUP_TOOLS.iter()) + .map(|tool| cargo_bin_path.join(format!("{tool}{EXE_SUFFIX}"))); + + for proxy_path in proxy_paths { + if is_same_file(&proxy_path, &rustup_path).unwrap_or(false) { + utils::remove_file("rustup tool proxy", &proxy_path)?; + } + } + info!("removing cargo home"); // Delete everything in CARGO_HOME *except* the rustup bin From cdf7906800c09ba9af47b0208ba068e0ec7d8b5d Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Sun, 31 May 2026 22:15:36 +0800 Subject: [PATCH 6/9] fix: preserve CARGO_HOME during uninstall fix: preserve CARGO_HOME during Windows uninstall --- src/cli/self_update.rs | 82 +++++++--------------------------- src/cli/self_update/windows.rs | 27 ++++++----- tests/suite/cli_self_upd.rs | 16 +++++-- 3 files changed, 44 insertions(+), 81 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 8a432243f1..3144345739 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -24,15 +24,14 @@ //! During uninstall (`rustup self uninstall`): //! //! * Delete `$RUSTUP_HOME`. -//! * Delete everything in `$CARGO_HOME`, including -//! the rustup binary and its hardlinks +//! * Delete rustup tool proxy binaries from `$CARGO_HOME`/bin. +//! * Delete `$CARGO_HOME`/bin if it is empty after uninstall. //! //! Deleting the running binary during uninstall is tricky //! and racy on Windows. use std::borrow::Cow; use std::env::{self, consts::EXE_SUFFIX}; -#[cfg(not(windows))] use std::io; use std::io::Write; use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; @@ -88,11 +87,11 @@ pub(crate) use unix::{run_update, self_replace}; mod windows; #[cfg(windows)] pub use windows::complete_windows_uninstall; -#[cfg(windows)] -use windows::do_add_to_path; #[cfg(all(windows, feature = "test"))] pub use windows::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; #[cfg(windows)] +use windows::{do_add_to_path, do_remove_from_path}; +#[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; pub(crate) struct InstallOpts<'a> { @@ -1048,6 +1047,12 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< Ok(()) } +/// Uninstall process: +/// 1. Remove rustup home. +/// 2. Clean up rustup tool proxies. +/// 3. Remove rustup binary file. +/// 4. Try to clean up $CARGO_HOME/bin if it's empty. +/// 5. Upon successfully removing $CARGO_HOME/bin, clean up $PATH. pub(crate) fn uninstall( no_prompt: bool, no_modify_path: bool, @@ -1104,76 +1109,23 @@ pub(crate) fn uninstall( } } - info!("removing cargo home"); - - // Delete everything in CARGO_HOME *except* the rustup bin - - // First everything except the bin directory - let diriter = fs::read_dir(&cargo_home).map_err(|e| CliError::ReadDirError { - p: cargo_home.clone(), - source: e, - })?; - for dirent in diriter { - let dirent = dirent.map_err(|e| CliError::ReadDirError { - p: cargo_home.clone(), - source: e, - })?; - if dirent.file_name().to_str() != Some("bin") { - if dirent.path().is_dir() { - utils::remove_dir("cargo_home", &dirent.path())?; - } else { - utils::remove_file("cargo_home", &dirent.path())?; - } - } - } - - // Then everything in bin except rustup and tools. These can't be unlinked - // until this process exits (on windows). - let tools = TOOLS - .iter() - .chain(DUP_TOOLS.iter()) - .map(|t| format!("{t}{EXE_SUFFIX}")); - let tools: Vec<_> = tools.chain(vec![format!("rustup{EXE_SUFFIX}")]).collect(); - let bin_dir = cargo_home.join("bin"); - let diriter = fs::read_dir(&bin_dir).map_err(|e| CliError::ReadDirError { - p: bin_dir.clone(), - source: e, - })?; - for dirent in diriter { - let dirent = dirent.map_err(|e| CliError::ReadDirError { - p: bin_dir.clone(), - source: e, - })?; - let name = dirent.file_name(); - let file_is_tool = name.to_str().map(|n| tools.iter().any(|t| *t == n)); - if file_is_tool == Some(false) { - if dirent.path().is_dir() { - utils::remove_dir("cargo_home", &dirent.path())?; - } else { - utils::remove_file("cargo_home", &dirent.path())?; - } - } - } - - info!("removing rustup binaries"); - - // Delete rustup. This is tricky because this is *probably* - // the running executable and on Windows can't be unlinked until - // the process exits. - #[cfg(windows)] - windows::clean_cargo_bin(no_modify_path, process)?; + // Remove rustup executable and then clean up `$CARGO_HOME/bin` if empty. + // Optionally remove it from $PATH if successfully removed. #[cfg(unix)] clean_cargo_bin(no_modify_path, process)?; + // NOTE: On windows, this is *tricky*, + // the running executable and on Windows can't be unlinked until the process exits. + // see: windows::{complete_windows_uninstall,clean_cargo_bin} + #[cfg(windows)] + windows::clean_cargo_bin(no_modify_path, process)?; info!("rustup is uninstalled"); - Ok(ExitCode::SUCCESS) } /// Remove the `$CARGO_HOME/bin` directory if it's empty. /// On success, remove it from `$PATH` unless `no_modify_path` is set. /// If the directory is not empty, emit a warning and return success. -#[cfg(unix)] fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result<()> { // Remove rustup binary let cargo_bin_path = process.cargo_home()?.join("bin"); diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index ae076866e8..87688cd6bd 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -348,16 +348,18 @@ fn has_windows_sdk_libs(process: &Process) -> bool { false } -/// Run by rustup-gc-$num.exe to delete CARGO_HOME +/// Run by rustup-gc-$num.exe to delete rustup binary #[tracing::instrument(level = "trace")] pub fn complete_windows_uninstall(process: &Process) -> Result { use std::process::Stdio; wait_for_parent()?; - // Now that the parent has exited there are hopefully no more files open in CARGO_HOME - let cargo_home = process.cargo_home()?; - utils::remove_dir("cargo_home", &cargo_home)?; + let no_modify_path = process.var_os(GC_MODIFY_PATH).as_deref() != Some(OsStr::new("1")); + + // Clean up CARGO_HOME/bin if it's empty now + // On success, also remove it from $PATH. + super::clean_cargo_bin(no_modify_path, process)?; // Now, run a *system* binary to inherit the DELETE_ON_CLOSE // handle to *this* process, then exit. The OS will delete the gc @@ -374,6 +376,10 @@ pub fn complete_windows_uninstall(process: &Process) -> Result Ok(utils::ExitCode(0)) } +// The rustup-gc executable cannot accept normal function call here, +// so we use env var here, notifying it if we need to remove $CARGO_HOME/bin from $PATH +const GC_MODIFY_PATH: &str = "RUSTUP_GC_MODIFY_PATH"; + pub(crate) fn wait_for_parent() -> Result<()> { use std::io; use std::mem; @@ -644,9 +650,9 @@ pub(crate) fn self_replace(process: &Process) -> Result { Ok(utils::ExitCode(0)) } -// The last step of uninstallation is to delete *this binary*, -// rustup.exe and the CARGO_HOME that contains it. On Unix, this -// works fine. On Windows you can't delete files while they are open, +// The last step of uninstallation is to delete *this binary*, rustup.exe. +// On Unix, this works fine. +// On Windows you can't delete files while they are open, // like when they are running. // // Here's what we're going to do: @@ -659,7 +665,7 @@ pub(crate) fn self_replace(process: &Process) -> Result { // processes created with the option to inherit handles // will also keep them open. // - Run the gc exe, which waits for the original rustup.exe -// process to close, then deletes CARGO_HOME. This process +// process to close, then deletes rustup.exe. This process // has inherited a FILE_FLAG_DELETE_ON_CLOSE handle to itself. // - Finally, spawn yet another system binary with the inherit handles // flag, so *it* inherits the FILE_FLAG_DELETE_ON_CLOSE handle to @@ -687,10 +693,6 @@ pub(crate) fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result // CARGO_HOME, hopefully empty except for bin/rustup.exe let cargo_home = process.cargo_home()?; - if !no_modify_path { - do_remove_from_path(process)?; - } - // The rustup.exe bin let rustup_path = cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")); @@ -738,6 +740,7 @@ pub(crate) fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result }; Command::new(gc_exe) + .env(GC_MODIFY_PATH, if no_modify_path { "0" } else { "1" }) .spawn() .context(CliError::WindowsUninstallMadness)?; diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index f821659935..1ddae7b6a9 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -259,13 +259,13 @@ async fn uninstall_works_if_rustup_home_doesnt_exist() { } #[tokio::test] -async fn uninstall_deletes_cargo_home() { +async fn uninstall_keeps_cargo_home() { let cx = setup_empty_installed().await; cx.config .expect(["rustup", "self", "uninstall", "-y"]) .await .is_ok(); - assert!(!cx.config.cargodir.exists()); + assert!(cx.config.cargodir.exists()); } #[tokio::test] @@ -300,7 +300,7 @@ async fn uninstall_self_delete_works() { assert!(out.status.success()); assert!(!rustup.exists()); - assert!(!cx.config.cargodir.exists()); + assert!(cx.config.cargodir.exists()); let rustc = cx.config.cargodir.join(format!("bin/rustc{EXE_SUFFIX}")); let rustdoc = cx.config.cargodir.join(format!("bin/rustdoc{EXE_SUFFIX}")); @@ -347,7 +347,15 @@ async fn uninstall_doesnt_leave_gc_file() { fn ensure_empty(dir: &Path) -> Result<(), GcErr> { let garbage = fs::read_dir(dir) .unwrap() - .map(|d| d.unwrap().path().to_string_lossy().to_string()) + .filter_map(|entry| { + let path = entry.unwrap().path(); + let name = path.file_name()?.to_str()?; + // On Windows, this binary is cleaned up on exit + if !(name.starts_with("rustup-gc-") && name.ends_with(EXE_SUFFIX)) { + return None; + } + Some(path.to_string_lossy().to_string()) + }) .collect::>(); match garbage.len() { 0 => Ok(()), From b80beaf29c47b076e2aeec771f3ecc12aa21e044 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:06:03 +0800 Subject: [PATCH 7/9] test: cover cargo bin removal during uninstall --- tests/suite/cli_self_upd.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 1ddae7b6a9..e34d452411 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -268,6 +268,37 @@ async fn uninstall_keeps_cargo_home() { assert!(cx.config.cargodir.exists()); } +#[tokio::test] +async fn uninstall_removes_empty_cargo_bin() { + let cx = setup_empty_installed().await; + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + assert!(!cx.config.cargodir.join("bin").exists()); +} + +#[tokio::test] +async fn uninstall_keeps_non_empty_cargo_bin() { + let cx = setup_empty_installed().await; + let cargo_bin = cx.config.cargodir.join("bin"); + + let mock_file = cargo_bin.join(".DS_Store"); + fs::write(&mock_file, "").unwrap(); + + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .with_stderr(snapbox::str![[r#" +... +warn: keeping non-empty cargo bin directory `[..]` +... +"#]]) + .is_ok(); + assert!(cargo_bin.exists()); + assert!(mock_file.exists()); +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await; From b7663c0959a1635f6d1960272c077fce0aa26b0f Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:06:30 +0800 Subject: [PATCH 8/9] test: cover cargo bin PATH cleanup during uninstall --- tests/suite/cli_self_upd.rs | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index e34d452411..c1e5923152 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -299,6 +299,60 @@ warn: keeping non-empty cargo bin directory `[..]` assert!(mock_file.exists()); } +#[cfg(not(windows))] +#[tokio::test] +async fn uninstall_removes_path_only_when_bin_removed() { + async fn install(cx: &CliTestContext) { + cx.config + .expect(["rustup-init", "-y", "--default-toolchain", "none"]) + .await + .is_ok(); + } + + fn source_line(cx: &CliTestContext) -> String { + format!( + r#". "{}/env" +"#, + cx.config.cargodir.display() + ) + } + + let cx = CliTestContext::new(Scenario::Empty).await; + install(&cx).await; + let profile = cx.config.homedir.join(".profile"); + assert!( + fs::read_to_string(&profile) + .unwrap() + .contains(&source_line(&cx)) + ); + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + assert!(!cx.config.cargodir.join("bin").exists()); + assert!( + !fs::read_to_string(&profile) + .unwrap() + .contains(&source_line(&cx)) + ); + + let cx = CliTestContext::new(Scenario::Empty).await; + install(&cx).await; + let cargo_bin = cx.config.cargodir.join("bin"); + let profile = cx.config.homedir.join(".profile"); + fs::write(cargo_bin.join("custom-tool"), "").unwrap(); + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + assert!(cargo_bin.exists()); + assert!( + fs::read_to_string(&profile) + .unwrap() + .contains(&source_line(&cx)) + ); +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await; From 1c41abeb77c718d63466630ea8d26d0d4788281d Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:06:53 +0800 Subject: [PATCH 9/9] test: cover Windows cargo bin cleanup during uninstall --- tests/suite/cli_self_upd.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index c1e5923152..bd1146073d 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -353,6 +353,26 @@ async fn uninstall_removes_path_only_when_bin_removed() { ); } +#[cfg(windows)] +#[tokio::test] +async fn windows_complete_uninstall_removes_empty_cargo_bin() { + let cx = setup_empty_installed().await; + let cargo_bin = cx.config.cargodir.join("bin"); + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + + let check = || { + if cargo_bin.exists() { + Err(format!("cargo bin still exists: {}", cargo_bin.display())) + } else { + Ok(()) + } + }; + retry(Fibonacci::from_millis(1).map(jitter).take(23), check).unwrap() +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await;