As detailed in this larger piece surveying build.rs scripts, a lot of build.rs scripts that generate FFI bindings (and thus need to pull in external sources) or need to pull in external corpuses of information (e.g. NLP-oriented crates) often need to download external artifacts themselves.
By artifacts, I mean:
- remote HTTP archives or Git repositories
- on-disk paths to executables the
build.rsneeds to shell out to and expects to be in$PATH - on-disk paths to foreign library information the
build.rsneeds to generate linker arguments/Rust code with
Usually, these artifacts are too large to be reasonably vendored in Git, either because of their size, or that the intention of the crate is actually to build against whatever the current OS's pkg-build provides, or because they're bindings over Absolute VendorWare that takes up 200 GiB and refuses to live anywhere other than /opt/NameBeforeTheAcquisition/PainAndSuffering/202x.y/Sdk
The ways build scripts accomplish this widely varies and any given build script may do any combination of the following:
- they make HTTP requests to download the file and maybe check the hash
- they git-clone the upstream repo and maybe check out a specific commit or tag.
- they download a specific release tarball from the upstream project website
- they use
pkg-buildto locate the OS installation of the headers - they search the filesystem for an existing installation of some VendorWare SDK at specific paths
With increased interest in sandboxing build scripts and increasing their security, it's worth investigating mechanisms through which said build scripts can better manage external dependencies. A lot of these build.rs scripts end up acting as ad-hoc, informally-specified, bug-ridden, slow (to compile) implementations of half of Common Lisp Nix.
Bringing them closer to Nix by encouraging explicit manifests of file/path/executable dependencies encourages reproducibility, auditability, caching, build times, and error handling when said artifacts are missing. It also means that said build scripts need less dependencies internally as they no longer have to bundle an HTTP client.
One might imagine a section in the Cargo.toml loosely like this:
[artifact-dependencies]
# git cloned to $OUT_DIR/artifacts/lib-source
lib-source = { git = "https://github.com/tukaani-project/xz.git", rev = "4b73f2ec19a99ef465282fbce633e8deb33691b3" }
# 700 MiB zipfile downloaded to $OUT_DIR/artifacts/build-rs.zip?
# You get a hash though.
"build-rs.zip" = { src = "https://sidequests.reduxrobotics.com/gacha/build-rs.zip", sha256 = "629ab10e62eeafa1efe34c0c45aec9607ec45d83032e976a400cb28d09d4f63b" }
# Only export as a path...Still some unsolved problems? Also platform-dependent behavior?
vendorware = { type = "path", env = "VENDORWARE_SDK", src = "file:/opt/UnpleasantVendorWare/sdk/include" }
os-package.pkg-config = true
[target.'cfg(target_os = "linux")'.artifact-dependencies]
# linux-only
bpf-scripts = { ... }
[patch.artifact-dependencies]
# what if i want to override things?????
[build-commands]
# We should probably also have some way of listing what commands need to exist for a build.rs to run.
# Nix flakes/derivations take it a step farther and let you pick _exactly_ which binaries
required-commands = ["git", "cbindgen", "rc.exe"]
This isn't a one-size fits-all solution, though. There's still quite a few open questions:
- If these artifacts are so large they are fetched externally, does it make sense to still
cargo-vendorthem?- For some people it might, for others it might not. The status quo is that vendored build scripts would still download external binaries.
- For build environments that are already sandboxed or provide their own reproducible artifact primitives, how can we pass them to Cargo without Cargo yelling about how it can't git-clone the remote?
- Can we do this at runtime without evil Cargo.toml hacking?
- How much complexity in platform-dependent artifact selection do we want to support via this happy path?
- For some artifacts, this is non-trivial
- For executables, you can use it to ask permission from the users to execute, but this manifest wouldn't tell the build script the conditions for the executables to actually be valid
This isn't going to solve every external dependency need a build.rs has, but I think there's a strong case for a nice happy path. I would probably treat this feature like const in the standard library: a strongly-encouraged nice-to-have that slowly becomes more available for a wider number of user cases, but not mandatory to get the job done.