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
131 changes: 86 additions & 45 deletions src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
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 @@ -553,8 +553,8 @@ fn canonical_cargo_home(process: &Process) -> Result<Cow<'static, str>> {
}

/// Installing is a simple matter of copying the running binary to
/// `CARGO_HOME`/bin, hard-linking the various Rust tools to it,
/// and adding `CARGO_HOME`/bin to PATH.
/// `$CARGO_HOME/bin`, hard-linking the various Rust tools to it,
/// and adding `$CARGO_HOME/bin` to PATH.
pub(crate) async fn install(
no_prompt: bool,
mut opts: InstallOpts<'_>,
Expand Down Expand Up @@ -1048,6 +1048,13 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result<
Ok(())
}

/// Uninstall process:
/// 1. Remove rustup home.
/// 2. Remove all entries in `$CARGO_HOME` except `bin`.
/// 3. Remove rustup tool links and binary.
/// 4. Try to remove $CARGO_HOME/bin directory if it's empty.
/// 5. Upon successfully removing $CARGO_HOME/bin, clean up $PATH.
/// 6. Try to remove $CARGO_HOME directory if it's empty.
pub(crate) fn uninstall(
no_prompt: bool,
no_modify_path: bool,
Expand Down Expand Up @@ -1092,11 +1099,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
Expand All @@ -1118,44 +1120,83 @@ 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
info!("removing rustup tool links and binary");

// Delete rustup.
#[cfg(unix)]
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.
// see: windows::{complete_windows_uninstall,spawn_uninstall_gc}
#[cfg(windows)]
windows::spawn_uninstall_gc(no_modify_path, process)?;

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

/// 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(|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.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)?;
}
}
Comment thread
Cloud0310 marked this conversation as resolved.

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)?;
let cargo_bin_display = cargo_bin.display();
info!("removing empty cargo bin directory `{cargo_bin_display}`");

info!("rustup is uninstalled");
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(ExitCode::SUCCESS)
// On Windows, also remove rustup from installed programs
#[cfg(windows)]
windows::do_remove_from_programs()?;
}
Ok(()) => {}
}

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)]
Expand Down Expand Up @@ -1195,7 +1236,7 @@ pub(crate) fn self_update_permitted(explicit: bool) -> Result<SelfUpdatePermissi
Ok(SelfUpdatePermission::Permit)
}

/// Self update downloads rustup-init to `CARGO_HOME`/bin/rustup-init
/// Self update downloads rustup-init to `$CARGO_HOME/bin/rustup-init`
/// and runs it.
///
/// It does a few things to accommodate self-delete problems on windows:
Expand All @@ -1208,7 +1249,7 @@ pub(crate) fn self_update_permitted(explicit: bool) -> Result<SelfUpdatePermissi
///
/// Because it's again difficult for rustup-init to delete itself
/// (and on windows this process will not be running to do it),
/// rustup-init is stored in `CARGO_HOME`/bin, and then deleted next
/// rustup-init is stored in `$CARGO_HOME/bin`, and then deleted next
/// time rustup runs.
pub(crate) async fn update(cfg: &Cfg<'_>) -> Result<ExitCode> {
common::warn_if_host_is_emulated(cfg.process);
Expand Down
7 changes: 1 addition & 6 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 Expand Up @@ -139,7 +134,7 @@ pub(crate) fn run_update(setup_path: &Path) -> Result<utils::ExitCode> {
}

/// This function is as the final step of a self-upgrade. It replaces
/// `CARGO_HOME`/bin/rustup with the running exe, and updates the
/// `$CARGO_HOME/bin/rustup` with the running exe, and updates the
/// links to it.
pub(crate) fn self_replace(process: &Process) -> Result<utils::ExitCode> {
install_bins(process)?;
Expand Down
28 changes: 17 additions & 11 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 clean CARGO_HOME
#[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)?;
Comment thread
Cloud0310 marked this conversation as resolved.
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. Then do the
// same for CARGO_HOME.
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
Expand Down Expand Up @@ -566,8 +568,7 @@ where

pub(crate) fn do_remove_from_path(process: &Process) -> Result<()> {
let new_path = _with_path_cargo_home_bin(_remove_from_path, process)?;
_apply_new_path(new_path)?;
do_remove_from_programs()
_apply_new_path(new_path)
}

const RUSTUP_UNINSTALL_ENTRY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup";
Expand Down Expand Up @@ -644,10 +645,10 @@ 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,
// 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
Expand All @@ -674,7 +675,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 spawn_uninstall_gc(no_modify_path: bool, process: &Process) -> Result<()> {
use std::io;
use std::ptr;
use std::thread;
Expand Down Expand Up @@ -734,6 +735,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 +751,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