diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 259535b0b6..021d7d28f7 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -58,7 +58,7 @@ use crate::{ errors::CliError, markdown::md, }, - config::Cfg, + config::{Cfg, default_host_tuple}, dist::{ DistOptions, PartialToolchainDesc, Profile, TargetTuple, ToolchainDesc, download::DownloadCfg, @@ -67,6 +67,7 @@ use crate::{ errors::RustupError, install::{InstallMethod, UpdateStatus}, process::Process, + settings::SettingsFile, toolchain::{ DistributableToolchain, MaybeOfficialToolchainName, ResolvableToolchainName, Toolchain, ToolchainName, @@ -106,6 +107,7 @@ pub(crate) struct InstallOpts<'a> { pub no_update_toolchain: bool, pub components: &'a [&'a str], pub targets: &'a [&'a str], + pub process: &'a Process, } impl InstallOpts<'_> { @@ -115,46 +117,52 @@ impl InstallOpts<'_> { /// - Copying the running binary to the binary directory. /// - Hard-linking the various Rust tools to that copied binary. /// - Adding the binary directory to the `$PATH` unless `no_modify_path` is set. - pub(crate) async fn install(mut self, no_prompt: bool, cfg: &mut Cfg<'_>) -> Result { + pub(crate) async fn install( + mut self, + current_dir: PathBuf, + no_prompt: bool, + quiet: bool, + ) -> Result { #[cfg_attr(not(unix), allow(unused_mut))] let mut exit_code = ExitCode::SUCCESS; - self.validate(cfg.process).map_err(|e| { + let process = self.process; + + self.validate(process).map_err(|e| { anyhow!( "Pre-checks for host and toolchain failed: {e}\n\ If you are unsure of suitable values, the 'stable' toolchain is the default.\n\ Valid host tuples look something like: {}", - TargetTuple::from_host_or_build(cfg.process) + TargetTuple::from_host_or_build(process) ) })?; - if cfg - .process + if process .var_os("RUSTUP_INIT_SKIP_EXISTENCE_CHECKS") .is_none_or(|s| s != "yes") { - check_existence_of_rustc_or_cargo_in_path(no_prompt, cfg.process)?; - check_existence_of_settings_file(cfg)?; + check_existence_of_rustc_or_cargo_in_path(no_prompt, process)?; + check_existence_of_settings_file(process)?; } #[cfg(unix)] { - exit_code &= unix::do_anti_sudo_check(no_prompt, cfg.process)?; + exit_code &= unix::do_anti_sudo_check(no_prompt, process)?; } - let mut term = cfg.process.stdout(); + let mut term = process.stdout(); #[cfg(windows)] - windows::maybe_install_msvc(&mut term, no_prompt, &self, &*cfg).await?; + windows::maybe_install_msvc(&mut term, no_prompt, quiet, &self, process).await?; if !no_prompt { - let msg = pre_install_msg(self.no_modify_path, cfg.process)?; + let msg = pre_install_msg(self.no_modify_path, process)?; md(&mut term, msg); let mut customized_install = false; loop { - md(&mut term, current_install_opts(&self, cfg.process)); - match common::confirm_advanced(customized_install, cfg.process)? { + md(&mut term, current_install_opts(&self)); + match common::confirm_advanced(customized_install, process)? { Confirm::No => { info!("aborting installation"); return Ok(ExitCode::SUCCESS); @@ -162,15 +170,15 @@ impl InstallOpts<'_> { Confirm::Yes => break, Confirm::Advanced => { customized_install = true; - self.customize(cfg.process)?; + self.customize(process)?; } } } } let no_modify_path = self.no_modify_path; - if let Err(e) = self.install_rust(cfg).await { - report_error(&e, cfg.process); + if let Err(e) = self.install_rust(current_dir, quiet).await { + report_error(&e, process); // On windows, where installation happens in a console // that may have opened just for this purpose, give @@ -178,13 +186,13 @@ impl InstallOpts<'_> { // window closes. #[cfg(windows)] if !no_prompt { - windows::ensure_prompt(cfg.process)?; + windows::ensure_prompt(process)?; } return Ok(ExitCode::FAILURE); } - let cargo_home = canonical_cargo_home(cfg.process)?; + let cargo_home = canonical_cargo_home(process)?; #[cfg(windows)] let cargo_home = cargo_home.replace('\\', r"\\"); #[cfg(windows)] @@ -197,7 +205,7 @@ impl InstallOpts<'_> { format!(post_install_msg_win!(), cargo_home = cargo_home) }; #[cfg(not(windows))] - let source_env_lines = shell::build_source_env_lines(cfg.process); + let source_env_lines = shell::build_source_env_lines(process); #[cfg(not(windows))] let msg = if no_modify_path { format!( @@ -215,33 +223,33 @@ impl InstallOpts<'_> { md(&mut term, msg); #[cfg(unix)] - warn_if_default_linker_missing(cfg.process); + warn_if_default_linker_missing(process); #[cfg(windows)] if !no_prompt { // On windows, where installation happens in a console // that may have opened just for this purpose, require // the user to press a key to continue. - windows::ensure_prompt(cfg.process)?; + windows::ensure_prompt(process)?; } Ok(exit_code) } /// Installs the rustup binary and proxies, and installs a toolchain if specified. - async fn install_rust(self, cfg: &mut Cfg<'_>) -> Result<()> { - install_bins(cfg.process)?; + async fn install_rust(self, current_dir: PathBuf, quiet: bool) -> Result<()> { + install_bins(self.process)?; #[cfg(unix)] - unix::do_write_env_files(cfg.process)?; + unix::do_write_env_files(self.process)?; if !self.no_modify_path { - do_add_to_path(cfg.process)?; + do_add_to_path(self.process)?; } // If RUSTUP_HOME is not set, make sure it exists - if cfg.process.var_os("RUSTUP_HOME").is_none() { - let home = cfg + if self.process.var_os("RUSTUP_HOME").is_none() { + let home = self .process .home_dir() .map(|p| p.join(".rustup")) @@ -250,19 +258,21 @@ impl InstallOpts<'_> { fs::create_dir_all(home).context("unable to create ~/.rustup")?; } + let mut cfg = Cfg::from_env(current_dir, quiet, false, self.process)?; + let (components, targets) = (self.components, self.targets); - let toolchain = self.select_toolchain(cfg)?; + let toolchain = self.select_toolchain(&mut cfg)?; if let Some(desc) = toolchain { let options = - DistOptions::new(components, targets, &desc, cfg.get_profile()?, true, cfg)?; - let status = if Toolchain::exists(cfg, &desc.clone().into())? { + DistOptions::new(components, targets, &desc, cfg.get_profile()?, true, &cfg)?; + let status = if Toolchain::exists(&cfg, &desc.clone().into())? { warn!("Updating existing toolchain, profile choice will be ignored"); // If we have a partial install we might not be able to read content here. We could: // - fail and folk have to delete the partially present toolchain to recover // - silently ignore it (and provide inconsistent metadata for reporting the install/update change) // - delete the partial install and start over // For now, we error. - let toolchain = DistributableToolchain::new(cfg, desc.clone())?; + let toolchain = DistributableToolchain::new(&cfg, desc.clone())?; InstallMethod::Dist(options.for_update(&toolchain, false)) .install(None) .await? @@ -274,7 +284,7 @@ impl InstallOpts<'_> { cfg.set_default(Some(&desc.clone().into()))?; writeln!(cfg.process.stdout().lock())?; - common::show_channel_update(cfg, PackageUpdate::Toolchain(desc), Ok(status))?; + common::show_channel_update(&cfg, PackageUpdate::Toolchain(desc), Ok(status))?; } Ok(()) } @@ -293,6 +303,7 @@ impl InstallOpts<'_> { no_update_toolchain, components, targets, + .. } = self; cfg.set_profile(profile)?; @@ -625,23 +636,22 @@ fn check_existence_of_rustc_or_cargo_in_path(no_prompt: bool, process: &Process) Ok(()) } -fn check_existence_of_settings_file(cfg: &Cfg<'_>) -> Result<()> { - let rustup_dir = cfg.process.rustup_home()?; - let settings_file_path = rustup_dir.join("settings.toml"); - if !utils::path_exists(&settings_file_path) { +fn check_existence_of_settings_file(process: &Process) -> Result<()> { + let rustup_dir = process.rustup_home()?; + let settings_file = SettingsFile::new(rustup_dir.join("settings.toml")); + if !utils::path_exists(&settings_file.path) { return Ok(()); } - let settings_toolchain = cfg - .settings_file - .with(|s| Ok(s.default_toolchain.clone()))?; + let settings_toolchain = settings_file.with(|s| Ok(s.default_toolchain.clone()))?; // If there is already a non-empty `settings.toml` file (e.g., not a fresh install), // then we warn the user that there was an already configured default toolchain. let Some(default_toolchain) = settings_toolchain else { return Ok(()); }; warn!("it looks like you have an existing rustup settings file at:"); - warn!("{}", settings_file_path.display()); - let inferred = PartialToolchainDesc::from_str("stable")?.resolve(&cfg.default_host_tuple()?)?; + warn!("{}", settings_file.path.display()); + let default_host_tuple = settings_file.with(|s| Ok(default_host_tuple(s, process)))?; + let inferred = PartialToolchainDesc::from_str("stable")?.resolve(&default_host_tuple)?; if default_toolchain != inferred.to_string() { warn!("rustup will install the default toolchain as specified in the settings file,"); warn!("instead of the one inferred from the default host tuple."); @@ -690,7 +700,7 @@ fn pre_install_msg(no_modify_path: bool, process: &Process) -> Result { } } -fn current_install_opts(opts: &InstallOpts<'_>, process: &Process) -> String { +fn current_install_opts(opts: &InstallOpts<'_>) -> String { format!( r"Current installation options: @@ -702,7 +712,7 @@ fn current_install_opts(opts: &InstallOpts<'_>, process: &Process) -> String { opts.default_host_tuple .as_ref() .map(TargetTuple::new) - .unwrap_or_else(|| TargetTuple::from_host_or_build(process)), + .unwrap_or_else(|| TargetTuple::from_host_or_build(opts.process)), match &opts.default_toolchain { Some(name) => name.to_string(), None => "stable (default)".to_owned(), @@ -1375,6 +1385,7 @@ mod tests { components: &[], targets: &[], no_update_toolchain: false, + process: &tp.process, }; assert_eq!( diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 84dad678d7..d108c9e629 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -20,7 +20,6 @@ use super::super::errors::CliError; use super::common; use super::{InstallOpts, install_bins, report_error}; use crate::cli::markdown::md; -use crate::config::Cfg; use crate::dist::TargetTuple; use crate::download::DownloadOptions; use crate::process::{ColorableTerminal, Process}; @@ -91,35 +90,36 @@ pub(crate) fn choose_vs_install(process: &Process) -> Result, - cfg: &Cfg<'_>, + process: &Process, ) -> Result<()> { - let Some(plan) = do_msvc_check(opts, cfg.process) else { + let Some(plan) = do_msvc_check(opts, process) else { return Ok(()); }; if no_prompt { warn!("installing msvc toolchain without its prerequisites"); - } else if !cfg.quiet && plan == VsInstallPlan::Automatic { + } else if !quiet && plan == VsInstallPlan::Automatic { md(term, MSVC_AUTO_INSTALL_MESSAGE); - match choose_vs_install(cfg.process)? { + match choose_vs_install(process)? { Some(VsInstallPlan::Automatic) => { - match try_install_msvc(opts, cfg).await { + match try_install_msvc(opts, process).await { Err(e) => { // Make sure the console doesn't exit before the user can // see the error and give the option to continue anyway. - report_error(&e, cfg.process); - if !common::question_bool("\nContinue?", false, cfg.process)? { + report_error(&e, process); + if !common::question_bool("\nContinue?", false, process)? { info!("aborting installation"); } } - Ok(ContinueInstall::No) => ensure_prompt(cfg.process)?, + Ok(ContinueInstall::No) => ensure_prompt(process)?, _ => {} } } Some(VsInstallPlan::Manual) => { md(term, MSVC_MANUAL_INSTALL_MESSAGE); - if !common::question_bool("\nContinue?", false, cfg.process)? { + if !common::question_bool("\nContinue?", false, process)? { info!("aborting installation"); } } @@ -128,7 +128,7 @@ pub(super) async fn maybe_install_msvc( } else { md(term, MSVC_MESSAGE); md(term, MSVC_MANUAL_INSTALL_MESSAGE); - if !common::question_bool("\nContinue?", false, cfg.process)? { + if !common::question_bool("\nContinue?", false, process)? { info!("aborting installation"); } } @@ -259,7 +259,7 @@ pub(crate) enum ContinueInstall { /// but the rustup install should not be continued at this time. pub(crate) async fn try_install_msvc( opts: &InstallOpts<'_>, - cfg: &Cfg<'_>, + process: &Process, ) -> Result { // download the installer let visual_studio_url = utils::parse_url("https://aka.ms/vs/17/release/vs_community.exe")?; @@ -270,8 +270,9 @@ pub(crate) async fn try_install_msvc( .context("error creating temp directory")?; let visual_studio = tempdir.path().join("vs_setup.exe"); + info!("downloading Visual Studio installer"); - DownloadOptions::try_from(cfg.process)? + DownloadOptions::try_from(process)? .start(&visual_studio_url, &visual_studio) .download() .await?; @@ -289,7 +290,7 @@ pub(crate) async fn try_install_msvc( // It's possible an earlier or later version of the Windows SDK has been // installed separately from Visual Studio so installing it can be skipped. - if !has_windows_sdk_libs(cfg.process) { + if !has_windows_sdk_libs(process) { cmd.args([ "--add", "Microsoft.VisualStudio.Component.Windows11SDK.26100", @@ -322,8 +323,8 @@ pub(crate) async fn try_install_msvc( // It's possible that the installer returned a non-zero exit code // even though the required components were successfully installed. // In that case we warn about the error but continue on. - let have_msvc = do_msvc_check(opts, cfg.process).is_none(); - let has_libs = has_windows_sdk_libs(cfg.process); + let have_msvc = do_msvc_check(opts, process).is_none(); + let has_libs = has_windows_sdk_libs(process); if have_msvc && has_libs { warn!("Visual Studio is installed but a problem occurred during installation"); warn!("{}", err); diff --git a/src/cli/setup_mode.rs b/src/cli/setup_mode.rs index 923dc295d1..b5ec7ef0f0 100644 --- a/src/cli/setup_mode.rs +++ b/src/cli/setup_mode.rs @@ -10,7 +10,6 @@ use crate::{ common::{self, update_console_filter}, self_update::{self, InstallOpts}, }, - config::Cfg, dist::Profile, process::Process, toolchain::MaybeOfficialToolchainName, @@ -128,8 +127,7 @@ pub async fn main( no_update_toolchain: no_update_default_toolchain, components: &component.iter().map(|s| &**s).collect::>(), targets: &target.iter().map(|s| &**s).collect::>(), + process, }; - - let mut cfg = Cfg::from_env(current_dir, quiet, false, process)?; - opts.install(no_prompt, &mut cfg).await + opts.install(current_dir, no_prompt, quiet).await } diff --git a/src/config.rs b/src/config.rs index a9740111c7..97fd283485 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1178,7 +1178,7 @@ impl State { } } -fn default_host_tuple(s: &Settings, process: &Process) -> TargetTuple { +pub(crate) fn default_host_tuple(s: &Settings, process: &Process) -> TargetTuple { s.default_host_tuple .as_ref() .map(TargetTuple::new) diff --git a/src/settings.rs b/src/settings.rs index c58a0f284c..da35a85037 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -15,7 +15,7 @@ use crate::utils; #[derive(Clone, Debug, Eq, PartialEq)] pub struct SettingsFile { - path: PathBuf, + pub(crate) path: PathBuf, cache: RefCell>, } @@ -38,25 +38,18 @@ impl SettingsFile { } fn read_settings(&self) -> Result<()> { - let mut needs_save = false; - { - let b = self.cache.borrow(); - if b.is_none() { - drop(b); - *self.cache.borrow_mut() = Some(if utils::is_file(&self.path) { - let content = utils::read_locked_file("settings", &self.path)?; - Settings::parse(&content).with_context(|| RustupError::ParsingFile { - name: "settings", - path: self.path.clone(), - })? - } else { - needs_save = true; - Default::default() - }); - } - } - if needs_save { - self.write_settings()?; + let b = self.cache.borrow(); + if b.is_none() { + drop(b); + *self.cache.borrow_mut() = Some(if utils::is_file(&self.path) { + let content = utils::read_locked_file("settings", &self.path)?; + Settings::parse(&content).with_context(|| RustupError::ParsingFile { + name: "settings", + path: self.path.clone(), + })? + } else { + Settings::default() + }); } Ok(()) } diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 9b75e951c4..80bb821e08 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -1,6 +1,7 @@ //! Tests of the interactive console installer use std::env::consts::EXE_SUFFIX; +use std::fs; use std::io::Write; use std::process::Stdio; @@ -685,3 +686,40 @@ warn: many Rust crates require a system C toolchain to build ... "#]]); } + +#[tokio::test] +async fn install_rejection_leaves_disk_untouched() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + + let rustupdir = &cx.config.rustupdir.rustupdir; + let settings_file = &rustupdir.join("settings.toml"); + fs::remove_dir_all(rustupdir).unwrap(); + + run_input(&cx.config, &["rustup-init", "--no-modify-path"], "3\n") + .with_stdout(snapbox::str![[r#" +... +This path needs to be in your PATH environment variable, +but will not be added automatically. + +You can uninstall at any time with rustup self uninstall and +these changes will be reverted. + +Current installation options: + + + default host tuple: [HOST_TUPLE] + default toolchain: stable (default) + profile: default + modify PATH variable: no + +1) Proceed with standard installation (default - just press enter) +2) Customize installation +3) Cancel installation +> + +"#]]) + .is_ok(); + + assert!(!settings_file.exists()); + assert!(!rustupdir.exists()); +}