Pre-RFC: Cargo features for configuring sys crates

Almost all sys crates have to deal with the question: should they link dynamically or statically?

It's such a simple binary question, but there is not good default for it. The reality is messy, dependent on the target platform and intended distribution channel for the application:

  • Linux distros prefer unbundling, so most sys crates on Linux should link dynamically. Except crates that are not available in a given distro, or when the application absolutely requires a version that is newer than what the distro ships with. And except MUSL and binaries distributed as tarballs rather than distro packages.

  • On macOS most libraries should be statically linked, except libraries that Apple ships with the OS. Except crates that are built as a Homebrew formula should link everything dynamically, but not when merely using Homebrew to build for a different distribution channel.

  • On Windows most libraries should be statically linked, but applications may decide on a case-by-case basis to ship some libraries as DLLs installed by the application's installer.

I think this problem needs a dedicated solution, because:

  • Almost every sys crate needs to deal with this decision.

  • Even though there's no perfect default setting, picking even half-decent default behaviors requires awareness of multiple operating systems. Currently majority of Rust users are on Linux, but in this area Linux is significantly less messy than other platforms, so I'm afraid Linux users will underestimate complexity of the problem.

  • When sys crate's default behavior is unsuitable, and the user needs to customize the setting, Cargo gets in the way.

Cargo features are ill-suited for this problem. The obvious way of having two separate features for static and dynamic makes the features mutually-exclusive, and Cargo features are not supposed to be used like this. Having one as a default and the other as a feature flag is more Cargo-friendly, but less obvious, and it can't be an actual default feature flag, because default-features=false is practically impossible to use. In either case, if any crate in the dependency tree sets the wrong flag, overriding it is impossible. Cargo features can't be set for a sub-sub-dependency, so projects that use a higher-level wrapper crate can't configure its sys dependencies in a straightforward way.

Sys crates also use environment flags for this, but Cargo is also unhelpful for this. Cargo workspaces can't set global env flags, so projects need another solution (e.g. layer another build system) to configure the environment first. Env flags for sys crates are not discoverable. There's no agreed standard naming or a way to set the defaults.

There are also smaller configuration problems that may be relevant:

  • If linking statically, should the crate use its vendored version, or search the system for a static version, or search some specific directory for a user-supplied custom build?
  • Which version or ABI configuration of the library is required?
  • Should the sys crate run bindgen, or can it use pregenerated bindings?
  • If linking dynamically, what rpath should it set? (e.g. on Linux it's usually just system-global default, but macOS almost always needs special settings for bundles and frameworks)
  • LGPL sys crates may want to build a shared library from vendored sources, and let the app link to them. That's currently very tricky to do with cargo, because OUT_DIR is well-hidden, and rpath is target-dependent.

On top of that, a project that is built for multiple platforms needs to have multiple versions of this configuration. Target triples are not even specific enough, because Windows apps may want to offer two configurations: with or without an installer ("portable" in Windows sense), and macOS builds are very different depending on whether they're Homebrew-dependent formulas or app bundles.

So in the end it's a negotiation between target system conventions and user requirements, and quirky needs of individual libraries. How should this config look like? Where should it go? How can sys crates be simpler to implement without making them ignore complexity of macOS and Windows?

13 Likes

I would love to see this problem addressed more systematically.

Having a top-level standardized switch for "I generally want to link libraries dynamically" or "I generally want to link libraries statically" would help greatly. (As would a "don't ever use vendored code" switch.) Having a standardized way to specify this per-crate would help as well, for many use cases.

This doesn't necessarily require completely solving the "finding local libraries" problem (which is potentially intractable in the fully general case); instead, we can standardize the manner of asking crates for this, standardize common code to respect that information provided by cargo, and then improve crates to respect that when they need to find libraries in a custom way.

With my Cargo team hat on: I'd love to see someone work on a systematic solution for this, and I'd be happy to work with them to talk about use cases and requirements.

1 Like

Here are some rough ideas:

From sys crate perspective

Currently cargo:rustc-link-lib=foo is documenteed as merely setting -l flag, so whether it links statically or dynamically is unclear, and I guess it's up to the linker. I wonder whether this could be changed to nudge sys crates toward the preferred option. If the sys crate doesn't explicitly specify static/dylib, and there are both static and dynamic libs available in the search dir, then Cargo could pick the option preferred by the user.

For more complex sys crates more logic is needed. Currently sys crates have their own logic for finding the library, e.g.:

if dynamic_is_ok {
    if find_dynamic() {
        return;
    }
}
if static_is_ok {
    if !find_static() {
        build_vendored_fallback();
    }
}

but that hardcodes order of preference and fallbacks. I think this could be changed by letting Cargo/user set a comma-separated list of things to try, in order of preference.

for option in env::var("CARGO_SYS_PREFERRED_LIB_SOURCES").unwrap().split(',') {
    match option {
        "dynamic" => if find_dynamic() {return;},
        "static" => if find_static() {return;},
        "vendored" => if build_vendored_fallback() {return;},
        _ => continue,
    }
}
panic!("missing libfoo");

This way sys crates could be told to suit certain use-cases, e.g. non-vendored code for a distro CARGO_SYS_PREFERRED_LIB_SOURCES=dynamic,static, or try to make the result as isolated from the OS as possible CARGO_SYS_PREFERRED_LIB_SOURCES=vendored,static, etc.

But I'm not sure how in this setup a sys crate can express its own preference for static vs dynamic. Some libraries may support both, but one of the options may be very problematic (e.g. libGOMP can be linked dynamically, but it would make executable dependent on the compiler). I suppose such crates could use env::var("CARGO_SYS_PREFERRED_LIB_SOURCES").unwrap().split(',').contains("static") only to check if they're allowed to use their preferred method, and ignore Cargo's ordering.

From user perspective

I presume the config needs to be a combination of:

  1. Target-triple-specific defaults. Having cargo build do the best it can out of the box is super valuable.

  2. In project's Cargo.toml (especially workspace root). I don't think it should be in .cargo/config, because that could end up using user's global config, and in turn make projects build in subtly incorrect ways when built outside of developer's own machine.

  3. Overrides by package maintainers. It could be env vars or a cargo command-line flags. Maybe it could be a path to a separate TOML file to keep one syntax for the actual configuration instead of inventing a flattened microsyntax for env vars/flags.

I wonder if Cargo custom profiles feature can be used for this, e.g.

[profiles.homebrew-formula.package."*"]
link-sys-crate-as = ["dynamic"]

[profiles.windows-portable.package."*"]
link-sys-crate-as = ["static"]

Plus os-specific exceptions like:

[target.`cfg(target_os="macos")`.package."*"]
link-sys-crate-as = ["static", "vendored"]

[target.`cfg(target_os="macos")`.package.libz-sys]
link-sys-crate-as = ["dynamic"]