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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
//! 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`.
//!
//! # Examples
//!
//! Set a `cfg` flag in `build.rs` if the running compiler was determined to be
//! at least version `1.13.0`:
//!
//! ```rust
//! extern crate version_check as rustc;
//!
//! if rustc::is_min_version("1.13.0").unwrap_or(false) {
//!     println!("cargo:rustc-cfg=question_mark_operator");
//! }
//! ```
//!
//! See [`is_max_version`] or [`is_exact_version`] to check if the compiler
//! is _at most_ or _exactly_ a certain version.
//!
//! Check that the running compiler was released on or after `2018-12-18`:
//!
//! ```rust
//! extern crate version_check as rustc;
//!
//! match rustc::is_min_date("2018-12-18") {
//!     Some(true) => "Yep! It's recent!",
//!     Some(false) => "No, it's older.",
//!     None => "Couldn't determine the rustc version."
//! };
//! ```
//!
//! See [`is_max_date`] or [`is_exact_date`] to check if the compiler was
//! released _prior to_ or _exactly on_ a certain date.
//!
//! Check that the running compiler supports feature flags:
//!
//! ```rust
//! extern crate version_check as rustc;
//!
//! match rustc::is_feature_flaggable() {
//!     Some(true) => "Yes! It's a dev or nightly release!",
//!     Some(false) => "No, it's stable or beta.",
//!     None => "Couldn't determine the rustc version."
//! };
//! ```
//!
//! Check that the running compiler is on the stable channel:
//!
//! ```rust
//! extern crate version_check as rustc;
//!
//! match rustc::Channel::read() {
//!     Some(c) if c.is_stable() => format!("Yes! It's stable."),
//!     Some(c) => format!("No, the channel {} is not stable.", c),
//!     None => format!("Couldn't determine the rustc version.")
//! };
//! ```
//!
//! To interact with the version, release date, and release channel as structs,
//! use [`Version`], [`Date`], and [`Channel`], respectively. The [`triple()`]
//! function returns all three values efficiently.
//!
//! # 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 if you
//! don't mind adding dependencies, see
//! [rustc_version](https://crates.io/crates/rustc_version).

#![allow(deprecated)]

mod version;
mod channel;
mod date;

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

#[doc(inline)] pub use version::*;
#[doc(inline)] pub use channel::*;
#[doc(inline)] pub use date::*;

/// Parses (version, date) as available from rustc version string.
fn version_and_date_from_rustc_version(s: &str) -> (Option<String>, Option<String>) {
    let last_line = s.lines().last().unwrap_or(s);
    let mut components = last_line.trim().split(" ");
    let version = components.nth(1);
    let date = components.filter(|c| c.ends_with(')')).next()
        .map(|s| s.trim_right().trim_right_matches(")").trim_left().trim_left_matches('('));
    (version.map(|s| s.to_string()), date.map(|s| s.to_string()))
}

/// Parses (version, date) as available from rustc verbose version output.
fn version_and_date_from_rustc_verbose_version(s: &str) -> (Option<String>, Option<String>) {
    let (mut version, mut date) = (None, None);
    for line in s.lines() {
        let split = |s: &str| s.splitn(2, ":").nth(1).map(|s| s.trim().to_string());
        match line.trim().split(" ").nth(0) {
            Some("rustc") => {
                let (v, d) = version_and_date_from_rustc_version(line);
                version = version.or(v);
                date = date.or(d);
            },
            Some("release:") => version = split(line),
            Some("commit-date:") if line.ends_with("unknown") => date = None,
            Some("commit-date:") => date = split(line),
            _ => continue
        }
    }

    (version, date)
}

/// Returns (version, date) as available from `rustc --version`.
fn get_version_and_date() -> Option<(Option<String>, Option<String>)> {
    let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
    Command::new(rustc).arg("--verbose").arg("--version").output().ok()
        .and_then(|output| String::from_utf8(output.stdout).ok())
        .map(|s| version_and_date_from_rustc_verbose_version(&s))
}

