From 68aafbbaf451c07442de9dcd61b6d93aff46c7b9 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Mon, 6 Oct 2025 18:34:28 -0400 Subject: [PATCH 1/5] Set `RUSTUP_TOOLCHAIN_SOURCE` --- src/cli/proxy_mode.rs | 11 ++++-- src/config.rs | 16 ++++++++ src/test/mock_bin_src.rs | 8 ++++ src/toolchain.rs | 25 ++++++++++++ tests/suite/cli_rustup.rs | 82 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 1cb24f8252..ef20029da8 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -5,6 +5,7 @@ use anyhow::Result; use crate::{ cli::{common::set_globals, job, self_update}, command::run_command_for_dir, + config::ActiveReason, process::Process, toolchain::ResolvableLocalToolchainName, }; @@ -24,6 +25,7 @@ pub async fn main(arg0: &str, current_dir: PathBuf, process: &Process) -> Result .filter(|arg| arg.starts_with('+')) .map(|name| ResolvableLocalToolchainName::try_from(&name.as_ref()[1..])) .transpose()?; + let toolchain_specified = toolchain.is_some(); // Build command args now while we know whether or not to skip arg 1. let cmd_args: Vec<_> = process @@ -32,9 +34,10 @@ pub async fn main(arg0: &str, current_dir: PathBuf, process: &Process) -> Result .collect(); let cfg = set_globals(current_dir, true, process)?; - let cmd = cfg - .resolve_local_toolchain(toolchain) - .await? - .command(arg0)?; + let toolchain = cfg.resolve_local_toolchain(toolchain).await?; + let mut cmd = toolchain.command(arg0)?; + if toolchain_specified { + toolchain.maybe_set_env_source(&mut cmd, || Some(ActiveReason::CommandLine)); + } run_command_for_dir(cmd, arg0, &cmd_args) } diff --git a/src/config.rs b/src/config.rs index b4c574fdc7..6469889fa6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -102,6 +102,22 @@ pub(crate) enum ActiveReason { ToolchainFile(PathBuf), } +impl ActiveReason { + /// Format `ActiveReason` for setting the `RUSTUP_TOOLCHAIN_SOURCE` environment variable. + /// + /// The format of the variable's content is `source toolchain`. Neither component should + /// contain spaces. + pub fn to_source(&self, name: &LocalToolchainName) -> String { + match self { + Self::Default => format!("default {name}"), + Self::Environment => format!("env {name}"), + Self::CommandLine => format!("cli {name}"), + Self::OverrideDB(_) => format!("override {name}"), + Self::ToolchainFile(_) => format!("config {name}"), + } + } +} + impl Display for ActiveReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> { match self { diff --git a/src/test/mock_bin_src.rs b/src/test/mock_bin_src.rs index d85b8002cd..5ca84dba18 100644 --- a/src/test/mock_bin_src.rs +++ b/src/test/mock_bin_src.rs @@ -106,6 +106,14 @@ fn main() { let mut out = io::stderr(); writeln!(out, "{}", std::env::current_exe().unwrap().display()).unwrap(); } + Some("--echo-rustup-toolchain-source") => { + let mut out = io::stderr(); + if let Ok(rustup_toolchain_source) = std::env::var("RUSTUP_TOOLCHAIN_SOURCE") { + writeln!(out, "{rustup_toolchain_source}").unwrap(); + } else { + panic!("RUSTUP_TOOLCHAIN_SOURCE environment variable not set"); + } + } arg => panic!("bad mock proxy commandline: {:?}", arg), } } diff --git a/src/toolchain.rs b/src/toolchain.rs index ad7183cfce..248207129d 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -181,10 +181,35 @@ impl<'a> Toolchain<'a> { env_var::inc("RUST_RECURSION_COUNT", cmd, self.cfg.process); + self.maybe_set_env_source(cmd, || { + if let Ok(Some((_, reason))) = self.cfg.active_toolchain() { + Some(reason) + } else { + None + } + }); + cmd.env("RUSTUP_TOOLCHAIN", format!("{}", self.name)); cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); } + /// Set the `RUSTUP_TOOLCHAIN_SOURCE` environment variable if not already set. + /// + /// `RUSTUP_TOOLCHAIN_SOURCE` indicates how the toolchain was determined. The environment + /// variable could have been set in proxy_mode.rs, in which case it should not be changed. + pub(crate) fn maybe_set_env_source( + &self, + cmd: &mut Command, + f: impl FnOnce() -> Option, + ) { + if env::var_os("RUSTUP_TOOLCHAIN_SOURCE").is_some() { + return; + } + if let Some(reason) = f() { + cmd.env("RUSTUP_TOOLCHAIN_SOURCE", reason.to_source(&self.name)); + } + } + /// Apply the appropriate LD path for a command being run from a toolchain. fn set_ldpath(&self, cmd: &mut Command) { #[cfg_attr(not(target_os = "macos"), allow(unused_mut))] diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 7b1b0787dc..2cf62353cc 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -3038,6 +3038,88 @@ error: no active toolchain .is_ok(); } +#[tokio::test] +async fn rustup_toolchain_source_cli() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect(&["rustup", "install", "nightly"]) + .await + .is_ok(); + cx.config + .expect(["cargo", "+nightly", "--echo-rustup-toolchain-source"]) + .await + .with_stderr(snapbox::str![[r#" +... +cli nightly-[HOST_TRIPLE] + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_env() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect_with_env( + ["cargo", "--echo-rustup-toolchain-source"], + [("RUSTUP_TOOLCHAIN", "nightly")], + ) + .await + .with_stderr(snapbox::str![[r#" +... +env nightly-[HOST_TRIPLE] + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_override() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect(["rustup", "override", "set", "nightly"]) + .await + .is_ok(); + cx.config + .expect(["cargo", "--echo-rustup-toolchain-source"]) + .await + .with_stderr(snapbox::str![[r#" +... +override nightly-[HOST_TRIPLE] + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_config() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + let toolchain_file = cx.config.current_dir().join("rust-toolchain.toml"); + raw::write_file(&toolchain_file, &format!("[toolchain]\nchannel='nightly'")).unwrap(); + cx.config + .expect(["cargo", "--echo-rustup-toolchain-source"]) + .await + .with_stderr(snapbox::str![[r#" +... +config nightly-[HOST_TRIPLE] + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_default() { + let cx = CliTestContext::new(Scenario::SimpleV2).await; + cx.config + .expect(&["rustup", "default", "stable"]) + .await + .is_ok(); + cx.config + .expect(["cargo", "--echo-rustup-toolchain-source"]) + .await + .with_stderr(snapbox::str![[r#" +... +default stable-[HOST_TRIPLE] + +"#]]); +} + #[tokio::test] async fn directory_override_doesnt_need_to_exist_unless_it_is_selected() { let cx = CliTestContext::new(Scenario::SimpleV2).await; From 87afccdf45859fb98ae6e75693dfc5e1900783ba Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Mon, 6 Oct 2025 18:49:37 -0400 Subject: [PATCH 2/5] Make Clippy happy --- tests/suite/cli_rustup.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 2cf62353cc..337f097bb2 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -3092,7 +3092,11 @@ override nightly-[HOST_TRIPLE] async fn rustup_toolchain_source_config() { let cx = CliTestContext::new(Scenario::SimpleV2).await; let toolchain_file = cx.config.current_dir().join("rust-toolchain.toml"); - raw::write_file(&toolchain_file, &format!("[toolchain]\nchannel='nightly'")).unwrap(); + raw::write_file( + &toolchain_file, + &"[toolchain]\nchannel='nightly'".to_string(), + ) + .unwrap(); cx.config .expect(["cargo", "--echo-rustup-toolchain-source"]) .await From 92b2ba434d2d47b64b9fdf67dbda771730024687 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Mon, 6 Oct 2025 18:55:41 -0400 Subject: [PATCH 3/5] Make Clippy even happier --- tests/suite/cli_rustup.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 337f097bb2..abca9fda8c 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -3092,11 +3092,7 @@ override nightly-[HOST_TRIPLE] async fn rustup_toolchain_source_config() { let cx = CliTestContext::new(Scenario::SimpleV2).await; let toolchain_file = cx.config.current_dir().join("rust-toolchain.toml"); - raw::write_file( - &toolchain_file, - &"[toolchain]\nchannel='nightly'".to_string(), - ) - .unwrap(); + raw::write_file(&toolchain_file, "[toolchain]\nchannel='nightly'").unwrap(); cx.config .expect(["cargo", "--echo-rustup-toolchain-source"]) .await From da1223a87ce44fd0437866af0dcb943ded375844 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Tue, 7 Oct 2025 05:15:45 -0400 Subject: [PATCH 4/5] Eliminate overlap with `RUSTUP_TOOLCHAIN` --- src/cli/proxy_mode.rs | 4 ++-- src/config.rs | 15 ++++++--------- src/toolchain.rs | 32 ++++++++++++++------------------ tests/suite/cli_rustup.rs | 10 +++++----- 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index ef20029da8..67829a6058 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -7,7 +7,7 @@ use crate::{ command::run_command_for_dir, config::ActiveReason, process::Process, - toolchain::ResolvableLocalToolchainName, + toolchain::{ResolvableLocalToolchainName, maybe_set_env_source}, }; #[tracing::instrument(level = "trace", skip(process))] @@ -37,7 +37,7 @@ pub async fn main(arg0: &str, current_dir: PathBuf, process: &Process) -> Result let toolchain = cfg.resolve_local_toolchain(toolchain).await?; let mut cmd = toolchain.command(arg0)?; if toolchain_specified { - toolchain.maybe_set_env_source(&mut cmd, || Some(ActiveReason::CommandLine)); + maybe_set_env_source(&mut cmd, || Some(ActiveReason::CommandLine)); } run_command_for_dir(cmd, arg0, &cmd_args) } diff --git a/src/config.rs b/src/config.rs index 6469889fa6..bab759ea0b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,16 +104,13 @@ pub(crate) enum ActiveReason { impl ActiveReason { /// Format `ActiveReason` for setting the `RUSTUP_TOOLCHAIN_SOURCE` environment variable. - /// - /// The format of the variable's content is `source toolchain`. Neither component should - /// contain spaces. - pub fn to_source(&self, name: &LocalToolchainName) -> String { + pub fn to_source(&self) -> &str { match self { - Self::Default => format!("default {name}"), - Self::Environment => format!("env {name}"), - Self::CommandLine => format!("cli {name}"), - Self::OverrideDB(_) => format!("override {name}"), - Self::ToolchainFile(_) => format!("config {name}"), + Self::Default => "default", + Self::Environment => "env", + Self::CommandLine => "cli", + Self::OverrideDB(_) => "override", + Self::ToolchainFile(_) => "config", } } } diff --git a/src/toolchain.rs b/src/toolchain.rs index 248207129d..4599e39bb8 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -181,7 +181,7 @@ impl<'a> Toolchain<'a> { env_var::inc("RUST_RECURSION_COUNT", cmd, self.cfg.process); - self.maybe_set_env_source(cmd, || { + maybe_set_env_source(cmd, || { if let Ok(Some((_, reason))) = self.cfg.active_toolchain() { Some(reason) } else { @@ -193,23 +193,6 @@ impl<'a> Toolchain<'a> { cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); } - /// Set the `RUSTUP_TOOLCHAIN_SOURCE` environment variable if not already set. - /// - /// `RUSTUP_TOOLCHAIN_SOURCE` indicates how the toolchain was determined. The environment - /// variable could have been set in proxy_mode.rs, in which case it should not be changed. - pub(crate) fn maybe_set_env_source( - &self, - cmd: &mut Command, - f: impl FnOnce() -> Option, - ) { - if env::var_os("RUSTUP_TOOLCHAIN_SOURCE").is_some() { - return; - } - if let Some(reason) = f() { - cmd.env("RUSTUP_TOOLCHAIN_SOURCE", reason.to_source(&self.name)); - } - } - /// Apply the appropriate LD path for a command being run from a toolchain. fn set_ldpath(&self, cmd: &mut Command) { #[cfg_attr(not(target_os = "macos"), allow(unused_mut))] @@ -630,3 +613,16 @@ impl<'a> Toolchain<'a> { .collect()) } } + +/// Set the `RUSTUP_TOOLCHAIN_SOURCE` environment variable if not already set. +/// +/// `RUSTUP_TOOLCHAIN_SOURCE` indicates how the toolchain was determined. The environment +/// variable could have been set in proxy_mode.rs, in which case it should not be changed. +pub(crate) fn maybe_set_env_source(cmd: &mut Command, f: impl FnOnce() -> Option) { + if env::var_os("RUSTUP_TOOLCHAIN_SOURCE").is_some() { + return; + } + if let Some(reason) = f() { + cmd.env("RUSTUP_TOOLCHAIN_SOURCE", reason.to_source()); + } +} diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index abca9fda8c..f59b92da95 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -3050,7 +3050,7 @@ async fn rustup_toolchain_source_cli() { .await .with_stderr(snapbox::str![[r#" ... -cli nightly-[HOST_TRIPLE] +cli "#]]); } @@ -3066,7 +3066,7 @@ async fn rustup_toolchain_source_env() { .await .with_stderr(snapbox::str![[r#" ... -env nightly-[HOST_TRIPLE] +env "#]]); } @@ -3083,7 +3083,7 @@ async fn rustup_toolchain_source_override() { .await .with_stderr(snapbox::str![[r#" ... -override nightly-[HOST_TRIPLE] +override "#]]); } @@ -3098,7 +3098,7 @@ async fn rustup_toolchain_source_config() { .await .with_stderr(snapbox::str![[r#" ... -config nightly-[HOST_TRIPLE] +config "#]]); } @@ -3115,7 +3115,7 @@ async fn rustup_toolchain_source_default() { .await .with_stderr(snapbox::str![[r#" ... -default stable-[HOST_TRIPLE] +default "#]]); } From 282ede1276e494e5924bb818954449dd43030c26 Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Tue, 7 Oct 2025 13:00:35 -0400 Subject: [PATCH 5/5] Describe `RUSTUP_TOOLCHAIN_SOURCE` in environment-variables.md --- doc/user-guide/src/environment-variables.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index a9669be902..a8240903dd 100644 --- a/doc/user-guide/src/environment-variables.md +++ b/doc/user-guide/src/environment-variables.md @@ -72,6 +72,8 @@ - `RUSTUP_CONCURRENT_DOWNLOADS` *unstable* (default: the number of components to download). Controls the number of downloads made concurrently. +- `RUSTUP_TOOLCHAIN_SOURCE`. Set by rustup to tell downstream tools how `RUSTUP_TOOLCHAIN` was determined. + [directive syntax]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives [dc]: https://docs.docker.com/storage/storagedriver/overlayfs-driver/#modifying-files-or-directories [override]: overrides.md