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.
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:
#![cfg_attr(feature = "cargo-clippy", allow(clippy::all))]
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));
}
}
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
.
Update 2021-1-19: Contextually silence clippy lints (line 1), assuming newish clippy while avoiding breakage on old rustc.
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.
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.