Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Probe rustc channel #33

Closed
wants to merge 11 commits into from
78 changes: 77 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ macro_rules! try {
};
}

use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::fs;
Expand All @@ -73,6 +74,7 @@ mod error;
pub use error::Error;

mod version;
pub use version::Channel;
use version::Version;

#[cfg(test)]
Expand All @@ -84,8 +86,10 @@ pub struct AutoCfg {
out_dir: PathBuf,
rustc: PathBuf,
rustc_version: Version,
rustc_channel: Channel,
target: Option<OsString>,
no_std: bool,
features: HashSet<String>,
rustflags: Option<Vec<String>>,
}

Expand Down Expand Up @@ -155,7 +159,7 @@ impl AutoCfg {
pub fn with_dir<T: Into<PathBuf>>(dir: T) -> Result<Self, Error> {
let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let rustc: PathBuf = rustc.into();
let rustc_version = try!(Version::from_rustc(&rustc));
let (rustc_version, rustc_channel) = try!(version::version_and_channel_from_rustc(&rustc));

let target = env::var_os("TARGET");

Expand Down Expand Up @@ -194,8 +198,10 @@ impl AutoCfg {
out_dir: dir,
rustc: rustc,
rustc_version: rustc_version,
rustc_channel: rustc_channel,
target: target,
no_std: false,
features: HashSet::new(),
rustflags: rustflags,
};

Expand Down Expand Up @@ -255,6 +261,11 @@ impl AutoCfg {
if self.no_std {
try!(stdin.write_all(b"#![no_std]\n").map_err(error::from_io));
}
for feature in &self.features {
try!(stdin
.write_all(format!("#![feature({})]\n", feature).as_bytes())
.map_err(error::from_io));
}
try!(stdin.write_all(code.as_ref()).map_err(error::from_io));
drop(stdin);

Expand Down Expand Up @@ -404,6 +415,71 @@ impl AutoCfg {
emit(cfg);
}
}

/// Returns the current `rustc` [`Channel`].
pub fn rustc_channel(&self) -> Channel {
self.rustc_channel
}

/// Tests whether the current `rustc` [`Channel`] supports the features of the requested
/// `channel`.
pub fn probe_rustc_channel(&self, channel: Channel) -> bool {
self.rustc_channel >= channel
}

/// Emits a `cfg` value of the form `rustc_channel_<channel>, like `rustc_channel_nightly`, if
/// the current `rustc` [`Channel`] supports the features of the requested `channel`.
pub fn emit_rustc_channel(&self, channel: Channel) {
if self.probe_rustc_channel(channel) {
emit(&format!(
"rustc_channel_{}",
match channel {
Channel::Stable => "stable",
Channel::Beta => "beta",
Channel::Nightly => "nightly",
Channel::Dev => "dev",
}
))
}
}

/// Tests whether `feature` is an available feature gate.
pub fn probe_feature(&self, feature: &str) -> bool {
if self.probe_rustc_channel(Channel::Nightly) {
self.probe(format!("#![feature({})]", mangle(feature)))
.unwrap_or(false)
} else {
// Features are only supported on nightly channels.
false
}
}

/// Emits the given `cfg` if `feature` is an available feature gate.
pub fn emit_feature_cfg(&self, feature: &str, cfg: &str) {
if self.probe_feature(feature) {
emit(cfg)
}
}

/// Sets `feature` as a gated feature for all probes.
///
/// This adds `#![feature(<feature>)]` at the start of all probes on nightly `rustc` channels.
/// Multiple features can be enabled at once. If `feature` is not valid, it is not enabled.
///
/// On non-nightly channels this does nothing.
pub fn set_feature(&mut self, feature: &str) {
if self.probe_feature(feature) {
self.features.insert(mangle(feature));
}
}

/// Removes `feature` as a gated feature for all probes, if it is set.
///
/// This removes `#![feature(<feature>)]` from the start of all probes on nightly `rustc`
/// channels. If the feature was not set, this does nothing.
pub fn unset_feature(&mut self, feature: &str) {
self.features.remove(&mangle(feature));
}
}

fn mangle(s: &str) -> String {
Expand Down
87 changes: 86 additions & 1 deletion src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::version::Version;
use super::AutoCfg;
use super::Channel;
use std::env;

impl AutoCfg {
Expand All @@ -15,6 +17,12 @@ impl AutoCfg {
assert_eq!(self.probe_rustc_version(major, minor), probe_result);
}

fn assert_on_channel(&self, channel: Channel, probe_result: bool) {
if self.rustc_channel == channel {
assert!(probe_result);
}
}

fn for_test() -> Result<Self, super::error::Error> {
match env::var_os("TESTS_TARGET_DIR") {
Some(d) => Self::with_dir(d),
Expand All @@ -30,9 +38,14 @@ fn autocfg_version() {
assert!(ac.probe_rustc_version(1, 0));
}

#[test]
fn autocfg_channel() {
let ac = AutoCfg::for_test().unwrap();
assert!(ac.probe_rustc_channel(Channel::Stable));
}

#[test]
fn version_cmp() {
use super::version::Version;
let v123 = Version::new(1, 2, 3);

assert!(Version::new(1, 0, 0) < v123);
Expand All @@ -43,6 +56,13 @@ fn version_cmp() {
assert!(Version::new(2, 0, 0) > v123);
}

#[test]
fn channel_cmp() {
assert!(Channel::Stable < Channel::Beta);
assert!(Channel::Beta < Channel::Nightly);
assert!(Channel::Nightly < Channel::Dev);
}

#[test]
fn probe_add() {
let ac = AutoCfg::for_test().unwrap();
Expand Down Expand Up @@ -132,6 +152,71 @@ fn probe_constant() {
ac.assert_min(1, 39, ac.probe_constant(r#""test".len()"#));
}

#[test]
fn probe_stable() {
let ac = AutoCfg::for_test().unwrap();

ac.assert_on_channel(Channel::Stable, ac.probe_rustc_channel(Channel::Stable));
ac.assert_on_channel(Channel::Beta, ac.probe_rustc_channel(Channel::Stable));
ac.assert_on_channel(Channel::Nightly, ac.probe_rustc_channel(Channel::Stable));
}

#[test]
fn probe_beta() {
let ac = AutoCfg::for_test().unwrap();

ac.assert_on_channel(Channel::Stable, !ac.probe_rustc_channel(Channel::Beta));
ac.assert_on_channel(Channel::Beta, ac.probe_rustc_channel(Channel::Beta));
ac.assert_on_channel(Channel::Nightly, ac.probe_rustc_channel(Channel::Beta));
}

#[test]
fn probe_nightly() {
let ac = AutoCfg::for_test().unwrap();

ac.assert_on_channel(Channel::Stable, !ac.probe_rustc_channel(Channel::Nightly));
ac.assert_on_channel(Channel::Beta, !ac.probe_rustc_channel(Channel::Nightly));
ac.assert_on_channel(Channel::Nightly, ac.probe_rustc_channel(Channel::Nightly));
}

#[test]
fn probe_dev() {
let ac = AutoCfg::for_test().unwrap();

ac.assert_on_channel(Channel::Stable, !ac.probe_rustc_channel(Channel::Dev));
ac.assert_on_channel(Channel::Beta, !ac.probe_rustc_channel(Channel::Dev));
ac.assert_on_channel(Channel::Nightly, !ac.probe_rustc_channel(Channel::Dev));
}

#[test]
fn probe_feature() {
let ac = AutoCfg::for_test().unwrap();

assert!(!ac.probe_feature("nonexistant_feature_abcdefg"));
// rust1 is the feature gate for Rust 1.0.
ac.assert_on_channel(Channel::Stable, !ac.probe_feature("rust1"));
ac.assert_on_channel(Channel::Beta, !ac.probe_feature("rust1"));
ac.assert_on_channel(Channel::Nightly, ac.probe_feature("rust1"));
}

#[test]
fn set_feature() {
let mut ac = AutoCfg::for_test().unwrap();
let step_trait = ac.core_std("iter::Step");

ac.set_feature("step_trait");
// As of Rust 1.50, the Step trait is experimental and therefore unaccessible on stable. Setting
// the feature should not allow access.
if ac.rustc_version <= Version::new(1, 50, 0) {
ac.assert_on_channel(Channel::Stable, !ac.probe_trait(&step_trait));
ac.assert_on_channel(Channel::Beta, !ac.probe_trait(&step_trait));
}
// The trait should be available on nightly, since the feature is set.
ac.assert_on_channel(Channel::Nightly, ac.probe_trait(&step_trait));
ac.unset_feature("step_trait");
ac.assert_on_channel(Channel::Nightly, !ac.probe_trait(&step_trait));
}

#[test]
fn dir_does_not_contain_target() {
assert!(!super::dir_contains_target(
Expand Down
109 changes: 78 additions & 31 deletions src/version.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
use std::cmp::Ordering;
use std::path::Path;
use std::process::Command;
use std::str;

use super::{error, Error};

/// The channel the current compiler was released on.
///
/// `Channel`s are orderable by their available features. The more features a channel supports, the
/// higher it is ordered. Specifically, channels are ordered as follows: `Stable < Beta < Nightly <
/// Dev`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Channel {
/// Stable channel.
Stable,
/// Beta channel.
Beta,
/// Nightly channel.
Nightly,
/// Dev channel.
Dev,
}

impl PartialOrd<Channel> for Channel {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
(*self as u8).partial_cmp(&(*other as u8))
}
}

impl Ord for Channel {
fn cmp(&self, other: &Self) -> Ordering {
(*self as u8).cmp(&(*other as u8))
}
}

/// A version structure for making relative comparisons.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
Expand All @@ -21,40 +51,57 @@ impl Version {
patch: patch,
}
}
}

pub fn version_and_channel_from_rustc(rustc: &Path) -> Result<(Version, Channel), Error> {
// Get rustc's verbose version
let output = try!(Command::new(rustc)
.args(&["--version", "--verbose"])
.output()
.map_err(error::from_io));
if !output.status.success() {
return Err(error::from_str("could not execute rustc"));
}
let output = try!(str::from_utf8(&output.stdout).map_err(error::from_utf8));

// Find the release line in the verbose version output.
let release = match output.lines().find(|line| line.starts_with("release: ")) {
Some(line) => &line["release: ".len()..],
None => return Err(error::from_str("could not find rustc release")),
};

pub fn from_rustc(rustc: &Path) -> Result<Self, Error> {
// Get rustc's verbose version
let output = try!(Command::new(rustc)
.args(&["--version", "--verbose"])
.output()
.map_err(error::from_io));
if !output.status.success() {
return Err(error::from_str("could not execute rustc"));
let mut release_split = release.split('-');
let version = match release_split.next() {
Some(version) => version,
None => return Err(error::from_str("could not parse rustc release")),
};
let channel = match release_split.next() {
Some(channel) => {
if channel.starts_with("beta") {
Channel::Beta
} else if channel.starts_with("nightly") {
Channel::Nightly
} else if channel.starts_with("dev") {
Channel::Dev
} else {
return Err(error::from_str("could not parse rustc channel"));
}
}
let output = try!(str::from_utf8(&output.stdout).map_err(error::from_utf8));

// Find the release line in the verbose version output.
let release = match output.lines().find(|line| line.starts_with("release: ")) {
Some(line) => &line["release: ".len()..],
None => return Err(error::from_str("could not find rustc release")),
};

// Strip off any extra channel info, e.g. "-beta.N", "-nightly"
let version = match release.find('-') {
Some(i) => &release[..i],
None => release,
};

// Split the version into semver components.
let mut iter = version.splitn(3, '.');
let major = try!(iter.next().ok_or(error::from_str("missing major version")));
let minor = try!(iter.next().ok_or(error::from_str("missing minor version")));
let patch = try!(iter.next().ok_or(error::from_str("missing patch version")));

Ok(Version::new(
None => Channel::Stable,
};

// Split the version into semver components.
let mut iter = version.splitn(3, '.');
let major = try!(iter.next().ok_or(error::from_str("missing major version")));
let minor = try!(iter.next().ok_or(error::from_str("missing minor version")));
let patch = try!(iter.next().ok_or(error::from_str("missing patch version")));

Ok((
Version::new(
try!(major.parse().map_err(error::from_num)),
try!(minor.parse().map_err(error::from_num)),
try!(patch.parse().map_err(error::from_num)),
))
}
),
channel,
))
}