Problem
Rust’s libc has a big portability problem resulting from libc’s decision to use FFI for binding the system C library. It does make cross-compiling a breeze, but it also makes libc effectively own the complete set of libc bindings for every version of every operating system that will ever run Rust. This has led to bloat in the libc crate. Worse, it’s led to several situations that libc simply cannot currently support:
-
libc can’t use some of FreeBSD 12’s new features without sacrificing support for FreeBSD 11 (it’s not a problem in C thanks to ELF symbol versioning, but libc doesn’t provide any way to select which set of symbols to use).
-
libc can only support one OpenBSD version at a time, because OpenBSD has no backwards- or forwards-compatibility guarantees.
-
Adding libc support for minor forks like Bitrig requires creating a whole new target_os in Rust. That’s way too much work, especially for minor forks like pfSense that will change no more than a handful of syscalls.
-
Building Rust programs for proprietary OS forks is basically impossible. The vendor would have to, at the very least, fork libc and fork every single dependent crate to depend on the forked version.
-
Programs that try to use newly introduced library functions will compile fine on older OSes that don’t have those functions, but fail at runtime when they can’t link them. It would be better to fail at compile time.
Over in the libc issue tracker people (including myself) have proposed a number of half-baked solutions. But the root cause of the problem is that the libc crate effectively owns the c library headers for every supported operating system. I propose a different solution: a small update to Cargo that would allow OS vendors to take ownership back from rust-lang/libc.
Solution
The solution is simple: Cargo should allow OS vendors to override crates.io’s libc crate. Other crates might need to be overrideable too, for example ncurses. OS vendors should be able to distribute their modified crates through whatever channel is customarily used for that OS. For example apt-get, pkg, etc. When building, Cargo should check a global config file for any OS-provided packages, and prefer using those over crates.io’s. The global config file would look something like this:
/usr/local/etc/cargo/config:
[source]
[source.freebsd-org]
directory = "/usr/local/cargo/vendor"
[source.crates-io]
replace-with = "freebsd-org"
With such an override in place, Cargo would be able to correctly build crates on OSes that rust-lang/libc doesn’t even know about. rust-lang/libc would also be absolved of maintaining bindings for minor forks like Bitrig. For OSes with versioning practices that libc can’t handle, like FreeBSD and OpenBSD, libc could either drop support entirely or else maintain bindings for one nominal version only. Users of a different version would rely on their OS-provided libc replacement. Cross-compiling would work as before – when targeting the nominal version. When targeting any other version, the build host would need additional information pointing to the target’s vendored libc. This need not be stored in the global Cargo config file; it could be placed in the local Cargo.toml file or $HOME/.cargo/config instead.
Implementation
Implementing this RFC would require just 2-3 changes to Cargo, and zero to libc.
- Firstly, Cargo would need to grow a global config file. The exact location would be OS-dependent and hard-coded into Cargo’s source code:
/etc/cargo/config on GNU/Linux, /usr/local/etc/cargo/config on FreeBSD, etc. Cargo already knows about multiple config files, it just doesn’t look in any global locations.
- Secondly, Cargo would need to allow modified vendored dependencies in that global config file. This could be done in either of two ways:
- Cargo could relieve source replacement of the assumption that vendored crates must be exactly the same as crates.io crates. That’s the option assumed by my examples.
- Or, Cargo could allow the
[patch] section in the global config file. Currently Cargo only seems to search for [patch] in the local Cargo.toml file.
- Thirdly, Cargo could optionally allow either or both of the
[patch] and [source.crates-io] sections to be platform-specific. That would ease cross-compiling to OSes that use a vendored libc. The syntax would look like this:
[source.freebsd-org]
directory = "vendor/freebsd"
[source.openbsd-org]
directory = "vendor/openbsd"
[source.'cfg(target_os = "freebsd")'.crates-io]
replace-with = "freebsd-org"
[source.'cfg(target_os = "openbsd")'.crates-io]
replace-with = "openbsd-org"