diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index a9669be902..a0930aa32f 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` *unstable*. Set by rustup to tell proxied 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 diff --git a/src/cli/common.rs b/src/cli/common.rs index bb5002cf9c..45200b3883 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -332,7 +332,7 @@ pub(crate) async fn list_toolchains( } else { let default_toolchain_name = cfg.get_default()?; let active_toolchain_name: Option = - 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) diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 1cb24f8252..c7bec9f132 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::ActiveSource, 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,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) } diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 46df7df561..0fc4759047 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -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}, @@ -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 @@ -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 { @@ -1088,16 +1091,16 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result { } 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(); @@ -1161,15 +1164,15 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result { 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())?; @@ -1205,14 +1208,14 @@ async fn show(cfg: &Cfg<'_>, verbose: bool) -> Result { #[tracing::instrument(level = "trace", skip_all)] async fn show_active_toolchain(cfg: &Cfg<'_>, verbose: bool) -> Result { 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() )?; @@ -1221,9 +1224,9 @@ async fn show_active_toolchain(cfg: &Cfg<'_>, verbose: bool) -> Result &"default" as &dyn fmt::Display, - _ => &reason, + match source { + ActiveSource::Default => &"default" as &dyn fmt::Display, + _ => &source.to_reason(), } )?; } diff --git a/src/config.rs b/src/config.rs index 9f238038d0..7df748e6f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -92,9 +92,9 @@ impl> From 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, @@ -102,18 +102,32 @@ pub(crate) enum ActiveReason { 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 @@ -505,7 +519,7 @@ impl<'a> Cfg<'a> { pub(crate) async fn maybe_ensure_active_toolchain( &self, force_ensure: Option, - ) -> Result> { + ) -> Result> { let should_ensure = if let Some(force) = force_ensure { force } else { @@ -524,23 +538,23 @@ impl<'a> Cfg<'a> { } } - pub(crate) fn active_toolchain(&self) -> Result> { + pub(crate) fn active_toolchain(&self) -> Result> { 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> { - let override_config: Option<(OverrideCfg, ActiveReason)> = + fn find_override_config(&self) -> Result> { + 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 { @@ -548,15 +562,15 @@ impl<'a> Cfg<'a> { // 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 { @@ -570,13 +584,13 @@ impl<'a> Cfg<'a> { &self, dir: &Path, settings: &Settings, - ) -> Result> { + ) -> Result> { 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). @@ -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' @@ -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(); @@ -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, @@ -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)) } 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 2fe7194ed4..a59c1fe3ad 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -21,7 +21,7 @@ use wait_timeout::ChildExt; use crate::{ RustupError, - config::{ActiveReason, Cfg, InstalledPath}, + config::{ActiveSource, Cfg, InstalledPath}, dist::{ PartialToolchainDesc, TargetTriple, component::{Component, Components}, @@ -73,11 +73,11 @@ impl<'a> Toolchain<'a> { } /// Calls Toolchain::new(), but augments the error message with more context - /// from the ActiveReason if the toolchain isn't installed. - pub(crate) fn with_reason( + /// from the ActiveSource if the toolchain isn't installed. + pub(crate) fn with_source( cfg: &'a Cfg<'a>, name: LocalToolchainName, - reason: &ActiveReason, + source: &ActiveSource, ) -> anyhow::Result { match Self::new(cfg, name.clone()) { Err(RustupError::ToolchainNotInstalled { .. }) => (), @@ -86,28 +86,28 @@ impl<'a> Toolchain<'a> { } } - let reason_err = match reason { - ActiveReason::Environment => { + let source_err = match source { + ActiveSource::Environment => { "the RUSTUP_TOOLCHAIN environment variable specifies an uninstalled toolchain" .to_string() } - ActiveReason::CommandLine => { + ActiveSource::CommandLine => { "the +toolchain on the command line specifies an uninstalled toolchain".to_string() } - ActiveReason::OverrideDB(path) => format!( + ActiveSource::OverrideDB(path) => format!( "the directory override for '{}' specifies an uninstalled toolchain", utils::canonicalize_path(path).display(), ), - ActiveReason::ToolchainFile(path) => format!( + ActiveSource::ToolchainFile(path) => format!( "the toolchain file at '{}' specifies an uninstalled toolchain", utils::canonicalize_path(path).display(), ), - ActiveReason::Default => { + ActiveSource::Default => { "the default toolchain does not describe an installed toolchain".to_string() } }; - Err(anyhow!(reason_err).context(format!("override toolchain '{name}' is not installed"))) + Err(anyhow!(source_err).context(format!("override toolchain '{name}' is not installed"))) } pub(crate) fn new(cfg: &'a Cfg<'a>, name: LocalToolchainName) -> Result { diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index d82aba33dc..09303bda60 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 + +"#]]); +} + +#[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 + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_path_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#" +... +path-override + +"#]]); +} + +#[tokio::test] +async fn rustup_toolchain_source_toolchain_file() { + 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'").unwrap(); + cx.config + .expect(["cargo", "--echo-rustup-toolchain-source"]) + .await + .with_stderr(snapbox::str![[r#" +... +toolchain-file + +"#]]); +} + +#[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 + +"#]]); +} + #[tokio::test] async fn directory_override_doesnt_need_to_exist_unless_it_is_selected() { let cx = CliTestContext::new(Scenario::SimpleV2).await;