Conditionally compile optional code using a feature when available


#1

In case this is useful to someone else, here is the build script that I’m adding to rust-url:

use std::process::{Command, Stdio};
use std::io::Write;

fn main() {
    let mut child = Command::new(option_env!("RUSTC").unwrap_or("rustc"))
        .args(&["-", "--crate-type", "lib", "-Z", "no-trans"])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .unwrap();
    child.stdin.as_mut().unwrap().write_all(b"use std::net::IpAddr;").unwrap();
    if child.wait().unwrap().success() {
        // We can use `IpAddr` as it is `#[stable]` in this version of Rust.
        println!("cargo:rustc-cfg=has_ipaddr")
    }
}

It compiles a small program (here just use std::net::IpAddr;) silently. If that succeeds, it enables a cfg flag that is used in the library like this:

#[cfg(has_ipaddr)] use std::net::IpAddr;

// ...

impl Url {
    #[cfg(has_ipaddr)]
    pub fn ip(&self) -> Option<IpAddr> { /* ... */ }
}

It’s like an optional Cargo feature with [features] in Cargo.toml, except it’s always enabled when possible. Users don’t need to enable it explicitly. It’s also more robust than testing rustc --version since you don’t have to figure out relevant versions numbers.


#2

The great advantage of working out the relevant version number is that you can document what version of Rust you need for various features. Took some time to do it, but I’m quite pleased with the result in the scan-rules docs.


#3

Anything users don’t need to enable explicitly can be yanked from under them surprisingly. Doesn’t this go against the principles of API stability and semver, that at least libstd takes so seriously? Do all the consumer crates that want to use Url::ip have to put the same hack into their build scripts?


#4

@DanielKeep it looks like you put way more effort into supporting old Rust versions than I want to :slight_smile:

@gkoz I don’t understand your first sentence. I won’t remove Url::ip without incrementing the major/breaking version number, per semver. The point of this build script is that users don’t need to have their own, or do anything beyond the usual [depedencies] url = "1.0" in Cargo.toml.


#5

[quote=“SimonSapin, post:4, topic:4625”]
I won’t remove Url::ip without incrementing the major/breaking version number, per semver.
[/quote]Since it’s conditional you do remove it when the condition is not satisfied. I guess in this case you could say it’s negligible because when Url::ip disappears, IpAddr it returns doesn’t exist either. This seems like a breaking change nonetheless.

[quote=“SimonSapin, post:4, topic:4625”]
The point of this build script is that users don’t need to have their own, or do anything beyond the usual [depedencies] url = "1.0" in Cargo.toml.
[/quote]If you need to detect whether IpAddr exists, why do the downstream crates not?


#6

This condition is not arbitrary. I’m relying on on the standard library’s own stability guarantees: IpAddr won’t disappear (at least until Rust 2.0, if that ever happens) now that it is #[stable]. Note that the test program does not use #![feature(…)].

Just like a program using std::net::IpAddr directly without #![feature(ip_addr)] will only work on Rust 1.7 or later, a program using Url::ip will only work on Rust 1.7 later. Making this conditional allows programs using Url but not Url::ip to run on some earlier Rust versions.

The only reason I bother is that 1.7 is not in the stable channel yet.


#7

Yeah, I should’ve been more precise: the question is about the crates which want to use Url::ip. They’re encouraged to perform the same kind of detection or they’ll fail to build on rustc 1.6. But it’s all implicit and outside of the ultimate user’s control, there isn’t a feature to toggle, a version to pin to cope with this.