Rust is very open, which is great in comparison to some other language communities. I’m reminded in particular of one run by Sun and later Oracle. Ever tried filing an issue on Java’s public bug tracker? But its hard to look at the Rust RFC process, and in particular at the RFCs one feels they need accepted, without finding some delay or failure evident from the sheer human scale and attempting to achieve such broad consensus.

Take for example, RFC 2495: Minimum Supported Rust Version. Unlike many other RFCs, this one is particularly simple: Add a field to Cargo.toml to express the minimum version of the rust compiler that a project needs to successfully build. Stepping back, its really hard to even imagine, given all the great progress and features of cargo, that it doesn’t yet have this basic feature as found in other languages with cargo like tools. Why wouldn’t we want to at least collect that data point when a crate is released? Why wouldn’t we use this data point for warning messages or refined dependency resolution for users that, for one reason or another, are stuck on older rust releases? RFC 2495 remains open after 10 months, apparently stalled with a notable lack of core team engagement.

RFC 2523: #[cfg(accessible(..) / version(..))] is closely related and could eventually offer a superset of features. It has had the engagement, but suffers from being overly broad in scope. The accessible(..) tests have unresolved implementation questions and version(..) tests are (to my surprise) controversial.

Enforcing MSRV with build.rs

Without these RFCs, Cargo’s build.rs feature allows us to take matters into our own hands. Here is a self contained recipe for enforcing a project’s MSRV. You’ll need to change just the PACKAGE and msrv lines for your own projects:

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

fn main() {
    static PACKAGE: &'static str = "tao-log";
    let msrv = vec![1, 31];

    static VERSION: &'static str = env!("CARGO_PKG_VERSION");
    static M_V: &'static str = "minimum supported rust version (MSRV)";

    let rustv = rustc_version();

    if rustv < msrv {
        panic!(
            "{} v{} {} is {} > {} (this rustc)",
            PACKAGE, VERSION, M_V, join(&msrv), join(&rustv));
    }

    if rustv >= vec![1, 32] {
        println!("cargo:rustc-cfg=tao_log_trailing_comma");
    }
}

fn join(ver: &Vec<u16>) -> String {
    let mut out = String::new();
    for v in ver {
        if !out.is_empty() { out.push('.'); }
        out.push_str(&v.to_string());
    }
    out
}

// Parse `rustc --version` and return as vector of integers, or panic.
fn rustc_version() -> Vec<u16> {
    let rustc = env::var("RUSTC").unwrap_or("rustc".to_owned());
    let out = Command::new(rustc).arg("--version").output().unwrap();
    let out = String::from_utf8(out.stdout).unwrap();
    for l in out.lines() {
        if l.starts_with("rustc ") {
            let mut v = &l[6..];
            if let Some(e) = v.find(" ") {
                v = &v[..e];
            }
            let mut vp = v.split("-");
            if let Some(v) = vp.next() {
                let vs: Vec<u16> = v.split(".")
                    .filter_map(|vss| vss.parse().ok())
                    .collect();
                if !vs.is_empty() {
                    return vs;
                }
            }
        }
    }
    panic!("rustc version not found")
}

The above and subsequent code snippets are from tao-log, released under dual MIT and Apache licenses.

Update 2019-6-4: Applied a fix to above function rustc_version.

Without the much more pleasant, declarative, RFC proposal, we are forced to carefully write (and test) forward/backward compatible rust code that will work with versions as old as might be attempted. This build script will work with rust as old as 1.0.0. Note the use of pre-2018-edition syntax, like the generous helping of 'static lifetimes. We must include the project name directly because env!("CARGO_PKG_NAME") isn’t available until cargo 0.10.0 and rust 1.9.0.

The version_check crate, or the more elaborate rustc_version or autocfg crates, could be used to minimize the per-project build.rs, at the expense of 1 or more additional build time dependencies. I had started with version_check but replaced it with the above, adding only 22 code lines, and with simpler error handling for this use case.

Don’t forget to add a build field to the [package] section of your Cargo.toml so that older cargo releases will always run this build script. Prior to rust/cargo 1.17.0, specifying this field was required:

[package]
build         = "build.rs"

