Permalink
Browse files

Teach rustup to override the toolchain from a version file

This adds a .rust-version file similar to rbenv's .ruby-version.

The form of toolchain supported here is limited to channel names,
explicit versions, and optional archive dates.
  • Loading branch information...
brson committed Jun 10, 2017
1 parent 2d8e6c0 commit 107d8e5f1ab83ce13cb33a7b4ca0f58198285ee8
Showing with 402 additions and 42 deletions.
  1. +62 −24 README.md
  2. +14 −0 src/rustup-dist/src/dist.rs
  3. +9 −0 src/rustup-mock/src/clitools.rs
  4. +75 −16 src/rustup/config.rs
  5. +242 −2 tests/cli-rustup.rs
@@ -17,8 +17,10 @@ And it runs on all platforms Rust supports, including Windows.
* [How rustup works](#how-rustup-works)
* [Keeping Rust up to date](#keeping-rust-up-to-date)
* [Working with nightly Rust](#working-with-nightly-rust)
* [Directory overrides](#directory-overrides)
* [Toolchain specification](#toolchain-specification)
* [Directory overrides](#directory-overrides)
* [The version file](#the-version-file)
* [Toolchain override shorthand](#toolchain-override-shorthand)
* [Cross-compilation](#cross-compilation)
* [Working with Rust on Windows](#working-with-rust-on-windows)
* [Working with custom toolchains](#working-with-custom-toolchains-and-local-builds)
@@ -231,29 +233,6 @@ info: downloading self-updates
```

## Directory overrides

Directories can be assigned their own Rust toolchain with
`rustup override`. When a directory has an override then
any time `rustc` or `cargo` is run inside that directory,
or one of its child directories, the override toolchain
will be invoked.

To pin to a specific nightly:

```
rustup override set nightly-2014-12-18
```

Or a specific stable release:

```
rustup override set 1.0.0
```

To see the active toolchain use `rustup show`. To remove the override
and use the default toolchain again, `rustup override unset`.

## Toolchain specification

Many `rustup` commands deal with *toolchains*, a single installation
@@ -299,6 +278,65 @@ Toolchain names that don't name a channel instead can be used to name
[MSVC-based toolchain]: https://www.rust-lang.org/downloads.html#win-foot
[custom toolchains]: #working-with-custom-toolchains-and-local-builds

## Toolchain override shorthand

The `rustup` toolchain proxies can be instructed directly to use a
specific toolchain, a convience for developers who often test
different toolchains. If the first argument to `cargo`, `rustc` or
other tools in the toolchain begins with `+`, it will be interpreted
as a rustup toolchain name, and that toolchain will be preferred,
as in

```
cargo +beta test
```

## Directory overrides

Directories can be assigned their own Rust toolchain with
`rustup override`. When a directory has an override then
any time `rustc` or `cargo` is run inside that directory,
or one of its child directories, the override toolchain
will be invoked.

To pin to a specific nightly:

```
rustup override set nightly-2014-12-18
```

Or a specific stable release:

```
rustup override set 1.0.0
```

To see the active toolchain use `rustup show`. To remove the override
and use the default toolchain again, `rustup override unset`.

## The version file

`rustup` directory overrides are a local configuration, stored in
`$RUSTUP_HOME`. Some projects though find themselves 'pinned' to a
specific release of Rust and want this information reflected in their
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-version`, the content of which is the name of
a single `rustup` toolchain, and which is suitable to check in to
source control.

The toolchains named in this file 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.

The version file has lower precedence than all other methods of
specifying the toolchain.

## Cross-compilation

Rust [supports a great number of platforms][p]. For many of these
@@ -302,6 +302,10 @@ impl PartialToolchainDesc {
target: TargetTriple(trip),
}
}

pub fn has_triple(&self) -> bool {
self.target.arch.is_some() || self.target.os.is_some() || self.target.env.is_some()
}
}

impl ToolchainDesc {
@@ -376,6 +380,16 @@ impl ToolchainDesc {
}
}

// A little convenience for just parsing a channel name or archived channel name
pub fn validate_channel_name(name: &str) -> Result<()> {
let toolchain = PartialToolchainDesc::from_str(&name)?;
if toolchain.has_triple() {
Err(format!("target triple in channel name '{}'", name).into())
} else {
Ok(())
}
}

#[derive(Debug)]
pub struct Manifest<'a>(temp::File<'a>, String);

@@ -34,6 +34,8 @@ pub struct Config {
pub homedir: PathBuf,
/// An empty directory. Tests should not write to this.
pub emptydir: PathBuf,
/// This is cwd for the test
pub workdir: PathBuf,
}

// Describes all the features of the mock dist server.
@@ -72,6 +74,7 @@ pub fn setup(s: Scenario, f: &Fn(&Config)) {
let cargodir = TempDir::new("rustup-cargo").unwrap();
let homedir = TempDir::new("rustup-home").unwrap();
let emptydir = TempDir::new("rustup-empty").unwrap();
let workdir = TempDir::new("rustup-workdir").unwrap();

// The uninstall process on windows involves using the directory above
// CARGO_HOME, so make sure it's a subdir of our tempdir
@@ -86,6 +89,7 @@ pub fn setup(s: Scenario, f: &Fn(&Config)) {
cargodir: cargodir,
homedir: homedir.path().to_owned(),
emptydir: emptydir.path().to_owned(),
workdir: workdir.path().to_owned(),
};

create_mock_dist_server(&config.distdir, s);
@@ -139,6 +143,11 @@ pub fn setup(s: Scenario, f: &Fn(&Config)) {
}
let _g = LOCK.lock();

// Change the cwd to a test-specific directory
let cwd = env::current_dir().unwrap();
env::set_current_dir(&config.workdir).unwrap();
let _g = scopeguard::guard(cwd, |d| env::set_current_dir(d).unwrap());

f(config);

// These are the bogus values the test harness sets "HOME" and "CARGO_HOME"
@@ -18,17 +18,19 @@ use settings::{TelemetryMode, SettingsFile, DEFAULT_METADATA_VERSION};
pub enum OverrideReason {
Environment,
OverrideDB(PathBuf),
VersionFile(PathBuf),
}



impl Display for OverrideReason {
fn fmt(&self, f: &mut fmt::Formatter) -> ::std::result::Result<(), fmt::Error> {
match *self {
OverrideReason::Environment => write!(f, "environment override by RUSTUP_TOOLCHAIN"),
OverrideReason::OverrideDB(ref path) => {
write!(f, "directory override for '{}'", path.display())
}
OverrideReason::VersionFile(ref path) => {
write!(f, "overridden by '{}'", path.display())
}
}
}
}
@@ -223,29 +225,86 @@ impl Cfg {
}

pub fn find_override(&self, path: &Path) -> Result<Option<(Toolchain, OverrideReason)>> {
let mut override_ = None;

// First check RUSTUP_TOOLCHAIN
if let Some(ref name) = self.env_override {
let toolchain = try!(self.verify_toolchain(name).chain_err(|| ErrorKind::ToolchainNotInstalled(name.to_string())));
override_ = Some((name.to_string(), OverrideReason::Environment));
}

// Then look in the override database
if override_.is_none() {
try!(self.settings_file.with(|s| {
let name = s.find_override(path, self.notify_handler.as_ref());
override_ = name.map(|(name, reason_path)| (name, OverrideReason::OverrideDB(reason_path)));

Ok(())
}));
}

return Ok(Some((toolchain, OverrideReason::Environment)));
// Then check the explicit version file
if override_.is_none() {
let name_path = self.find_override_version_file(path)?;
override_ = name_path.map(|(name, path)| (name, OverrideReason::VersionFile(path)));
}

let result = try!(self.settings_file.with(|s| {
Ok(s.find_override(path, self.notify_handler.as_ref()))
}));
if let Some((name, reason_path)) = result {
let toolchain = match self.verify_toolchain(&name) {
Ok(t) => { t },
if let Some((name, 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.

let reason_err = match reason {
OverrideReason::Environment => {
format!("the RUSTUP_TOOLCHAIN environment variable specifies an uninstalled toolchain")
}
OverrideReason::OverrideDB(ref path) => {
format!("the directory override for '{}' specifies an uninstalled toolchain", path.display())
}
OverrideReason::VersionFile(ref path) => {
format!("the version file at '{}' specifies an uninstalled toolchain", path.display())
}
};

match self.verify_toolchain(&name) {
Ok(t) => {
Ok(Some((t, reason)))
}
Err(Error(ErrorKind::Utils(::rustup_utils::ErrorKind::NotADirectory { .. }), _)) => {
// Strip the confusing NotADirectory error and only mention that the override
// toolchain is not installed.
return Err(ErrorKind::OverrideToolchainNotInstalled(name.to_string()).into())
Err(Error::from(reason_err))
.chain_err(|| ErrorKind::OverrideToolchainNotInstalled(name.to_string()))
},
Err(e) => return Err(e).chain_err(|| {
ErrorKind::OverrideToolchainNotInstalled(name.to_string())
})
Err(e) => {
Err(e)
.chain_err(|| Error::from(reason_err))
.chain_err(|| ErrorKind::OverrideToolchainNotInstalled(name.to_string()))
}
}
} else {
Ok(None)
}
}

};
return Ok(Some((toolchain, OverrideReason::OverrideDB(reason_path))));
/// Starting in path walk up the tree looking for .rust-version
fn find_override_version_file(&self, path: &Path) -> Result<Option<(String, PathBuf)>> {
let mut path = Some(path);

while let Some(p) = path {
let version_file = p.join(".rust-version");
if let Ok(s) = utils::read_file("version file", &version_file) {
if let Some(s) = s.lines().next() {
let toolchain_name = s.trim();
dist::validate_channel_name(&toolchain_name)
.chain_err(|| format!("invalid channel name '{}' in '{}'",
toolchain_name,
version_file.display()))?;

return Ok(Some((toolchain_name.to_string(), version_file)));
}
}

path = p.parent();
}

Ok(None)
Oops, something went wrong.

0 comments on commit 107d8e5

Please sign in to comment.