diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 0c4eaf53e2..c518ecd001 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -1165,11 +1165,11 @@ fn show(cfg: &Cfg) -> Result { match active_toolchain { Ok(atc) => match atc { - (ref toolchain, Some(ref reason)) => { + (ref toolchain, _, Some(ref reason)) => { writeln!(t, "{} ({})", toolchain.name(), reason)?; writeln!(t, "{}", toolchain.rustc_version())?; } - (ref toolchain, None) => { + (ref toolchain, _, None) => { writeln!(t, "{} (default)", toolchain.name())?; writeln!(t, "{}", toolchain.rustc_version())?; } @@ -1221,7 +1221,7 @@ fn show_active_toolchain(cfg: &Cfg, m: &ArgMatches<'_>) -> Result { + Ok((toolchain, _, reason)) => { if let Some(reason) = reason { writeln!(process().stdout(), "{} ({})", toolchain.name(), reason)?; } else { @@ -1376,7 +1376,7 @@ fn explicit_or_dir_toolchain<'a>(cfg: &'a Cfg, m: &ArgMatches<'_>) -> Result, } impl OverrideFile { @@ -64,6 +67,19 @@ impl ToolchainSection { } } +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +enum EnvOverride { + Literal(String), + Table { + value: String, + #[serde(default)] + force: bool, + #[serde(default)] + relative: bool, + }, +} + impl> From for OverrideFile { fn from(channel: T) -> Self { let override_ = channel.into(); @@ -73,6 +89,7 @@ impl> From for OverrideFile { path: Some(PathBuf::from(override_)), ..Default::default() }, + env: Default::default(), } } else { Self { @@ -80,6 +97,7 @@ impl> From for OverrideFile { channel: Some(override_), ..Default::default() }, + env: Default::default(), } } } @@ -110,6 +128,7 @@ struct OverrideCfg<'a> { components: Vec, targets: Vec, profile: Option, + env: HashMap, } impl<'a> OverrideCfg<'a> { @@ -150,6 +169,30 @@ impl<'a> OverrideCfg<'a> { .as_deref() .map(dist::Profile::from_str) .transpose()?, + env: file + .env + .into_iter() + .filter_map(|(k, v)| { + let (v, force, relative) = match v { + EnvOverride::Literal(v) => (v, false, false), + EnvOverride::Table { + value, + force, + relative, + } => (value, force, relative), + }; + + if relative { + return Some(Err(anyhow!( + "Rustup does not yet support config-relative values" + ))); + } + if !force && process().var_os(&k).is_some() { + return None; + } + Some(Ok((k, v))) + }) + .collect::>()?, }) } } @@ -492,7 +535,7 @@ impl Cfg { } pub fn which_binary(&self, path: &Path, binary: &str) -> Result> { - let (toolchain, _) = self.find_or_install_override_toolchain_or_default(path)?; + let (toolchain, _, _) = self.find_or_install_override_toolchain_or_default(path)?; Ok(Some(toolchain.binary_file(binary))) } @@ -729,7 +772,11 @@ impl Cfg { pub fn find_or_install_override_toolchain_or_default( &self, path: &Path, - ) -> Result<(Toolchain<'_>, Option)> { + ) -> Result<( + Toolchain<'_>, + HashMap, + Option, + )> { fn components_exist( distributable: &DistributableToolchain<'_>, components: &[&str], @@ -773,7 +820,7 @@ impl Cfg { } } - if let Some((toolchain, components, targets, reason, profile)) = + if let Some((toolchain, components, targets, reason, profile, env)) = match self.find_override_config(path)? { Some(( OverrideCfg { @@ -781,6 +828,7 @@ impl Cfg { components, targets, profile, + env, }, reason, )) => { @@ -790,13 +838,13 @@ impl Cfg { None }; - toolchain - .or(default) - .map(|toolchain| (toolchain, components, targets, Some(reason), profile)) + toolchain.or(default).map(|toolchain| { + (toolchain, components, targets, Some(reason), profile, env) + }) } None => self .find_default()? - .map(|toolchain| (toolchain, vec![], vec![], None, None)), + .map(|toolchain| (toolchain, vec![], vec![], None, None, Default::default())), } { if toolchain.is_custom() { @@ -816,7 +864,7 @@ impl Cfg { } } - Ok((toolchain, reason)) + Ok((toolchain, env, reason)) } else { // No override and no default set Err(RustupError::ToolchainNotSelected.into()) @@ -905,21 +953,29 @@ impl Cfg { pub fn toolchain_for_dir( &self, path: &Path, - ) -> Result<(Toolchain<'_>, Option)> { + ) -> Result<( + Toolchain<'_>, + HashMap, + Option, + )> { self.find_or_install_override_toolchain_or_default(path) } pub fn create_command_for_dir(&self, path: &Path, binary: &str) -> Result { - let (ref toolchain, _) = self.toolchain_for_dir(path)?; + let (ref toolchain, ref env, _) = self.toolchain_for_dir(path)?; - if let Some(cmd) = self.maybe_do_cargo_fallback(toolchain, binary)? { + let mut cmd = if let Some(cmd) = self.maybe_do_cargo_fallback(toolchain, binary)? { Ok(cmd) } else { // NB this can only fail in race conditions since we used toolchain // for dir. let installed = toolchain.as_installed_common()?; installed.create_command(binary) - } + }?; + + cmd.envs(env); + + Ok(cmd) } pub fn create_command_for_toolchain( @@ -1043,7 +1099,8 @@ mod tests { components: None, targets: None, profile: None, - } + }, + env: Default::default(), } ); } @@ -1070,7 +1127,8 @@ profile = "default" "thumbv2-none-eabi".into() ]), profile: Some("default".into()), - } + }, + env: Default::default(), } ); } @@ -1091,7 +1149,8 @@ channel = "nightly-2020-07-10" components: None, targets: None, profile: None, - } + }, + env: Default::default(), } ); } @@ -1112,7 +1171,8 @@ path = "foobar" components: None, targets: None, profile: None, - } + }, + env: Default::default(), } ); } @@ -1134,7 +1194,8 @@ components = [] components: Some(vec![]), targets: None, profile: None, - } + }, + env: Default::default(), } ); } @@ -1156,7 +1217,8 @@ targets = [] components: None, targets: Some(vec![]), profile: None, - } + }, + env: Default::default(), } ); } @@ -1177,7 +1239,8 @@ components = [ "rustfmt" ] components: Some(vec!["rustfmt".into()]), targets: None, profile: None, - } + }, + env: Default::default(), } ); } @@ -1195,6 +1258,103 @@ components = [ "rustfmt" ] )); } + #[test] + fn parse_toml_toolchain_file_env_literal() { + // XXX: It'd be nice if it was possible to specify [env] but _not_ [toolchain], + // but that seems to currently cause an "empty config" error. + let contents = r#" +[toolchain] +channel = "nightly-2020-07-10" +[env] +OPENSSL_DIR = "/opt/openssl" +"#; + + let result = Cfg::parse_override_file(contents, ParseMode::Both); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + path: None, + components: None, + targets: None, + profile: None, + }, + env: HashMap::from([( + String::from("OPENSSL_DIR"), + EnvOverride::Literal(String::from("/opt/openssl")) + )]), + } + ); + } + + #[test] + fn parse_toml_toolchain_file_env_table() { + let contents = r#" +[toolchain] +channel = "nightly-2020-07-10" +[env] +TMPDIR = { value = "/home/tmp", force = true } +OPENSSL_DIR = { value = "vendor/openssl", relative = true } +"#; + + let result = Cfg::parse_override_file(contents, ParseMode::Both); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + path: None, + components: None, + targets: None, + profile: None, + }, + env: HashMap::from([ + ( + String::from("TMPDIR"), + EnvOverride::Table { + value: String::from("/home/tmp"), + force: true, + relative: false + } + ), + ( + String::from("OPENSSL_DIR"), + EnvOverride::Table { + value: String::from("vendor/openssl"), + force: false, + relative: true + } + ) + ]), + } + ); + } + + #[test] + fn parse_empty_toml_toolchain_file_env() { + let contents = r#" +[toolchain] +channel = "nightly-2020-07-10" +[env] +"#; + + let result = Cfg::parse_override_file(contents, ParseMode::Both); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + path: None, + components: None, + targets: None, + profile: None, + }, + env: Default::default(), + } + ); + } + #[test] fn parse_empty_toolchain_file() { let contents = ""; diff --git a/tests/cli-rustup.rs b/tests/cli-rustup.rs index f7ff77d701..230513a224 100644 --- a/tests/cli-rustup.rs +++ b/tests/cli-rustup.rs @@ -12,7 +12,8 @@ use rustup::utils::raw; use crate::mock::clitools::{ self, expect_err, expect_not_stderr_ok, expect_not_stdout_ok, expect_ok, expect_ok_ex, - expect_stderr_ok, expect_stdout_ok, run, set_current_dist_date, Config, Scenario, + expect_stderr_ok, expect_stdout_ok, print_command, print_indented, run, set_current_dist_date, + Config, Scenario, }; macro_rules! for_host_and_home { @@ -2202,3 +2203,70 @@ fn warn_on_duplicate_rust_toolchain_file() { ); }); } + +/// Checks that `[env]` in `rust-toolchain.toml` is respected +#[test] +fn rust_toolchain_toml_env() { + setup(&|config| { + let env_contains = |env_in, expected| { + let args = &["rustc", "--print-env"]; + let out = run(config, args[0], &args[1..], env_in); + if !out.ok || !out.stdout.contains(expected) { + print_command(args, &out); + println!("expected.ok: true"); + print_indented("expected.stdout.contains", expected); + panic!(); + } + }; + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain.toml"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +channel = "nightly" +[env] +SET_BY_RUSTUP = "hello" +"#, + ) + .unwrap(); + + // If not set in the environment, Rustup sets the envvar. + env_contains(&[], "SET_BY_RUSTUP = hello"); + + // If it is set, then the value form the environment wins. + env_contains(&[("SET_BY_RUSTUP", "world")], "SET_BY_RUSTUP = world"); + + // Unless force is set + raw::write_file( + &toolchain_file, + r#" +[toolchain] +channel = "nightly" +[env] +SET_BY_RUSTUP = { value = "hello", force = true } +"#, + ) + .unwrap(); + env_contains(&[("SET_BY_RUSTUP", "world")], "SET_BY_RUSTUP = hello"); + + // Trying to set relative doesn't work (for now), + // and produces a reasonable error message. + raw::write_file( + &toolchain_file, + r#" +[toolchain] +channel = "nightly" +[env] +SET_BY_RUSTUP = { value = "hello", relative = true } +"#, + ) + .unwrap(); + expect_err( + config, + &["rustc", "--print-env"], + "error: Rustup does not yet support config-relative values", + ); + }); +} diff --git a/tests/mock/clitools.rs b/tests/mock/clitools.rs index dfa3ad6239..d845170b16 100644 --- a/tests/mock/clitools.rs +++ b/tests/mock/clitools.rs @@ -437,7 +437,7 @@ pub fn expect_component_not_executable(config: &Config, cmd: &str) { } } -fn print_command(args: &[&str], out: &SanitizedOutput) { +pub fn print_command(args: &[&str], out: &SanitizedOutput) { print!("\n>"); for arg in args { if arg.contains(' ') { @@ -452,7 +452,7 @@ fn print_command(args: &[&str], out: &SanitizedOutput) { print_indented("out.stderr", &out.stderr); } -fn print_indented(heading: &str, text: &str) { +pub fn print_indented(heading: &str, text: &str) { let mut lines = text.lines().count(); // The standard library treats `a\n` and `a` as both being one line. // This is confusing when the test fails because of a missing newline. diff --git a/tests/mock/mock_bin_src.rs b/tests/mock/mock_bin_src.rs index b386ebab02..a36213b918 100644 --- a/tests/mock/mock_bin_src.rs +++ b/tests/mock/mock_bin_src.rs @@ -67,6 +67,11 @@ fn main() { writeln!(out, "{}", arg.to_string_lossy()).unwrap(); } } + Some("--print-env") => { + for (k, v) in env::vars() { + println!("{} = {}", k, v); + } + } _ => panic!("bad mock proxy commandline"), } }