With this in place, for tao-log master branch, if you attempt to compile with say 1.16.0, this is the output:

  Compiling tao-log v0.2.0 (file:///home/david/src/tao-log)
error: failed to run custom build command for
`tao-log v0.2.0 (file:///home/david/src/tao-log)`
process didn't exit successfully: `/home/david/src/tao-log/target/debug/build/
tao-log-318f175d5ac13527/build-script-build` (exit code: 101)
--- stderr
thread 'main' panicked at 'tao-log v0.2.0 minimum supported rust version (MSRV)
is 1.31.0 > 1.16.0 (this rustc)', build.rs:20
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Without this, you would get the following, as the first of many fatal errors using 1.16.0:

warning: unused manifest key: package.edition
   Compiling tao-log v0.2.0 (file:///home/david/src/tao-log)
error[E0432]: unresolved import
   --> src/lib.rs:171:9
    |
171 | pub use ::log;
    |         ^^^^^

error: aborting due to previous error

error: Could not compile `tao-log`.

In considering the value of this, please put yourself in the shoes of prospective new rust users, and new users of your project. Would seeing the latter error breed confidence in either? I think I would have assumed I needed to add another dependency. But that really won’t help here.

For 1.27.0 through 1.30.0 the build will fail before the build script, as the included cargo is aware of the edition field, but not ready for the 2018 edition. These rust versions never reach the build script stage. With 1.30.0:

error: Edition 2018 is unstable and only available for nightly builds of rustc.

…which is confusing, from our current vantage point, because the 2018 edition was released in stable 1.31.0. Even a cargo/rustc as new as 1.30.0 is not going to offer the more useful and current truth. With 1.27.0 it is slightly worse, with a suggestion that is distracting at best:

Caused by:
  editions are unstable

Caused by:
  feature `edition` is required

this Cargo does not support nightly features, but if you switch to nightly
channel you can add `cargo-features = ["edition"]` to enable this feature

These last issues are unique to the 2018 Edition, however, and only for 1.27.0 through 1.30.0.

Another potential problem is that your dependencies can fail prior to the build script running. Worse, since builds are run in parallel, they may fail inconsistently based on timing (though they do always fail.) For tao-log, the log dep requires 1.16.0, so below that version log will fail in its own perplexing way, for example with 1.15.0:

error: borrowed value does not live long enough
   --> /home/david/.cargo/registry/src/github.com-1ecc6299db9ec823/log-0.4.6/src/lib.rs:830:36
    |
830 |                 args: format_args!(""),
    |                                    ^^ does not live long enough
...
837 |     }
    |     - temporary value only lives until here

If dependencies are updated to enforce MSRV themselves, then this could also be avoided.

The following table summaries the compile outcomes for tao-log using various rust versions, with or without the above build.rs. The first row case depends on the MSRV of dependencies.

rust version with build.rs without
—1.15.0
(MSRV of deps)
perplexing (deps) or
clear MSRV error
perplexing (ours or deps)
1.16.0—1.26.0 clear MSRV error perplexing
1.27.0—1.29.0 editions unstable editions unstable
1.30.0 edition 2018 unstable edition 2018 unstable
1.31.0 success success
1.32.0— success (optimized) success

As rust versions progress and we decide to raise our MSRV or otherwise use new version features when available (see below), this recipe will continue to work correctly, and the various problem cases should become less frequent. Even for some future new rust edition, we might optimistically hope for better considered error messages.

Configuring for versions beyond MSRV

With the above build.rs in place, it becomes trivial to add additional cfg flags based on versions beyond MSRV, on the end of the main function:

    if rustv >= vec![1, 32] {
        println!("cargo:rustc-cfg=tao_log_trailing_comma");
    }

…then in the code:

#[doc(hidden)]
#[macro_export]
#[cfg(tao_log_trailing_comma)]
macro_rules! __tao_logv {
    ($lvl:expr, $exp:expr $(,)?) => (...)
}

The $(,)? syntax is for ignoring a trailing comma in macro rule patterns. It is first available in rust 1.32.0. In our MSRV 1.31.0, we settle for $(,)* which is more lenient then we really want.

Note the “tao_log_” prefix as namespace. There are other cargo/rustc defined flags, and its unclear if these cfg flags are applied globally. Regardless, the prefix makes it clear that this is defined in tao-log itself.