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
118 changes: 77 additions & 41 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 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.
//!
//! 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 @@ -1048,6 +1048,12 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result<
Ok(())
}

/// Uninstall process:
/// 1. Remove rustup home.
/// 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.
pub(crate) fn uninstall(
no_prompt: bool,
no_modify_path: bool,
Expand Down Expand Up @@ -1092,11 +1098,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 +1119,79 @@ 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(()) => {}
}

Ok(ExitCode::SUCCESS)
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
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 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 @@ -644,10 +646,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 +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 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 +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
112 changes: 111 additions & 1 deletion tests/suite/cli_self_upd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,108 @@ 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());
}

#[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());
}

#[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;
Expand Down Expand Up @@ -347,7 +449,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
Comment thread
Cloud0310 marked this conversation as resolved.
if !(name.starts_with("rustup-gc-") && name.ends_with(EXE_SUFFIX)) {
return None;
}
Some(path.to_string_lossy().to_string())
})
.collect::<Vec<_>>();
match garbage.len() {
0 => Ok(()),
Expand Down
Loading