From 88388d111210ad93b7eac975202941f9e1fbac90 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 31 Aug 2025 22:57:14 +0800 Subject: [PATCH 1/4] test: make tests agnostic to external `RUSTUP_AUTO_INSTALL` and `RUST_RECURSION_COUNT` This commit clears the value of `RUSTUP_AUTO_INSTALL` and the `RUST_RECURSION_COUNT` environment variables before running tests, to make sure that every test case relying on their effects is explicitly setting them. --- src/test/clitools.rs | 6 ++++++ tests/suite/cli_misc.rs | 7 +++++-- tests/suite/cli_rustup.rs | 10 ++++++++-- tests/suite/cli_v1.rs | 2 +- tests/suite/cli_v2.rs | 9 ++++++--- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 4dcf2b79e3..1311322696 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -314,6 +314,12 @@ impl Config { "/bogus-config-file.toml", ); + // Clear current recursion count to avoid messing up related logic + cmd.env("RUST_RECURSION_COUNT", ""); + + // Clear override for auto installation of active toolchain unless explicitly requested + cmd.env("RUSTUP_AUTO_INSTALL", ""); + // Pass `RUSTUP_CI` over to the test process in case it is required downstream if let Some(ci) = env::var_os("RUSTUP_CI") { cmd.env("RUSTUP_CI", ci); diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 5316a2db84..a50fe0dcd5 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -64,7 +64,7 @@ async fn rustc_with_bad_rustup_toolchain_env_var() { .expect_with_env(["rustc"], [("RUSTUP_TOOLCHAIN", "bogus")]) .await .with_stderr(snapbox::str![[r#" -error: override toolchain 'bogus' is not installed[..] +error:[..] toolchain 'bogus' is not installed[..] "#]]) .is_err(); @@ -1381,7 +1381,10 @@ async fn which_asking_uninstalled_toolchain() { "#]]) .is_ok(); cx.config - .expect(["rustup", "which", "--toolchain=nightly", "rustc"]) + .expect_with_env( + ["rustup", "which", "--toolchain=nightly", "rustc"], + [("RUSTUP_AUTO_INSTALL", "1")], + ) .await .with_stderr(snapbox::str![[r#" error: toolchain 'nightly-[HOST_TUPLE]' is not installed diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 82fd9278d1..6ceb147e9c 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -1247,7 +1247,7 @@ async fn show_toolchain_override_not_installed() { .await .is_ok(); cx.config - .expect(["rustup", "show"]) + .expect_with_env(["rustup", "show"], [("RUSTUP_AUTO_INSTALL", "1")]) .await .extend_redactions([("[RUSTUP_DIR]", &cx.config.rustupdir.to_string())]) .with_stdout(snapbox::str![[r#" @@ -1349,7 +1349,13 @@ installed targets: async fn show_toolchain_env_not_installed() { let cx = CliTestContext::new(Scenario::SimpleV2).await; cx.config - .expect_with_env(["rustup", "show"], [("RUSTUP_TOOLCHAIN", "nightly")]) + .expect_with_env( + ["rustup", "show"], + [ + ("RUSTUP_TOOLCHAIN", "nightly"), + ("RUSTUP_AUTO_INSTALL", "1"), + ], + ) .await .extend_redactions([("[RUSTUP_DIR]", &cx.config.rustupdir.to_string())]) .is_ok() diff --git a/tests/suite/cli_v1.rs b/tests/suite/cli_v1.rs index 465b26b59e..2a98f95b65 100644 --- a/tests/suite/cli_v1.rs +++ b/tests/suite/cli_v1.rs @@ -271,7 +271,7 @@ async fn remove_override_toolchain_err_handling() { .await .is_ok(); cx.config - .expect(["rustc", "--version"]) + .expect_with_env(["rustc", "--version"], [("RUSTUP_AUTO_INSTALL", "1")]) .await .with_stdout(snapbox::str![[r#" 1.2.0 (hash-beta-1.2.0) diff --git a/tests/suite/cli_v2.rs b/tests/suite/cli_v2.rs index 73eda294a6..956466625e 100644 --- a/tests/suite/cli_v2.rs +++ b/tests/suite/cli_v2.rs @@ -478,7 +478,7 @@ async fn remove_override_toolchain_err_handling() { .await .is_ok(); cx.config - .expect(["rustc", "--version"]) + .expect_with_env(["rustc", "--version"], [("RUSTUP_AUTO_INSTALL", "1")]) .await .with_stdout(snapbox::str![[r#" 1.2.0 (hash-beta-1.2.0) @@ -511,7 +511,7 @@ async fn file_override_toolchain_err_handling() { let toolchain_file = cwd.join("rust-toolchain"); rustup::utils::raw::write_file(&toolchain_file, "beta").unwrap(); cx.config - .expect(["rustc", "--version"]) + .expect_with_env(["rustc", "--version"], [("RUSTUP_AUTO_INSTALL", "1")]) .await .with_stdout(snapbox::str![[r#" 1.2.0 (hash-beta-1.2.0) @@ -553,7 +553,10 @@ error: toolchain 'beta-[HOST_TUPLE]' is not installed "#]]) .is_err(); cx.config - .expect(["rustc", "+beta", "--version"]) + .expect_with_env( + ["rustc", "+beta", "--version"], + [("RUSTUP_AUTO_INSTALL", "1")], + ) .await .with_stdout(snapbox::str![[r#" 1.2.0 (hash-beta-1.2.0) From 78962814c74d81ee29a180383f606c278002edaf Mon Sep 17 00:00:00 2001 From: rami3l Date: Fri, 8 May 2026 18:53:46 +0200 Subject: [PATCH 2/4] refactor(config): extract `Ensured<>` wrapper type --- src/cli/rustup_mode.rs | 2 +- src/config.rs | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index e059cc8176..b6a6d98c5a 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -826,7 +826,7 @@ async fn default_( let status = cfg .ensure_installed(&desc, vec![], vec![], None, force_non_host, true) .await? - .0; + .status; cfg.set_default(Some(&(&desc).into()))?; diff --git a/src/config.rs b/src/config.rs index a9991b9ad4..b674604bc8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,6 +124,22 @@ impl Display for ActiveSource { } } +/// Represents the result of an operation that may ensure the installation of a certain toolchain. +#[derive(Clone, Debug)] +pub(crate) struct Ensured { + pub inner: T, + pub status: UpdateStatus, +} + +impl Ensured { + pub fn new(toolchain: impl Into, status: UpdateStatus) -> Self { + Self { + inner: toolchain.into(), + status, + } + } +} + // Represents a toolchain override from a +toolchain command line option, // RUSTUP_TOOLCHAIN environment variable, or rust-toolchain.toml file etc. Can // include components and targets from a rust-toolchain.toml that should be @@ -787,7 +803,7 @@ impl<'a> Cfg<'a> { profile: Option, force_non_host: bool, verbose: bool, - ) -> Result<(UpdateStatus, Toolchain<'_>)> { + ) -> Result>> { common::check_non_host_toolchain( toolchain.to_string(), &TargetTuple::from_host_or_build(self.process), @@ -831,7 +847,7 @@ impl<'a> Cfg<'a> { } Err(e) => return Err(e.into()), }; - Ok((status, toolchain.into())) + Ok(Ensured::new(toolchain, status)) } /// Get the configured default toolchain. From d9ba088f27b6590526249108ef2e03b6ba47d789 Mon Sep 17 00:00:00 2001 From: rami3l Date: Fri, 8 May 2026 18:53:46 +0200 Subject: [PATCH 3/4] refactor(config): return `Ensured<>` from more functions --- src/cli/rustup_mode.rs | 7 +++++-- src/config.rs | 28 +++++++++++++++------------- src/toolchain.rs | 12 +++++++----- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index b6a6d98c5a..4e1dc2ceef 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -1074,7 +1074,10 @@ async fn update( exit_code &= self_update_mode.update(should_self_update, &dl_cfg).await?; } else if ensure_active_toolchain { let (toolchain, source) = cfg.ensure_active_toolchain(force_non_host, true).await?; - info!("the active toolchain `{toolchain}` has been installed"); + info!( + "the active toolchain `{}` has been installed", + toolchain.inner, + ); info!("it's active because: {}", source.to_reason()); exit_code &= self_update_mode.update(should_self_update, &dl_cfg).await?; } else { @@ -1097,7 +1100,7 @@ async fn run( ) -> Result { let toolchain = toolchain.resolve(&cfg.default_host_tuple()?)?; let toolchain = Toolchain::from_local(toolchain, install, cfg).await?; - let cmd = toolchain.command(&command[0])?; + let cmd = toolchain.inner.command(&command[0])?; command::run_command_for_dir(cmd, &command[0], &command[1..]) } diff --git a/src/config.rs b/src/config.rs index b674604bc8..68fcec4c46 100644 --- a/src/config.rs +++ b/src/config.rs @@ -735,10 +735,8 @@ impl<'a> Cfg<'a> { match name { Some((tc, source)) => { let install_if_missing = self.should_auto_install()?; - Ok(( - Toolchain::from_local(tc, install_if_missing, self).await?, - source, - )) + let tc = Toolchain::from_local(tc, install_if_missing, self).await?; + Ok((tc.inner, source)) } None => { let (tc, source) = self @@ -755,10 +753,10 @@ impl<'a> Cfg<'a> { &self, force_non_host: bool, verbose: bool, - ) -> Result<(LocalToolchainName, ActiveSource)> { + ) -> Result<(Ensured, ActiveSource)> { if let Some((override_config, source)) = self.find_override_config()? { let toolchain = override_config.clone().into_local_toolchain_name(); - if let OverrideCfg::Official { + let status = if let OverrideCfg::Official { toolchain, components, targets, @@ -773,20 +771,24 @@ impl<'a> Cfg<'a> { force_non_host, verbose, ) - .await?; + .await? + .status } else { Toolchain::with_source(self, toolchain.clone(), &source)?; - } - Ok((toolchain, source)) + UpdateStatus::Unchanged + }; + Ok((Ensured::new(toolchain, status), source)) } else if let Some(toolchain) = self.get_default()? { let source = ActiveSource::Default; - if let ToolchainName::Official(desc) = &toolchain { + let status = if let ToolchainName::Official(desc) = &toolchain { self.ensure_installed(desc, vec![], vec![], None, force_non_host, verbose) - .await?; + .await? + .status } else { Toolchain::with_source(self, toolchain.clone().into(), &source)?; - } - Ok((toolchain.into(), source)) + UpdateStatus::Unchanged + }; + Ok((Ensured::new(toolchain, status), source)) } else { Err(no_toolchain_error(self.process)) } diff --git a/src/toolchain.rs b/src/toolchain.rs index 695a14c045..121d4545ef 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -21,13 +21,14 @@ use wait_timeout::ChildExt; use crate::{ RustupError, - config::{ActiveSource, Cfg, InstalledPath}, + config::{ActiveSource, Cfg, Ensured, InstalledPath}, dist::{ DistOptions, PartialToolchainDesc, TargetTuple, component::{Component, Components}, prefix::InstallPrefix, }, - env_var, install, + env_var, + install::{self, UpdateStatus}, utils::{self, raw::open_dir_following_links}, }; @@ -54,15 +55,16 @@ impl<'a> Toolchain<'a> { name: LocalToolchainName, install_if_missing: bool, cfg: &'a Cfg<'a>, - ) -> anyhow::Result> { + ) -> anyhow::Result>> { match Self::new(cfg, name) { - Ok(tc) => Ok(tc), + Ok(tc) => Ok(Ensured::new(tc, UpdateStatus::Unchanged)), Err(RustupError::ToolchainNotInstalled { name: ToolchainName::Official(desc), .. }) if install_if_missing => { let options = DistOptions::new(&[], &[], &desc, cfg.get_profile()?, true, cfg)?; - Ok(DistributableToolchain::install(options).await?.1.toolchain) + let tc = DistributableToolchain::install(options).await?.1.toolchain; + Ok(Ensured::new(tc, UpdateStatus::Installed)) } Err(e) => Err(e.into()), } From a9611a3c867e54b2f36df1de14e68cb5e774409e Mon Sep 17 00:00:00 2001 From: rami3l Date: Sat, 16 May 2026 10:29:28 +0200 Subject: [PATCH 4/4] feat(config): warn user if auto-install is enabled --- src/config.rs | 20 +++++++++++++++++- tests/suite/cli_misc.rs | 46 +++++++++++++++++++++++++++++++++++++++++ tests/suite/cli_v2.rs | 6 ++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 68fcec4c46..07c8eade6e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -533,7 +533,11 @@ impl<'a> Cfg<'a> { } match self.ensure_active_toolchain(true, false).await { - Ok(r) => Ok(Some(r)), + Ok(r) => { + let (tc, source) = r; + auto_install_warning(self.process, &tc.inner, &tc.status); + Ok(Some((tc.inner, source))) + } Err(e) => match e.downcast_ref::() { Some(RustupError::ToolchainNotSelected(_)) => Ok(None), _ => Err(e), @@ -736,6 +740,7 @@ impl<'a> Cfg<'a> { Some((tc, source)) => { let install_if_missing = self.should_auto_install()?; let tc = Toolchain::from_local(tc, install_if_missing, self).await?; + auto_install_warning(self.process, tc.inner.name(), &tc.status); Ok((tc.inner, source)) } None => { @@ -1021,6 +1026,19 @@ fn no_toolchain_error(process: &Process) -> anyhow::Error { RustupError::ToolchainNotSelected(process.name().unwrap_or_else(|| "Rust".into())).into() } +fn auto_install_warning(process: &Process, toolchain: impl Display, status: &UpdateStatus) { + // If we're already in a recursion, or we haven't just installed the active toolchain, then + // don't print the warning. + let recursions = process.var("RUST_RECURSION_COUNT"); + if recursions.is_ok_and(|it| it != "0") || !matches!(status, UpdateStatus::Installed) { + return; + } + + warn!("the missing active toolchain `{toolchain}` has been auto-installed"); + warn!("this might cause rustup commands to take longer time to finish than expected"); + info!("you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable`"); +} + /// Specifies how a `rust-toolchain`/`rust-toolchain.toml` configuration file should be parsed. enum ParseMode { /// Only permit TOML format in a configuration file. diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index a50fe0dcd5..5fb3987a67 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1749,3 +1749,49 @@ info: falling back to "[EXTERN_PATH]" "#]]) .is_ok(); } + +#[tokio::test] +async fn warn_auto_install_on_proxy() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect_with_env( + ["rustc", "--version"], + [("RUSTUP_TOOLCHAIN", "stable"), ("RUSTUP_AUTO_INSTALL", "1")], + ) + .await + .with_stdout(snapbox::str![[r#" +1.1.0 (hash-stable-1.1.0) + +"#]]) + .with_stderr(snapbox::str![[r#" +... +warn: the missing active toolchain `stable-[HOST_TUPLE]` has been auto-installed +warn: this might cause rustup commands to take longer time to finish than expected +info: you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable` +... +"#]]) + .is_ok(); +} + +#[tokio::test] +async fn warn_auto_install_on_rustup() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect_with_env( + ["rustup", "doc", "--path"], + [("RUSTUP_TOOLCHAIN", "stable"), ("RUSTUP_AUTO_INSTALL", "1")], + ) + .await + .with_stdout(snapbox::str![[r#" +[..]/toolchains/stable-[HOST_TUPLE]/share/doc/rust/html/index.html + +"#]]) + .with_stderr(snapbox::str![[r#" +... +warn: the missing active toolchain `stable-[HOST_TUPLE]` has been auto-installed +warn: this might cause rustup commands to take longer time to finish than expected +info: you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable` +... +"#]]) + .is_ok(); +} diff --git a/tests/suite/cli_v2.rs b/tests/suite/cli_v2.rs index 956466625e..47aad869a6 100644 --- a/tests/suite/cli_v2.rs +++ b/tests/suite/cli_v2.rs @@ -488,6 +488,9 @@ async fn remove_override_toolchain_err_handling() { info: syncing channel updates for beta-[HOST_TUPLE] info: latest update on 2015-01-02 for version 1.2.0 (hash-beta-1.2.0) info: downloading 4 components +warn: the missing active toolchain `beta-[HOST_TUPLE]` has been auto-installed +warn: this might cause rustup commands to take longer time to finish than expected +info: you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable` "#]]) .is_ok(); @@ -521,6 +524,9 @@ async fn file_override_toolchain_err_handling() { info: syncing channel updates for beta-[HOST_TUPLE] info: latest update on 2015-01-02 for version 1.2.0 (hash-beta-1.2.0) info: downloading 4 components +warn: the missing active toolchain `beta-[HOST_TUPLE]` has been auto-installed +warn: this might cause rustup commands to take longer time to finish than expected +info: you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable` "#]]) .is_ok();