Skip to content
Merged
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
2 changes: 2 additions & 0 deletions doc/user-guide/src/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` *unstable*. Set by rustup to tell proxied tools how `RUSTUP_TOOLCHAIN` was determined.
Copy link
Member

Choose a reason for hiding this comment

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

AFAIK, rust-analyzer (and some other tools) overrides RUSTUP_TOOLCHAIN environment variable directly based on the result of its probing 1. They also documents that user might set that directly 2.

My questions are:

  • Should those cases above all be considered as environment overrides source? What is the source of true of RUSTUP_TOOLCHAIN_SOURCE?
  • Should tool override and provide the original RUSTUP_TOOLCHAIN_SOURCE if they have resolved to the actual toolchain first on their own through?

Note that this is probably not going to affect general user cases, and RUSTUP_TOOLCHAIN_SOURCE can just be a best-effort env. Just FYI at $WORK we have a quite complex layer of rustup overrides and I wonder whether we should fully trust this or reset it whenever we override RUSTUP_TOOLCHAIN.

Footnotes

  1. https://github.com/rust-lang/rust-analyzer/blob/1e20331e42449dfc0b44bce84147a06772d045d7/crates/rust-analyzer/src/flycheck.rs#L639

  2. https://github.com/rust-lang/rust-analyzer/blob/1e20331e42449dfc0b44bce84147a06772d045d7/docs/book/src/installation.md?plain=1#L24-L26

Copy link
Contributor Author

@smoelius smoelius Oct 18, 2025

Choose a reason for hiding this comment

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

AFAIK, rust-analyzer (and some other tools) overrides RUSTUP_TOOLCHAIN environment variable directly based on the result of its probing 1. They also documents that user might set that directly 2.

My questions are:

  • Should those cases above all be considered as environment overrides source? What is the source of true of RUSTUP_TOOLCHAIN_SOURCE?

Could your question be restated as, "What are the definitions of the five RUSTUP_TOOLCHAIN_SOURCE categories?"

I would argue they are the same as in the "Overrides" section of the Rustup book.

For the sake of your examples, the toolchain would not be specified on the command line,1 so the toolchain should be determined by RUSTUP_TOOLCHAIN. In that sense, it seems correct that RUSTUP_TOOLCHAIN_SOURCE should be set to env.

  • Should tool override and provide the original RUSTUP_TOOLCHAIN_SOURCE if they have resolved to the actual toolchain first on their own through?

...Just FYI at $WORK we have a quite complex layer of rustup overrides and I wonder whether we should fully trust this or reset it whenever we override RUSTUP_TOOLCHAIN.

My non-authoritative opinion is "no", tools other that rustup should not set RUSTUP_TOOLCHAIN_SOURCE.

I am reminded of a comment @epage made in rust-lang/cargo#15099 (comment):

In the Cargo team meeting, someone brought up users manually unsetting CARGO but I worry about how error prone that is and how difficult to debug.

If non-rustup tools are allowed to set RUSTUP_TOOLCHAIN_SOURCE, I could imagine it leading to a million bug reports down the road.

Should we expand the documentation to say that non-rustup tools should not set RUSTUP_TOOLCHAIN_SOURCE?

Footnotes

  1. It's also not clear to me what that would mean.

Copy link
Member

Choose a reason for hiding this comment

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

the toolchain would not be specified on the command line,1 so the toolchain should be determined by RUSTUP_TOOLCHAIN. In that sense, it seems correct that RUSTUP_TOOLCHAIN_SOURCE should be set to env.

Given the current definition of RUSTUP_TOOLCHAIN_SOURCE, I agree. Although it’s somewhat of an optimization on rust-analyzer’s side, the value r-a sets is still retrieved through the rustup proxy.

I guess my question could be rephrased as: “If a tool caches the rustup-resolved toolchain path for optimization, should it also propagate the original RUSTUP_TOOLCHAIN_SOURCE?”

I don’t think we need to overthink this, since rust-analyzer users likely won’t be affected. That said, we should keep in mind that some “rustup” wrappers might produce unexpected or less precise diagnostics if Cargo starts treating RUSTUP_TOOLCHAIN_SOURCE as the source of truth. (Those wrappers probably want to preserve the original source instead of being treated as env.)

Anyway, I agree that we may want to make this clearer in the documentation, especially which environments are meant for reading and which are for writing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess my question could be rephrased as: “If a tool caches the rustup-resolved toolchain path for optimization, should it also propagate the original RUSTUP_TOOLCHAIN_SOURCE?”

It seems reasonable to answer this with yes.

I wonder if we can test this whole thing before we release rustup with this code? I guess it would need some of the relevant Cargo changes (not sure if anyone is working on those already) and then pulling in both as snapshot builds.

Copy link
Member

@rami3l rami3l Oct 19, 2025

Choose a reason for hiding this comment

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

IMHO I agreed to include this patch based on the premise that this is a) a private contract between us and cargo; b) a best-effort optimization to communicate with the latter why a toolchain is activated.

As such, I am wondering what will the problems caused by overriding that env var be if you are already bypassing rustup's toolchain resolution. So "yes" to the question above, since the results are purely cargo-oriented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess it would need some of the relevant Cargo changes (not sure if anyone is working on those already)

@djc Just to answer your question: rust-lang/cargo#16131


[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
Expand Down
2 changes: 1 addition & 1 deletion src/cli/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ pub(crate) async fn list_toolchains(
} else {
let default_toolchain_name = cfg.get_default()?;
let active_toolchain_name: Option<ToolchainName> =
if let Ok(Some((LocalToolchainName::Named(toolchain), _reason))) =
if let Ok(Some((LocalToolchainName::Named(toolchain), _source))) =
cfg.maybe_ensure_active_toolchain(None).await
{
Some(toolchain)
Expand Down
16 changes: 12 additions & 4 deletions src/cli/proxy_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use anyhow::Result;
use crate::{
cli::{common::set_globals, job, self_update},
command::run_command_for_dir,
config::ActiveSource,
process::Process,
toolchain::ResolvableLocalToolchainName,
};
Expand All @@ -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
Expand All @@ -32,9 +34,15 @@ 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 {
cmd.env(
"RUSTUP_TOOLCHAIN_SOURCE",
ActiveSource::CommandLine.to_string(),
);
} else if let Ok(Some((_, source))) = cfg.active_toolchain() {
cmd.env("RUSTUP_TOOLCHAIN_SOURCE", source.to_string());
}
run_command_for_dir(cmd, arg0, &cmd_args)
}
45 changes: 24 additions & 21 deletions src/cli/rustup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::{
topical_doc,
},
command, component_for_bin,
config::{ActiveReason, Cfg},
config::{ActiveSource, Cfg},
dist::{
AutoInstallMode, PartialToolchainDesc, Profile, TargetTriple,
manifest::{Component, ComponentStatus},
Expand Down Expand Up @@ -779,10 +779,13 @@ async fn default_(
}
};

if let Some((toolchain, reason)) = cfg.active_toolchain()?
&& !matches!(reason, ActiveReason::Default)
if let Some((toolchain, source)) = cfg.active_toolchain()?
&& !matches!(source, ActiveSource::Default)
{
info!("note that the toolchain '{toolchain}' is currently in use ({reason})");
info!(
"note that the toolchain '{toolchain}' is currently in use ({})",
source.to_reason()
);
}
} else {
let default_toolchain = cfg
Expand Down Expand Up @@ -997,9 +1000,9 @@ async fn update(
exit_code &= self_update::self_update(cfg.process).await?;
}
} else if ensure_active_toolchain {
let (toolchain, reason) = cfg.ensure_active_toolchain(force_non_host, true).await?;
let (toolchain, source) = cfg.ensure_active_toolchain(force_non_host, true).await?;
info!("the active toolchain `{toolchain}` has been installed");
info!("it's active because: {reason}");
info!("it's active because: {}", source.to_reason());
} else {
exit_code &= common::update_all_channels(cfg, opts.force).await?;
if self_update {
Expand Down Expand Up @@ -1088,16 +1091,16 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result<utils::ExitCode> {
}

let installed_toolchains = cfg.list_toolchains()?;
let active_toolchain_and_reason: Option<(ToolchainName, ActiveReason)> =
if let Ok(Some((LocalToolchainName::Named(toolchain_name), reason))) =
let active_toolchain_and_source: Option<(ToolchainName, ActiveSource)> =
if let Ok(Some((LocalToolchainName::Named(toolchain_name), source))) =
cfg.maybe_ensure_active_toolchain(None).await
{
Some((toolchain_name, reason))
Some((toolchain_name, source))
} else {
None
};

let (active_toolchain_name, _active_reason) = active_toolchain_and_reason
let (active_toolchain_name, _active_source) = active_toolchain_and_source
.as_ref()
.map(|atar| (&atar.0, &atar.1))
.unzip();
Expand Down Expand Up @@ -1161,15 +1164,15 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result<utils::ExitCode> {

print_header(&mut t, "active toolchain")?;

match active_toolchain_and_reason {
Some((active_toolchain_name, active_reason)) => {
let active_toolchain = Toolchain::with_reason(
match active_toolchain_and_source {
Some((active_toolchain_name, active_source)) => {
let active_toolchain = Toolchain::with_source(
cfg,
active_toolchain_name.clone().into(),
&active_reason,
&active_source,
)?;
writeln!(t.lock(), "name: {}", active_toolchain.name())?;
writeln!(t.lock(), "active because: {active_reason}")?;
writeln!(t.lock(), "active because: {}", active_source.to_reason())?;
if verbose {
writeln!(t.lock(), "compiler: {}", active_toolchain.rustc_version())?;
writeln!(t.lock(), "path: {}", active_toolchain.path().display())?;
Expand Down Expand Up @@ -1205,14 +1208,14 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result<utils::ExitCode> {
#[tracing::instrument(level = "trace", skip_all)]
async fn show_active_toolchain(cfg: &Cfg<'_>, verbose: bool) -> Result<utils::ExitCode> {
match cfg.maybe_ensure_active_toolchain(None).await? {
Some((toolchain_name, reason)) => {
let toolchain = Toolchain::with_reason(cfg, toolchain_name.clone(), &reason)?;
Some((toolchain_name, source)) => {
let toolchain = Toolchain::with_source(cfg, toolchain_name.clone(), &source)?;
if verbose {
writeln!(
cfg.process.stdout().lock(),
"{}\nactive because: {}\ncompiler: {}\npath: {}",
toolchain.name(),
reason,
source.to_reason(),
toolchain.rustc_version(),
toolchain.path().display()
)?;
Expand All @@ -1221,9 +1224,9 @@ async fn show_active_toolchain(cfg: &Cfg<'_>, verbose: bool) -> Result<utils::Ex
cfg.process.stdout().lock(),
"{} ({})",
toolchain.name(),
match reason {
ActiveReason::Default => &"default" as &dyn fmt::Display,
_ => &reason,
match source {
ActiveSource::Default => &"default" as &dyn fmt::Display,
_ => &source.to_reason(),
}
)?;
}
Expand Down
78 changes: 46 additions & 32 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,28 +92,42 @@ impl<T: Into<String>> From<T> for OverrideFile {
}
}

// Represents the reason why the active toolchain is active.
// Represents the source that determined the current active toolchain.
#[derive(Debug)]
pub(crate) enum ActiveReason {
pub(crate) enum ActiveSource {
Default,
Environment,
CommandLine,
OverrideDB(PathBuf),
ToolchainFile(PathBuf),
}

impl Display for ActiveReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
impl ActiveSource {
pub fn to_reason(&self) -> String {
match self {
Self::Default => write!(f, "it's the default toolchain"),
Self::Environment => write!(f, "overridden by environment variable RUSTUP_TOOLCHAIN"),
Self::CommandLine => write!(f, "overridden by +toolchain on the command line"),
Self::OverrideDB(path) => write!(f, "directory override for '{}'", path.display()),
Self::ToolchainFile(path) => write!(f, "overridden by '{}'", path.display()),
Self::Default => String::from("it's the default toolchain"),
Self::Environment => {
String::from("overridden by environment variable RUSTUP_TOOLCHAIN")
}
Self::CommandLine => String::from("overridden by +toolchain on the command line"),
Self::OverrideDB(path) => format!("directory override for '{}'", path.display()),
Self::ToolchainFile(path) => format!("overridden by '{}'", path.display()),
}
}
}

impl Display for ActiveSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Default => "default",
Self::Environment => "env",
Self::CommandLine => "cli",
Self::OverrideDB(_) => "path-override",
Self::ToolchainFile(_) => "toolchain-file",
})
}
}

// 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 @@ -505,7 +519,7 @@ impl<'a> Cfg<'a> {
pub(crate) async fn maybe_ensure_active_toolchain(
&self,
force_ensure: Option<bool>,
) -> Result<Option<(LocalToolchainName, ActiveReason)>> {
) -> Result<Option<(LocalToolchainName, ActiveSource)>> {
let should_ensure = if let Some(force) = force_ensure {
force
} else {
Expand All @@ -524,39 +538,39 @@ impl<'a> Cfg<'a> {
}
}

pub(crate) fn active_toolchain(&self) -> Result<Option<(LocalToolchainName, ActiveReason)>> {
pub(crate) fn active_toolchain(&self) -> Result<Option<(LocalToolchainName, ActiveSource)>> {
Ok(
if let Some((override_config, reason)) = self.find_override_config()? {
Some((override_config.into_local_toolchain_name(), reason))
if let Some((override_config, source)) = self.find_override_config()? {
Some((override_config.into_local_toolchain_name(), source))
} else {
self.get_default()?
.map(|x| (x.into(), ActiveReason::Default))
.map(|x| (x.into(), ActiveSource::Default))
},
)
}

fn find_override_config(&self) -> Result<Option<(OverrideCfg, ActiveReason)>> {
let override_config: Option<(OverrideCfg, ActiveReason)> =
fn find_override_config(&self) -> Result<Option<(OverrideCfg, ActiveSource)>> {
let override_config: Option<(OverrideCfg, ActiveSource)> =
// First check +toolchain override from the command line
if let Some(name) = &self.toolchain_override {
let override_config = name.resolve(&self.get_default_host_triple()?)?.into();
Some((override_config, ActiveReason::CommandLine))
Some((override_config, ActiveSource::CommandLine))
}
// Then check the RUSTUP_TOOLCHAIN environment variable
else if let Some(name) = &self.env_override {
// Because path based toolchain files exist, this has to support
// custom, distributable, and absolute path toolchains otherwise
// rustup's export of a RUSTUP_TOOLCHAIN when running a process will
// error when a nested rustup invocation occurs
Some((name.clone().into(), ActiveReason::Environment))
Some((name.clone().into(), ActiveSource::Environment))
}
// Then walk up the directory tree from 'path' looking for either the
// directory in the override database, or a `rust-toolchain{.toml}` file,
// in that order.
else if let Some((override_cfg, active_reason)) = self.settings_file.with(|s| {
else if let Some((override_cfg, active_source)) = self.settings_file.with(|s| {
self.find_override_from_dir_walk(&self.current_dir, s)
})? {
Some((override_cfg, active_reason))
Some((override_cfg, active_source))
}
// Otherwise, there is no override.
else {
Expand All @@ -570,13 +584,13 @@ impl<'a> Cfg<'a> {
&self,
dir: &Path,
settings: &Settings,
) -> Result<Option<(OverrideCfg, ActiveReason)>> {
) -> Result<Option<(OverrideCfg, ActiveSource)>> {
let mut dir = Some(dir);

while let Some(d) = dir {
// First check the override database
if let Some(name) = settings.dir_override(d) {
let reason = ActiveReason::OverrideDB(d.to_owned());
let source = ActiveSource::OverrideDB(d.to_owned());
// Note that `rustup override set` fully resolves it's input
// before writing to settings.toml, so resolving here may not
// be strictly necessary (could instead model as ToolchainName).
Expand All @@ -586,7 +600,7 @@ impl<'a> Cfg<'a> {
let toolchain_name = ResolvableToolchainName::try_from(name)?
.resolve(&get_default_host_triple(settings, self.process))?;
let override_cfg = toolchain_name.into();
return Ok(Some((override_cfg, reason)));
return Ok(Some((override_cfg, source)));
}

// Then look for 'rust-toolchain' or 'rust-toolchain.toml'
Expand Down Expand Up @@ -670,9 +684,9 @@ impl<'a> Cfg<'a> {
}
}

let reason = ActiveReason::ToolchainFile(toolchain_file);
let source = ActiveSource::ToolchainFile(toolchain_file);
let override_cfg = OverrideCfg::from_file(self, override_file)?;
return Ok(Some((override_cfg, reason)));
return Ok(Some((override_cfg, source)));
}

dir = d.parent();
Expand Down Expand Up @@ -766,8 +780,8 @@ impl<'a> Cfg<'a> {
&self,
force_non_host: bool,
verbose: bool,
) -> Result<(LocalToolchainName, ActiveReason)> {
if let Some((override_config, reason)) = self.find_override_config()? {
) -> Result<(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 {
toolchain,
Expand All @@ -786,18 +800,18 @@ impl<'a> Cfg<'a> {
)
.await?;
} else {
Toolchain::with_reason(self, toolchain.clone(), &reason)?;
Toolchain::with_source(self, toolchain.clone(), &source)?;
}
Ok((toolchain, reason))
Ok((toolchain, source))
} else if let Some(toolchain) = self.get_default()? {
let reason = ActiveReason::Default;
let source = ActiveSource::Default;
if let ToolchainName::Official(desc) = &toolchain {
self.ensure_installed(desc, vec![], vec![], None, force_non_host, verbose)
.await?;
} else {
Toolchain::with_reason(self, toolchain.clone().into(), &reason)?;
Toolchain::with_source(self, toolchain.clone().into(), &source)?;
}
Ok((toolchain.into(), reason))
Ok((toolchain.into(), source))
} else {
Err(no_toolchain_error(self.process))
}
Expand Down
8 changes: 8 additions & 0 deletions src/test/mock_bin_src.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Expand Down
Loading
Loading