use std::env; use std::ffi::OsString; use std::fmt::{self, Display}; use std::io; use std::process::Command; use std::str::FromStr; use std::string::FromUtf8Error; use crate::date::Date; use crate::version::{Channel::*, Version}; use proc_macro2::Span; #[derive(Debug)] pub enum Error { Exec(io::Error), Utf8(FromUtf8Error), Parse(String), } pub type Result = std::result::Result; impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Error::*; match self { Exec(e) => write!(f, "failed to run `rustc --version`: {}", e), Utf8(e) => write!(f, "failed to parse output of `rustc --version`: {}", e), Parse(string) => write!( f, "unexpected output from `rustc --version`, please file an issue: {:?}", string, ), } } } impl From for Error { fn from(err: FromUtf8Error) -> Self { Error::Utf8(err) } } impl From for syn::Error { fn from(err: Error) -> Self { syn::Error::new(Span::call_site(), err) } } pub fn version() -> Result { let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc")); let output = Command::new(rustc) .arg("--version") .output() .map_err(Error::Exec)?; let string = String::from_utf8(output.stdout)?; match parse(&string) { Some(version) => Ok(version), None => Err(Error::Parse(string)), } } fn parse(string: &str) -> Option { let last_line = string.lines().last().unwrap_or(&string); let mut words = last_line.trim().split(' '); if words.next()? != "rustc" { return None; } let mut version_channel = words.next()?.split('-'); let version = version_channel.next()?; let channel = version_channel.next(); let mut digits = version.split('.'); let major = digits.next()?; if major != "1" { return None; } let minor = digits.next()?.parse().ok()?; let patch = digits.next().unwrap_or("0").parse().ok()?; let channel = match channel { None => Stable, Some(channel) if channel == "dev" => Dev, Some(channel) if channel.starts_with("beta") => Beta, Some(channel) if channel == "nightly" => { match words.next() { Some(hash) => { if !hash.starts_with('(') { return None; } let date = words.next()?; if !date.ends_with(')') { return None; } let date = Date::from_str(&date[..date.len() - 1]).ok()?; Nightly(date) } None => Dev, } } Some(_) => return None, }; Some(Version { minor, patch, channel, }) } #[test] fn test_parse() { let cases = &[ ( "rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)", Version { minor: 0, patch: 0, channel: Stable, }, ), ( "rustc 1.18.0", Version { minor: 18, patch: 0, channel: Stable, }, ), ( "rustc 1.24.1 (d3ae9a9e0 2018-02-27)", Version { minor: 24, patch: 1, channel: Stable, }, ), ( "rustc 1.35.0-beta.3 (c13114dc8 2019-04-27)", Version { minor: 35, patch: 0, channel: Beta, }, ), ( "rustc 1.36.0-nightly (938d4ffe1 2019-04-27)", Version { minor: 36, patch: 0, channel: Nightly(Date { year: 2019, month: 4, day: 27, }), }, ), ( "rustc 1.36.0-dev", Version { minor: 36, patch: 0, channel: Dev, }, ), ( "rustc 1.36.0-nightly", Version { minor: 36, patch: 0, channel: Dev, }, ), ( "warning: invalid logging spec 'warning', ignoring it rustc 1.30.0-nightly (3bc2ca7e4 2018-09-20)", Version { minor: 30, patch: 0, channel: Nightly(Date { year: 2018, month: 9, day: 20, }), }, ), ]; for (string, expected) in cases { assert_eq!(parse(string).unwrap(), *expected); } }