1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! This tiny crate checks that the running or installed `rustc` meets some
//! version requirements. The version is queried by calling the Rust compiler
//! with `--version`. The path to the compiler is determined first via the
//! `RUSTC` environment variable. If it is not set, then `rustc` is used. If
//! that fails, no determination is made, and calls return `None`.
//!
//! # Example
//!
//! Check that the running compiler is a nightly release:
//!
//! ```rust
//! extern crate version_check;
//!
//! match version_check::is_nightly() {
//!     Some(true) => "running a nightly",
//!     Some(false) => "not nightly",
//!     None => "couldn't figure it out"
//! };
//! ```
//!
//! Check that the running compiler is at least version `1.13.0`:
//!
//! ```rust
//! extern crate version_check;
//!
//! match version_check::is_min_version("1.13.0") {
//!     Some((true, version)) => format!("Yes! It's: {}", version),
//!     Some((false, version)) => format!("No! {} is too old!", version),
//!     None => "couldn't figure it out".into()
//! };
//! ```
//!
//! Check that the running compiler was released on or after `2016-12-18`:
//!
//! ```rust
//! extern crate version_check;
//!
//! match version_check::is_min_date("2016-12-18") {
//!     Some((true, date)) => format!("Yes! It's: {}", date),
//!     Some((false, date)) => format!("No! {} is too long ago!", date),
//!     None => "couldn't figure it out".into()
//! };
//! ```
//!
//! # Alternatives
//!
//! This crate is dead simple with no dependencies. If you need something more
//! and don't care about panicking if the version cannot be obtained or adding
//! dependencies, see [rustc_version](https://crates.io/crates/rustc_version).

use std::env;
use std::process::Command;

// Convert a string of %Y-%m-%d to a single u32 maintaining ordering.
fn str_to_ymd(ymd: &str) -> Option<u32> {
    let ymd: Vec<u32> = ymd.split("-").filter_map(|s| s.parse::<u32>().ok()).collect();
    if ymd.len() != 3 {
        return None
    }

    let (y, m, d) = (ymd[0], ymd[1], ymd[2]);
    Some((y << 9) | (m << 5) | d)
}

// Convert a string with prefix major-minor-patch to a single u64 maintaining
// ordering. Assumes none of the components are > 1048576.
fn str_to_mmp(mmp: &str) -> Option<u64> {
    let mmp: Vec<u16> = mmp.split('-')
        .nth(0)
        .unwrap_or("")
        .split('.')
        .filter_map(|s| s.parse::<u16>().ok())
        .collect();

    if mmp.len() != 3 {
        return None
    }

    let (maj, min, patch) = (mmp[0] as u64, mmp[1] as u64, mmp[2] as u64);
    Some((maj << 32) | (min << 16) | patch)
}

fn get_version_and_date() -> Option<(String, String)> {
    let output = env::var("RUSTC").ok()
        .and_then(|rustc| Command::new(rustc).arg("--version").output().ok())
        .or_else(|| Command::new("rustc").arg("--version").output().ok())
        .and_then(|output| String::from_utf8(output.stdout).ok())
        .map(|s| {
            let mut components = s.split(" ");
            let version = components.nth(1);
            let date = components.nth(1).map(|s| s.trim_right().trim_right_matches(")"));
            (version.map(|s| s.to_string()), date.map(|s| s.to_string()))
        });

    match output {
        Some((Some(version), Some(date))) => Some((version, date)),
        _ => None
    }
}

/// Checks that the running or installed `rustc` was released no earlier than
/// some date.
///
/// The format of `min_date` must be YYYY-MM-DD. For instance: `2016-12-20` or
/// `2017-01-09`.
///
/// If the date cannot be retrieved or parsed, or if `min_date` could not be
/// parsed, returns `None`. Otherwise returns a tuple where the first value is
/// `true` if the installed `rustc` is at least from `min_data` and the second
/// value is the date (in YYYY-MM-DD) of the installed `rustc`.
pub fn is_min_date(min_date: &str) -> Option<(bool, String)> {
    if let Some((_, actual_date_str)) = get_version_and_date() {
        str_to_ymd(&actual_date_str)
            .and_then(|actual| str_to_ymd(min_date).map(|min| (min, actual)))
            .map(|(min, actual)| (actual >= min, actual_date_str))
    } else {
        None
    }
}

/// Checks that the running or installed `rustc` is at least some minimum
/// version.
///
/// The format of `min_version` is a semantic version: `1.15.0-beta`, `1.14.0`,
/// `1.16.0-nightly`, etc.
///
/// If the version cannot be retrieved or parsed, or if `min_version` could not
/// be parsed, returns `None`. Otherwise returns a tuple where the first value
/// is `true` if the installed `rustc` is at least `min_version` and the second
/// value is the version (semantic) of the installed `rustc`.
pub fn is_min_version(min_version: &str) -> Option<(bool, String)> {
    if let Some((actual_version_str, _)) = get_version_and_date() {
        str_to_mmp(&actual_version_str)
            .and_then(|actual| str_to_mmp(min_version).map(|min| (min, actual)))
            .map(|(min, actual)| (actual >= min, actual_version_str))
    } else {
        None
    }
}

/// Determines whether the running or installed `rustc` is on the nightly
/// channel.
///
/// If the version could not be determined, returns `None`. Otherwise returns
/// `Some(true)` if the running version is a nightly release, and `Some(false)`
/// otherwise.
pub fn is_nightly() -> Option<bool> {
    get_version_and_date()
        .map(|(actual_version_str, _)| actual_version_str.contains("nightly"))
}

/// Determines whether the running or installed `rustc` is on the beta channel.
///
/// If the version could not be determined, returns `None`. Otherwise returns
/// `Some(true)` if the running version is a beta release, and `Some(false)`
/// otherwise.
pub fn is_beta() -> Option<bool> {
    get_version_and_date()
        .map(|(actual_version_str, _)| actual_version_str.contains("beta"))
}