A second ecosystem: Cargo¶
pydd's core knows no ecosystem. The Rust lane tests that claim. It uses
only pydd.core: Planner, Derivation, the writers, and the root
wrapper lib/core/root.nix. It has no plan document and no graph.nix.
root = pydd.rust.cargo { project = ./.; };
The result is an ordinary derivation of the project's binaries, as for
Python. It takes override over the arguments, overrideAttrs and the
install hooks.
Cargo plans, pydd writes¶
Python needs a planner of its own, because a lock file of Python says
nothing of how to build a package. Cargo already has a planner.
cargo build --unit-graph gives each unit of a build: its features, its
profile, its edition, whether it builds for the host or the target, and
what it depends on. Cargo.lock holds none of the first four.
So the planner runs cargo, offline, over a vendor directory that nixpkgs
makes from Cargo.lock (rustPlatform.importCargoLock). Each crate of
that directory is its own store path, so a node depends only on its own
crate. The planner sets RUSTC_BOOTSTRAP=1, because --unit-graph is
unstable.
One node per unit¶
The planner writes one node for each unit. That is cargo's own granularity, and pydd batches nothing.
unit |
node |
output |
|---|---|---|
library |
|
|
proc-macro |
|
|
build script, compile |
|
|
build script, run |
|
|
binary |
|
|
One more node joins the binaries of the roots, under the project's name.
tests/rust/serde-app has 24 units, so 25 nodes.
A node reads its build script's output when it builds. No evaluation,
and no planner, knows what a build script prints. The run node writes
what the script printed to flags.json, and each node of the package
reads that file. The keys reach the units that cargo gives them to:
key |
reaches |
|---|---|
|
every unit of the package |
|
the package, and every unit that links it, through |
|
the package's library, or every target when it has none |
|
the package's binaries, tests, examples, benches and cdylib |
|
those targets of the package |
|
the build scripts of the packages that depend on it, as |
A build script also gets CARGO, RUSTC, TARGET, HOST, PROFILE,
OPT_LEVEL, DEBUG, the CARGO_CFG_* of the target and the
CARGO_FEATURE_* of the unit, as from cargo. rustc-check-cfg is
dropped: rustc then checks no cfg names, and a check warns and fails
nothing.
-C metadata comes from what cargo decided. It hashes the package ID,
the target, the mode, the platform, the features, the profile and the
metadata of each dependency. It holds no path of the host, so every mode
gives the same file names.
Projects and sources¶
project is a package or the root of a workspace (Lillecarl/pydd#18).
A virtual workspace names no package, so the call passes name. Every
other choice is cargo's own flag, and the planner passes it to the unit
graph:
argument |
cargo flag |
|---|---|
|
|
|
|
|
|
|
|
A path dependency inside the project needs nothing. One outside it
needs root, a directory that holds both: tests/rust/monorepo builds
tool with shared beside it.
A git dependency needs its hash, as importCargoLock takes it:
outputHashes."itoa-1.0.15" = "sha256-...";, or
allowBuiltinFetchGit = true. nixpkgs writes cargo's source replacement
for each git source into the vendor directory, and the planner uses that
file.
The root joins the binaries of every root unit. A workspace's libraries
build too, as with cargo build, and install nothing.
Tests¶
root.tests runs every test of the project, as cargo test does. It is
a second planner, over cargo test --no-run:
unit |
nodes |
|---|---|
a test binary ( |
one compiles it, one runs it |
an integration test |
the same, with |
the doctests of a library |
one runs |
Each node that runs a test keeps its output as log, and the root links
every log. So the root builds only when every test passes, and a failed
test names its own node. A unit that the tests' graph shares with the
build's graph is the same node. tests/rust/workspace has one test of
each kind.
Real projects¶
tests/rust-live builds ripgrep, fd, hyperfine and bat from GitHub, as
they are, in three modes: nix run --file . test-rust-live. Its README
lists what each one needed. Two changes came from them:
LTO by the role of the unit. The unit graph gives each unit the profile's
lto. Cargo runs LTO only where it links a binary, and the libraries that binary links keep their bitcode. A build script and a proc-macro get none. The planner does the same.LIBRARY_PATHfor each tool. libz-sys probes for zlib by linking with-lz, and builds its own copy when that fails. gcc readsLIBRARY_PATHfor-l.
Native libraries¶
A crate that links a system library, such as openssl-sys, needs that
library and often pkg-config (Lillecarl/pydd#17). Three sources give
the nodes of a crate its tools, and a later one wins for the crate it
names:
source |
example |
holds |
|---|---|---|
nixpkgs hints, |
|
tools, variables |
the project |
|
tools, |
the call |
|
tools, |
The hints are nixpkgs' own table. defaultCrateOverrides tells
buildRustCrate the inputs of each crate that needs one, so an overlay
that changes it changes pydd's nodes too. A variable in capitals in an
entry becomes a variable of the node: zstd-sys gets
ZSTD_SYS_USE_PKG_CONFIG, and so links the libzstd of nixpkgs rather
than the copy it bundles.
A tool reaches the build script and the compile of its crate, and no
other node. bin goes on PATH, lib/pkgconfig and share/pkgconfig
on PKG_CONFIG_PATH, lib on LIBRARY_PATH, and include into
CFLAGS and CXXFLAGS as -isystem. The library is linked where the build script says, and the
linker wrapper of nixpkgs writes the RUNPATH.
tests/rust/native-app builds openssl and zstd with no declaration.
tests/rust/native-declared turns the hints off and declares the same
inputs, in Cargo.toml for openssl-sys and in the call for zstd-sys.
With the hints off and nothing declared, openssl-sys fails: it finds
neither pkg-config nor OpenSSL.
cargo-dyndrv has no hints. Its caller writes an extern set by hand,
keyed by the full crate ID, with the PATH and the environment of each
build script.
A development shell¶
root.shell has cargo, rustc, clippy, rustfmt and rust-analyzer, and the
tools and variables of every crate. They go through stdenv's setup hooks,
as a nixpkgs build gets them, so a plain cargo build in the shell finds
OpenSSL and zstd. examples/cargo shows it.
Cross-compilation¶
targetPkgs names the package set the binaries run on, such as
pkgs.pkgsCross.aarch64-multiplatform. rustc, cargo, build scripts and
proc-macros run on its buildPackages; the libraries, the hints and the
linker of the target's units come from it. A build script gets
TARGET_CC, TARGET_AR, HOST_CC and the target's pkg-config, as the
cc and pkg-config crates read them.
A hint's native input is the build machine's. nixpkgs splices
pkg-config in a cross package set, and the plain value is built for the
target. This machine runs aarch64 binaries through binfmt, so such a
pkg-config works in a shell, and fails in the sandbox with status 255.
The hints take __spliced.buildHost.
tests/rust builds three projects for aarch64, and runs each binary under
qemu-aarch64. The rest is deferred, and Lillecarl/pydd#21 lists it:
tests under cross, a declared tool's platform, and other targets.
The modes¶
Each project of tests/rust gives one binary and one root in four modes:
nix run --file . test-rust.
mode |
Nix |
writes through |
|---|---|---|
|
master, |
|
|
master, |
huggorm |
|
master, |
|
|
2.35, recursive-nix |
|
out cannot build a Cargo project. In out, the host makes every
node, and the planner writes only its root. Only cargo, in a build, knows
the units. So pydd.rust.cargo refuses plannerProtocol = "out". Python
gets around this because the host can make a node for every wheel of the
lock. A Cargo lock gives no such list of nodes.
Compared with cargo-dyndrv¶
cargo-dyndrv also
writes one derivation per unit of the unit graph. It is a Rust binary that
replaces cargo inside buildRustPackage, and talks to the daemon through
harmonia. It keeps build-script output as files of flags, and runs each
build script through a small wrapper binary.
pydd's lane is a Python planner over pydd.core, so it runs in every
mode that the core has, recursive-nix included. A node builds with a
Python script and rustc, and keeps the output of a build script as JSON.
cargo-dyndrv also runs outside a build: it writes the derivations through the user's daemon and prints the root. pydd has no such mode yet (Lillecarl/pydd#22).
What is missing¶
need |
issue |
|---|---|
fetch only the crates that the graph uses |
Lillecarl/pydd#20 |
cross: tests, declared tools, other targets |
Lillecarl/pydd#21 |
run the planner on the host, as cargo-dyndrv can |
Lillecarl/pydd#22 |
Every crate of the lock is fetched, also the ones that never build.
importCargoLock makes a fetch for each crate of Cargo.lock, so a Linux
build fetches windows-sys too. #20 holds the alternative.