Skip to content
120 changes: 58 additions & 62 deletions src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -47,7 +46,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};

Expand Down Expand Up @@ -80,7 +79,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};

Expand All @@ -91,7 +90,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};

Expand Down Expand Up @@ -1048,6 +1047,12 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result<
Ok(())
}

/// Uninstall process:
/// 1. Remove rustup home.
/// 2. Remove rustup tool links.
/// 3. 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.
pub(crate) fn uninstall(
no_prompt: bool,
no_modify_path: bool,
Expand All @@ -1061,7 +1066,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());
}

Expand Down Expand Up @@ -1090,72 +1096,62 @@ pub(crate) fn uninstall(
utils::remove_dir("rustup_home", &rustup_dir)?;
}

info!("removing cargo home");

// Remove CARGO_HOME/bin from PATH
if !no_modify_path {
do_remove_from_path(process)?;
}
// Remove rustup tool links and executable, then remove `$CARGO_HOME/bin` directory 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)?;

@rami3l rami3l Jun 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the impression that we are first adding this line in a previous commit, then removing and re-adding it here?

View changes since the review


// Delete everything in CARGO_HOME *except* the rustup bin
info!("rustup is uninstalled");
Ok(ExitCode::SUCCESS)
}

// 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())?;
}
}
}
Comment thread
rami3l marked this conversation as resolved.
/// Remove rustup tool links and executable, then remove `$CARGO_HOME/bin` directory if 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.
fn clean_cargo_bin(no_modify_path: bool, process: &Process) -> Result<()> {
let cargo_bin_path = process.cargo_home()?.join("bin");
let rustup_path = cargo_bin_path.join(format!("rustup{EXE_SUFFIX}"));

// Then everything in bin except rustup and tools. These can't be unlinked
// until this process exits (on windows).
let tools = TOOLS
// Remove rustup tool links
let proxy_paths = 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())?;
}
.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 rustup binaries");
// Remove rustup binary
utils::remove_file("rustup_bin", &rustup_path)?;

// 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)?;
// Remove $CARGO_HOME/bin
let cargo_bin_path_display = cargo_bin_path.display();
info!("removing empty cargo bin directory `{cargo_bin_path_display}`");

info!("rustup is uninstalled");
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)?;
}

Ok(ExitCode::SUCCESS)
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)]
Expand Down
5 changes: 0 additions & 5 deletions src/cli/self_update/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ pub(crate) fn do_anti_sudo_check(no_prompt: bool, process: &Process) -> Result<u
Ok(utils::ExitCode(0))
}

pub(crate) fn delete_rustup_and_cargo_home(process: &Process) -> 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();
Expand Down
25 changes: 16 additions & 9 deletions src/cli/self_update/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Cloud0310 marked this conversation as resolved.
#[tracing::instrument(level = "trace")]
pub fn complete_windows_uninstall(process: &Process) -> Result<utils::ExitCode> {
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
Expand Down Expand Up @@ -644,9 +646,9 @@ pub(crate) fn self_replace(process: &Process) -> Result<utils::ExitCode> {
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:
Expand All @@ -659,7 +661,7 @@ pub(crate) fn self_replace(process: &Process) -> Result<utils::ExitCode> {
// 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
Expand All @@ -674,7 +676,7 @@ pub(crate) fn self_replace(process: &Process) -> Result<utils::ExitCode> {
//
// .. 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(no_modify_path: bool, process: &Process) -> Result<()> {
use std::io;
use std::ptr;
use std::thread;
Expand Down Expand Up @@ -734,6 +736,7 @@ pub(crate) fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> {
};

Command::new(gc_exe)
.env(GC_MODIFY_PATH, if no_modify_path { "0" } else { "1" })
.spawn()
.context(CliError::WindowsUninstallMadness)?;

Expand All @@ -749,6 +752,10 @@ pub(crate) fn delete_rustup_and_cargo_home(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<Option<Value>> {
USER_PATH.get()
Expand Down
Loading
Loading