In Rust eco­sys­tem it is fairly pop­u­lar for a FFI bind­ing lib­rary de­clare the “nat­ive” lib­rar­ies it links to in a build.rs script. If the bind­ing is in­ten­ded for a cross-­plat­form use, chances are that the build.rs script is writ­ten in­cor­rectly.

As­sume a some­what com­mon case where link­age de­cisions are plat­form de­pend­ent. One then might write the build.rs script as such:

#[cfg(windows)]
fn main() {
    println!("cargo:rustc-link-lib=something");
    println!("cargo:rustc-link-lib=someotherthing");
    // ...
}

#[cfg(not(windows))]
fn main() {
    println!("cargo:rustc-link-lib=somelib");
    println!("cargo:rustc-link-lib=someotherlib");
    // ...
}

This will work great if the target = host. However, it is not a case in a cross-­com­pil­a­tion scen­ario, when target is not the same as the host.

Con­sider target = windows and host = linux for ex­ample. This is what would hap­pen, when one is cross-­com­pil­ing us­ing the mingw tool­chain from a linux sys­tem. In this scen­ario, the build.rs script runs on the ma­chine which does the com­pil­a­tion, so it is com­piled for linux and the con­fig­ur­a­tion vari­ables as­sume val­ues typ­ical of a linux tar­get. This means that the build.rs script in ques­tion will out­put the lib­rar­ies for #[cfg(not(windows))], rather than #[cfg(windows)] case… but we’re tar­get­ing Win­dows and want the Win­dows lib­rar­ies! This ob­vi­ously can’t work! What a mess!

That’s ex­actly the bug I had to solve in lib­load­ing1. The libloading lib­rary ex­poses a cross-­plat­form API for dy­nam­ic­ally load­ing (and un­load­ing) lib­rar­ies. Rel­ev­ant sys­tem APIs, on UNIX-­like sys­tems come in a form of dlopen, dlclose, dlsym, et cet­era. These, as it turns out, are provided by dif­fer­ent lib­rar­ies on dif­fer­ent sys­tems. On Linux-­likes it comes from libdl, FreeBSD et al provide it in libc, whereas vari­ants of OpenBSD will make these sym­bols avail­able in any dy­namic ex­ecut­able, no link­ing in­volved. To en­able such con­di­tional reas­on­ing in build.rs scripts I had re­sor­ted to writ­ing tar­get_build_utils, which would go as far as to rep­lic­ate the rustc be­ha­viour and even parse the cus­tom tar­get spe­cific­a­tions.

Sadly, tar­get_build_utils is not the nicest lib­rary in the world as it pulls along quite a num­ber of heavy de­pend­en­cies. To every­body’s re­joice, since the last time I worked on this… some­time between Rust ver­sion 1.13 and 1.14… Cargo began ex­port­ing some un­doc­u­mented, but very use­ful, en­vir­on­ment vari­ables dur­ing the ex­e­cu­tion of the build.rs scripts:

These vari­ables cor­res­pond to equi­val­ent cfg(...) at­trib­utes in the source code and are oth­er­wise ex­actly what it says on the la­bel. The dif­fer­ence from the reg­u­lar cfg(...) at­trib­utes lies in these vari­ables as­sum­ing val­ues for the tar­get sys­tem, rather than the host sys­tem. This makes it pos­sible to cor­rectly handle the cross-­com­pil­a­tion scen­ario without re­sort­ing to lib­rar­ies like tar­get_build_utils. By us­ing these vari­ables, it is pos­sible to write a build.rs that’s cor­rect in the cross-­com­pil­a­tion scen­ario de­scribed above. Fol­low­ing code snip­pet is how a cor­rect build.rs script might end up look­ing like:

fn main() {
    let target_os = env::var("CARGO_CFG_TARGET_OS");
    match target_os.as_ref().map(|x| &**x) {
        Ok("linux") | Ok("android") => println!("cargo:rustc-link-lib=dl"),
        Ok("freebsd") | Ok("dragonfly") => println!("cargo:rustc-link-lib=c"),
        Ok("openbsd") | Ok("bitrig") | Ok("netbsd") | Ok("macos") | Ok("ios") => {}
        Ok("windows") => {}
        tos => panic!("unknown target os {:?}!", tos)
    }
}

Glad to see the tool­ing im­prov­ing at such a break­neck pace. Cheers for ever im­prov­ing cross-­com­pil­a­tion story in Rust!


  1. A long time ago. Com­mit logs sug­gest me work­ing on it in Ju­ly, 2016.↩︎