Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/cli/rustup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))?;

Expand Down Expand Up @@ -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!(
Copy link
Copy Markdown
Contributor

@djc djc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be replaced by a Deref impl on Ensured?

View changes since the review

"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 {
Expand All @@ -1097,7 +1100,7 @@ async fn run(
) -> Result<ExitStatus> {
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..])
}

Expand Down
68 changes: 52 additions & 16 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Copy link
Copy Markdown
Contributor

@djc djc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensured is a pretty generic name. Something more specific might be nicer?

View changes since the review

pub inner: T,
pub status: UpdateStatus,
}

impl<T> Ensured<T> {
pub fn new(toolchain: impl Into<T>, 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
Expand Down Expand Up @@ -517,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::<RustupError>() {
Some(RustupError::ToolchainNotSelected(_)) => Ok(None),
_ => Err(e),
Expand Down Expand Up @@ -719,10 +739,9 @@ 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?;
auto_install_warning(self.process, tc.inner.name(), &tc.status);
Ok((tc.inner, source))
}
None => {
let (tc, source) = self
Expand All @@ -739,10 +758,10 @@ impl<'a> Cfg<'a> {
&self,
force_non_host: bool,
verbose: bool,
) -> Result<(LocalToolchainName, ActiveSource)> {
) -> Result<(Ensured<LocalToolchainName>, 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,
Expand All @@ -757,20 +776,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))
}
Expand All @@ -787,7 +810,7 @@ impl<'a> Cfg<'a> {
profile: Option<Profile>,
force_non_host: bool,
verbose: bool,
) -> Result<(UpdateStatus, Toolchain<'_>)> {
) -> Result<Ensured<Toolchain<'_>>> {
common::check_non_host_toolchain(
toolchain.to_string(),
&TargetTuple::from_host_or_build(self.process),
Expand Down Expand Up @@ -831,7 +854,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.
Expand Down Expand Up @@ -1003,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) {
Copy link
Copy Markdown
Contributor

@djc djc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could/should this take an Ensured? Could it be a method on Ensured? I'd suggest moving the Process argument last, since it's more boring/long-lived/less relevant.

View changes since the review

// 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.
Expand Down
6 changes: 6 additions & 0 deletions src/test/clitools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 7 additions & 5 deletions src/toolchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand All @@ -54,15 +55,16 @@ impl<'a> Toolchain<'a> {
name: LocalToolchainName,
install_if_missing: bool,
cfg: &'a Cfg<'a>,
) -> anyhow::Result<Toolchain<'a>> {
) -> anyhow::Result<Ensured<Toolchain<'a>>> {
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()),
}
Expand Down
53 changes: 51 additions & 2 deletions tests/suite/cli_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1746,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();
}
10 changes: 8 additions & 2 deletions tests/suite/cli_rustup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/suite/cli_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 12 additions & 3 deletions tests/suite/cli_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -511,7 +514,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)
Expand All @@ -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();
Expand Down Expand Up @@ -553,7 +559,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)
Expand Down
Loading