From e54dfa244259f00ce6597ffb9b7f339a5b92e172 Mon Sep 17 00:00:00 2001 From: rami3l Date: Tue, 28 Apr 2026 21:52:45 +0200 Subject: [PATCH 1/5] feat(config): add `Cfg` field to force-disable auto-installation --- src/config.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/config.rs b/src/config.rs index a4682ff6fd..7b0086e0fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -246,6 +246,12 @@ pub(crate) struct Cfg<'a> { pub quiet: bool, pub current_dir: PathBuf, pub process: &'a Process, + + /// If this flag is set to `true`, it can stop `rustup` from automatically installing the active + /// toolchain in certain undesired cases, such as under `rustup component` and `rustup target` + /// subcommands. This effect has higher precedence than the `RUSTUP_AUTO_INSTALL` environment + /// variable and the `rustup set auto-install` setting. + pub skip_auto_install: bool, } impl<'a> Cfg<'a> { @@ -317,6 +323,7 @@ impl<'a> Cfg<'a> { quiet, current_dir, process, + skip_auto_install: false, }; // Run some basic checks against the constructed configuration @@ -373,6 +380,10 @@ impl<'a> Cfg<'a> { } pub(crate) fn should_auto_install(&self) -> Result { + if self.skip_auto_install { + return Ok(false); + } + if let Ok(mode) = self.process.var("RUSTUP_AUTO_INSTALL") { Ok(mode != "0") } else { @@ -959,6 +970,7 @@ impl Debug for Cfg<'_> { dist_root_url, quiet, current_dir, + skip_auto_install, process: _, } = self; @@ -976,6 +988,7 @@ impl Debug for Cfg<'_> { .field("dist_root_url", dist_root_url) .field("quiet", quiet) .field("current_dir", current_dir) + .field("skip_auto_install", skip_auto_install) .finish() } } From 8ab1b84a304cf5a311a83440bb6293a1e93c4dd9 Mon Sep 17 00:00:00 2001 From: rami3l Date: Tue, 28 Apr 2026 22:01:08 +0200 Subject: [PATCH 2/5] fix(cli/rustup_mode)!: (wip) skip auto-installation in some `rustup` commands --- src/cli/rustup_mode.rs | 60 +++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 45e793cfd7..86d8d9d5ad 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -689,34 +689,40 @@ pub async fn main( toolchain, force_non_host, } => default_(cfg, toolchain, force_non_host).await, - RustupSubcmd::Target { subcmd } => match subcmd { - TargetSubcmd::List { - toolchain, - installed, - quiet, - } => handle_epipe(target_list(cfg, toolchain, installed, quiet).await), - TargetSubcmd::Add { target, toolchain } => target_add(cfg, target, toolchain).await, - TargetSubcmd::Remove { target, toolchain } => { - target_remove(cfg, target, toolchain).await + RustupSubcmd::Target { subcmd } => { + cfg.skip_auto_install = true; + match subcmd { + TargetSubcmd::List { + toolchain, + installed, + quiet, + } => handle_epipe(target_list(cfg, toolchain, installed, quiet).await), + TargetSubcmd::Add { target, toolchain } => target_add(cfg, target, toolchain).await, + TargetSubcmd::Remove { target, toolchain } => { + target_remove(cfg, target, toolchain).await + } } - }, - RustupSubcmd::Component { subcmd } => match subcmd { - ComponentSubcmd::List { - toolchain, - installed, - quiet, - } => handle_epipe(component_list(cfg, toolchain, installed, quiet).await), - ComponentSubcmd::Add { - component, - toolchain, - target, - } => component_add(cfg, component, toolchain, target).await, - ComponentSubcmd::Remove { - component, - toolchain, - target, - } => component_remove(cfg, component, toolchain, target).await, - }, + } + RustupSubcmd::Component { subcmd } => { + cfg.skip_auto_install = true; + match subcmd { + ComponentSubcmd::List { + toolchain, + installed, + quiet, + } => handle_epipe(component_list(cfg, toolchain, installed, quiet).await), + ComponentSubcmd::Add { + component, + toolchain, + target, + } => component_add(cfg, component, toolchain, target).await, + ComponentSubcmd::Remove { + component, + toolchain, + target, + } => component_remove(cfg, component, toolchain, target).await, + } + } RustupSubcmd::Override { subcmd } => match subcmd { OverrideSubcmd::List => handle_epipe(common::list_overrides(cfg)), OverrideSubcmd::Set { toolchain, path } => { From 0cde1e9eaa3590b608edb7045c25735a9f355acb Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 24 Aug 2025 21:00:38 +0800 Subject: [PATCH 3/5] refactor(toolchain)!: postpone eval of `install_if_missing` in `Toolchain::from_local()` --- src/cli/rustup_mode.rs | 2 +- src/config.rs | 11 ++++------- src/toolchain.rs | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 86d8d9d5ad..7e22d627dd 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -1067,7 +1067,7 @@ async fn run( install: bool, ) -> Result { let toolchain = toolchain.resolve(&cfg.get_default_host_triple()?)?; - let toolchain = Toolchain::from_local(toolchain, install, cfg).await?; + let toolchain = Toolchain::from_local(toolchain, || Ok(install), cfg).await?; let cmd = toolchain.command(&command[0])?; command::run_command_for_dir(cmd, &command[0], &command[1..]) } diff --git a/src/config.rs b/src/config.rs index 7b0086e0fe..b5e20da814 100644 --- a/src/config.rs +++ b/src/config.rs @@ -717,13 +717,10 @@ impl<'a> Cfg<'a> { name: Option<(LocalToolchainName, ActiveSource)>, ) -> Result<(Toolchain<'_>, ActiveSource)> { match name { - Some((tc, source)) => { - let install_if_missing = self.should_auto_install()?; - Ok(( - Toolchain::from_local(tc, install_if_missing, self).await?, - source, - )) - } + Some((tc, src)) => Ok(( + Toolchain::from_local(tc, || self.should_auto_install(), self).await?, + src, + )), None => { let (tc, source) = self .maybe_ensure_active_toolchain(None) diff --git a/src/toolchain.rs b/src/toolchain.rs index 2dc0b23506..5cd54ac2c8 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -52,7 +52,7 @@ pub(crate) struct Toolchain<'a> { impl<'a> Toolchain<'a> { pub(crate) async fn from_local( name: LocalToolchainName, - install_if_missing: bool, + install_if_missing: impl Fn() -> anyhow::Result, cfg: &'a Cfg<'a>, ) -> anyhow::Result> { match Self::new(cfg, name) { @@ -60,7 +60,7 @@ impl<'a> Toolchain<'a> { Err(RustupError::ToolchainNotInstalled { name: ToolchainName::Official(desc), .. - }) if install_if_missing => { + }) if install_if_missing()? => { let options = DistOptions::new(&[], &[], &desc, cfg.get_profile()?, true, cfg)?; Ok(DistributableToolchain::install(options).await?.1.toolchain) } From 8bd06992e8291b92033152ea37c718d42a1d0eff Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 24 Aug 2025 20:43:45 +0800 Subject: [PATCH 4/5] feat(config): warn user if auto-install is enabled --- src/config.rs | 24 +++++++++++++++++++----- src/test/clitools.rs | 3 +++ tests/suite/cli_misc.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index b5e20da814..023867e239 100644 --- a/src/config.rs +++ b/src/config.rs @@ -384,12 +384,26 @@ impl<'a> Cfg<'a> { return Ok(false); } - if let Ok(mode) = self.process.var("RUSTUP_AUTO_INSTALL") { - Ok(mode != "0") - } else { - self.settings_file - .with(|s| Ok(s.auto_install != Some(AutoInstallMode::Disable))) + let should_auto = match self.process.var("RUSTUP_AUTO_INSTALL") { + Ok(mode) => mode != "0", + Err(_) => self + .settings_file + .with(|s| Ok(s.auto_install != Some(AutoInstallMode::Disable)))?, + }; + if !should_auto { + return Ok(false); } + + // We also need to suppress this warning if we're deep inside a recursive call. + let recursions = self.process.var("RUST_RECURSION_COUNT"); + if recursions.is_ok_and(|it| it != "0") { + return Ok(true); + } + + warn!("auto-install is enabled, active toolchain will be installed if absent"); + 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`"); + Ok(true) } // Returns a profile, if one exists in the settings file. diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 0b5032d3cd..d676f530d4 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -314,6 +314,9 @@ impl Config { "/bogus-config-file.toml", ); + // Clear current recursion count to avoid messing up related logic + cmd.env("RUST_RECURSION_COUNT", ""); + // 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 5eb1b11f04..1cd2174bc8 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1746,3 +1746,30 @@ info: falling back to "[EXTERN_PATH]" "#]]) .is_ok(); } + +#[tokio::test] +async fn warn_auto_install() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect_with_env( + ["rustc", "--version"], + [ + ("RUSTUP_TOOLCHAIN", "stable"), + ("RUSTUP_AUTO_INSTALL", "1"), + ("RUST_RECURSION_COUNT", ""), + ], + ) + .await + .with_stdout(snapbox::str![[r#" +1.1.0 (hash-stable-1.1.0) + +"#]]) + .with_stderr(snapbox::str![[r#" +... +warn: auto-install is enabled, active toolchain will be installed if absent +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(); +} From 491befaa5e222bd574e227123bc9f548ac177dab Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 31 Aug 2025 22:57:14 +0800 Subject: [PATCH 5/5] test: make most tests agnostic to `RUSTUP_AUTO_INSTALL` This commit ensures that every test case relying on the `RUSTUP_AUTO_INSTALL` config option is explicitly setting it, and every other test case will pass regardless of its value. --- src/test/clitools.rs | 3 +++ tests/suite/cli_misc.rs | 15 +++++++-------- tests/suite/cli_rustup.rs | 10 ++++++++-- tests/suite/cli_v1.rs | 2 +- tests/suite/cli_v2.rs | 9 ++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index d676f530d4..2102bf64c4 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -317,6 +317,9 @@ impl Config { // 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 1cd2174bc8..0641385e37 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_stdout(snapbox::str![[r#" [..]/toolchains/nightly-[HOST_TUPLE]/bin/rustc[EXE] @@ -1512,7 +1515,7 @@ active because: overridden by +toolchain on the command line .expect(["rustup", "+foo", "which", "rustc"]) .await .with_stderr(snapbox::str![[r#" -error: override toolchain 'foo' is not installed: the +toolchain on the command line specifies an uninstalled toolchain +error:[..] toolchain 'foo' is not installed[..] "#]]) .is_err(); @@ -1753,11 +1756,7 @@ async fn warn_auto_install() { cx.config .expect_with_env( ["rustc", "--version"], - [ - ("RUSTUP_TOOLCHAIN", "stable"), - ("RUSTUP_AUTO_INSTALL", "1"), - ("RUST_RECURSION_COUNT", ""), - ], + [("RUSTUP_TOOLCHAIN", "stable"), ("RUSTUP_AUTO_INSTALL", "1")], ) .await .with_stdout(snapbox::str![[r#" diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 9581f8e23f..d10757d33c 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -1240,7 +1240,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#" @@ -1355,7 +1355,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 cfc84af3ed..aff293239d 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)