diff --git a/README.md b/README.md index b24f4f0219..e6a5c5f71e 100644 --- a/README.md +++ b/README.md @@ -404,9 +404,21 @@ source repository. This is most often the case for nightly-only software that pins to a revision from the release archives. In these cases the toolchain can be named in the project's directory -in a file called `rust-toolchain`, the content of which is the name of -a single `rustup` toolchain, and which is suitable to check in to -source control. This file has to be encoded in US-ASCII (if you are on +in a file called `rust-toolchain`, the content of which is either the name of +a single `rustup` toolchain, or a TOML file with the following layout: + +``` toml +[toolchain] +channel = "nightly-2020-07-10" +components = [ "rustfmt", "rustc-dev" ] +targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ] +``` + +If the TOML format is used, the `[toolchain]` section is mandatory, +and at least one property must be specified. + +The `rust-toolchain` file is suitable to check in to source control. +This file has to be encoded in US-ASCII (if you are on Windows, check the encoding and that it does not starts with a BOM). The toolchains named in this file have a more restricted form than diff --git a/src/cli/common.rs b/src/cli/common.rs index 6ecbaf4e65..619b5da07a 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -480,9 +480,8 @@ pub fn list_toolchains(cfg: &Cfg, verbose: bool) -> Result<utils::ExitCode> { String::new() }; let cwd = utils::current_dir()?; - let ovr_toolchain_name = if let Ok(Some((ovr_toolchain, _reason))) = cfg.find_override(&cwd) - { - ovr_toolchain.name().to_string() + let ovr_toolchain_name = if let Ok(Some((toolchain, _reason))) = cfg.find_override(&cwd) { + toolchain.name().to_string() } else { String::new() }; diff --git a/src/config.rs b/src/config.rs index 4e82c5bbb6..de0e6e00a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use std::sync::Arc; use pgp::{Deserializable, SignedPublicKey}; +use serde::Deserialize; use crate::dist::download::DownloadCfg; use crate::dist::{dist, temp}; @@ -18,6 +19,41 @@ use crate::settings::{Settings, SettingsFile, DEFAULT_METADATA_VERSION}; use crate::toolchain::{DistributableToolchain, Toolchain, UpdateStatus}; use crate::utils::utils; +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +struct OverrideFile { + toolchain: ToolchainSection, +} + +impl OverrideFile { + fn is_empty(&self) -> bool { + self.toolchain.is_empty() + } +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +struct ToolchainSection { + channel: Option<String>, + components: Option<Vec<String>>, + targets: Option<Vec<String>>, +} + +impl ToolchainSection { + fn is_empty(&self) -> bool { + self.channel.is_none() && self.components.is_none() && self.targets.is_none() + } +} + +impl<T: Into<String>> From<T> for OverrideFile { + fn from(channel: T) -> Self { + Self { + toolchain: ToolchainSection { + channel: Some(channel.into()), + ..Default::default() + }, + } + } +} + #[derive(Debug)] pub enum OverrideReason { Environment, @@ -37,6 +73,26 @@ impl Display for OverrideReason { } } +#[derive(Default)] +struct OverrideCfg<'a> { + toolchain: Option<Toolchain<'a>>, + components: Vec<String>, + targets: Vec<String>, +} + +impl<'a> OverrideCfg<'a> { + fn from_file(cfg: &'a Cfg, file: OverrideFile) -> Result<Self> { + Ok(Self { + toolchain: match file.toolchain.channel { + Some(name) => Some(Toolchain::from(cfg, &name)?), + None => None, + }, + components: file.toolchain.components.unwrap_or_default(), + targets: file.toolchain.targets.unwrap_or_default(), + }) + } +} + lazy_static::lazy_static! { static ref BUILTIN_PGP_KEY: SignedPublicKey = pgp::SignedPublicKey::from_armor_single( io::Cursor::new(&include_bytes!("rust-key.pgp.ascii")[..]) @@ -402,16 +458,27 @@ impl Cfg { } pub fn find_override(&self, path: &Path) -> Result<Option<(Toolchain<'_>, OverrideReason)>> { + self.find_override_config(path).map(|opt| { + opt.and_then(|(override_cfg, reason)| { + override_cfg.toolchain.map(|toolchain| (toolchain, reason)) + }) + }) + } + + fn find_override_config( + &self, + path: &Path, + ) -> Result<Option<(OverrideCfg<'_>, OverrideReason)>> { let mut override_ = None; // First check toolchain override from command if let Some(ref name) = self.toolchain_override { - override_ = Some((name.to_string(), OverrideReason::CommandLine)); + override_ = Some((name.into(), OverrideReason::CommandLine)); } // Check RUSTUP_TOOLCHAIN if let Some(ref name) = self.env_override { - override_ = Some((name.to_string(), OverrideReason::Environment)); + override_ = Some((name.into(), OverrideReason::Environment)); } // Then walk up the directory tree from 'path' looking for either the @@ -424,7 +491,7 @@ impl Cfg { })?; } - if let Some((name, reason)) = override_ { + if let Some((file, reason)) = override_ { // This is hackishly using the error chain to provide a bit of // extra context about what went wrong. The CLI will display it // on a line after the proximate error. @@ -448,19 +515,22 @@ impl Cfg { ), }; - let toolchain = Toolchain::from(self, &name)?; - // Overridden toolchains can be literally any string, but only - // distributable toolchains will be auto-installed by the wrapping - // code; provide a nice error for this common case. (default could - // be set badly too, but that is much less common). - if !toolchain.exists() && toolchain.is_custom() { - // Strip the confusing NotADirectory error and only mention that the - // override toolchain is not installed. - Err(Error::from(reason_err)) - .chain_err(|| ErrorKind::OverrideToolchainNotInstalled(name.to_string())) - } else { - Ok(Some((toolchain, reason))) + let override_cfg = OverrideCfg::from_file(self, file)?; + if let Some(toolchain) = &override_cfg.toolchain { + // Overridden toolchains can be literally any string, but only + // distributable toolchains will be auto-installed by the wrapping + // code; provide a nice error for this common case. (default could + // be set badly too, but that is much less common). + if !toolchain.exists() && toolchain.is_custom() { + // Strip the confusing NotADirectory error and only mention that the + // override toolchain is not installed. + return Err(Error::from(reason_err)).chain_err(|| { + ErrorKind::OverrideToolchainNotInstalled(toolchain.name().into()) + }); + } } + + Ok(Some((override_cfg, reason))) } else { Ok(None) } @@ -470,7 +540,7 @@ impl Cfg { &self, dir: &Path, settings: &Settings, - ) -> Result<Option<(String, OverrideReason)>> { + ) -> Result<Option<(OverrideFile, OverrideReason)>> { let notify = self.notify_handler.as_ref(); let dir = utils::canonicalize_path(dir, notify); let mut dir = Some(&*dir); @@ -479,14 +549,14 @@ impl Cfg { // First check the override database if let Some(name) = settings.dir_override(d, notify) { let reason = OverrideReason::OverrideDB(d.to_owned()); - return Ok(Some((name, reason))); + return Ok(Some((name.into(), reason))); } // Then look for 'rust-toolchain' let toolchain_file = d.join("rust-toolchain"); - if let Ok(s) = utils::read_file("toolchain file", &toolchain_file) { - if let Some(s) = s.lines().next() { - let toolchain_name = s.trim(); + if let Ok(contents) = utils::read_file("toolchain file", &toolchain_file) { + let override_file = Cfg::parse_override_file(contents)?; + if let Some(toolchain_name) = &override_file.toolchain.channel { let all_toolchains = self.list_toolchains()?; if !all_toolchains.iter().any(|s| s == toolchain_name) { // The given name is not resolvable as a toolchain, so @@ -499,9 +569,10 @@ impl Cfg { ) })?; } - let reason = OverrideReason::ToolchainFile(toolchain_file); - return Ok(Some((toolchain_name.to_string(), reason))); } + + let reason = OverrideReason::ToolchainFile(toolchain_file); + return Ok(Some((override_file, reason))); } dir = d.parent(); @@ -510,26 +581,100 @@ impl Cfg { Ok(None) } + fn parse_override_file<S: AsRef<str>>(contents: S) -> Result<OverrideFile> { + let contents = contents.as_ref(); + + match contents.lines().count() { + 0 => return Err(ErrorKind::EmptyOverrideFile.into()), + 1 => { + let channel = contents.trim(); + + if channel.is_empty() { + Err(ErrorKind::EmptyOverrideFile.into()) + } else { + Ok(channel.into()) + } + } + _ => { + let override_file = toml::from_str::<OverrideFile>(contents) + .map_err(ErrorKind::ParsingOverrideFile)?; + + if override_file.is_empty() { + Err(ErrorKind::InvalidOverrideFile.into()) + } else { + Ok(override_file) + } + } + } + } + pub fn find_or_install_override_toolchain_or_default( &self, path: &Path, ) -> Result<(Toolchain<'_>, Option<OverrideReason>)> { - if let Some((toolchain, reason)) = - if let Some((toolchain, reason)) = self.find_override(path)? { - Some((toolchain, Some(reason))) - } else { - self.find_default()?.map(|toolchain| (toolchain, None)) + fn components_exist( + distributable: &DistributableToolchain<'_>, + components: &[&str], + targets: &[&str], + ) -> Result<bool> { + let components_requested = !components.is_empty() || !targets.is_empty(); + + match (distributable.list_components(), components_requested) { + // If the toolchain does not support components but there were components requested, bubble up the error + (Err(e), true) => Err(e), + (Ok(installed_components), _) => { + Ok(components.iter().chain(targets.iter()).all(|name| { + installed_components.iter().any(|status| { + status.component.short_name_in_manifest() == name && status.installed + }) + })) + } + _ => Ok(true), + } + } + + if let Some((toolchain, components, targets, reason)) = + match self.find_override_config(path)? { + Some(( + OverrideCfg { + toolchain, + components, + targets, + }, + reason, + )) => { + let default = if toolchain.is_none() { + self.find_default()? + } else { + None + }; + + toolchain + .or(default) + .map(|toolchain| (toolchain, components, targets, Some(reason))) + } + None => self + .find_default()? + .map(|toolchain| (toolchain, vec![], vec![], None)), } { - if !toolchain.exists() { - if toolchain.is_custom() { + if toolchain.is_custom() { + if !toolchain.exists() { return Err( ErrorKind::ToolchainNotInstalled(toolchain.name().to_string()).into(), ); } + } else { + let components: Vec<_> = components.iter().map(AsRef::as_ref).collect(); + let targets: Vec<_> = targets.iter().map(AsRef::as_ref).collect(); + let distributable = DistributableToolchain::new(&toolchain)?; - distributable.install_from_dist(true, false, &[], &[])?; + if !toolchain.exists() || !components_exist(&distributable, &components, &targets)? + { + distributable.install_from_dist(true, false, &components, &targets)?; + } } + Ok((toolchain, reason)) } else { // No override and no default set @@ -722,3 +867,175 @@ impl Cfg { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_legacy_toolchain_file() { + let contents = "nightly-2020-07-10"; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some(contents.into()), + components: None, + targets: None, + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file() { + let contents = r#"[toolchain] +channel = "nightly-2020-07-10" +components = [ "rustfmt", "rustc-dev" ] +targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ] +"#; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + components: Some(vec!["rustfmt".into(), "rustc-dev".into()]), + targets: Some(vec![ + "wasm32-unknown-unknown".into(), + "thumbv2-none-eabi".into() + ]), + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file_only_channel() { + let contents = r#"[toolchain] +channel = "nightly-2020-07-10" +"#; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + components: None, + targets: None, + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file_empty_components() { + let contents = r#"[toolchain] +channel = "nightly-2020-07-10" +components = [] +"#; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + components: Some(vec![]), + targets: None, + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file_empty_targets() { + let contents = r#"[toolchain] +channel = "nightly-2020-07-10" +targets = [] +"#; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: Some("nightly-2020-07-10".into()), + components: None, + targets: Some(vec![]), + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file_no_channel() { + let contents = r#"[toolchain] +components = [ "rustfmt" ] +"#; + + let result = Cfg::parse_override_file(contents); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: None, + components: Some(vec!["rustfmt".into()]), + targets: None, + } + } + ); + } + + #[test] + fn parse_empty_toml_toolchain_file() { + let contents = r#" +[toolchain] +"#; + + let result = Cfg::parse_override_file(contents); + assert!(matches!( + result.unwrap_err().kind(), + ErrorKind::InvalidOverrideFile + )); + } + + #[test] + fn parse_empty_toolchain_file() { + let contents = ""; + + let result = Cfg::parse_override_file(contents); + assert!(matches!( + result.unwrap_err().kind(), + ErrorKind::EmptyOverrideFile + )); + } + + #[test] + fn parse_whitespace_toolchain_file() { + let contents = " "; + + let result = Cfg::parse_override_file(contents); + assert!(matches!( + result.unwrap_err().kind(), + ErrorKind::EmptyOverrideFile + )); + } + + #[test] + fn parse_toml_syntax_error() { + let contents = r#"[toolchain] +channel = nightly +"#; + + let result = Cfg::parse_override_file(contents); + assert!(matches!( + result.unwrap_err().kind(), + ErrorKind::ParsingOverrideFile(..) + )); + } +} diff --git a/src/errors.rs b/src/errors.rs index 71d24a3442..eb411de371 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -374,6 +374,15 @@ error_chain! { BrokenPartialFile { description("partially downloaded file may have been damaged and was removed, please try again") } + EmptyOverrideFile { + description("empty toolchain override file detected. Please remove it, or else specify the desired toolchain properties in the file") + } + InvalidOverrideFile { + description("missing toolchain properties in toolchain override file") + } + ParsingOverrideFile(e: toml::de::Error) { + description("error parsing override file") + } } } diff --git a/tests/cli-rustup.rs b/tests/cli-rustup.rs index 2e393a9c6c..28def93ff8 100644 --- a/tests/cli-rustup.rs +++ b/tests/cli-rustup.rs @@ -11,8 +11,8 @@ use rustup::test::this_host_triple; use rustup::utils::raw; use crate::mock::clitools::{ - self, expect_err, expect_ok, expect_ok_ex, expect_stderr_ok, expect_stdout_ok, run, - set_current_dist_date, Config, Scenario, + self, expect_err, expect_not_stdout_ok, expect_ok, expect_ok_ex, expect_stderr_ok, + expect_stdout_ok, run, set_current_dist_date, Config, Scenario, }; macro_rules! for_host_and_home { @@ -1402,6 +1402,152 @@ fn file_override_with_archive() { }); } +#[test] +fn file_override_toml_format_select_installed_toolchain() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly-2015-01-01", + "--no-self-update", + ], + ); + + expect_stdout_ok(config, &["rustc", "--version"], "hash-stable-1.1.0"); + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +channel = "nightly-2015-01-01" +"#, + ) + .unwrap(); + + expect_stdout_ok(config, &["rustc", "--version"], "hash-nightly-1"); + }); +} + +#[test] +fn file_override_toml_format_install_both_toolchain_and_components() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_stdout_ok(config, &["rustc", "--version"], "hash-stable-1.1.0"); + expect_not_stdout_ok( + config, + &["rustup", "component", "list"], + "rust-src (installed)", + ); + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +channel = "nightly-2015-01-01" +components = [ "rust-src" ] +"#, + ) + .unwrap(); + + expect_stdout_ok(config, &["rustc", "--version"], "hash-nightly-1"); + expect_stdout_ok( + config, + &["rustup", "component", "list"], + "rust-src (installed)", + ); + }); +} + +#[test] +fn file_override_toml_format_add_missing_components() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_not_stdout_ok( + config, + &["rustup", "component", "list"], + "rust-src (installed)", + ); + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +components = [ "rust-src" ] +"#, + ) + .unwrap(); + + expect_stdout_ok( + config, + &["rustup", "component", "list"], + "rust-src (installed)", + ); + }); +} + +#[test] +fn file_override_toml_format_add_missing_targets() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_not_stdout_ok( + config, + &["rustup", "component", "list"], + "arm-linux-androideabi (installed)", + ); + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +targets = [ "arm-linux-androideabi" ] +"#, + ) + .unwrap(); + + expect_stdout_ok( + config, + &["rustup", "component", "list"], + "arm-linux-androideabi (installed)", + ); + }); +} + +#[test] +fn file_override_toml_format_skip_invalid_component() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + + let cwd = config.current_dir(); + let toolchain_file = cwd.join("rust-toolchain"); + raw::write_file( + &toolchain_file, + r#" +[toolchain] +components = [ "rust-bongo" ] +"#, + ) + .unwrap(); + + expect_stderr_ok( + config, + &["rustc", "--version"], + "warning: Force-skipping unavailable component 'rust-bongo", + ); + }); +} + #[test] fn directory_override_beats_file_override() { setup(&|config| {