Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 55 additions & 67 deletions src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,15 @@
//! During uninstall (`rustup self uninstall`):
//!
//! * Delete `$RUSTUP_HOME`.
//! * Delete everything in `$CARGO_HOME`, including
//! the rustup binary and its hardlinks
//! * Delete rustup-managed 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::io::{self, Write};
use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
Expand All @@ -47,7 +45,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 +78,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::{cleanup_cargo_bin, do_add_to_path, do_remove_from_path};
#[cfg(unix)]
pub(crate) use unix::{run_update, self_replace};

Expand All @@ -91,7 +89,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::{cleanup_cargo_bin, do_add_to_path, do_remove_from_path};
#[cfg(windows)]
pub(crate) use windows::{run_update, self_replace};

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

pub(crate) fn do_cargo_bin_clean(
cargo_bin_path: PathBuf,
no_modify_path: bool,
process: &Process,
) -> Result<()> {
let Err(e) = fs::remove_dir(&cargo_bin_path) else {
if !no_modify_path {
do_remove_from_path(process)?;
info!("removed cargo bin from $PATH");
}

info!("removed empty cargo bin");
return Ok(());
};

if e.kind() == io::ErrorKind::DirectoryNotEmpty {
let cargo_bin_path = cargo_bin_path.display();
warn!("not removing cargo bin `{cargo_bin_path}` because it is not empty");

return Ok(());
}

let cargo_bin_path = cargo_bin_path.display();

Err(e).with_context(|| format!("failed to remove cargo bin directory `{cargo_bin_path}`"))
}

/// 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,
Expand All @@ -1061,7 +1092,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,71 +1122,27 @@ 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)?;
}

// 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
// Clean up rustup tool links
let cargo_bin_path = cargo_home.join("bin");
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");

// 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)?;
// Delete rustup. This is tricky because this is *probably* the running executable,
// and on Windows it can't be unlinked until the process exits.
// Also remove `$CARGO_HOME/bin` if it's empty, and clean up PATH on success.
// On Windows, this happens within `windows::complete_windows_uninstall`, after
// the rustup binary is uninstalled.
cleanup_cargo_bin(process, no_modify_path)?;

info!("rustup is uninstalled");

Ok(ExitCode::SUCCESS)
}

Expand Down
14 changes: 11 additions & 3 deletions src/cli/self_update/unix.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::env::consts::EXE_SUFFIX;
use std::path::{Path, PathBuf};
use std::process::Command;

Expand Down Expand Up @@ -47,9 +48,16 @@ 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 cleanup_cargo_bin(process: &Process, no_modify_path: bool) -> Result<()> {
use super::do_cargo_bin_clean;
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)?;

// Clean up CARGO_HOME/bin, if it's empty now
// On success, remove it from $PATH
do_cargo_bin_clean(cargo_bin_path, no_modify_path, process)
}

pub(crate) fn do_remove_from_path(process: &Process) -> Result<()> {
Expand Down
31 changes: 22 additions & 9 deletions src/cli/self_update/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use crate::download::DownloadOptions;
use crate::process::{ColorableTerminal, Process};
use crate::utils;

const GC_NO_MODIFY_PATH: &str = "RUSTUP_GC_NO_MODIFY_PATH";

pub(crate) fn ensure_prompt(process: &Process) -> Result<()> {
writeln!(process.stdout().lock(),)?;
writeln!(process.stdout().lock(), "Press the Enter key to continue.")?;
Expand Down Expand Up @@ -348,16 +350,26 @@ 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<utils::ExitCode> {
use super::do_cargo_bin_clean;
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)?;
// Now that the parent has exited, rustup.exe is hopefully no longer open.
let cargo_home_dir = process.cargo_home()?;
let rustup_bin = cargo_home_dir.join(format!("bin/rustup{EXE_SUFFIX}"));
utils::remove_file("rustup_bin", &rustup_bin)?;

// Best effort remove for cargo bin.
let no_modify_path = process.var_os(GC_NO_MODIFY_PATH).as_deref() == Some(OsStr::new("1"));
let cargo_bin_path = cargo_home_dir.join("bin");

// Clean up CARGO_HOME/bin if it's empty now.
// On success, also remove it from $PATH.
do_cargo_bin_clean(cargo_bin_path, 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 +656,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 +671,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 +686,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 cleanup_cargo_bin(process: &Process, no_modify_path: bool) -> Result<()> {
use std::io;
use std::ptr;
use std::thread;
Expand Down Expand Up @@ -734,6 +746,7 @@ pub(crate) fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> {
};

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

Expand Down
Loading
Loading