diff --git a/Cargo.toml b/Cargo.toml index 23807c7ad4..a88aff4cb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ features = [ "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", + "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_ToolHelp", "Win32_System_IO", @@ -130,6 +131,7 @@ features = [ "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI", + "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", ] version = "0.61" diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 6fae0b3729..5f5f5cca4d 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -14,7 +14,7 @@ pub async fn main( current_dir: PathBuf, process: &Process, ) -> anyhow::Result { - self_update::cleanup_self_updater(process, &process.cargo_home()?.join("bin"))?; + self_update::cleanup_self_updater(process, &process.bin_home()?)?; let _setup = job::setup(); let mut args = process.args_os().skip(1); diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 3a703bc0b9..37d3e7966e 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -385,7 +385,7 @@ enum ShowSubcmd { verbose: bool, }, - /// Display the computed value of RUSTUP_HOME + /// Display resolved Rustup home directories Home, /// Show the default profile used for the `rustup install` command @@ -700,7 +700,7 @@ pub async fn main( .bin("rustup") .complete(); - self_update::cleanup_self_updater(process, &process.cargo_home()?.join("bin"))?; + self_update::cleanup_self_updater(process, &cfg.bin_dir)?; use clap::error::ErrorKind::*; let matches = match Rustup::try_parse_from(process.args_os()) { @@ -1318,14 +1318,27 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> anyhow::Result { cfg.default_host_tuple()? )?; - // Print rustup home directory { let mut t = t.lock(); - writeln!( - t, - "{HEADER}rustup home: {HEADER:#}{}", - cfg.rustup_dir.display() - )?; + if cfg.process.use_category_home() { + writeln!(t, "{HEADER}rustup homes:{HEADER:#}")?; + for (name, home) in [ + ("config", &cfg.config_dir), + ("state", &cfg.state_dir), + ("data", &cfg.data_dir), + ("cache", &cfg.cache_dir), + ("bin", &cfg.bin_dir), + ] { + writeln!(t, " {name}: {}", home.display())?; + } + } else { + // In legacy mode, all four category homes equal the resolved RUSTUP_HOME. + writeln!( + t, + "{HEADER}rustup home: {HEADER:#}{}", + cfg.data_dir.display() + )?; + } writeln!(t)?; } @@ -1473,7 +1486,21 @@ async fn show_active_toolchain(cfg: &Cfg<'_>, verbose: bool) -> anyhow::Result) -> anyhow::Result { - writeln!(cfg.process.stdout().lock(), "{}", cfg.rustup_dir.display())?; + let mut stdout = cfg.process.stdout().lock(); + if !cfg.process.use_category_home() { + // In legacy mode, all four category homes equal the resolved RUSTUP_HOME. + writeln!(stdout, "{}", cfg.data_dir.display())?; + return Ok(ExitCode::SUCCESS); + } + + for (name, home) in [ + ("config", &cfg.config_dir), + ("state", &cfg.state_dir), + ("data", &cfg.data_dir), + ("cache", &cfg.cache_dir), + ] { + writeln!(stdout, "{name}: {}", home.display())?; + } Ok(ExitCode::SUCCESS) } @@ -2191,6 +2218,10 @@ date = "2025-01-01" "RUSTUP_HOME".to_owned(), rustup_home.path().display().to_string(), ), + ( + "CARGO_HOME".to_owned(), + rustup_home.path().display().to_string(), + ), ( "RUSTUP_OVERRIDE_UNIX_FALLBACK_SETTINGS".to_owned(), rustup_home diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 9e153decd0..693c7b2db7 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -8,19 +8,19 @@ //! //! During install (as `rustup-init`): //! -//! * copy the self exe to $CARGO_HOME/bin -//! * hardlink rustc, etc to *that* +//! * copy the self exe to the Rustup bin home +//! * hardlink rustc, etc. to *that* //! * update the PATH in a system-specific way //! * run the equivalent of `rustup default stable` //! //! During upgrade (`rustup self update`): //! -//! * download rustup-init to a managed path under `$RUSTUP_HOME` +//! * download rustup-init to `self-update` under the Rustup state home //! * run the downloaded binary in replacement mode //! * atomically replace rustup and update its proxy links. On Windows //! this happens after the update command exits. //! -//! During uninstall (`rustup self uninstall`): +//! During legacy-mode uninstall (`rustup self uninstall`): //! //! * Delete `$RUSTUP_HOME`. //! * Delete all entries in `$CARGO_HOME` except `bin`. @@ -28,6 +28,9 @@ //! * Delete `$CARGO_HOME/bin` if it is empty after uninstall. //! * Delete `$CARGO_HOME` if it is empty after uninstall. //! +//! Category-mode uninstall removes the current toolchains, env files and binaries, +//! preserving other files in the category homes without separately cleaning legacy homes. +//! //! Deleting the running binary during uninstall is tricky //! and racy on Windows. @@ -37,7 +40,7 @@ use std::{ env::{self, consts::EXE_SUFFIX}, fmt, fs, io::{self, Write}, - path::{Component, MAIN_SEPARATOR, Path, PathBuf}, + path::{MAIN_SEPARATOR, Path, PathBuf}, process::Command, str::FromStr, }; @@ -198,30 +201,33 @@ impl InstallOpts<'_> { return Ok(ExitCode::FAILURE); } - let cargo_home = process.cargo_home()?; + let bin_home = process.bin_home()?; let home_dir = process.home_dir(); let msg = if no_modify_path { format!( post_install_msg_no_modify_path!(), - cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), + cargo_bin_dir = HomeDisplay::new(&bin_home, home_dir.as_deref()), ) } else { format!( post_install_msg!(), - cargo_bin_dir = HomeDisplay::new(&cargo_home.join("bin"), home_dir.as_deref()), + cargo_bin_dir = HomeDisplay::new(&bin_home, home_dir.as_deref()), ) }; md(&mut term, msg); #[cfg(not(windows))] - md( - &mut term, - format!( - post_install_msg_unix!(), - env_dir = HomeDisplay::new(&cargo_home, home_dir.as_deref()), - source_env_lines = - shell::build_source_env_lines(process, &cargo_home, home_dir.as_deref()), - ), - ); + { + let env_home = process.env_home()?; + md( + &mut term, + format!( + post_install_msg_unix!(), + env_dir = HomeDisplay::new(&env_home, home_dir.as_deref()), + source_env_lines = + shell::build_source_env_lines(process, &env_home, home_dir.as_deref()), + ), + ); + } #[cfg(unix)] warn_if_default_linker_missing(process); @@ -244,8 +250,8 @@ impl InstallOpts<'_> { quiet: bool, process: &Process, ) -> anyhow::Result<()> { - let cargo_bin = process.cargo_home()?.join("bin"); - install_bins(process, &cargo_bin, force_hard_links(process))?; + let bin_home = process.bin_home()?; + install_bins(process, &bin_home, force_hard_links(process))?; #[cfg(unix)] unix::write_env_files(process)?; @@ -257,15 +263,9 @@ impl InstallOpts<'_> { #[cfg(windows)] add_uninstall_registry_entry(process)?; - // If RUSTUP_HOME is not set, make sure it exists - if process.var_os("RUSTUP_HOME").is_none() { - let home = process - .home_dir() - .map(|p| p.join(".rustup")) - .ok_or_else(|| anyhow::anyhow!("could not find home dir to put .rustup in"))?; - - fs::create_dir_all(home).context("unable to create ~/.rustup")?; - } + let home_dirs = process.home_dirs()?; + utils::ensure_dir_exists("cache home", &home_dirs.cache)?; + utils::ensure_dir_exists("data home", &home_dirs.data)?; let mut cfg = Cfg::from_env(current_dir, quiet, false, process)?; @@ -290,7 +290,7 @@ impl InstallOpts<'_> { ChannelToolchain::install(options).await?.status }; - check_proxy_sanity(&cargo_bin, components, &desc)?; + check_proxy_sanity(&bin_home, components, &desc)?; cfg.set_default(Some(&partial_desc.into()))?; writeln!(cfg.process.stdout().lock())?; @@ -630,26 +630,21 @@ impl fmt::Display for HomeDisplay<'_> { } fn rustc_or_cargo_exists_in_path(process: &Process) -> anyhow::Result<()> { - // Ignore rustc and cargo if present in $HOME/.cargo/bin or a few other directories - #[allow(clippy::ptr_arg)] - fn ignore_paths(path: &PathBuf) -> bool { - !path - .components() - .any(|c| c == Component::Normal(".cargo".as_ref())) - } - - if let Some(paths) = process.var_os("PATH") { - let paths = env::split_paths(&paths).filter(ignore_paths); + let Some(paths) = process.var_os("PATH") else { + return Ok(()); + }; + let bin_home = process.bin_home()?; - for path in paths { - let rustc = path.join(format!("rustc{EXE_SUFFIX}")); - let cargo = path.join(format!("cargo{EXE_SUFFIX}")); + let conflict = env::split_paths(&paths).find(|path| { + path != &bin_home + && (path.join(format!("rustc{EXE_SUFFIX}")).exists() + || path.join(format!("cargo{EXE_SUFFIX}")).exists()) + }); - if rustc.exists() || cargo.exists() { - return Err(anyhow!("{}", path.to_str().unwrap().to_owned())); - } - } + if let Some(path) = conflict { + anyhow::bail!("{}", path.display()); } + Ok(()) } @@ -681,8 +676,9 @@ fn check_existence_of_rustc_or_cargo_in_path( } fn check_existence_of_settings_file(process: &Process) -> anyhow::Result<()> { - let rustup_dir = process.rustup_home()?; - let settings_file = SettingsFile::new(rustup_dir.join("settings.toml")); + // TODO: Setting file's category is still under discussion + // See: [discussion issue](https://github.com/rust-lang/rustup/issues/4944) + let settings_file = SettingsFile::new(process.home_dirs()?.config.join("settings.toml")); if !utils::path_exists(&settings_file.path) { return Ok(()); } @@ -706,43 +702,72 @@ fn check_existence_of_settings_file(process: &Process) -> anyhow::Result<()> { fn pre_install_msg(no_modify_path: bool, process: &Process) -> anyhow::Result { let cargo_home = process.cargo_home()?; - let cargo_bin_dir = cargo_home.join("bin"); - let rustup_home = home::rustup_home()?; - - if !no_modify_path { - // Brittle code warning: some duplication in unix::add_to_path - #[cfg(not(windows))] - { - let rcfiles = shell::get_available_shells(process) - .flat_map(|sh| sh.rcs(process).into_iter()) - .map(|rc| format!(" {}", rc.display())) - .collect::>(); - let plural = if rcfiles.len() > 1 { "s" } else { "" }; - let rcfiles = rcfiles.join("\n"); - Ok(format!( - pre_install_msg_unix!(), - cargo_home = cargo_home.display(), - cargo_bin_dir = cargo_bin_dir.display(), - plural = plural, - rcfiles = rcfiles, - rustup_home = rustup_home.display(), - )) - } - #[cfg(windows)] - Ok(format!( - pre_install_msg_win!(), - cargo_home = cargo_home.display(), - cargo_bin_dir = cargo_bin_dir.display(), - rustup_home = rustup_home.display(), - )) + let cargo_bin_dir = process.bin_home()?; + let home_dirs = process.home_dirs()?; + let home_dir = process.home_dir(); + let rustup_home_message = if process.use_category_home() { + format!( + "Rustup will use these directories: + + config: {} + state: {} + data: {} + cache: {} + +They can be modified individually with +`RUSTUP_CONFIG_HOME`, `RUSTUP_STATE_HOME`, `RUSTUP_DATA_HOME`, and +`RUSTUP_CACHE_HOME`.", + HomeDisplay::new(&home_dirs.config, home_dir.as_deref()), + HomeDisplay::new(&home_dirs.state, home_dir.as_deref()), + HomeDisplay::new(&home_dirs.data, home_dir.as_deref()), + HomeDisplay::new(&home_dirs.cache, home_dir.as_deref()), + ) } else { - Ok(format!( + format!( + "Rustup metadata and toolchains will be installed into the Rustup +home directory, located at: + + {} + +This can be modified with the `RUSTUP_HOME` environment variable.", + HomeDisplay::new(&home_dirs.data, home_dir.as_deref()), + ) + }; + + if no_modify_path { + return Ok(format!( pre_install_msg_no_modify_path!(), - cargo_home = cargo_home.display(), - cargo_bin_dir = cargo_bin_dir.display(), - rustup_home = rustup_home.display(), + cargo_home = HomeDisplay::new(&cargo_home, home_dir.as_deref()), + cargo_bin_dir = HomeDisplay::new(&cargo_bin_dir, home_dir.as_deref()), + rustup_home_message = rustup_home_message, + )); + } + + // Brittle code warning: some duplication in unix::add_to_path + #[cfg(not(windows))] + { + let rcfiles = shell::get_available_shells(process) + .flat_map(|sh| sh.rcs(process).into_iter()) + .map(|rc| format!(" {}", HomeDisplay::new(&rc, home_dir.as_deref()))) + .collect::>(); + let plural = if rcfiles.len() > 1 { "s" } else { "" }; + let rcfiles = rcfiles.join("\n"); + Ok(format!( + pre_install_msg_unix!(), + cargo_home = HomeDisplay::new(&cargo_home, home_dir.as_deref()), + cargo_bin_dir = HomeDisplay::new(&cargo_bin_dir, home_dir.as_deref()), + plural = plural, + rcfiles = rcfiles, + rustup_home_message = rustup_home_message, )) } + #[cfg(windows)] + Ok(format!( + pre_install_msg_win!(), + cargo_home = HomeDisplay::new(&cargo_home, home_dir.as_deref()), + cargo_bin_dir = HomeDisplay::new(&cargo_bin_dir, home_dir.as_deref()), + rustup_home_message = rustup_home_message, + )) } #[cfg(unix)] @@ -796,7 +821,7 @@ fn install_bins(process: &Process, bin_path: &Path, force_hard_links: bool) -> a } pub(crate) fn install_proxies(process: &Process) -> anyhow::Result<()> { - let bin_path = process.cargo_home()?.join("bin"); + let bin_path = process.bin_home()?; install_proxies_with_opts(&bin_path, force_hard_links(process)) } @@ -933,7 +958,7 @@ fn check_proxy_sanity( Ok(()) } -/// Uninstall process: +/// Legacy uninstall process: /// 1. Remove all installed toolchains. /// 2. Remove rustup home. /// 3. Remove all entries in `$CARGO_HOME` except `bin`. @@ -941,6 +966,9 @@ fn check_proxy_sanity( /// 5. Try to remove $CARGO_HOME/bin directory if it's empty. /// 6. Upon successfully removing $CARGO_HOME/bin, clean up $PATH. /// 7. Try to remove $CARGO_HOME directory if it's empty. +/// +/// Category mode removes the current toolchains, env files and binaries, preserving +/// other files in the category homes without separately cleaning legacy homes. pub(crate) fn uninstall( no_prompt: bool, no_modify_path: bool, @@ -953,18 +981,31 @@ pub(crate) fn uninstall( } let process = cfg.process; - let cargo_home = process.cargo_home()?; + let bin_home = process.bin_home()?; - if !cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")).exists() { - return Err(CliError::NotSelfInstalled { p: cargo_home }.into()); + if !bin_home.join(format!("rustup{EXE_SUFFIX}")).exists() { + return Err(CliError::NotSelfInstalled { p: bin_home }.into()); } if !no_prompt { writeln!(process.stdout().lock())?; - let msg = if no_modify_path { + let msg = if process.use_category_home() { + let home_dir = process.home_dir(); + let path_message = if no_modify_path { + "Your `PATH` environment variable and shell profiles will not be modified." + } else { + "Rustup shell setup and PATH entries will be cleaned up where applicable." + }; + format!( + pre_uninstall_category_msg!(), + toolchains_dir = HomeDisplay::new(&cfg.toolchains_dir, home_dir.as_deref()), + env_home = HomeDisplay::new(&cfg.data_dir, home_dir.as_deref()), + bin_home = HomeDisplay::new(&bin_home, home_dir.as_deref()), + path_message = path_message, + ) + } else if no_modify_path { pre_uninstall_msg_no_modify_path!().to_owned() } else { - let bin_home = cargo_home.join("bin"); let cargo_bin_dir = HomeDisplay::new(&bin_home, process.home_dir().as_deref()); format!(pre_uninstall_msg!(), cargo_bin_dir = cargo_bin_dir) }; @@ -980,29 +1021,76 @@ pub(crate) fn uninstall( Toolchain::ensure_removed(cfg, toolchain.into())?; } - info!("removing rustup home"); - - // Delete RUSTUP_HOME - let rustup_dir = home::rustup_home()?; - if rustup_dir.exists() { - utils::remove_dir("rustup_home", &rustup_dir)?; + if !process.use_category_home() { + info!("removing rustup home"); + let rustup_dir = process.rustup_home()?; + if rustup_dir.exists() { + utils::remove_dir("rustup_home", &rustup_dir)?; + } } // Delete rustup. #[cfg(unix)] - clean_cargo_home(no_modify_path, process, &cargo_home)?; + if process.use_category_home() { + let env_home = process.env_home()?; + for sh in shell::get_available_shells(process) { + let env_file = env_home.join(sh.env_script().name); + if env_file.is_file() { + utils::remove_file("env", &env_file)?; + } + } + clean_bin_home(no_modify_path, process, &bin_home)?; + } else { + clean_cargo_home(no_modify_path, process, &process.cargo_home()?)?; + } // 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, &cargo_home)?; + windows::spawn_uninstall_gc(no_modify_path)?; info!("rustup is uninstalled"); Ok(ExitCode::SUCCESS) } +/// Removes Rustup's proxies and binary, then removes the bin directory if empty. +fn clean_bin_home(no_modify_path: bool, process: &Process, bin_home: &Path) -> anyhow::Result<()> { + info!("removing rustup tool links and binary"); + + let rustup_path = bin_home.join(format!("rustup{EXE_SUFFIX}")); + for tool in TOOLS.iter().chain(DUP_TOOLS.iter()) { + let proxy_path = bin_home.join(format!("{tool}{EXE_SUFFIX}")); + if is_same_file(&proxy_path, &rustup_path).unwrap_or(false) { + utils::remove_file("rustup tool proxy", &proxy_path)?; + } + } + utils::remove_file("rustup_bin", &rustup_path)?; + + #[cfg(windows)] + remove_uninstall_registry_entry(process)?; + + let bin_home_display = bin_home.display(); + info!("removing empty bin directory `{bin_home_display}`"); + + match fs::remove_dir(bin_home) { + Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => { + warn!("keeping non-empty bin directory `{bin_home_display}`"); + } + Err(e) => { + return Err(e) + .with_context(|| format!("failed to remove bin directory `{bin_home_display}`")); + } + Ok(()) if !no_modify_path => { + info!("removing bin directory `{bin_home_display}` from $PATH"); + remove_from_path(process)?; + } + Ok(()) => {} + } + Ok(()) +} + /// Remove rustup-owned cargo-home state. /// This removes non-`bin` entries in `$CARGO_HOME`, removes rustup tool links and executable from /// `$CARGO_HOME/bin`, then removes `$CARGO_HOME/bin` and `$CARGO_HOME` only if they are empty. @@ -1012,8 +1100,6 @@ fn clean_cargo_home( process: &Process, cargo_home: &Path, ) -> anyhow::Result<()> { - let cargo_bin = cargo_home.join("bin"); - info!("removing cargo home"); // Delete everything in CARGO_HOME except the bin directory first. @@ -1035,44 +1121,7 @@ fn clean_cargo_home( } } - info!("removing rustup tool links and binary"); - - let rustup_path = cargo_bin.join(format!("rustup{EXE_SUFFIX}")); - - let proxy_paths = TOOLS - .iter() - .chain(DUP_TOOLS.iter()) - .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)?; - } - } - - utils::remove_file("rustup_bin", &rustup_path)?; - - #[cfg(windows)] - remove_uninstall_registry_entry(process)?; - - let cargo_bin_display = cargo_bin.display(); - info!("removing empty cargo bin directory `{cargo_bin_display}`"); - - 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"); - remove_from_path(process)?; - } - Ok(()) => {} - } + clean_bin_home(no_modify_path, process, &cargo_home.join("bin"))?; let cargo_home_display = cargo_home.display(); info!("removing empty cargo home directory `{cargo_home_display}`"); @@ -1211,11 +1260,11 @@ fn parse_new_rustup_version(version: String) -> String { } async fn prepare_update(dl_cfg: &DownloadCfg<'_>) -> anyhow::Result> { - let cargo_home = dl_cfg.process.cargo_home()?; - let rustup_path = cargo_home.join(format!("bin{MAIN_SEPARATOR}rustup{EXE_SUFFIX}")); + let bin_home = dl_cfg.process.bin_home()?; + let rustup_path = bin_home.join(format!("rustup{EXE_SUFFIX}")); if !rustup_path.exists() { - return Err(CliError::NotSelfInstalled { p: cargo_home }.into()); + return Err(CliError::NotSelfInstalled { p: bin_home }.into()); } let self_update_lock = SelfUpdateLock::lock(dl_cfg.process)?; @@ -1438,6 +1487,7 @@ mod tests { with_rustup_home(|home| { let mut vars = HashMap::new(); home.apply(&mut vars); + vars.env("CARGO_HOME", &home.rustupdir); let tp = TestProcess::with_vars(vars); let mut cfg = Cfg::from_env(tp.process.current_dir().unwrap(), false, true, &tp.process).unwrap(); diff --git a/src/cli/self_update/msg.rs b/src/cli/self_update/msg.rs index 98292dd463..58c40156c6 100644 --- a/src/cli/self_update/msg.rs +++ b/src/cli/self_update/msg.rs @@ -10,12 +10,7 @@ macro_rules! pre_install_msg_template { This will download and install the official compiler for the Rust programming language, and its package manager, Cargo. -Rustup metadata and toolchains will be installed into the Rustup -home directory, located at: - - {rustup_home} - -This can be modified with the `RUSTUP_HOME` environment variable. +{rustup_home_message} The Cargo home directory is located at: @@ -24,7 +19,7 @@ The Cargo home directory is located at: This can be modified with the `CARGO_HOME` environment variable. The `cargo`, `rustc`, `rustup` and other commands will be added to -Cargo's bin directory, located at: +the bin home directory, located at: {cargo_bin_dir} @@ -76,7 +71,7 @@ macro_rules! post_install_msg { To get started you may need to restart your current shell. This would reload your `PATH` environment variable to include -Cargo's bin directory (`{cargo_bin_dir}`). +the bin home directory (`{cargo_bin_dir}`). " }; } @@ -85,7 +80,7 @@ macro_rules! post_install_msg_no_modify_path { () => { r"# Rust is installed now. Great! -To get started you need Cargo's bin directory (`{cargo_bin_dir}`) in your `PATH` +To get started you need the bin home directory (`{cargo_bin_dir}`) in your `PATH` environment variable. This has not been done automatically. " }; @@ -117,6 +112,25 @@ This will uninstall all Rust toolchains and data, and remove }; } +macro_rules! pre_uninstall_category_msg { + () => { + r"# Thanks for hacking in Rust! + +This will uninstall the toolchains managed by this installation and remove +Rustup's shell environment files and binaries: + + toolchains: {toolchains_dir} + env files: {env_home} + binaries: {bin_home} + +Other files in these directories and Cargo home data will be kept. + +{path_message} + +" + }; +} + macro_rules! pre_uninstall_msg_no_modify_path { () => { r"# Thanks for hacking in Rust! diff --git a/src/cli/self_update/shell.rs b/src/cli/self_update/shell.rs index 92b4dc984a..3402893592 100644 --- a/src/cli/self_update/shell.rs +++ b/src/cli/self_update/shell.rs @@ -27,13 +27,13 @@ use std::path::{Path, PathBuf}; use anyhow::bail; -use super::utils; +use super::{HomeDisplay, utils}; use crate::process::Process; #[derive(Debug, PartialEq)] pub(crate) struct ShellScript { content: &'static str, - name: &'static str, + pub(super) name: &'static str, } // TODO: Update into a bytestring. @@ -66,7 +66,11 @@ pub(crate) fn build_source_env_lines( ) -> String { let mut groups = Vec::<(_, Vec<_>)>::new(); for shell in get_available_shells(process) { - let Ok(src) = shell.source_string(env_dir, home_dir) else { + let mut env_display = HomeDisplay::new(env_dir, home_dir); + if let Some(p) = env_display.home_prefix.as_mut() { + *p = shell.home_var(); + } + let Ok(src) = shell.source_string(Path::new(&env_display.to_string()), None) else { continue; }; if let Some(names) = groups diff --git a/src/cli/self_update/stage.rs b/src/cli/self_update/stage.rs index a8e99c52bc..e0434c9a66 100644 --- a/src/cli/self_update/stage.rs +++ b/src/cli/self_update/stage.rs @@ -280,7 +280,7 @@ pub fn updater_path(stage: &Path) -> PathBuf { } fn stage_root(process: &Process) -> anyhow::Result { - Ok(process.rustup_home()?.join(SELF_UPDATE_DIRECTORY)) + Ok(process.home_dirs()?.state.join(SELF_UPDATE_DIRECTORY)) } pub const SELF_UPDATE_DIRECTORY: &str = "self-update"; @@ -315,6 +315,28 @@ mod tests { assert!(!Marker::Complete.path(&stage).exists()); } + #[tokio::test] + async fn updater_path_is_stable_with_category_mode_enabled() { + let root = test_dir().unwrap(); + let state_home = root.path().join("state"); + let mut vars = HashMap::new(); + vars.env("RUSTUP_HOME", root.path().join("rustup")); + vars.env("RUSTUP_STATE_HOME", &state_home); + vars.env("RUSTUP_USE_CATEGORY_HOME", "1"); + let process = TestProcess::with_vars(vars); + let first = SelfUpdateLock::lock(&process.process).unwrap(); + let first_path = updater_path(&first.directory); + let stage = first.directory.clone(); + assert_eq!(stage, state_home.join(SELF_UPDATE_DIRECTORY)); + fs::write(&first_path, "").unwrap(); + fs::write(Marker::Complete.path(&stage), "").unwrap(); + drop(first); + let second = prepared_updater(&process); + + assert_eq!(first_path, *second); + assert!(!Marker::Complete.path(&stage).exists()); + } + #[tokio::test] async fn self_update_lock_is_global() { let root = test_dir().unwrap(); diff --git a/src/cli/self_update/unix.rs b/src/cli/self_update/unix.rs index e2ab288f1a..0f51d99aed 100644 --- a/src/cli/self_update/unix.rs +++ b/src/cli/self_update/unix.rs @@ -51,22 +51,25 @@ pub(crate) fn anti_sudo_check( } pub(crate) fn remove_from_path(process: &Process) -> anyhow::Result<()> { - let cargo_home = process.cargo_home()?; + let env_home = process.env_home()?; let home_dir = process.home_dir(); for sh in shell::get_available_shells(process) { - let source_cmd = sh.source_string(&cargo_home, home_dir.as_deref())?; + let source_cmd = sh.source_string(&env_home, home_dir.as_deref())?; // Check more files for cleanup than normally are updated. remove_source_command(&source_cmd, &sh.rc_candidates(process))?; } - remove_legacy_paths(process, &cargo_home, home_dir.as_deref()) + if !process.use_category_home() { + remove_legacy_paths(process, &env_home, home_dir.as_deref())?; + } + Ok(()) } pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { - let cargo_home = process.cargo_home()?; + let env_home = process.env_home()?; let home_dir = process.home_dir(); for sh in shell::get_available_shells(process) { - let source_cmd = sh.source_string(&cargo_home, home_dir.as_deref())?; + let source_cmd = sh.source_string(&env_home, home_dir.as_deref())?; let source_cmd_with_newline = format!("\n{source_cmd}"); for rc in sh.rcs(process) { @@ -88,22 +91,25 @@ pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { } } - remove_legacy_paths(process, &cargo_home, home_dir.as_deref())?; + if !process.use_category_home() { + remove_legacy_paths(process, &env_home, home_dir.as_deref())?; + } Ok(()) } pub(crate) fn write_env_files(process: &Process) -> anyhow::Result<()> { - let cargo_home = process.cargo_home()?; - let bin_dir = cargo_home.join("bin"); + let env_home = process.env_home()?; + let bin_dir = process.bin_home()?; let home_dir = process.home_dir(); + utils::ensure_dir_exists("env", &env_home)?; let mut written = vec![]; for sh in shell::get_available_shells(process) { let script = sh.env_script(); // Only write each possible script once. if !written.contains(&script) { - sh.write_script(&script, &cargo_home, &bin_dir, home_dir.as_deref())?; + sh.write_script(&script, &env_home, &bin_dir, home_dir.as_deref())?; written.push(script); } } @@ -129,16 +135,19 @@ pub(super) fn run_update( Ok(utils::ExitCode(0)) } -/// This function is as the final step of a self-upgrade. It replaces -/// `$CARGO_HOME/bin/rustup` with the running exe, and updates the -/// links to it. +/// This function is the final step of a self-upgrade. It replaces Rustup in +/// the Rustup bin home and updates the proxy links. pub(crate) fn self_replace(process: &Process) -> anyhow::Result { let self_update_lock = SelfUpdateLock::lock(process)?; #[cfg(feature = "test")] process.checkpoint(super::CHECKPOINT_SELF_REPLACE_READY); - let result = process.cargo_home().and_then(|cargo_home| { - self_update_lock.install_bins(&cargo_home.join("bin"), super::force_hard_links(process)) - }); + + let result = process + .bin_home() + .map_err(anyhow::Error::from) + .and_then(|bin_home| { + self_update_lock.install_bins(&bin_home, super::force_hard_links(process)) + }); stage::mark_result(result.is_ok(), process); result?; diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 0305fadb61..65549c0ed7 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -1,16 +1,16 @@ use std::{ borrow::Cow, - env::{consts::EXE_SUFFIX, split_paths}, + env::split_paths, ffi::{OsStr, OsString}, fmt, - fs::OpenOptions, + fs::{File, OpenOptions}, io::{self, Write}, mem, os::windows::{ fs::OpenOptionsExt, io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle}, }, - path::{Path, PathBuf}, + path::PathBuf, process::{Child, Command, Stdio}, ptr, thread, time::Duration, @@ -381,13 +381,17 @@ 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 finish deleting the installation. #[tracing::instrument(level = "trace")] pub fn complete_windows_uninstall(process: &Process) -> anyhow::Result { let uninstall = wait_for_parent(process).and_then(|()| { let no_modify_path = process.var_os(GC_MODIFY_PATH).as_deref() != Some(OsStr::new("1")); - // Now that the parent has exited there are hopefully no more files open in CARGO_HOME. + // Now that the parent has exited, its installed binary can be removed. + if process.use_category_home() { + return super::clean_bin_home(no_modify_path, process, &process.bin_home()?); + } + let cargo_home = process.cargo_home()?; super::clean_cargo_home(no_modify_path, process, &cargo_home) }); @@ -505,7 +509,7 @@ fn wait_for_parent_legacy() -> anyhow::Result<()> { } pub(crate) fn add_to_path(process: &Process) -> anyhow::Result<()> { - let new_path = _with_path_cargo_home_bin(_add_to_path, process)?; + let new_path = _with_path_rustup_bin(_add_to_path, process)?; _apply_new_path(new_path, process) } @@ -599,18 +603,17 @@ fn _remove_from_path(old_path: HSTRING, path_str: HSTRING) -> Option { const PATH_SEPARATOR: u16 = b';' as u16; -fn _with_path_cargo_home_bin(f: F, process: &Process) -> anyhow::Result> +fn _with_path_rustup_bin(f: F, process: &Process) -> anyhow::Result> where F: FnOnce(HSTRING, HSTRING) -> Option, { let windows_path = get_windows_path_var(process)?; - let mut path_str = process.cargo_home()?; - path_str.push("bin"); + let path_str = process.bin_home()?; Ok(windows_path.and_then(|old_path| f(old_path, HSTRING::from(path_str.as_path())))) } pub(crate) fn remove_from_path(process: &Process) -> anyhow::Result<()> { - let new_path = _with_path_cargo_home_bin(_remove_from_path, process)?; + let new_path = _with_path_rustup_bin(_remove_from_path, process)?; _apply_new_path(new_path, process) } @@ -671,8 +674,7 @@ pub(crate) fn add_uninstall_registry_entry(process: &Process) -> anyhow::Result< } } - let mut path = process.cargo_home()?; - path.push("bin\\rustup.exe"); + let path = process.bin_home()?.join("rustup.exe"); let mut uninstall_cmd = OsString::from("\""); uninstall_cmd.push(path); uninstall_cmd.push("\" self uninstall"); @@ -706,10 +708,13 @@ pub(super) fn run_update( pub(crate) fn self_replace(process: &Process) -> anyhow::Result { wait_for_parent(process)?; let self_update_lock = SelfUpdateLock::lock(process)?; - let result = process.cargo_home().and_then(|cargo_home| { - self_update_lock.install_bins(&cargo_home.join("bin"), super::force_hard_links(process))?; - update_uninstall_registry_display_version(env!("CARGO_PKG_VERSION"), process) - }); + let result = process + .bin_home() + .map_err(anyhow::Error::from) + .and_then(|bin_home| { + self_update_lock.install_bins(&bin_home, super::force_hard_links(process))?; + update_uninstall_registry_display_version(env!("CARGO_PKG_VERSION"), process) + }); stage::mark_result(result.is_ok(), process); result?; @@ -722,15 +727,15 @@ pub(crate) fn self_replace(process: &Process) -> anyhow::Result // 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 -// CARGO_HOME/../rustup-gc-$random.exe. +// - Copy the running rustup.exe to a temporary file in +// the system temporary directory as rustup-gc-$random.exe. // - Open the gc exe with the FILE_FLAG_DELETE_ON_CLOSE and // FILE_SHARE_DELETE flags. This is going to be the last // file to remove, and the OS is going to do it for us. // Pass this handle as stdin so the standard library manages inheritance. // GC does not read stdin; it uses it only to carry the deletion handle. // - Run the gc exe, which waits for the original rustup.exe -// process to close, then deletes CARGO_HOME. This process +// process to close, then cleans up the installation. This process // has inherited a FILE_FLAG_DELETE_ON_CLOSE handle to itself. // - Finally, spawn yet another system binary inheriting stdin, // so *it* inherits the FILE_FLAG_DELETE_ON_CLOSE handle to @@ -745,21 +750,24 @@ pub(crate) fn self_replace(process: &Process) -> anyhow::Result // // .. augmented with this SO answer // https://stackoverflow.com/questions/10319526/understanding-a-self-deleting-program-in-c -pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, cargo_home: &Path) -> anyhow::Result<()> { - // The rustup.exe bin - let rustup_path = cargo_home.join(format!("bin/rustup{EXE_SUFFIX}")); - - // The directory containing CARGO_HOME - let work_path = cargo_home - .parent() - .expect("CARGO_HOME doesn't have a parent?"); - - // Generate a unique name for the files we're about to move out - // of CARGO_HOME. - let numbah: u32 = rand::random(); - let gc_exe = work_path.join(format!("rustup-gc-{numbah:x}.exe")); - // Copy rustup (probably this process's exe) to the gc exe - utils::copy_file_symlink_to_source(&rustup_path, &gc_exe)?; +pub(crate) fn spawn_uninstall_gc(no_modify_path: bool) -> anyhow::Result<()> { + // Copy the running executable so GC does not depend on the installed copy. + let rustup_path = utils::current_exe()?; + let mut source = File::open(&rustup_path) + .with_context(|| format!("could not open rustup '{}'", rustup_path.display()))?; + // Use the system temporary directory so GC creation does not require + // write access to CARGO_HOME's parent. + let mut gc_file = tempfile::Builder::new() + .prefix("rustup-gc-") + .suffix(".exe") + .tempfile() + .context("error creating temporary GC executable")?; + // `io::copy` writes its contents into this independent regular file, + // so DELETE_ON_CLOSE applies to the GC copy rather than the source target. + io::copy(&mut source, gc_file.as_file_mut()) + .with_context(|| format!("could not copy rustup from '{}'", rustup_path.display()))?; + // Close the write handle before opening the executable for reading. + let gc_exe = gc_file.into_temp_path(); // OpenOptions preserves the read, sharing and delete-on-close flags while // letting File own the handle until it is passed to Command below. let gc_handle = OpenOptions::new() @@ -769,6 +777,10 @@ pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, cargo_home: &Path) -> any .open(&gc_exe) .context(CliError::WindowsUninstallMadness)?; + // Transfer cleanup to Windows only after the DELETE_ON_CLOSE handle is + // open. Until then, TempPath attempts cleanup if preparation fails. + let gc_exe = gc_exe.keep()?; + let mut command = Command::new(gc_exe); command .stdin(gc_handle) @@ -789,7 +801,7 @@ pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, cargo_home: &Path) -> any } // 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 +// so we use env var here, notifying it if we need to remove the bin home from $PATH const GC_MODIFY_PATH: &str = "RUSTUP_GC_MODIFY_PATH"; // Decimal value of the process handle inherited by GC or the self-replacer. @@ -872,6 +884,61 @@ mod tests { } } + #[test] + fn legacy_mode_uninstall_uses_cargo_bin() { + let cwd = std::env::current_dir().unwrap(); + let cargo_home = cwd.join("cargo home"); + let tp = TestProcess::with_vars(HashMap::from([ + (RUSTUP_REGISTRY_TEST_ID.to_owned(), test_id()), + ( + "CARGO_HOME".to_owned(), + cargo_home.to_str().unwrap().to_owned(), + ), + ("RUSTUP_USE_CATEGORY_HOME".to_owned(), "0".to_owned()), + ])); + add_uninstall_registry_entry(&tp.process).unwrap(); + assert_eq!( + rustup_uninstall_registry_key(&tp.process) + .unwrap() + .get_string("UninstallString") + .unwrap(), + format!( + "\"{}\" self uninstall", + cargo_home.join("bin").join("rustup.exe").display() + ) + ); + } + + #[test] + fn category_mode_uninstall_uses_rustup_bin() { + let cwd = std::env::current_dir().unwrap(); + let cargo_home = cwd.join("cargo home"); + let bin_home = cwd.join("category bin"); + let tp = TestProcess::with_vars(HashMap::from([ + (RUSTUP_REGISTRY_TEST_ID.to_owned(), test_id()), + ( + "CARGO_HOME".to_owned(), + cargo_home.to_str().unwrap().to_owned(), + ), + ( + "RUSTUP_BIN_HOME".to_owned(), + bin_home.to_str().unwrap().to_owned(), + ), + ("RUSTUP_USE_CATEGORY_HOME".to_owned(), "1".to_owned()), + ])); + add_uninstall_registry_entry(&tp.process).unwrap(); + assert_eq!( + rustup_uninstall_registry_key(&tp.process) + .unwrap() + .get_string("UninstallString") + .unwrap(), + format!( + "\"{}\" self uninstall", + bin_home.join("rustup.exe").display() + ) + ); + } + #[test] fn windows_registry_isolated_per_test_id() { let first_id = test_id(); @@ -1066,7 +1133,7 @@ mod tests { // Ok(None) signals no change to the PATH setting layer assert_eq!( None, - _with_path_cargo_home_bin(|_, _| panic!("called"), &tp.process).unwrap() + _with_path_rustup_bin(|_, _| panic!("called"), &tp.process).unwrap() ); assert_eq!( diff --git a/src/config.rs b/src/config.rs index 1b15357ef9..44fb3e2797 100644 --- a/src/config.rs +++ b/src/config.rs @@ -321,7 +321,13 @@ pub(crate) struct Cfg<'a> { fallback_settings: Option, pub toolchains_dir: PathBuf, update_hash_dir: PathBuf, + pub bin_dir: PathBuf, + pub cache_dir: PathBuf, + pub config_dir: PathBuf, + pub data_dir: PathBuf, + pub state_dir: PathBuf, pub download_dir: PathBuf, + pub tmp_dir: PathBuf, pub toolchain_override: Option>, env_override: Option>, pub(crate) dist_root_server: String, @@ -348,10 +354,21 @@ impl<'a> Cfg<'a> { ) -> anyhow::Result { // Set up the rustup home directory let rustup_dir = process.rustup_home()?; + let bin_dir = process.bin_home()?; + let home_dirs = process.home_dirs()?; + let cache_dir = home_dirs.cache; + let config_dir = home_dirs.config; + let data_dir = home_dirs.data; + let state_dir = home_dirs.state; + + if process.use_category_home() { + utils::ensure_dir_exists("config home", &config_dir)?; + utils::ensure_dir_exists("state home", &state_dir)?; + } else { + utils::ensure_dir_exists("home", &config_dir)?; + } - utils::ensure_dir_exists("home", &rustup_dir)?; - - let settings_file = SettingsFile::new(rustup_dir.join("settings.toml")); + let settings_file = SettingsFile::new(config_dir.join("settings.toml")); settings_file.with(|s| { debug!("read metadata version: {}", s.version); if s.version == MetadataVersion::default() { @@ -363,7 +380,7 @@ impl<'a> Cfg<'a> { } })?; - let state_file = StateFile::new(rustup_dir.join("state.toml")); + let state_file = StateFile::new(state_dir.join("state.toml")); // Centralised file for multi-user systems to provide admin/distributor set initial values. #[cfg(unix)] @@ -378,9 +395,10 @@ impl<'a> Cfg<'a> { #[cfg(windows)] let fallback_settings = None; - let toolchains_dir = rustup_dir.join("toolchains"); - let update_hash_dir = rustup_dir.join("update-hashes"); - let download_dir = rustup_dir.join("downloads"); + let toolchains_dir = data_dir.join("toolchains"); + let update_hash_dir = cache_dir.join("update-hashes"); + let download_dir = cache_dir.join("downloads"); + let tmp_dir = cache_dir.join("tmp"); // Environment override let env_override = match &process.var_opt("RUSTUP_TOOLCHAIN")? { @@ -399,7 +417,13 @@ impl<'a> Cfg<'a> { fallback_settings, toolchains_dir, update_hash_dir, + bin_dir, + cache_dir, + config_dir, + data_dir, + state_dir, download_dir, + tmp_dir, toolchain_override: None, env_override, dist_root_server, @@ -1177,7 +1201,13 @@ impl Debug for Cfg<'_> { fallback_settings, toolchains_dir, update_hash_dir, + bin_dir, + cache_dir, + config_dir, + data_dir, + state_dir, download_dir, + tmp_dir, toolchain_override, env_override, dist_root_server, @@ -1196,7 +1226,13 @@ impl Debug for Cfg<'_> { .field("fallback_settings", fallback_settings) .field("toolchains_dir", toolchains_dir) .field("update_hash_dir", update_hash_dir) + .field("bin_dir", bin_dir) + .field("cache_dir", cache_dir) + .field("config_dir", config_dir) + .field("data_dir", data_dir) + .field("state_dir", state_dir) .field("download_dir", download_dir) + .field("tmp_dir", tmp_dir) .field("toolchain_override", toolchain_override) .field("env_override", env_override) .field("dist_root_server", dist_root_server) diff --git a/src/dist/download.rs b/src/dist/download.rs index 01dc0d2942..e6416596fe 100644 --- a/src/dist/download.rs +++ b/src/dist/download.rs @@ -42,7 +42,7 @@ impl<'a> DownloadCfg<'a> { pub(crate) fn new(cfg: &'a Cfg<'a>) -> Self { DownloadCfg { tmp_cx: Arc::new(temp::Context::new( - cfg.rustup_dir.join("tmp"), + cfg.tmp_dir.clone(), cfg.dist_root_server.as_str(), )), download_dir: &cfg.download_dir, diff --git a/src/process.rs b/src/process.rs index 53508789a5..e544f6c861 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,6 +18,7 @@ use std::{ thread, }; +use ::home::env as home_env; use anstream::ColorChoice; use anyhow::{Context, bail}; use indicatif::ProgressDrawTarget; @@ -35,6 +36,8 @@ use crate::{ }; mod file_source; +mod home; +pub(crate) use home::HomeDirs; mod terminal_source; pub use terminal_source::ColorableTerminal; @@ -65,15 +68,65 @@ impl Process { } pub(crate) fn home_dir(&self) -> Option { - home::env::home_dir_with_env(self) + home_env::home_dir_with_env(self) } pub(crate) fn cargo_home(&self) -> anyhow::Result { - home::env::cargo_home_with_env(self).context("failed to determine cargo home") + home_env::cargo_home_with_env(self).context("failed to determine cargo home") } pub(crate) fn rustup_home(&self) -> anyhow::Result { - home::env::rustup_home_with_env(self).context("failed to determine rustup home dir") + home_env::rustup_home_with_env(self).context("failed to determine rustup home dir") + } + + /// Returns Rustup's cache, config, data, and state directories. + /// + /// Category mode resolves each directory independently through [`home`]. + /// Legacy mode uses the resolved Rustup home for all four categories. + /// See [`home`] for platform defaults and path rules. + pub(crate) fn home_dirs(&self) -> io::Result { + if self.use_category_home() { + return HomeDirs::from_env(self); + } + + let home = home_env::rustup_home_with_env(self)?; + Ok(HomeDirs { + cache: home.clone(), + config: home.clone(), + data: home.clone(), + state: home, + }) + } + + /// Returns the directory containing Rustup's shell environment scripts. + /// + /// Category mode uses the data home; legacy mode uses the Cargo home. + #[cfg(any(unix, test))] + pub(crate) fn env_home(&self) -> io::Result { + if self.use_category_home() { + return home::category_home(home::HomeCategory::Data, self); + } + + home_env::cargo_home_with_env(self) + } + + /// Returns Rustup's binary installation directory. + /// + /// Category mode uses the binary directory resolver in [`home`]. + /// Legacy mode appends `bin` to the resolved Cargo home. + pub(crate) fn bin_home(&self) -> io::Result { + if self.use_category_home() { + return home::bin_home(self); + } + + Ok(home_env::cargo_home_with_env(self)?.join("bin")) + } + + /// Category mode is enabled when `RUSTUP_USE_CATEGORY_HOME` is non-empty + /// and not "0"; values such as "false" also enable it. + pub(crate) fn use_category_home(&self) -> bool { + self.var_os("RUSTUP_USE_CATEGORY_HOME") + .is_some_and(|value| value != "0") } pub fn io_thread_count(&self) -> anyhow::Result { @@ -305,10 +358,10 @@ impl From for usize { } } -impl home::env::Env for Process { +impl home_env::Env for Process { fn home_dir(&self) -> Option { match self { - Self::OsProcess(_) => home::env::OS_ENV.home_dir(), + Self::OsProcess(_) => home_env::OS_ENV.home_dir(), #[cfg(feature = "test")] Self::TestProcess(_) => self.var("HOME").ok().map(|v| v.into()), } @@ -316,7 +369,7 @@ impl home::env::Env for Process { fn current_dir(&self) -> Result { match self { - Self::OsProcess(_) => home::env::OS_ENV.current_dir(), + Self::OsProcess(_) => home_env::OS_ENV.current_dir(), #[cfg(feature = "test")] Self::TestProcess(_) => self.current_dir(), } @@ -441,7 +494,7 @@ pub struct TestContext { #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{collections::HashMap, path::Path}; use super::*; use crate::{process::TestProcess, test::Env}; @@ -462,4 +515,79 @@ mod tests { // non-tty + `auto` does not enable the colors. assert_color_choice("aUTo", false, ColorChoice::Never); } + + #[test] + fn legacy_mode_uses_legacy_homes() -> io::Result<()> { + let home = crate::test::test_dir()?; + let home_dir = home.path(); + let mut vars = HashMap::new(); + vars.env("RUSTUP_USE_CATEGORY_HOME", "0"); + vars.env("HOME", home_dir); + vars.env("RUSTUP_CACHE_HOME", "cache"); + vars.env("RUSTUP_CONFIG_HOME", "config"); + vars.env("RUSTUP_DATA_HOME", "data"); + vars.env("RUSTUP_STATE_HOME", "state"); + vars.env("RUSTUP_BIN_HOME", "bin"); + + let tp = TestProcess::with_vars(vars.clone()); + let homes = tp.process.home_dirs()?; + for home in [&homes.cache, &homes.config, &homes.data, &homes.state] { + assert_eq!(home, &home_dir.join(".rustup")); + } + assert_eq!(tp.process.env_home()?, home_dir.join(".cargo")); + assert_eq!(tp.process.bin_home()?, home_dir.join(".cargo/bin")); + + let rustup_home = home_dir.join("legacy"); + let cargo_home = home_dir.join("cargo"); + vars.env("RUSTUP_HOME", &rustup_home); + vars.env("CARGO_HOME", &cargo_home); + let tp = TestProcess::with_vars(vars); + let homes = tp.process.home_dirs()?; + for home in [&homes.cache, &homes.config, &homes.data, &homes.state] { + assert_eq!(home, &rustup_home); + } + assert_eq!(tp.process.env_home()?, cargo_home); + assert_eq!(tp.process.bin_home()?, cargo_home.join("bin")); + Ok(()) + } + + #[test] + fn category_mode_uses_category_homes() -> io::Result<()> { + let home = crate::test::test_dir()?; + let home_dir = home.path(); + let mut vars = HashMap::new(); + vars.env("RUSTUP_USE_CATEGORY_HOME", "1"); + vars.env("HOME", home_dir); + + let tp = TestProcess::with_vars(vars.clone()); + // TestProcess cannot mock the Windows Known Folder API, so default + // category paths are only tested on Unix. + #[cfg(unix)] + { + let homes = tp.process.home_dirs()?; + assert_eq!(homes.cache, home_dir.join(".cache/rustup")); + assert_eq!(homes.config, home_dir.join(".config/rustup")); + assert_eq!(homes.data, home_dir.join(".local/share/rustup")); + assert_eq!(homes.state, home_dir.join(".local/state/rustup")); + assert_eq!(tp.process.env_home()?, home_dir.join(".local/share/rustup")); + } + assert_eq!(tp.process.bin_home()?, home_dir.join(".local/bin")); + + vars.env("RUSTUP_HOME", home_dir.join("legacy")); + vars.env("CARGO_HOME", home_dir.join("cargo")); + vars.env("RUSTUP_CACHE_HOME", "cache"); + vars.env("RUSTUP_CONFIG_HOME", "config"); + vars.env("RUSTUP_DATA_HOME", "data"); + vars.env("RUSTUP_STATE_HOME", "state"); + vars.env("RUSTUP_BIN_HOME", "bin"); + let tp = TestProcess::with_vars(vars); + let homes = tp.process.home_dirs()?; + assert_eq!(homes.cache, Path::new("cache")); + assert_eq!(homes.config, Path::new("config")); + assert_eq!(homes.data, Path::new("data")); + assert_eq!(homes.state, Path::new("state")); + assert_eq!(tp.process.env_home()?, Path::new("data")); + assert_eq!(tp.process.bin_home()?, Path::new("bin")); + Ok(()) + } } diff --git a/src/process/home.rs b/src/process/home.rs new file mode 100644 index 0000000000..ddaec8f1e6 --- /dev/null +++ b/src/process/home.rs @@ -0,0 +1,258 @@ +/*! +Resolve Rustup's category and binary directories in category mode. + +`Process` selects the mode and handles legacy paths separately. This module +only computes paths; it does not check the mode switch, create directories, +or choose paths based on whether they exist. + +# Category directories + +Cache, config, data, and state each resolve independently, in this order: + +1. `RUSTUP__HOME`. +2. `RUSTUP_HOME`. +3. The platform's category directory with `rustup` appended. + +# Binary directory + +The bin directory resolves in this order: + +1. `RUSTUP_BIN_HOME`. +2. `CARGO_HOME` with `bin` appended. +3. The user home directory with `.local/bin` appended. + +Platform category directories: + - follow XDG rules on Unix + - use Known Folders on Windows +Bin Home uses `~/.local/bin` on both platforms + +TODO: The Windows bin default is not yet finalized. +*/ + +use std::{io, path::PathBuf}; + +use home::env::Env; + +#[cfg(unix)] +use self::unix::category_dir; +#[cfg(windows)] +use self::windows::category_dir; + +#[cfg(unix)] +mod unix; +#[cfg(windows)] +mod windows; + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct HomeDirs { + pub(crate) cache: PathBuf, + pub(crate) config: PathBuf, + pub(crate) data: PathBuf, + pub(crate) state: PathBuf, +} + +impl HomeDirs { + /// Resolves each category's directory in category mode. + pub(super) fn from_env(env: &impl Env) -> io::Result { + Ok(Self { + cache: category_home(HomeCategory::Cache, env)?, + config: category_home(HomeCategory::Config, env)?, + data: category_home(HomeCategory::Data, env)?, + state: category_home(HomeCategory::State, env)?, + }) + } +} + +/// Resolves a category directory in category mode. +pub(super) fn category_home(category: HomeCategory, env: &impl Env) -> io::Result { + if let Some(explicit_override) = path_from_env(category.override_env_var(), env) { + return Ok(explicit_override); + } + + if let Some(rustup_home) = path_from_env("RUSTUP_HOME", env) { + if rustup_home.is_absolute() { + return Ok(rustup_home); + } + return Ok(env.current_dir()?.join(rustup_home)); + } + + category_dir(category, env).map(|platform_dir| platform_dir.join("rustup")) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum HomeCategory { + Cache, + Config, + Data, + State, +} + +impl HomeCategory { + const fn override_env_var(self) -> &'static str { + match self { + Self::Cache => "RUSTUP_CACHE_HOME", + Self::Config => "RUSTUP_CONFIG_HOME", + Self::Data => "RUSTUP_DATA_HOME", + Self::State => "RUSTUP_STATE_HOME", + } + } +} + +/// Resolves the binary directory in category mode. +pub(super) fn bin_home(env: &impl Env) -> io::Result { + if let Some(explicit_override) = path_from_env("RUSTUP_BIN_HOME", env) { + return Ok(explicit_override); + } + + if let Some(cargo_home) = path_from_env("CARGO_HOME", env) { + let cargo_home = if cargo_home.is_absolute() { + cargo_home + } else { + env.current_dir()?.join(cargo_home) + }; + return Ok(cargo_home.join("bin")); + } + + env.home_dir() + .map(|home_dir| home_dir.join(".local/bin")) + .ok_or_else(|| io::Error::other("could not find home dir")) +} + +fn path_from_env(key: &str, env: &impl Env) -> Option { + env.var_os(key) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, env}; + + use super::*; + use crate::{process::TestProcess, test::Env as _}; + + #[test] + fn category_home_resolves_explicit_override() -> io::Result<()> { + let cwd = env::current_dir()?; + let overrides = [ + (HomeCategory::Cache, "RUSTUP_CACHE_HOME", cwd.join("cache")), + (HomeCategory::Config, "RUSTUP_CONFIG_HOME", "config".into()), + (HomeCategory::Data, "RUSTUP_DATA_HOME", cwd.join("data")), + (HomeCategory::State, "RUSTUP_STATE_HOME", "state".into()), + ]; + let mut vars = HashMap::new(); + vars.env("RUSTUP_HOME", cwd.join("rustup")); + vars.env("HOME", cwd.join("home")); + for (_, key, explicit_override) in &overrides { + vars.env(key, explicit_override); + } + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + for (category, _, expected) in overrides { + assert_eq!(category_home(category, &tp.process)?, expected); + } + Ok(()) + } + + #[test] + fn category_home_resolves_absolute_rustup_home() -> io::Result<()> { + let cwd = env::current_dir()?; + let rustup_home = cwd.join("legacy"); + let home_dir = cwd.join("home"); + let mut vars = HashMap::new(); + vars.env("RUSTUP_HOME", &rustup_home); + vars.env("HOME", &home_dir); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + assert_eq!(category_home(category, &tp.process)?, rustup_home); + } + Ok(()) + } + + #[test] + fn category_home_resolves_relative_rustup_home() -> io::Result<()> { + let cwd = env::current_dir()?; + let rustup_home = "rustup"; + let mut vars = HashMap::new(); + vars.env("RUSTUP_HOME", rustup_home); + vars.env("HOME", cwd.join("home")); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + let expected = cwd.join("rustup"); + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + assert_eq!(category_home(category, &tp.process)?, expected); + } + Ok(()) + } + + #[test] + fn bin_home_resolves_explicit_override() -> io::Result<()> { + let cwd = env::current_dir()?; + for explicit_override in [cwd.join("tools"), "tools".into()] { + let mut vars = HashMap::new(); + vars.env("RUSTUP_BIN_HOME", &explicit_override); + vars.env("CARGO_HOME", cwd.join("cargo")); + vars.env("HOME", cwd.join("home")); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + assert_eq!(bin_home(&tp.process)?, explicit_override); + } + Ok(()) + } + + #[test] + fn bin_home_resolves_absolute_cargo_home() -> io::Result<()> { + let cwd = env::current_dir()?; + let cargo_home = cwd.join("cargo-home"); + let mut vars = HashMap::new(); + vars.env("CARGO_HOME", &cargo_home); + vars.env("HOME", cwd.join("home")); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + assert_eq!(bin_home(&tp.process)?, cargo_home.join("bin")); + Ok(()) + } + + #[test] + fn bin_home_resolves_relative_cargo_home() -> io::Result<()> { + let cwd = env::current_dir()?; + let mut vars = HashMap::new(); + vars.env("CARGO_HOME", "cargo"); + vars.env("HOME", cwd.join("home")); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + assert_eq!(bin_home(&tp.process)?, cwd.join("cargo/bin")); + Ok(()) + } + + #[test] + fn bin_home_resolves_platform_default() -> io::Result<()> { + let cwd = env::current_dir()?; + let home_dir = cwd.join("home"); + let mut vars = HashMap::new(); + vars.env("HOME", &home_dir); + + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + assert_eq!(bin_home(&tp.process)?, home_dir.join(".local/bin")); + Ok(()) + } + + #[test] + fn bin_home_without_home_errors() { + let tp = TestProcess::default(); + let error = bin_home(&tp.process).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::Other); + assert_eq!(error.to_string(), "could not find home dir"); + } +} diff --git a/src/process/home/unix.rs b/src/process/home/unix.rs new file mode 100644 index 0000000000..3204d5fc4e --- /dev/null +++ b/src/process/home/unix.rs @@ -0,0 +1,175 @@ +//! Unix XDG platform defaults. +//! +//! Empty and relative XDG values are ignored; defaults are relative to the +//! user home directory returned by [`Env::home_dir`]. + +use std::{ + io::{self, Result}, + path::PathBuf, +}; + +use home::env::Env; +use tracing::warn; + +use super::{HomeCategory, path_from_env}; + +/// See . +pub(super) fn category_dir(category: HomeCategory, env: &impl Env) -> Result { + let xdg_env_var = category.xdg_env_var(); + let relative_xdg_path = match path_from_env(xdg_env_var, env) { + Some(path) if path.is_absolute() => return Ok(path), + path => path, + }; + let Some(home_dir) = env.home_dir() else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "home directory is not set", + )); + }; + + let fallback = home_dir.join(category.fallback_subdir()); + if let Some(relative) = relative_xdg_path { + warn!( + "ignoring relative {xdg_env_var} path {}; falling back to {}", + relative.display(), + fallback.display() + ); + } + Ok(fallback) +} + +impl HomeCategory { + const fn xdg_env_var(self) -> &'static str { + match self { + Self::Cache => "XDG_CACHE_HOME", + Self::Config => "XDG_CONFIG_HOME", + Self::Data => "XDG_DATA_HOME", + Self::State => "XDG_STATE_HOME", + } + } + + const fn fallback_subdir(self) -> &'static str { + match self { + Self::Cache => ".cache", + Self::Config => ".config", + Self::Data => ".local/share", + Self::State => ".local/state", + } + } +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, env, path::Path}; + + use super::*; + use crate::{process::TestProcess, test::Env as _}; + + #[test] + fn category_dir_resolves_absolute_xdg_home() -> Result<()> { + let cwd = env::current_dir()?; + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + let xdg_home = cwd.join(category.fallback_subdir()); + let mut vars = HashMap::new(); + vars.env(category.xdg_env_var(), &xdg_home); + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + + assert_eq!(category_dir(category, &tp.process)?, xdg_home); + } + Ok(()) + } + + #[test] + fn category_dir_resolves_platform_default() -> Result<()> { + let cwd = env::current_dir()?; + let mut vars = HashMap::new(); + vars.env("HOME", &cwd); + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + assert_eq!( + category_dir(category, &tp.process)?, + cwd.join(category.fallback_subdir()) + ); + } + Ok(()) + } + + #[test] + fn category_dir_ignores_relative_xdg_home() -> Result<()> { + let cwd = env::current_dir()?; + let relative_dir = "theory/of/relativity"; + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + let key = category.xdg_env_var(); + let mut vars = HashMap::new(); + vars.env("HOME", &cwd); + vars.env(key, relative_dir); + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + let expected = cwd.join(category.fallback_subdir()); + + assert_eq!(category_dir(category, &tp.process)?, expected); + assert_eq!( + tp.stderr(), + format!( + "warn: ignoring relative {key} path {relative_dir}; falling back to {}\n", + expected.display() + ) + .as_bytes() + ); + } + Ok(()) + } + + #[test] + fn category_dir_resolves_relative_home() -> Result<()> { + let cwd = env::current_dir()?; + let home_dir = Path::new("theory/of/relativity"); + let mut vars = HashMap::new(); + vars.env("HOME", home_dir); + let tp = TestProcess::new(&cwd, &[] as &[&str], vars, ""); + + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + assert_eq!( + category_dir(category, &tp.process)?, + home_dir.join(category.fallback_subdir()) + ); + } + Ok(()) + } + + #[test] + fn category_dir_without_home_errors() { + let tp = TestProcess::default(); + + for category in [ + HomeCategory::Cache, + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + let error = category_dir(category, &tp.process).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert_eq!(error.to_string(), "home directory is not set"); + } + } +} diff --git a/src/process/home/windows.rs b/src/process/home/windows.rs new file mode 100644 index 0000000000..4e0f857058 --- /dev/null +++ b/src/process/home/windows.rs @@ -0,0 +1,70 @@ +use std::{ffi::OsString, io, os::windows::ffi::OsStringExt, path::PathBuf, ptr, slice}; + +use home::env::Env; +use windows_result::HRESULT; +use windows_sys::Win32::{ + System::Com::CoTaskMemFree, + UI::Shell::{ + FOLDERID_LocalAppData, FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath, + }, +}; + +use super::HomeCategory; + +pub(super) fn category_dir(category: HomeCategory, _env: &impl Env) -> io::Result { + known_folder(match category { + HomeCategory::Cache => &FOLDERID_LocalAppData, + HomeCategory::Config | HomeCategory::Data | HomeCategory::State => &FOLDERID_RoamingAppData, + }) +} + +fn known_folder(id: &windows_sys::core::GUID) -> io::Result { + let mut path = ptr::null_mut(); + + // SAFETY: `SHGetKnownFolderPath` initializes `path` with a CoTaskMem-allocated, + // null-terminated UTF-16 string on success. `CoTaskMemFree` accepts null and is + // called on both result paths; the success path reads only through the terminator. + unsafe { + let result = HRESULT(SHGetKnownFolderPath( + id, + KF_FLAG_DONT_VERIFY as u32, + ptr::null_mut(), + &mut path, + )); + if let Err(error) = result.ok() { + CoTaskMemFree(path.cast()); + return Err(error.into()); + } + + let result = OsString::from_wide(slice::from_raw_parts(path, wcslen(path))); + CoTaskMemFree(path.cast()); + Ok(result.into()) + } +} + +unsafe extern "C" { + fn wcslen(buf: *const u16) -> usize; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::TestProcess; + + #[test] + fn category_dir_resolves_platform_default() -> io::Result<()> { + let tp = TestProcess::default(); + let local = known_folder(&FOLDERID_LocalAppData)?; + let roaming = known_folder(&FOLDERID_RoamingAppData)?; + + assert_eq!(category_dir(HomeCategory::Cache, &tp.process)?, local); + for category in [ + HomeCategory::Config, + HomeCategory::Data, + HomeCategory::State, + ] { + assert_eq!(category_dir(category, &tp.process)?, roaming); + } + Ok(()) + } +} diff --git a/src/test/clitools.rs b/src/test/clitools.rs index b5268216b2..cecf4054c3 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -802,6 +802,12 @@ async fn setup_test_state(test_dist_dir: TempDir) -> (TempDir, Config) { env::remove_var("CARGO"); env::remove_var("RUSTUP_AUTO_INSTALL"); env::remove_var("RUSTUP_UPDATE_ROOT"); + env::remove_var("RUSTUP_CACHE_HOME"); + env::remove_var("RUSTUP_CONFIG_HOME"); + env::remove_var("RUSTUP_DATA_HOME"); + env::remove_var("RUSTUP_STATE_HOME"); + env::remove_var("RUSTUP_BIN_HOME"); + env::remove_var("RUSTUP_USE_CATEGORY_HOME"); env::remove_var("RUSTUP_TOOLCHAIN"); env::remove_var("SHELL"); env::remove_var("ZDOTDIR"); @@ -811,6 +817,9 @@ async fn setup_test_state(test_dist_dir: TempDir) -> (TempDir, Config) { env::set_var("TERM", "dumb"); // Removed to avoid leaking the developer's environment into the test env::remove_var("XDG_CONFIG_HOME"); + env::remove_var("XDG_CACHE_HOME"); + env::remove_var("XDG_DATA_HOME"); + env::remove_var("XDG_STATE_HOME"); match env::var("RUSTUP_BACKTRACE") { Ok(val) => env::set_var("RUST_BACKTRACE", val), diff --git a/src/toolchain.rs b/src/toolchain.rs index 1fa1275956..95a8e98a26 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -146,15 +146,29 @@ impl<'a> Toolchain<'a> { // cargo home. Rustup does not read HOME on Windows whereas the older // versions of Cargo did. Rustup and Cargo should be in sync now (both // using the same `home` crate), but this is retained to ensure cargo - // and rustup agree in older versions. - if let Ok(cargo_home) = self.cfg.process.cargo_home() { + // and rustup agree in older versions in legacy mode. + // In category mode, + // only resolve a non-empty CARGO_HOME; otherwise leave the inherited + // environment unchanged so Cargo can choose its own default. + let use_category_home = self.cfg.process.use_category_home(); + // TODO: replace `process.cargo_home()` once we don't rely on `home` crate anymore + // currently CARGO_HOME visiblility is checked twice. + if (!use_category_home || self.cfg.process.var_os("CARGO_HOME").is_some()) + && let Ok(cargo_home) = self.cfg.process.cargo_home() + { cmd.env("CARGO_HOME", &cargo_home); } - env_var::inc("RUST_RECURSION_COUNT", cmd, self.cfg.process); cmd.env("RUSTUP_TOOLCHAIN", format!("{}", self.name)); - cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); + cmd.env("RUSTUP_CACHE_HOME", &self.cfg.cache_dir); + cmd.env("RUSTUP_CONFIG_HOME", &self.cfg.config_dir); + cmd.env("RUSTUP_DATA_HOME", &self.cfg.data_dir); + cmd.env("RUSTUP_STATE_HOME", &self.cfg.state_dir); + cmd.env("RUSTUP_BIN_HOME", &self.cfg.bin_dir); + if !use_category_home { + cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); + } } /// Apply the appropriate LD path for a command being run from a toolchain. @@ -202,14 +216,11 @@ impl<'a> Toolchain<'a> { env_var::insert_path(sysenv::LOADER_PATH, new_path, None, cmd, self.cfg.process); - // Prepend CARGO_HOME/bin to the PATH variable so that we're sure to run + // Prepend the proxy directory to the PATH variable so that we're sure to run // cargo/rustc via the proxy bins. There is no fallback case for if the // proxy bins don't exist. We'll just be running whatever happens to // be on the PATH. - let mut path_entries = vec![]; - if let Ok(cargo_home) = self.cfg.process.cargo_home() { - path_entries.push(cargo_home.join("bin")); - } + let mut path_entries = vec![self.cfg.bin_dir.clone()]; // On Windows, we append the "bin" directory to PATH by default. // Windows loads DLLs from PATH and the "bin" directory contains DLLs diff --git a/src/toolchain/channels.rs b/src/toolchain/channels.rs index 3429f7da71..c3d4e27f59 100644 --- a/src/toolchain/channels.rs +++ b/src/toolchain/channels.rs @@ -226,7 +226,7 @@ impl<'a> ChannelToolchain<'a> { // the documentation for the lpCommandLine argument of CreateProcess. #[cfg(windows)] let exe_path = { - let fallback_dir = self.toolchain.cfg.rustup_dir.join("fallback"); + let fallback_dir = self.toolchain.cfg.data_dir.join("fallback"); fs::create_dir_all(&fallback_dir) .context("unable to create dir to hold fallback exe")?; let fallback_file = fallback_dir.join("cargo.exe"); diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index 20ebaf71bd..da49cf3685 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -657,6 +657,48 @@ help: run 'rustup default stable' to download the latest stable release of Rust "#]]); } +#[tokio::test] +async fn default_config_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let config_home = cx.config.current_dir().join("relative/config"); + let config_home_env = config_home.to_str().unwrap(); + std::fs::remove_file(cx.config.rustupdir.join("settings.toml")).unwrap(); + + cx.config + .expect_with_env( + ["rustup", "default", "stable"], + [ + ("RUSTUP_CONFIG_HOME", config_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .is_ok(); + + assert!(config_home.join("settings.toml").is_file()); +} + +#[tokio::test] +async fn default_config_with_category_mode_disabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let config_home = cx.config.current_dir().join("relative/config"); + let config_home_env = config_home.to_str().unwrap(); + std::fs::remove_file(cx.config.rustupdir.join("settings.toml")).unwrap(); + + cx.config + .expect_with_env( + ["rustup", "default", "stable"], + [ + ("RUSTUP_CONFIG_HOME", config_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "0"), + ], + ) + .await + .is_ok(); + + assert!(cx.config.rustupdir.join("settings.toml").is_file()); +} + #[tokio::test] async fn list_targets() { let cx = CliTestContext::new(Scenario::SimpleV2).await; diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 4f94466b5c..45b6b71f62 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -70,8 +70,16 @@ async fn smoke_case_install_no_modify_path() { ("SHELL", "/bin/sh"), ], ) + .extend_redactions([("[RUSTUP_DIR]", &cx.config.rustupdir.to_string())]) .with_stdout(snapbox::str![[r#" ... +Rustup metadata and toolchains will be installed into the Rustup +home directory, located at: + + [RUSTUP_DIR] + +This can be modified with the RUSTUP_HOME environment variable. +... This path needs to be in your PATH environment variable, but will not be added automatically. @@ -102,7 +110,7 @@ Rust is installed now. Great! ... Rust is installed now. Great! -To get started you need Cargo's bin directory (%USERPROFILE%/.cargo/bin) in[..] +To get started you need the bin home directory (%USERPROFILE%/.cargo/bin) in[..] your PATH environment variable. This has not been done automatically. @@ -113,7 +121,7 @@ Press the Enter key to continue. ... Rust is installed now. Great! -To get started you need Cargo's bin directory ($HOME/.cargo/bin) in your PATH +To get started you need the bin home directory ($HOME/.cargo/bin) in your PATH environment variable. This has not been done automatically. To configure your current shell, you need to source the @@ -155,7 +163,7 @@ Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include -Cargo's bin directory (%USERPROFILE%/.cargo/bin). +the bin home directory (%USERPROFILE%/.cargo/bin). Press the Enter key to continue. @@ -166,7 +174,7 @@ Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include -Cargo's bin directory ($HOME/.cargo/bin). +the bin home directory ($HOME/.cargo/bin). To configure your current shell, you need to source the corresponding env file under $HOME/.cargo. @@ -179,6 +187,73 @@ Consider running the right command for your shell (note the leading DOT): }); } +#[cfg(unix)] +#[tokio::test] +async fn category_install_displays_shell_setup() { + let cx = CliTestContext::new(Scenario::Empty).await; + let data_home = cx.config.homedir.join("data"); + let bin_home = cx.config.homedir.join("test-bin-home"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_DATA_HOME", data_home.to_str().unwrap()), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ("PATH", cx.config.exedir.to_str().unwrap()), + ("SHELL", "/bin/sh"), + ]; + + cx.config + .expect_with_env(["rustup-init", "-y", "--default-toolchain", "none"], env) + .await + .is_ok() + .with_stdout(snapbox::str![[r#" +... +the bin home directory ($HOME/test-bin-home). +... +corresponding env file under $HOME/data. +... + . "$HOME/data/env" # For sh/ash/dash/pdksh +... +"#]]); +} + +#[tokio::test] +async fn reinstall_ignores_own_bin_in_path_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::Empty).await; + let bin_home = cx.config.homedir.join("bin"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ("PATH", bin_home.to_str().unwrap()), + ("RUSTUP_INIT_SKIP_PATH_CHECK", ""), + ]; + cx.config + .expect_with_env( + [ + "rustup-init", + "-y", + "--default-toolchain", + "none", + "--no-modify-path", + ], + env, + ) + .await + .is_ok(); + run_input_with_env( + &cx.config, + &[ + "rustup-init", + "--default-toolchain", + "none", + "--no-modify-path", + ], + "\n\n", + &env, + ) + .is_ok() + .without_stderr("It looks like you have an existing installation of Rust"); +} + #[tokio::test] async fn blank_lines_around_stderr_log_output_update() { let cx = CliTestContext::new(Scenario::SimpleV2).await; @@ -333,6 +408,109 @@ no active toolchain .is_ok(); } +#[tokio::test] +async fn install_initializes_homes_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let config_home = cx.config.current_dir().join("relative/config"); + let state_home = cx.config.current_dir().join("relative/state"); + let data_home = cx.config.current_dir().join("relative/data"); + let cache_home = cx.config.current_dir().join("relative/cache"); + let mut cmd = cx.config.cmd( + "rustup-init", + ["-y", "--no-modify-path", "--default-toolchain", "none"], + ); + cmd.env_remove("RUSTUP_HOME"); + cmd.env("RUSTUP_CONFIG_HOME", "relative/config"); + cmd.env("RUSTUP_STATE_HOME", "relative/state"); + cmd.env("RUSTUP_DATA_HOME", "relative/data"); + cmd.env("RUSTUP_CACHE_HOME", "relative/cache"); + cmd.env("RUSTUP_USE_CATEGORY_HOME", "1"); + assert!(cmd.output().unwrap().status.success()); + + assert!(!cx.config.homedir.join(".rustup").exists()); + assert!(config_home.is_dir()); + assert!(state_home.is_dir()); + assert!(data_home.is_dir()); + assert!(cache_home.is_dir()); +} + +#[tokio::test] +async fn install_displays_homes_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let config_home = cx.config.homedir.join("config"); + let state_home = cx.config.homedir.join("state"); + let data_home = cx.config.homedir.join("data"); + let cache_home = cx.config.homedir.join("cache"); + let home_var = if cfg!(windows) { + "%USERPROFILE%" + } else { + "$HOME" + }; + + run_input_with_env( + &cx.config, + &["rustup-init", "--no-modify-path"], + "3\n", + &[ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_CONFIG_HOME", config_home.to_str().unwrap()), + ("RUSTUP_STATE_HOME", state_home.to_str().unwrap()), + ("RUSTUP_DATA_HOME", data_home.to_str().unwrap()), + ("RUSTUP_CACHE_HOME", cache_home.to_str().unwrap()), + ], + ) + .extend_redactions([("[HOME_VAR]", home_var)]) + .with_stdout(snapbox::str![[r#" +... +Rustup will use these directories: + + config: [HOME_VAR]/config + state: [HOME_VAR]/state + data: [HOME_VAR]/data + cache: [HOME_VAR]/cache + +They can be modified individually with +RUSTUP_CONFIG_HOME, RUSTUP_STATE_HOME, RUSTUP_DATA_HOME, and +RUSTUP_CACHE_HOME. +... +"#]]) + .is_ok(); +} + +#[tokio::test] +async fn install_displays_category_homes_when_paths_match_legacy() { + let cx = CliTestContext::new(Scenario::Empty).await; + let home = cx.config.rustupdir.to_string(); + run_input_with_env( + &cx.config, + &["rustup-init", "--no-modify-path"], + "3\n", + &[ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_CONFIG_HOME", &home), + ("RUSTUP_STATE_HOME", &home), + ("RUSTUP_DATA_HOME", &home), + ("RUSTUP_CACHE_HOME", &home), + ], + ) + .extend_redactions([("[RUSTUP_DIR]", &home)]) + .with_stdout(snapbox::str![[r#" +... +Rustup will use these directories: + + config: [RUSTUP_DIR] + state: [RUSTUP_DIR] + data: [RUSTUP_DIR] + cache: [RUSTUP_DIR] + +They can be modified individually with +RUSTUP_CONFIG_HOME, RUSTUP_STATE_HOME, RUSTUP_DATA_HOME, and +RUSTUP_CACHE_HOME. +... +"#]]) + .is_ok(); +} + #[tokio::test] async fn with_no_toolchain_doesnt_hang() { let cx = CliTestContext::new(Scenario::SimpleV2).await; @@ -715,12 +893,44 @@ version = "12""#, let temp_dir_path = temp_dir.path().to_str().unwrap(); let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect_with_env( + ["rustup-init", "-y", "--no-modify-path"], + [("RUSTUP_HOME", temp_dir_path)], + ) + .await + .is_ok() + .with_stderr(snapbox::str![[r#" +... +warn: it looks like you have an existing rustup settings file at: +... +"#]]); +} + +#[tokio::test] +async fn install_warns_about_existing_settings_file_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let config_home = cx.config.current_dir().join("relative/config"); + let config_home_env = config_home.to_str().unwrap(); + let settings_file = config_home.join("settings.toml"); + fs::create_dir_all(settings_file.parent().unwrap()).unwrap(); + raw::write_file( + &settings_file, + &format!( + r#"default_toolchain = "{}" +profile = "default" +version = "12""#, + this_host_tuple() + ), + ) + .unwrap(); + cx.config .expect_with_env( ["rustup-init", "-y", "--no-modify-path"], [ - ("RUSTUP_INIT_SKIP_PATH_CHECK", "no"), - ("RUSTUP_HOME", temp_dir_path), + ("RUSTUP_CONFIG_HOME", config_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), ], ) .await diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 46d748ada4..f9a350a62a 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -487,6 +487,115 @@ async fn rustup_doesnt_prepend_path_unnecessarily() { ); } +#[tokio::test] +async fn rustup_doesnt_prepend_path_unnecessarily_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let bin_home = cx.config.current_dir().join("bin"); + let bin_home_env = bin_home.to_str().unwrap(); + cx.config + .expect_with_env( + ["rustup", "default", "nightly"], + [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home_env), + ], + ) + .await + .is_ok(); + + let assert_ok_with_paths = |assert: &Assert, data| { + assert.is_ok(); + let stderr = std::env::split_paths(&assert.output.stderr) + .format_with("\n", |p, f| f(&p.display())) + .to_string(); + let stderr = assert.redact(&stderr); + snapbox::assert_data_eq!(stderr, data); + }; + + // For all of these, bin home will be auto-prepended. + assert_ok_with_paths( + cx.config + .expect_with_env( + ["cargo", "--echo-env", "PATH"], + [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home_env), + ], + ) + .await + .extend_redactions([("[BIN_HOME]", &bin_home)]), + snapbox::str![[r#" +[BIN_HOME] +... +"#]], + ); + + assert_ok_with_paths( + cx.config + .expect_with_env( + ["cargo", "--echo-env", "PATH"], + [ + ("PATH", ""), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home_env), + ], + ) + .await + .extend_redactions([("[BIN_HOME]", &bin_home)]), + snapbox::str![[r#" +[BIN_HOME] +... +"#]], + ); + + // Check that bin home is prepended to path. + assert_ok_with_paths( + cx.config + .expect_with_env( + ["cargo", "--echo-env", "PATH"], + [ + ("PATH", &*cx.config.exedir.display().to_string()), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home_env), + ], + ) + .await + .extend_redactions([("[BIN_HOME]", &bin_home), ("[EXEDIR]", &cx.config.exedir)]), + snapbox::str![[r#" +[BIN_HOME] +[EXEDIR] +... +"#]], + ); + + // But if bin home is already on PATH, it will not be prepended again, + // so exedir will take precedence. + assert_ok_with_paths( + cx.config + .expect_with_env( + ["cargo", "--echo-env", "PATH"], + [ + ( + "PATH", + std::env::join_paths([&cx.config.exedir, &bin_home]) + .unwrap() + .to_str() + .unwrap(), + ), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home_env), + ], + ) + .await + .extend_redactions([("[BIN_HOME]", &bin_home), ("[EXEDIR]", &cx.config.exedir)]), + snapbox::str![[r#" +[EXEDIR] +[BIN_HOME] +... +"#]], + ); +} + #[tokio::test] async fn rustup_failed_path_search() { let cx = CliTestContext::new(Scenario::SimpleV2).await; diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index cc723459ef..20067902fd 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -69,6 +69,55 @@ export PATH="$HOME/apple/bin" } } + #[tokio::test] + async fn category_install_creates_env_in_data_home() { + let cx = CliTestContext::new(Scenario::Empty).await; + let data_home = cx.config.homedir.join("data"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_DATA_HOME", data_home.to_str().unwrap()), + ("PATH", cx.config.exedir.to_str().unwrap()), + ("SHELL", "/bin/sh"), + ]; + + cx.config.expect_with_env(INIT_NONE, env).await.is_ok(); + + assert!(data_home.join("env").is_file()); + } + + #[tokio::test] + async fn category_uninstall_preserves_legacy_installation() { + let cx = CliTestContext::new(Scenario::Empty).await; + cx.config.expect(INIT_NONE).await.is_ok(); + let legacy_env = fs::read_to_string(cx.config.cargodir.join("env")).unwrap(); + let profile = cx.config.homedir.join(".profile"); + let legacy_profile = fs::read_to_string(&profile).unwrap(); + let data_home = cx.config.homedir.join("data"); + let bin_home = cx.config.homedir.join("bin"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_DATA_HOME", data_home.to_str().unwrap()), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ]; + cx.config.expect_with_env(INIT_NONE, env).await.is_ok(); + let settings_file = cx.config.rustupdir.rustupdir.join("settings.toml"); + let settings = fs::read_to_string(&settings_file).unwrap(); + cx.config + .expect_with_env(["rustup", "self", "uninstall", "-y"], env) + .await + .is_ok(); + + assert!(!data_home.join("env").exists()); + assert!(!bin_home.exists()); + assert_eq!(fs::read_to_string(&profile).unwrap(), legacy_profile); + assert_eq!( + fs::read_to_string(cx.config.cargodir.join("env")).unwrap(), + legacy_env + ); + assert!(cx.config.cargodir.join("bin/rustup").is_file()); + assert_eq!(fs::read_to_string(settings_file).unwrap(), settings); + } + #[tokio::test] async fn install_updates_bash_rcs() { let cx = CliTestContext::new(Scenario::Empty).await; @@ -552,6 +601,45 @@ mod windows { ); } + #[tokio::test] + async fn category_install_uninstall_affect_path() { + let cx = CliTestContext::new(Scenario::Empty).await; + let test_id = &cx.config.test_registry_id; + let bin_home = cx.config.homedir.join("bin"); + let bin_home_os_str = bin_home.as_os_str(); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ]; + let read_path = |test_id: &str| -> Option<_> { + retry( + Fibonacci::from_millis(1).map(jitter).take(21), + || match get_path(test_id).unwrap() { + Some(v) => OperationResult::Ok(HSTRING::try_from(v).unwrap().to_os_string()), + None => OperationResult::Retry(()), + }, + ) + .ok() + }; + + cx.config.expect_with_env(INIT_NONE, env).await.is_ok(); + let after_install = read_path(test_id).unwrap_or_default(); + assert!( + os_str_contains(&after_install, bin_home_os_str), + "`{bin_home_os_str:?}` not in `{after_install:?}`", + ); + + cx.config + .expect_with_env(["rustup", "self", "uninstall", "-y"], env) + .await + .is_ok(); + let after_uninstall = read_path(test_id).unwrap_or_default(); + assert!( + !os_str_contains(&after_uninstall, bin_home_os_str), + "`{bin_home_os_str:?}` in `{after_uninstall:?}`", + ); + } + #[tokio::test] async fn uninstall_keeps_path_when_cargo_bin_is_non_empty() { let cx = CliTestContext::new(Scenario::Empty).await; diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index b5bbd98fd3..87cf7633d5 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -769,7 +769,7 @@ custom async fn fallback_cargo_calls_correct_rustc() { let cx = CliTestContext::new(Scenario::SimpleV2).await; // Hm, this is the _only_ test that assumes that toolchain proxies - // exist in CARGO_HOME. Adding that proxy here. + // exist in bin home. Adding that proxy here. let rustup_path = cx.config.exedir.join(format!("rustup{EXE_SUFFIX}")); let cargo_bin_path = cx.config.cargodir.join("bin"); fs::create_dir_all(&cargo_bin_path).unwrap(); @@ -824,6 +824,76 @@ async fn fallback_cargo_calls_correct_rustc() { .is_ok(); } +#[tokio::test] +async fn fallback_cargo_calls_correct_rustc_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let data_home = cx.config.current_dir().join("relative/data"); + let data_home_env = data_home.to_str().unwrap(); + let data_env = [ + ("RUSTUP_DATA_HOME", data_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ]; + // Hm, this is the _only_ test that assumes that toolchain proxies + // exist in bin home. Adding that proxy here. + let rustup_path = cx.config.exedir.join(format!("rustup{EXE_SUFFIX}")); + let cargo_bin_path = cx.config.cargodir.join("bin"); + fs::create_dir_all(&cargo_bin_path).unwrap(); + let rustc_path = cargo_bin_path.join(format!("rustc{EXE_SUFFIX}")); + fs::hard_link(rustup_path, &rustc_path).unwrap(); + + // Install a custom toolchain and a nightly toolchain for the cargo fallback + let path = cx.config.customdir.join("custom-1"); + let path = path.to_string_lossy(); + cx.config + .expect_with_env(["rustup", "toolchain", "link", "custom", &path], data_env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustup", "default", "custom"], data_env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustup", "update", "nightly"], data_env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustc", "--version"], data_env) + .await + .with_stdout(snapbox::str![[r#" +1.0.0 (hash-c-1) + +"#]]) + .is_ok(); + cx.config + .expect_with_env(["cargo", "--version"], data_env) + .await + .with_stdout(snapbox::str![[r#" +1.3.0 (hash-nightly-2) + +"#]]) + .is_ok(); + + assert!(rustc_path.exists()); + + // Here --call-rustc tells the mock cargo bin to exec `rustc --version`. + // We should be ultimately calling the custom rustc, according to the + // RUSTUP_TOOLCHAIN variable set by the original "cargo" proxy, and + // interpreted by the nested "rustc" proxy. + cx.config + .expect_with_env(["cargo", "--call-rustc"], data_env) + .await + .with_stdout(snapbox::str![[r#" +1.0.0 (hash-c-1) + +"#]]) + .is_ok(); + + #[cfg(windows)] + { + assert!(data_home.join("fallback/cargo.exe").is_file()); + } +} + // Checks that cargo can recursively invoke itself with rustup shorthand (via // the proxy). // @@ -891,6 +961,144 @@ error: infinite recursion detected .is_err(); } +#[tokio::test] +async fn recursive_cargo_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::ArchivesV2).await; + cx.config + .expect_with_env( + ["rustup", "default", "nightly"], + [("RUSTUP_USE_CATEGORY_HOME", "1")], + ) + .await + .is_ok(); + + // We need an intermediary to run cargo itself. + // The "mock" cargo can't do that because on Windows it will check + // for a `cargo.exe` in the current directory before checking PATH. + // + // The solution here is to copy from the "mock" `cargo.exe` into + // `~/.cargo/bin/cargo-foo`. This is just for convenience to avoid + // needing to build another executable just for this test. + let which_cargo = cx + .config + .expect_with_env( + ["rustup", "which", "cargo"], + [("RUSTUP_USE_CATEGORY_HOME", "1")], + ) + .await; + let real_mock_cargo = which_cargo.output.stdout.trim(); + let cargo_bin_path = cx.config.cargodir.join("bin"); + let cargo_subcommand = cargo_bin_path.join(format!("cargo-foo{EXE_SUFFIX}")); + fs::create_dir_all(&cargo_bin_path).unwrap(); + fs::copy(real_mock_cargo, cargo_subcommand).unwrap(); + + cx.config + .expect_with_env( + ["cargo", "--recursive-cargo-subcommand"], + [("RUSTUP_USE_CATEGORY_HOME", "1")], + ) + .await + .with_stdout(snapbox::str![[r#" +1.3.0 (hash-nightly-2) + +"#]]) + .is_ok(); + + // If we have set the current recursion count to the maximum while doing + // another recursion, then rustup should bail out warning about it. + cx.config + .expect_with_env( + ["cargo", "--recursive-cargo-subcommand"], + [ + ( + "RUST_RECURSION_COUNT", + &*RUST_RECURSION_COUNT_MAX.to_string(), + ), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .with_stderr(snapbox::str![[r#" +error: infinite recursion detected +... +"#]]) + .is_err(); +} + +#[tokio::test] +async fn show_category_homes() { + let cx = CliTestContext::new(Scenario::None).await; + let config_home = cx.config.homedir.join("config home"); + let state_home = cx.config.homedir.join("state home"); + let data_home = cx.config.homedir.join("data home"); + let cache_home = cx.config.homedir.join("cache home"); + let bin_home = cx.config.homedir.join("bin home"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_CONFIG_HOME", config_home.to_str().unwrap()), + ("RUSTUP_STATE_HOME", state_home.to_str().unwrap()), + ("RUSTUP_DATA_HOME", data_home.to_str().unwrap()), + ("RUSTUP_CACHE_HOME", cache_home.to_str().unwrap()), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ]; + cx.config + .expect_with_env(["rustup", "show", "home"], env) + .await + .extend_redactions([("[HOME]", &cx.config.homedir)]) + .with_stdout(snapbox::str![[r#" +config: [HOME]/config home +state: [HOME]/state home +data: [HOME]/data home +cache: [HOME]/cache home + +"#]]) + .is_ok(); + + cx.config + .expect_with_env(["rustup", "show"], env) + .await + .extend_redactions([("[HOME]", &cx.config.homedir)]) + .with_stdout(snapbox::str![[r#" +Default host: [HOST_TUPLE] +rustup homes: + config: [HOME]/config home + state: [HOME]/state home + data: [HOME]/data home + cache: [HOME]/cache home + bin: [HOME]/bin home + +installed toolchains +-------------------- + +active toolchain +---------------- +no active toolchain + +"#]]) + .is_ok(); +} + +#[cfg(unix)] +#[tokio::test] +async fn show_category_platform_defaults() { + let cx = CliTestContext::new(Scenario::None).await; + cx.config + .expect_with_env( + ["rustup", "show", "home"], + [("RUSTUP_USE_CATEGORY_HOME", "1"), ("RUSTUP_HOME", "")], + ) + .await + .extend_redactions([("[HOME]", &cx.config.homedir)]) + .with_stdout(snapbox::str![[r#" +config: [HOME]/.config/rustup +state: [HOME]/.local/state/rustup +data: [HOME]/.local/share/rustup +cache: [HOME]/.cache/rustup + +"#]]) + .is_ok(); +} + #[tokio::test] async fn show_home() { let cx = CliTestContext::new(Scenario::None).await; @@ -1088,6 +1296,39 @@ hint: a new stable Rust release is available, run `rustup update stable` to inst .is_ok(); } +#[tokio::test] +async fn notify_release_hint_at_most_once_per_day_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let state_home = cx.config.current_dir().join("relative/state"); + let state_home_env = state_home.to_str().unwrap(); + let state_env = [ + ("RUSTUP_STATE_HOME", state_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ]; + cx.config + .expect_with_env(["rustup", "set", "release-hint", "enable"], state_env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustup", "update", "stable"], state_env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustup", "show"], state_env) + .await + .with_stderr(snapbox::str![[r#" +hint: a new stable Rust release is available, run `rustup update stable` to install it + +"#]]) + .is_ok(); + cx.config + .expect_with_env(["rustup", "show"], state_env) + .await + .with_stderr(snapbox::str![[""]]) + .is_ok(); + assert!(state_home.join("state.toml").is_file()); +} + #[tokio::test] async fn notify_release_hint_skipped_for_recent_release() { let cx = CliTestContext::new(Scenario::RecentStable).await; diff --git a/tests/suite/cli_rustup_ui/rustup_show_cmd_help_flag.stdout.term.svg b/tests/suite/cli_rustup_ui/rustup_show_cmd_help_flag.stdout.term.svg index 59c4ce8314..dd6c448fc0 100644 --- a/tests/suite/cli_rustup_ui/rustup_show_cmd_help_flag.stdout.term.svg +++ b/tests/suite/cli_rustup_ui/rustup_show_cmd_help_flag.stdout.term.svg @@ -32,7 +32,7 @@ active-toolchain Show the active toolchain - home Display the computed value of RUSTUP_HOME + home Display resolved Rustup home directories profile Show the default profile used for the `rustup install` command diff --git a/tests/suite/cli_rustup_ui/rustup_show_cmd_home_cmd_help_flag.stdout.term.svg b/tests/suite/cli_rustup_ui/rustup_show_cmd_home_cmd_help_flag.stdout.term.svg index 1c9515f722..81cd67043d 100644 --- a/tests/suite/cli_rustup_ui/rustup_show_cmd_home_cmd_help_flag.stdout.term.svg +++ b/tests/suite/cli_rustup_ui/rustup_show_cmd_home_cmd_help_flag.stdout.term.svg @@ -19,7 +19,7 @@ - Display the computed value of RUSTUP_HOME + Display resolved Rustup home directories diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 82ea7a71c9..d6d1e3688e 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -84,7 +84,7 @@ info: default toolchain set to stable fn check(path: &Path) { assert!(path.exists()); } - #[cfg(not(windows))] + #[cfg(unix)] fn check(path: &Path) { fn is_exe(path: &Path) -> bool { use std::os::unix::fs::MetadataExt; @@ -100,6 +100,46 @@ info: default toolchain set to stable } } +#[tokio::test] +async fn install_bins_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let bin_home = cx.config.current_dir().join("relative/bin"); + let bin_home_env = bin_home.to_str().unwrap(); + + cx.config + .expect_with_env( + ["rustup-init", "-y"], + [ + ("RUSTUP_BIN_HOME", bin_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .with_stdout(snapbox::str![[r#" +... + stable-[HOST_TUPLE] installed - 1.1.0 (hash-stable-1.1.0) +... +"#]]) + .with_stderr(snapbox::str![[r#" +... +info: syncing channel updates for stable-[HOST_TUPLE] +info: latest update on 2015-01-02 for version 1.1.0 (hash-stable-1.1.0) +info: downloading 4 components +info: default toolchain set to stable + +"#]]) + .is_ok(); + for tool in TOOLS.iter().chain(DUP_TOOLS.iter()) { + let path = &bin_home.join(format!("{tool}{EXE_SUFFIX}")); + assert!(path.is_file()); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!(path.metadata().unwrap().permissions().mode() & 0o777, 0o755); + } + } +} + /// Ensure that proxies are relative symlinks. #[tokio::test] async fn proxies_are_relative_symlinks() { @@ -324,7 +364,7 @@ async fn complete_uninstall_keeps_non_empty_cargo_bin() { .await .with_stderr(snapbox::str![[r#" ... -warn: keeping non-empty cargo bin directory `[..]` +warn: keeping non-empty bin directory `[..]` ... "#]]) .is_ok(); @@ -387,33 +427,32 @@ async fn uninstall_self_delete_works() { } // On windows rustup self uninstall temporarily puts a rustup-gc-$randomnumber.exe -// file in CONFIG.CARGODIR/.. ; check that it doesn't exist. +// file in the system temporary directory; check that it is cleaned up. #[tokio::test] #[cfg(windows)] async fn uninstall_doesnt_leave_gc_file() { let cx = setup_empty_installed().await; + let gc_dir = tempfile::tempdir().unwrap(); + let gc_path = gc_dir.path().to_str().unwrap(); + // std::env::temp_dir() uses TMP, or SystemTemp for SYSTEM processes: + // https://doc.rust-lang.org/std/env/fn.temp_dir.html + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2w cx.config - .expect(["rustup", "self", "uninstall", "-y"]) + .expect_with_env( + ["rustup", "self", "uninstall", "-y"], + [("SystemTemp", gc_path), ("TMP", gc_path)], + ) .await .is_ok(); - let parent = cx.config.cargodir.parent().unwrap(); // The gc removal happens after rustup terminates. Typically under // 100ms, but during the contention of test suites can be substantially // longer while still succeeding. let check = || { - let garbage = fs::read_dir(parent) + let garbage = fs::read_dir(gc_dir.path()) .unwrap() - .filter_map(|entry| { - let path = entry.unwrap().path(); - let name = path.file_name()?.to_str()?; - // On Windows, this binary is cleaned up on exit - if !(name.starts_with("rustup-gc-") && name.ends_with(EXE_SUFFIX)) { - return None; - } - Some(path.to_string_lossy().to_string()) - }) + .map(|entry| entry.unwrap().path()) .collect::>(); if garbage.is_empty() { Ok(()) @@ -570,11 +609,11 @@ async fn update_but_not_installed() { cx.config .expect(["rustup", "self", "update"]) .await - .extend_redactions([("[CARGO_DIR]", cx.config.cargodir)]) + .extend_redactions([("[BIN_HOME]", cx.config.cargodir.join("bin"))]) .is_err() .with_stdout(snapbox::str![[""]]) .with_stderr(snapbox::str![[r#" -error: rustup is not installed at '[CARGO_DIR]' +error: rustup is not installed at '[BIN_HOME]' "#]]); } @@ -728,6 +767,45 @@ async fn update_updates_rustup_bin() { assert_ne!(before_hash, after_hash); } +#[tokio::test] +async fn update_updates_rustup_bin_with_category_mode_enabled() { + let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + let state_home = cx.config.homedir.join("state"); + let bin_home = cx.config.homedir.join("bin"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_STATE_HOME", state_home.to_str().unwrap()), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ]; + cx.config + .expect_with_env( + [ + "rustup-init", + "-y", + "--no-modify-path", + "--default-toolchain", + "none", + ], + env, + ) + .await + .is_ok(); + + let bin = bin_home.join(format!("rustup{EXE_SUFFIX}")); + let before_hash = calc_hash(&bin); + let mut cmd = Command::new(&bin); + cx.config.env(&mut cmd); + cmd.envs(env).args(["self", "update"]); + let out = cmd.output().unwrap(); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + wait_for_completed_update(&state_home); + assert_ne!(before_hash, calc_hash(&bin)); +} + #[tokio::test] async fn update_bad_schema() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; @@ -932,6 +1010,40 @@ async fn updater_is_deleted_after_running_rustc() { assert!(!managed_updater(&cx.config.rustupdir.rustupdir).exists()); } +#[tokio::test] +async fn updater_is_deleted_after_running_rustc_with_category_mode_enabled() { + let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + let state_home = cx.config.current_dir().join("state"); + let bin_home = cx.config.current_dir().join("bin"); + let env = [ + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ("RUSTUP_STATE_HOME", state_home.to_str().unwrap()), + ("RUSTUP_BIN_HOME", bin_home.to_str().unwrap()), + ]; + cx.config + .expect_with_env(["rustup-init", "-y", "--no-modify-path"], env) + .await + .is_ok(); + cx.config + .expect_with_env(["rustup", "default", "nightly"], env) + .await + .is_ok(); + let rustup = bin_home.join(format!("rustup{EXE_SUFFIX}")); + cx.config + .expect_with_env([rustup.to_str().unwrap(), "self", "update"], env) + .await + .is_ok(); + wait_for_completed_update(&state_home); + assert!(managed_updater(&state_home).is_file()); + + let rustc = bin_home.join(format!("rustc{EXE_SUFFIX}")); + cx.config + .expect_with_env([rustc.to_str().unwrap(), "--version"], env) + .await + .is_ok(); + assert!(!managed_updater(&state_home).exists()); +} + #[tokio::test] async fn rustup_still_works_after_update() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; @@ -1314,8 +1426,8 @@ async fn install_minimal_profile() { cx.config.expect_component_not_executable("cargo").await; } -fn wait_for_completed_update(rustup_home: &Path) { - let stage = rustup_home.join(SELF_UPDATE_DIRECTORY); +fn wait_for_completed_update(state_home: &Path) { + let stage = state_home.join(SELF_UPDATE_DIRECTORY); retry(Fibonacci::from_millis(1).map(jitter).take(23), || { if Marker::Complete.path(&stage).is_file() { Ok(()) @@ -1328,8 +1440,8 @@ fn wait_for_completed_update(rustup_home: &Path) { .unwrap(); } -fn managed_updater(rustup_home: &Path) -> PathBuf { - updater_path(&rustup_home.join(SELF_UPDATE_DIRECTORY)) +fn managed_updater(state_home: &Path) -> PathBuf { + updater_path(&state_home.join(SELF_UPDATE_DIRECTORY)) } const TEST_VERSION: &str = "1.1.1"; diff --git a/tests/suite/cli_v2.rs b/tests/suite/cli_v2.rs index 97813807f5..f88777b166 100644 --- a/tests/suite/cli_v2.rs +++ b/tests/suite/cli_v2.rs @@ -5,6 +5,7 @@ use std::{fs, io::Write, path::PathBuf}; use rustup::{ dist::{TargetTuple, manifest::Manifest}, + for_host, test::{ CROSS_ARCH1, CROSS_ARCH2, CliTestContext, Config, Scenario, create_hash, this_host_tuple, }, @@ -2088,6 +2089,128 @@ warn: removing the last target; no build targets will be available .is_ok(); } +#[tokio::test] +async fn install_cache_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let cache_home = cx.config.current_dir().join("relative/cache"); + let cache_home_env = cache_home.to_str().unwrap(); + let toolchain = for_host!("stable-{}"); + let update_hash = cache_home.join("update-hashes").join(toolchain); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "install", "stable"], + [ + ("RUSTUP_CACHE_HOME", cache_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .is_ok(); + + assert!(cache_home.join("tmp").is_dir()); + assert!(cache_home.join("downloads").is_dir()); + assert!(update_hash.is_file()); + assert!(cx.config.rustupdir.has(format!("toolchains/{toolchain}"))); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "remove", "stable"], + [ + ("RUSTUP_CACHE_HOME", cache_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .is_ok(); + assert!(!update_hash.is_file()); +} + +#[tokio::test] +async fn install_cache_with_category_mode_disabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let cache_home = cx.config.current_dir().join("relative/cache"); + let cache_home_env = cache_home.to_str().unwrap(); + let toolchain = for_host!("stable-{}"); + let update_hash = cx.config.rustupdir.join("update-hashes").join(toolchain); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "install", "stable"], + [ + ("RUSTUP_CACHE_HOME", cache_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "0"), + ], + ) + .await + .is_ok(); + + assert!(cx.config.rustupdir.join("tmp").is_dir()); + assert!(cx.config.rustupdir.join("downloads").is_dir()); + assert!(update_hash.is_file()); + assert!(cx.config.rustupdir.has(format!("toolchains/{toolchain}"))); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "remove", "stable"], + [ + ("RUSTUP_CACHE_HOME", cache_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "0"), + ], + ) + .await + .is_ok(); + assert!(!update_hash.is_file()); +} + +#[tokio::test] +async fn install_data_with_category_mode_enabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let data_home = cx.config.current_dir().join("relative/data"); + let data_home_env = data_home.to_str().unwrap(); + let toolchain = for_host!("stable-{}"); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "install", "stable"], + [ + ("RUSTUP_DATA_HOME", data_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "1"), + ], + ) + .await + .is_ok(); + + assert!(data_home.join("toolchains").join(toolchain).is_dir()); +} + +#[tokio::test] +async fn install_data_with_category_mode_disabled() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let data_home = cx.config.current_dir().join("relative/data"); + let data_home_env = data_home.to_str().unwrap(); + let toolchain = for_host!("stable-{}"); + + cx.config + .expect_with_env( + ["rustup", "toolchain", "install", "stable"], + [ + ("RUSTUP_DATA_HOME", data_home_env), + ("RUSTUP_USE_CATEGORY_HOME", "0"), + ], + ) + .await + .is_ok(); + + assert!( + cx.config + .rustupdir + .join("toolchains") + .join(toolchain) + .is_dir() + ); +} + #[tokio::test] // Issue #304 async fn remove_target_missing_update_hash() {