Fast, high-level parsing, canonical serialization, and graph operations for
Nix flake.lock files, without linking to Nix.
The crate parses versions 5 through 7 directly into a compact indexed graph
and emits the canonical version 7 JSON representation used by Nix. Fetching,
registry resolution, Nix evaluation, and store access are intentionally outside
the library boundary. The 0.1 graph API is intentionally read-only;
into_owned() detaches it from the input buffer but does not make it mutable.
use nix_flake_lock::LockFile;
let bytes = std::fs::read("flake.lock")?;
let lock = LockFile::parse(&bytes)?;
lock.validate()?;
if let Some(nixpkgs) = lock.resolve(["nixpkgs"])? {
println!("{:?}", lock.node(nixpkgs).unwrap().locked());
}
let canonical = lock.to_nix_bytes();
# Ok::<(), Box<dyn std::error::Error>>(())- Unescaped strings borrow directly from the input.
- Nodes live in one contiguous arena and edges use 32-bit indices.
- Small attribute and input maps are sorted flat vectors.
- JSON object members are collected and sorted once, avoiding quadratic insertion behavior on reverse-ordered input.
- Parsing does not construct a generic JSON value tree.
- Serialization writes directly into a caller-provided
Vec<u8>. followsresolution is iterative and uses constant-time cycle detection.
LockFile::parse validates JSON and the structural lock-file representation.
LockFile::validate additionally checks that every non-empty follows target
exists and that follows chains are acyclic. It deliberately does not fetch
inputs or apply fetcher-specific network and lockedness checks.
Representative Criterion results on an AMD Ryzen 7 7840S against Nix 2.34.8's in-process C++ lock-file codec:
| Operation | Fixture | nix-flake-lock |
Nix core | Speedup |
|---|---|---|---|---|
| Parse | tiny (70 B) | 280 ns | 1.09 µs | 3.9× |
| Serialize | tiny (70 B) | 123 ns | 1.07 µs | 8.7× |
| Parse | typical (1,073 B) | 2.41 µs | 22.19 µs | 9.2× |
| Serialize | typical (1,073 B) | 1.16 µs | 13.71 µs | 11.9× |
The adversarial scaling benchmark keeps sorted and reverse-ordered 8,000-node documents in the same complexity class:
| Order | Size | nix-flake-lock |
serde_json::Value |
|---|---|---|---|
| Sorted | 848,057 B | 10.3 ms | 22.4 ms |
| Reverse | 848,057 B | 8.5 ms | 54.3 ms |
These measurements exclude Nix initialization, subprocess startup, fetching, evaluation, store access, and file I/O. Both implementations receive the same input bytes, produce owned output bytes, and must agree on the canonical JSON before timing begins. Results vary by machine; reproduce them locally with the commands below.
Run the same release checks and minimum-version check as CI with:
devenv tasks run ci:all
devenv --profile msrv tasks run ci:msrvRun benchmarks separately with:
devenv shell -- cargo bench --bench codec
devenv shell -- cargo bench --manifest-path benchmarks/nix/Cargo.tomlThe property tests use the Antithesis Rust SDK for controlled randomness and test-property reporting. The SDK is a development dependency only, and the same properties also fail through ordinary Rust assertions when run locally or in CI.
An external corpus is opt-in so a missing corpus cannot appear as a passing test:
NIX_FLAKE_LOCK_CORPUS=/path/to/repos \
devenv shell -- cargo test --test corpus -- --ignored --nocaptureThe standalone native crate can also compare every corpus file directly with Nix:
devenv shell -- cargo run --release \
--manifest-path benchmarks/nix/Cargo.toml \
--bin corpus -- /path/to/reposThe native benchmark covers versions 5, 6, and 7 and runs in process against Nix 2.34's C++ lock-file codec. See the benchmark methodology for its exact boundary and native build requirements.
The minimum supported Rust version is 1.85. The crate is licensed under Apache-2.0.