/// Reads the triple of [`Version`], [`Channel`], and [`Date`] of the installed
/// or running `rustc`.
///
/// If any attribute cannot be determined (see the [top-level
/// documentation](crate)), returns `None`.
///
/// To obtain only one of three attributes, use [`Version::read()`],
/// [`Channel::read()`], or [`Date::read()`].
pub fn triple() -> Option<(Version, Channel, Date)> {
    let (version_str, date_str) = match get_version_and_date() {
        Some((Some(version), Some(date))) => (version, date),
        _ => return None
    };

    // Can't use `?` or `try!` for `Option` in 1.0.0.
    match Version::parse(&version_str) {
        Some(version) => match Channel::parse(&version_str) {
            Some(channel) => match Date::parse(&date_str) {
                Some(date) => Some((version, channel, date)),
                _ => None,
            },
            _ => None,
        },
        _ => None
    }
}

/// Checks that the running or installed `rustc` was released **on or after**
/// 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 `true` if the installed `rustc`
/// was release on or after `min_date` and `false` otherwise.
pub fn is_min_date(min_date: &str) -> Option<bool> {
    match (Date::read(), Date::parse(min_date)) {
        (Some(rustc_date), Some(min_date)) => Some(rustc_date >= min_date),
        _ => None
    }
}

/// Checks that the running or installed `rustc` was released **on or before**
/// some date.
///
/// The format of `max_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 `max_date` could not be
/// parsed, returns `None`. Otherwise returns `true` if the installed `rustc`
/// was release on or before `max_date` and `false` otherwise.
pub fn is_max_date(max_date: &str) -> Option<bool> {
    match (Date::read(), Date::parse(max_date)) {
        (Some(rustc_date), Some(max_date)) => Some(rustc_date <= max_date),
        _ => None
    }
}

/// Checks that the running or installed `rustc` was released **exactly** on
/// some date.
///
/// The format of `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 `date` could not be parsed,
/// returns `None`. Otherwise returns `true` if the installed `rustc` was
/// release on `date` and `false` otherwise.
pub fn is_exact_date(date: &str) -> Option<bool> {
    match (Date::read(), Date::parse(date)) {
        (Some(rustc_date), Some(date)) => Some(rustc_date == date),
        _ => None
    }
}

/// Checks that the running or installed `rustc` is **at least** some minimum
/// version.
///
/// The format of `min_version` is a semantic version: `1.3.0`, `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 `true` if the installed `rustc`
/// is at least `min_version` and `false` otherwise.
pub fn is_min_version(min_version: &str) -> Option<bool> {
    match (Version::read(), Version::parse(min_version)) {
        (Some(rustc_ver), Some(min_ver)) => Some(rustc_ver >= min_ver),
        _ => None
    }
}

/// Checks that the running or installed `rustc` is **at most** some maximum
/// version.
///
/// The format of `max_version` is a semantic version: `1.3.0`, `1.15.0-beta`,
/// `1.14.0`, `1.16.0-nightly`, etc.
///
/// If the version cannot be retrieved or parsed, or if `max_version` could not
/// be parsed, returns `None`. Otherwise returns `true` if the installed `rustc`
/// is at most `max_version` and `false` otherwise.
pub fn is_max_version(max_version: &str) -> Option<bool> {
    match (Version::read(), Version::parse(max_version)) {
        (Some(rustc_ver), Some(max_ver)) => Some(rustc_ver <= max_ver),
        _ => None
    }
}

/// Checks that the running or installed `rustc` is **exactly** some version.
///
/// The format of `version` is a semantic version: `1.3.0`, `1.15.0-beta`,
/// `1.14.0`, `1.16.0-nightly`, etc.
///
/// If the version cannot be retrieved or parsed, or if `version` could not be
/// parsed, returns `None`. Otherwise returns `true` if the installed `rustc` is
/// exactly `version` and `false` otherwise.
pub fn is_exact_version(version: &str) -> Option<bool> {
    match (Version::read(), Version::parse(version)) {
        (Some(rustc_ver), Some(version)) => Some(rustc_ver == version),
        _ => None
    }
}

/// Checks whether the running or installed `rustc` supports feature flags.
///
/// In other words, if the channel is either "nightly" or "dev".
///
/// If the version could not be determined, returns `None`. Otherwise returns
/// `true` if the running version supports feature flags and `false` otherwise.
pub fn is_feature_flaggable() -> Option<bool> {
    Channel::read().map(|c| c.supports_features())
}

#[cfg(test)]
mod tests {
    use super::version_and_date_from_rustc_version;
    use super::version_and_date_from_rustc_verbose_version;

