Skip to content

Repository files navigation

nix-flake-lock

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>>(())

Performance model

  • 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>.
  • follows resolution is iterative and uses constant-time cycle detection.

Correctness scope

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.

Benchmarks

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:msrv

Run benchmarks separately with:

devenv shell -- cargo bench --bench codec
devenv shell -- cargo bench --manifest-path benchmarks/nix/Cargo.toml

The 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 --nocapture

The 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/repos

The 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.

About

Fast, high-level parsing, canonical serialization, and graph operations for Nix flake.lock files,

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages