From 5c926468824a307d36939cbba097e864fc577282 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:53:04 +0800 Subject: [PATCH 01/10] refactor: rename Windows uninstall gc helper --- src/cli/self_update.rs | 9 +++++++-- src/cli/self_update/windows.rs | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 564fe080d8..80e3a0855c 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -1148,10 +1148,15 @@ pub(crate) fn uninstall( info!("removing rustup binaries"); - // Delete rustup. This is tricky because this is *probably* + // Delete rustup. + #[cfg(unix)] + delete_rustup_and_cargo_home(process)?; + // NOTE: On windows, 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)?; + // see: windows::{complete_windows_uninstall,spawn_uninstall_gc} + #[cfg(windows)] + windows::spawn_uninstall_gc(process)?; info!("rustup is uninstalled"); diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 07578e3ec1..58636eb013 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -644,10 +644,10 @@ 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, -// like when they are running. +// Spawn a temporary rustup-gc-$random.exe to finish Windows uninstall +// after the original rustup.exe process exits. On Unix, the running +// executable can be deleted directly. On Windows you can't delete files +// while they are open, like when they are running. // // Here's what we're going to do: // - Copy rustup.exe to a temporary file in @@ -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 spawn_uninstall_gc(process: &Process) -> Result<()> { use std::io; use std::ptr; use std::thread; From f35f5f0ffa49c0b4e2761a9d9028c289a5201324 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:43:36 +0800 Subject: [PATCH 02/10] refactor(cli/self-update): move `unix::delete_rustup_and_cargo_home()` to parent module --- src/cli/self_update.rs | 8 +++++++- src/cli/self_update/unix.rs | 5 ----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 80e3a0855c..6764e618ec 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::{do_add_to_path, do_remove_from_path}; #[cfg(unix)] pub(crate) use unix::{run_update, self_replace}; @@ -1163,6 +1163,12 @@ pub(crate) fn uninstall( Ok(ExitCode::SUCCESS) } +#[cfg(unix)] +fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> { + let cargo_home = process.cargo_home()?; + utils::remove_dir("cargo_home", &cargo_home) +} + #[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 05c598f551..44f073a3f8 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -47,11 +47,6 @@ pub(crate) fn do_anti_sudo_check(no_prompt: bool, process: &Process) -> Result Result<()> { - let cargo_home = process.cargo_home()?; - 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 b9e273593dc22cb4a4c29354e48dd420b66ef37c Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:15:04 +0800 Subject: [PATCH 03/10] refactor: route Windows cargo home cleanup through parent helper --- src/cli/self_update.rs | 3 +-- src/cli/self_update/windows.rs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 6764e618ec..cdb90ca585 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -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::{do_add_to_path, do_remove_from_path}; #[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; @@ -1163,7 +1163,6 @@ pub(crate) fn uninstall( Ok(ExitCode::SUCCESS) } -#[cfg(unix)] fn delete_rustup_and_cargo_home(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 58636eb013..5e8ca29881 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -355,9 +355,8 @@ pub fn complete_windows_uninstall(process: &Process) -> Result 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)?; + // Now that the parent has exited there are hopefully no more files open in CARGO_HOME. + super::delete_rustup_and_cargo_home(process)?; // Now, run a *system* binary to inherit the DELETE_ON_CLOSE // handle to *this* process, then exit. The OS will delete the gc From 847be82993d67de45c2e0e2357ef7ee5a9a7a895 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:15:57 +0800 Subject: [PATCH 04/10] refactor(self-update): postpone PATH cleanup --- src/cli/self_update.rs | 15 +++++++-------- src/cli/self_update/windows.rs | 11 +++++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index cdb90ca585..676019163f 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 @@ -1150,21 +1145,25 @@ pub(crate) fn uninstall( // Delete rustup. #[cfg(unix)] - delete_rustup_and_cargo_home(process)?; + delete_rustup_and_cargo_home(no_modify_path, process)?; // NOTE: On windows, this is tricky because this is *probably* // the running executable and on Windows can't be unlinked until // the process exits. // see: windows::{complete_windows_uninstall,spawn_uninstall_gc} #[cfg(windows)] - windows::spawn_uninstall_gc(process)?; + windows::spawn_uninstall_gc(no_modify_path, process)?; info!("rustup is uninstalled"); Ok(ExitCode::SUCCESS) } -fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> { +fn delete_rustup_and_cargo_home(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 5e8ca29881..1c83537691 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -355,8 +355,10 @@ pub fn complete_windows_uninstall(process: &Process) -> Result wait_for_parent()?; + let no_modify_path = process.var_os(GC_MODIFY_PATH).as_deref() != Some(OsStr::new("1")); + // Now that the parent has exited there are hopefully no more files open in CARGO_HOME. - super::delete_rustup_and_cargo_home(process)?; + super::delete_rustup_and_cargo_home(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 @@ -673,7 +675,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 spawn_uninstall_gc(process: &Process) -> Result<()> { +pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, process: &Process) -> Result<()> { use std::io; use std::ptr; use std::thread; @@ -733,6 +735,7 @@ pub(crate) fn spawn_uninstall_gc(process: &Process) -> Result<()> { }; Command::new(gc_exe) + .env(GC_MODIFY_PATH, if no_modify_path { "0" } else { "1" }) .spawn() .context(CliError::WindowsUninstallMadness)?; @@ -748,6 +751,10 @@ pub(crate) fn spawn_uninstall_gc(process: &Process) -> Result<()> { Ok(()) } +// 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"; + #[cfg(any(test, feature = "test"))] pub fn get_path() -> Result> { USER_PATH.get() From 5798e2ddfdff8df76b1050f5a159dd2fdda0fdd9 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:13:06 +0800 Subject: [PATCH 05/10] doc: add uninstall process overall docstring --- src/cli/self_update.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 676019163f..5fbea32028 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -1048,6 +1048,11 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< Ok(()) } +/// Uninstall process: +/// 1. Remove rustup home. +/// 2. Remove other tools in $CARGO_HOME/bin, but skipping rustup binary file and links to rustup. +/// 3. If no `--no-modify-path` is passed $CARGO_HOME/bin, clean up $PATH. +/// 4. Remove $CARGO_HOME pub(crate) fn uninstall( no_prompt: bool, no_modify_path: bool, From b12619f6f3d19507f746baf1e80ea12064eaca85 Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:04:18 +0800 Subject: [PATCH 06/10] feat(self-update)!: limit `$CARGO_HOME/bin` cleanup to `rustup` removal --- src/cli/self_update.rs | 85 +++++++++++++++++++--------------- src/cli/self_update/windows.rs | 5 +- tests/suite/cli_self_upd.rs | 10 +++- 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 5fbea32028..fdfd491280 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -24,15 +24,15 @@ //! During uninstall (`rustup self uninstall`): //! //! * Delete `$RUSTUP_HOME`. -//! * Delete everything in `$CARGO_HOME`, including -//! the rustup binary and its hardlinks +//! * Delete rustup from `$CARGO_HOME`/bin. +//! * Delete `$CARGO_HOME`/bin if it is empty after uninstall. +//! * Delete `$CARGO_HOME` 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}; @@ -1050,9 +1050,10 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< /// Uninstall process: /// 1. Remove rustup home. -/// 2. Remove other tools in $CARGO_HOME/bin, but skipping rustup binary file and links to rustup. -/// 3. If no `--no-modify-path` is passed $CARGO_HOME/bin, clean up $PATH. -/// 4. Remove $CARGO_HOME +/// 2. Remove rustup binary. +/// 3. Try to remove $CARGO_HOME/bin directory if it's empty. +/// 4. Upon successfully removing $CARGO_HOME/bin, clean up $PATH. +/// 5. Try to remove $CARGO_HOME directory if it's empty. pub(crate) fn uninstall( no_prompt: bool, no_modify_path: bool, @@ -1118,34 +1119,6 @@ pub(crate) fn uninstall( } } - // 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. @@ -1163,13 +1136,51 @@ pub(crate) fn uninstall( Ok(ExitCode::SUCCESS) } +/// Remove rustup executable, then remove `$CARGO_HOME/bin` if empty. On success, remove it from +/// `$PATH` unless `no_modify_path` is set. Finally, remove `$CARGO_HOME` if empty. Nonempty +/// directories are left in place and a warning is emitted instead. fn delete_rustup_and_cargo_home(no_modify_path: bool, process: &Process) -> Result<()> { let cargo_home = process.cargo_home()?; - if !no_modify_path { - do_remove_from_path(process)?; + let cargo_bin = cargo_home.join("bin"); + let rustup_path = cargo_bin.join(format!("rustup{EXE_SUFFIX}")); + + utils::remove_file("rustup_bin", &rustup_path)?; + + let cargo_bin_display = cargo_bin.display(); + info!("removing empty cargo bin directory `{cargo_bin_display}`"); + + match fs::remove_dir(&cargo_bin) { + Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => { + warn!("keeping non-empty cargo bin directory `{cargo_bin_display}`") + } + Err(e) => { + return Err(e).with_context(|| { + format!("failed to remove cargo bin directory `{cargo_bin_display}`") + }); + } + Ok(()) if !no_modify_path => { + info!("removing cargo bin directory `{cargo_bin_display}` from $PATH"); + do_remove_from_path(process)?; + } + Ok(()) => {} } - utils::remove_dir("cargo_home", &cargo_home) + let cargo_home_display = cargo_home.display(); + info!("removing empty cargo home directory `{cargo_home_display}`"); + + match fs::remove_dir(&cargo_home) { + Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => { + warn!("keeping non-empty cargo home directory `{cargo_home_display}`"); + } + Err(e) => { + return Err(e).with_context(|| { + format!("failed to remove cargo home directory `{cargo_home_display}`") + }); + } + Ok(()) => {} + } + + Ok(()) } #[derive(Clone, Copy, Debug)] diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 1c83537691..3ae0d18bcf 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -348,7 +348,7 @@ 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 clean CARGO_HOME #[tracing::instrument(level = "trace")] pub fn complete_windows_uninstall(process: &Process) -> Result { use std::process::Stdio; @@ -357,7 +357,8 @@ pub fn complete_windows_uninstall(process: &Process) -> Result let no_modify_path = process.var_os(GC_MODIFY_PATH).as_deref() != Some(OsStr::new("1")); - // Now that the parent has exited there are hopefully no more files open in CARGO_HOME. + // Clean up CARGO_HOME/bin if it's empty now on success, also remove it from $PATH. Then do the + // same for CARGO_HOME. super::delete_rustup_and_cargo_home(no_modify_path, process)?; // Now, run a *system* binary to inherit the DELETE_ON_CLOSE diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index f821659935..e1a1aae5e7 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -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 66192c67533fb80afb71106334257783e97dec5b Mon Sep 17 00:00:00 2001 From: Cloud0310 <60375730+Cloud0310@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:06:26 +0800 Subject: [PATCH 07/10] feat(self-update)!: remove rustup proxy links in `$CARGO_HOME/bin` cleanup --- src/cli/self_update.rs | 31 +++++++++++++++++++++---------- src/cli/self_update/windows.rs | 2 +- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index fdfd491280..a2d4dd7aff 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -24,7 +24,7 @@ //! During uninstall (`rustup self uninstall`): //! //! * Delete `$RUSTUP_HOME`. -//! * Delete rustup from `$CARGO_HOME`/bin. +//! * Delete rustup and tool proxy binaries from `$CARGO_HOME`/bin. //! * Delete `$CARGO_HOME`/bin if it is empty after uninstall. //! * Delete `$CARGO_HOME` if it is empty after uninstall. //! @@ -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}; @@ -1050,7 +1050,7 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< /// Uninstall process: /// 1. Remove rustup home. -/// 2. Remove rustup binary. +/// 2. Remove rustup tool links and binary. /// 3. Try to remove $CARGO_HOME/bin directory if it's empty. /// 4. Upon successfully removing $CARGO_HOME/bin, clean up $PATH. /// 5. Try to remove $CARGO_HOME directory if it's empty. @@ -1119,11 +1119,11 @@ pub(crate) fn uninstall( } } - info!("removing rustup binaries"); + info!("removing rustup tool links and binary"); // Delete rustup. #[cfg(unix)] - delete_rustup_and_cargo_home(no_modify_path, process)?; + clean_cargo_home(no_modify_path, process)?; // NOTE: On windows, this is tricky because this is *probably* // the running executable and on Windows can't be unlinked until // the process exits. @@ -1132,18 +1132,29 @@ pub(crate) fn uninstall( windows::spawn_uninstall_gc(no_modify_path, process)?; info!("rustup is uninstalled"); - Ok(ExitCode::SUCCESS) } -/// Remove rustup executable, then remove `$CARGO_HOME/bin` if empty. On success, remove it from -/// `$PATH` unless `no_modify_path` is set. Finally, remove `$CARGO_HOME` if empty. Nonempty -/// directories are left in place and a warning is emitted instead. -fn delete_rustup_and_cargo_home(no_modify_path: bool, process: &Process) -> Result<()> { +/// Remove rustup tool links and executable, then remove `$CARGO_HOME/bin` if empty. On success, +/// remove it from `$PATH` unless `no_modify_path` is set. Finally, remove `$CARGO_HOME` if empty. +/// Nonempty directories are left in place and a warning is emitted instead. +fn clean_cargo_home(no_modify_path: bool, process: &Process) -> Result<()> { let cargo_home = process.cargo_home()?; let cargo_bin = cargo_home.join("bin"); let rustup_path = cargo_bin.join(format!("rustup{EXE_SUFFIX}")); + // Remove rustup tool links + let proxy_paths = TOOLS + .iter() + .chain(DUP_TOOLS.iter()) + .map(|tool| cargo_bin.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)?; + } + } + + // Remove rustup binary utils::remove_file("rustup_bin", &rustup_path)?; let cargo_bin_display = cargo_bin.display(); diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 3ae0d18bcf..a027948819 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -359,7 +359,7 @@ pub fn complete_windows_uninstall(process: &Process) -> Result // Clean up CARGO_HOME/bin if it's empty now on success, also remove it from $PATH. Then do the // same for CARGO_HOME. - super::delete_rustup_and_cargo_home(no_modify_path, process)?; + super::clean_cargo_home(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 From 03e6def662e941a71d4338801b028a02e3c6b9f5 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 08/10] 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 e1a1aae5e7..d5c4f5ef73 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -268,6 +268,37 @@ async fn uninstall_deletes_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 faeef252478f9fdd5a884eafb3381d7767872d5e 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 09/10] test: cover cargo bin PATH cleanup during uninstall --- tests/suite/cli_self_upd.rs | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index d5c4f5ef73..39b589202b 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -299,6 +299,58 @@ warn: keeping non-empty cargo bin directory `[..]` assert!(mock_file.exists()); } +#[cfg(not(windows))] +#[tokio::test] +async fn complete_uninstall_removes_empty_cargo_bin() { + let cx = CliTestContext::new(Scenario::Empty).await; + let cargodir = &cx.config.cargodir; + cx.config + .expect(["rustup-init", "-y", "--default-toolchain", "none"]) + .await + .is_ok(); + + let profile = cx.config.homedir.join(".profile"); + let source_line = format!( + r#". "{}/env" +"#, + cargodir.display() + ); + assert!(fs::read_to_string(&profile).unwrap().contains(&source_line)); + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + assert!(!cargodir.join("bin").exists()); + assert!(!fs::read_to_string(&profile).unwrap().contains(&source_line)); + assert!(!cargodir.exists()); +} + +#[cfg(not(windows))] +#[tokio::test] +async fn complete_uninstall_keeps_non_empty_cargo_bin() { + let cx = CliTestContext::new(Scenario::Empty).await; + cx.config + .expect(["rustup-init", "-y", "--default-toolchain", "none"]) + .await + .is_ok(); + + let cargodir = &cx.config.cargodir; + let profile = cx.config.homedir.join(".profile"); + let source_line = format!( + r#". "{}/env" +"#, + cargodir.display() + ); + fs::write(cargodir.join("bin/custom-tool"), "").unwrap(); + cx.config + .expect(["rustup", "self", "uninstall", "-y"]) + .await + .is_ok(); + assert!(cargodir.join("bin").exists()); + assert!(fs::read_to_string(&profile).unwrap().contains(&source_line)); + assert!(cargodir.exists()); +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await; From dcb6e6804d2cdff5294bbb270f77d43cff83290f 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 10/10] test: cover Windows cargo bin cleanup during uninstall --- tests/suite/cli_self_upd.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 39b589202b..883949c8c4 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -351,6 +351,25 @@ async fn complete_uninstall_keeps_non_empty_cargo_bin() { assert!(cargodir.exists()); } +#[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(); + + retry(Fibonacci::from_millis(1).map(jitter).take(23), || { + if cargo_bin.exists() { + return Err(format!("cargo bin still exists: {}", cargo_bin.display())); + } + Ok(()) + }) + .unwrap() +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await;