Skip to content
67 changes: 35 additions & 32 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 61 additions & 62 deletions src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
//! 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.
Expand All @@ -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};

Expand Down Expand Up @@ -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};

Expand All @@ -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};

Expand Down Expand Up @@ -1048,6 +1048,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.
Comment on lines +1051 to +1056
Copy link
Copy Markdown
Contributor

@djc djc Jun 1, 2026

Choose a reason for hiding this comment

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

Suggest introducing this in a separate first commit, and then updating it in each commit (where necessary) as a way of documenting what is changed.

View changes since the review

pub(crate) fn uninstall(
no_prompt: bool,
no_modify_path: bool,
Expand All @@ -1061,7 +1067,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 +1097,64 @@ pub(crate) fn uninstall(
utils::remove_dir("rustup_home", &rustup_dir)?;
}

info!("removing cargo home");
// 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}")));
Comment thread
Cloud0310 marked this conversation as resolved.

// Remove CARGO_HOME/bin from PATH
if !no_modify_path {
do_remove_from_path(process)?;
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)?;
}
}

// Delete everything in CARGO_HOME *except* the rustup bin
// 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(process, no_modify_path)?;
// 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(process, no_modify_path)?;

// 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 on lines -1103 to -1119
Copy link
Copy Markdown
Contributor

@djc djc Jun 1, 2026

Choose a reason for hiding this comment

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

It is not clear from reviewing this (presumably Unix-only) commit why this (presumably platform-independent) logic could be removed.

View changes since the review

info!("rustup is uninstalled");
Ok(ExitCode::SUCCESS)
}

// 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())?;
}
/// 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.
fn clean_cargo_bin(process: &Process, no_modify_path: bool) -> 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)?;
}
}

info!("removing rustup binaries");
return Ok(());
};

// 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)?;
if e.kind() == io::ErrorKind::DirectoryNotEmpty {
warn!("keeping non-empty cargo bin directory `{cargo_bin_path_display}`");

info!("rustup is uninstalled");
return Ok(());
}

Ok(ExitCode::SUCCESS)
Err(e)
.with_context(|| format!("failed to remove cargo bin directory `{cargo_bin_path_display}`"))
}

#[derive(Clone, Copy, Debug)]
Expand Down
Loading
Loading