diff --git a/doc/src/overrides.md b/doc/src/overrides.md index 7d36e7864d..a45b203861 100644 --- a/doc/src/overrides.md +++ b/doc/src/overrides.md @@ -89,7 +89,7 @@ profile = "minimal" ``` The `[toolchain]` section is mandatory, and at least one property must be -specified. +specified. `channel` and `path` are mutually exclusive. For backwards compatibility, `rust-toolchain` files also support a legacy format that only contains a toolchain name without any TOML encoding, e.g. @@ -104,7 +104,19 @@ The toolchains named in these files have a more restricted form than `rustup` toolchains generally, and may only contain the names of the three release channels, 'stable', 'beta', 'nightly', Rust version numbers, like '1.0.0', and optionally an archive date, like 'nightly-2017-01-01'. They may not name -custom toolchains, nor host-specific toolchains. +custom toolchains, nor host-specific toolchains. To use a custom local +toolchain, you can instead use a `path` toolchain: + +``` toml +[toolchain] +path = "/path/to/local/toolchain" +``` + +Since a `path` directive directly names a local toolchain, other options +like `components`, `targets`, and `profile` have no effect. `channel` +and `path` are mutually exclusive, since a `path` already points to a +specific toolchain. A relative `path` is resolved relative to the +location of the `rust-toolchain.toml` file. ## Default toolchain diff --git a/src/config.rs b/src/config.rs index e96b6b485b..a873482249 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,7 @@ impl OverrideFile { #[derive(Debug, Default, Deserialize, PartialEq, Eq)] struct ToolchainSection { channel: Option, + path: Option, components: Option>, targets: Option>, profile: Option, @@ -40,17 +41,30 @@ struct ToolchainSection { impl ToolchainSection { fn is_empty(&self) -> bool { - self.channel.is_none() && self.components.is_none() && self.targets.is_none() + self.channel.is_none() + && self.components.is_none() + && self.targets.is_none() + && self.path.is_none() } } impl> From for OverrideFile { fn from(channel: T) -> Self { - Self { - toolchain: ToolchainSection { - channel: Some(channel.into()), - ..Default::default() - }, + let override_ = channel.into(); + if Path::new(&override_).is_absolute() { + Self { + toolchain: ToolchainSection { + path: Some(PathBuf::from(override_)), + ..Default::default() + }, + } + } else { + Self { + toolchain: ToolchainSection { + channel: Some(override_), + ..Default::default() + }, + } } } } @@ -74,7 +88,7 @@ impl Display for OverrideReason { } } -#[derive(Default)] +#[derive(Default, Debug)] struct OverrideCfg<'a> { toolchain: Option>, components: Vec, @@ -83,11 +97,27 @@ struct OverrideCfg<'a> { } impl<'a> OverrideCfg<'a> { - fn from_file(cfg: &'a Cfg, file: OverrideFile) -> Result { + fn from_file( + cfg: &'a Cfg, + cfg_path: Option>, + file: OverrideFile, + ) -> Result { Ok(Self { - toolchain: match file.toolchain.channel { - Some(name) => Some(Toolchain::from(cfg, &name)?), - None => None, + toolchain: match (file.toolchain.channel, file.toolchain.path) { + (Some(name), None) => Some(Toolchain::from(cfg, &name)?), + (None, Some(path)) => { + if file.toolchain.targets.is_some() + || file.toolchain.components.is_some() + || file.toolchain.profile.is_some() + { + return Err(ErrorKind::CannotSpecifyPathAndOptions(path.into()).into()); + } + Some(Toolchain::from_path(cfg, cfg_path, &path)?) + } + (Some(channel), Some(path)) => { + return Err(ErrorKind::CannotSpecifyChannelAndPath(channel, path.into()).into()) + } + (None, None) => None, }, components: file.toolchain.components.unwrap_or_default(), targets: file.toolchain.targets.unwrap_or_default(), @@ -522,15 +552,21 @@ impl Cfg { } OverrideReason::OverrideDB(ref path) => format!( "the directory override for '{}' specifies an uninstalled toolchain", - path.display() + utils::canonicalize_path(path, self.notify_handler.as_ref()).display(), ), OverrideReason::ToolchainFile(ref path) => format!( "the toolchain file at '{}' specifies an uninstalled toolchain", - path.display() + utils::canonicalize_path(path, self.notify_handler.as_ref()).display(), ), }; - let override_cfg = OverrideCfg::from_file(self, file)?; + let cfg_file = if let OverrideReason::ToolchainFile(ref path) = reason { + Some(path) + } else { + None + }; + + let override_cfg = OverrideCfg::from_file(self, cfg_file, 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 @@ -557,8 +593,7 @@ impl Cfg { settings: &Settings, ) -> Result> { let notify = self.notify_handler.as_ref(); - let dir = utils::canonicalize_path(dir, notify); - let mut dir = Some(&*dir); + let mut dir = Some(dir); while let Some(d) = dir { // First check the override database @@ -955,6 +990,7 @@ mod tests { OverrideFile { toolchain: ToolchainSection { channel: Some(contents.into()), + path: None, components: None, targets: None, profile: None, @@ -978,6 +1014,7 @@ profile = "default" OverrideFile { toolchain: ToolchainSection { channel: Some("nightly-2020-07-10".into()), + path: None, components: Some(vec!["rustfmt".into(), "rustc-dev".into()]), targets: Some(vec![ "wasm32-unknown-unknown".into(), @@ -1001,6 +1038,28 @@ channel = "nightly-2020-07-10" OverrideFile { toolchain: ToolchainSection { channel: Some("nightly-2020-07-10".into()), + path: None, + components: None, + targets: None, + profile: None, + } + } + ); + } + + #[test] + fn parse_toml_toolchain_file_only_path() { + let contents = r#"[toolchain] +path = "foobar" +"#; + + let result = Cfg::parse_override_file(contents, ParseMode::Both); + assert_eq!( + result.unwrap(), + OverrideFile { + toolchain: ToolchainSection { + channel: None, + path: Some("foobar".into()), components: None, targets: None, profile: None, @@ -1022,6 +1081,7 @@ components = [] OverrideFile { toolchain: ToolchainSection { channel: Some("nightly-2020-07-10".into()), + path: None, components: Some(vec![]), targets: None, profile: None, @@ -1043,6 +1103,7 @@ targets = [] OverrideFile { toolchain: ToolchainSection { channel: Some("nightly-2020-07-10".into()), + path: None, components: None, targets: Some(vec![]), profile: None, @@ -1063,6 +1124,7 @@ components = [ "rustfmt" ] OverrideFile { toolchain: ToolchainSection { channel: None, + path: None, components: Some(vec!["rustfmt".into()]), targets: None, profile: None, diff --git a/src/errors.rs b/src/errors.rs index e221e9d5ec..2e43e86b42 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -188,6 +188,18 @@ error_chain! { description("invalid toolchain name") display("invalid toolchain name: '{}'", t) } + InvalidToolchainPath(p: PathBuf) { + description("invalid toolchain path"), + display("invalid toolchain path: '{}'", p.to_string_lossy()) + } + CannotSpecifyPathAndOptions(path: PathBuf) { + description("toolchain options are ignored for path toolchains"), + display("toolchain options are ignored for path toolchain ({})", path.display()) + } + CannotSpecifyChannelAndPath(channel: String, path: PathBuf) { + description("cannot specify channel and path simultaneously"), + display("cannot specify both channel ({}) and path ({}) simultaneously", channel, path.display()) + } InvalidProfile(t: String) { description("invalid profile name") display("invalid profile name: '{}'; valid names are: {}", t, valid_profile_names()) diff --git a/src/notifications.rs b/src/notifications.rs index beaa8a6a92..1250a81498 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -143,8 +143,14 @@ impl<'a> Display for Notification<'a> { } => write!( f, "both `{0}` and `{1}` exist. Using `{0}`", - rust_toolchain.display(), - rust_toolchain_toml.display() + rust_toolchain + .canonicalize() + .unwrap_or_else(|_| PathBuf::from(rust_toolchain)) + .display(), + rust_toolchain_toml + .canonicalize() + .unwrap_or_else(|_| PathBuf::from(rust_toolchain_toml)) + .display(), ), } } diff --git a/src/toolchain.rs b/src/toolchain.rs index bbdee214c8..b2931b565c 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -74,6 +74,34 @@ impl<'a> Toolchain<'a> { }) } + pub fn from_path( + cfg: &'a Cfg, + cfg_file: Option>, + path: impl AsRef, + ) -> Result { + let path = if let Some(cfg_file) = cfg_file { + cfg_file.as_ref().parent().unwrap().join(path) + } else { + path.as_ref().to_path_buf() + }; + + // Perform minimal validation; there should at least be a `bin/` that might + // contain things for us to run. + if !path.join("bin").is_dir() { + return Err(ErrorKind::InvalidToolchainPath(path.into()).into()); + } + + Ok(Toolchain { + cfg, + name: utils::canonicalize_path(&path, cfg.notify_handler.as_ref()) + .to_str() + .ok_or_else(|| ErrorKind::InvalidToolchainPath(path.clone().into()))? + .to_owned(), + path, + dist_handler: Box::new(move |n| (cfg.notify_handler)(n.into())), + }) + } + pub fn as_installed_common(&'a self) -> Result> { if !self.exists() { // Should be verify perhaps? @@ -256,6 +284,15 @@ impl<'a> Toolchain<'a> { } } +impl<'a> std::fmt::Debug for Toolchain<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Toolchain") + .field("name", &self.name) + .field("path", &self.path) + .finish() + } +} + /// Newtype hosting functions that apply to both custom and distributable toolchains that are installed. pub struct InstalledCommonToolchain<'a>(&'a Toolchain<'a>); diff --git a/tests/cli-rustup.rs b/tests/cli-rustup.rs index 1ebfd7e53a..9b4db3cc89 100644 --- a/tests/cli-rustup.rs +++ b/tests/cli-rustup.rs @@ -1384,6 +1384,267 @@ fn file_override() { }); } +#[test] +fn env_override_path() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + + let mut cmd = clitools::cmd(config, "rustc", &["--version"]); + clitools::env(config, &mut cmd); + cmd.env("RUSTUP_TOOLCHAIN", toolchain_path.to_str().unwrap()); + + let out = cmd.output().unwrap(); + assert!(String::from_utf8(out.stdout) + .unwrap() + .contains("hash-nightly-2")); + }); +} + +#[test] +fn plus_override_path() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + expect_stdout_ok( + config, + &[ + "rustup", + "run", + toolchain_path.to_str().unwrap(), + "rustc", + "--version", + ], + "hash-nightly-2", + ); + }); +} + +#[test] +fn file_override_path() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + let toolchain_file = config.current_dir().join("rust-toolchain.toml"); + raw::write_file( + &toolchain_file, + &format!("[toolchain]\npath='{}'", toolchain_path.to_str().unwrap()), + ) + .unwrap(); + + expect_stdout_ok(config, &["rustc", "--version"], "hash-nightly-2"); + + // Check that the toolchain has the right name + expect_stdout_ok( + config, + &["rustup", "show", "active-toolchain"], + &format!("nightly-{}", this_host_triple()), + ); + }); +} + +#[test] +fn proxy_override_path() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + let toolchain_file = config.current_dir().join("rust-toolchain.toml"); + raw::write_file( + &toolchain_file, + &format!("[toolchain]\npath='{}'", toolchain_path.to_str().unwrap()), + ) + .unwrap(); + + expect_stdout_ok(config, &["cargo", "--call-rustc"], "hash-nightly-2"); + }); +} + +#[test] +fn file_override_path_relative() { + setup(&|config| { + expect_ok(config, &["rustup", "default", "stable"]); + expect_ok( + config, + &[ + "rustup", + "toolchain", + "install", + "nightly", + "--no-self-update", + ], + ); + + let toolchain_path = config + .rustupdir + .join("toolchains") + .join(format!("nightly-{}", this_host_triple())); + let toolchain_file = config.current_dir().join("rust-toolchain.toml"); + + // Find shared prefix so we can determine a relative path + let mut p1 = toolchain_path.components().peekable(); + let mut p2 = toolchain_file.components().peekable(); + while let (Some(p1p), Some(p2p)) = (p1.peek(), p2.peek()) { + if p1p == p2p { + let _ = p1.next(); + let _ = p2.next(); + } else { + // The two paths diverge here + break; + } + } + let mut relative_path = PathBuf::new(); + // NOTE: We skip 1 since we don't need to .. across the .toml file at the end of the path + for _ in p2.skip(1) { + relative_path.push(".."); + } + for p in p1 { + relative_path.push(p); + } + assert!(relative_path.is_relative()); + + raw::write_file( + &toolchain_file, + &format!("[toolchain]\npath='{}'", relative_path.to_str().unwrap()), + ) + .unwrap(); + + // Change into an ephemeral dir so that we test that the path is relative to the override + let ephemeral = config.current_dir().join("ephemeral"); + fs::create_dir_all(&ephemeral).unwrap(); + config.change_dir(&ephemeral, || { + expect_stdout_ok(config, &["rustc", "--version"], "hash-nightly-2"); + }); + }); +} + +#[test] +fn file_override_path_no_options() { + setup(&|config| { + // Make a plausible-looking toolchain + let cwd = config.current_dir(); + let toolchain_path = cwd.join("ephemeral"); + let toolchain_bin = toolchain_path.join("bin"); + fs::create_dir_all(&toolchain_bin).unwrap(); + + let toolchain_file = cwd.join("rust-toolchain.toml"); + raw::write_file( + &toolchain_file, + "[toolchain]\npath=\"ephemeral\"\ntargets=[\"dummy\"]", + ) + .unwrap(); + + expect_err( + config, + &["rustc", "--version"], + "toolchain options are ignored for path toolchain (ephemeral)", + ); + + raw::write_file( + &toolchain_file, + "[toolchain]\npath=\"ephemeral\"\ncomponents=[\"dummy\"]", + ) + .unwrap(); + + expect_err( + config, + &["rustc", "--version"], + "toolchain options are ignored for path toolchain (ephemeral)", + ); + + raw::write_file( + &toolchain_file, + "[toolchain]\npath=\"ephemeral\"\nprofile=\"minimal\"", + ) + .unwrap(); + + expect_err( + config, + &["rustc", "--version"], + "toolchain options are ignored for path toolchain (ephemeral)", + ); + }); +} + +#[test] +fn file_override_path_xor_channel() { + setup(&|config| { + // Make a plausible-looking toolchain + let cwd = config.current_dir(); + let toolchain_path = cwd.join("ephemeral"); + let toolchain_bin = toolchain_path.join("bin"); + fs::create_dir_all(&toolchain_bin).unwrap(); + + let toolchain_file = cwd.join("rust-toolchain.toml"); + raw::write_file( + &toolchain_file, + "[toolchain]\npath=\"ephemeral\"\nchannel=\"nightly\"", + ) + .unwrap(); + + expect_err( + config, + &["rustc", "--version"], + "cannot specify both channel (nightly) and path (ephemeral) simultaneously", + ); + }); +} + #[test] fn file_override_subdir() { setup(&|config| {