    macro_rules! check_parse {
        (@ $f:expr, $s:expr => $v:expr, $d:expr) => ({
            if let (Some(v), d) = $f($s) {
                let e_d: Option<&str> = $d.into();
                assert_eq!((v, d), ($v.into(), e_d.map(|s| s.into())));
            } else {
                panic!("{:?} didn't parse for version testing.", $s);
            }
        });
        ($f:expr, $s:expr => $v:expr, $d:expr) => ({
            let warn = "warning: invalid logging spec 'warning', ignoring it";
            let warn2 = "warning: sorry, something went wrong :(sad)";
            check_parse!(@ $f, $s => $v, $d);
            check_parse!(@ $f, &format!("{}\n{}", warn, $s) => $v, $d);
            check_parse!(@ $f, &format!("{}\n{}", warn2, $s) => $v, $d);
            check_parse!(@ $f, &format!("{}\n{}\n{}", warn, warn2, $s) => $v, $d);
            check_parse!(@ $f, &format!("{}\n{}\n{}", warn2, warn, $s) => $v, $d);
        })
    }

    macro_rules! check_terse_parse {
        ($($s:expr => $v:expr, $d:expr,)+) => {$(
            check_parse!(version_and_date_from_rustc_version, $s => $v, $d);
        )+}
    }

    macro_rules! check_verbose_parse {
        ($($s:expr => $v:expr, $d:expr,)+) => {$(
            check_parse!(version_and_date_from_rustc_verbose_version, $s => $v, $d);
        )+}
    }

    #[test]
    fn test_version_parse() {
        check_terse_parse! {
            "rustc 1.18.0" => "1.18.0", None,
            "rustc 1.8.0" => "1.8.0", None,
            "rustc 1.20.0-nightly" => "1.20.0-nightly", None,
            "rustc 1.20" => "1.20", None,
            "rustc 1.3" => "1.3", None,
            "rustc 1" => "1", None,
            "rustc 1.5.1-beta" => "1.5.1-beta", None,
            "rustc 1.20.0 (2017-07-09)" => "1.20.0", Some("2017-07-09"),
            "rustc 1.20.0-dev (2017-07-09)" => "1.20.0-dev", Some("2017-07-09"),
            "rustc 1.20.0-nightly (d84693b93 2017-07-09)" => "1.20.0-nightly", Some("2017-07-09"),
            "rustc 1.20.0 (d84693b93 2017-07-09)" => "1.20.0", Some("2017-07-09"),
            "rustc 1.30.0-nightly (3bc2ca7e4 2018-09-20)" => "1.30.0-nightly", Some("2018-09-20"),
        };
    }

    #[test]
    fn test_verbose_version_parse() {
        check_verbose_parse! {
            "rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)\n\
                binary: rustc\n\
                commit-hash: a59de37e99060162a2674e3ff45409ac73595c0e\n\
                commit-date: 2015-05-13\n\
                build-date: 2015-05-14\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.0.0" => "1.0.0", Some("2015-05-13"),

            "rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)\n\
                commit-hash: a59de37e99060162a2674e3ff45409ac73595c0e\n\
                commit-date: 2015-05-13\n\
                build-date: 2015-05-14\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.0.0" => "1.0.0", Some("2015-05-13"),

            "rustc 1.50.0 (cb75ad5db 2021-02-10)\n\
                binary: rustc\n\
                commit-hash: cb75ad5db02783e8b0222fee363c5f63f7e2cf5b\n\
                commit-date: 2021-02-10\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.50.0" => "1.50.0", Some("2021-02-10"),

            "rustc 1.52.0-nightly (234781afe 2021-03-07)\n\
                binary: rustc\n\
                commit-hash: 234781afe33d3f339b002f85f948046d8476cfc9\n\
                commit-date: 2021-03-07\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.52.0-nightly\n\
                LLVM version: 12.0.0" => "1.52.0-nightly", Some("2021-03-07"),

            "rustc 1.41.1\n\
                binary: rustc\n\
                commit-hash: unknown\n\
                commit-date: unknown\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.41.1\n\
                LLVM version: 7.0" => "1.41.1", None,

            "rustc 1.49.0\n\
                binary: rustc\n\
                commit-hash: unknown\n\
                commit-date: unknown\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.49.0" => "1.49.0", None,

            "rustc 1.50.0 (Fedora 1.50.0-1.fc33)\n\
                binary: rustc\n\
                commit-hash: unknown\n\
                commit-date: unknown\n\
                host: x86_64-unknown-linux-gnu\n\
                release: 1.50.0" => "1.50.0", None,
        };
    }
}