nixfmt-rs
A drop-in replacement for nixfmt: same binary name, same flags, byte-identical output — just faster and embeddable.
- Drop-in. Verified byte-for-byte against
nixfmtv1.2.0 across all of nixpkgs; swap the binary and nothing in your tree reformats. - Fast. Formats the entire nixpkgs checkout in under 2 s — ~130× the Haskell implementation single-threaded (benchmarks).
- Embeddable. Usable as a Rust library (
#![forbid(unsafe_code)], two dependencies) or in the browser via the WebAssembly build. - Helpful errors. rustc-style diagnostics with source snippets and fix-it hints (examples).
Install
# Nix
# Cargo
# From source
Prebuilt static binaries are attached to each
GitHub release with a
SHA256SUMS file and Sigstore-backed provenance:
NixOS / home-manager (via flake input):
{
inputs.nixfmt-rs.url = "github:Mic92/nixfmt-rs";
outputs = { nixpkgs, nixfmt-rs, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
modules = [
({ pkgs, ... }: {
environment.systemPackages = [ nixfmt-rs.packages.${pkgs.system}.default ];
# or, in home-manager:
# home.packages = [ nixfmt-rs.packages.${pkgs.system}.default ];
})
];
};
};
}
Usage
The binary is named nixfmt and is flag-compatible with upstream.
# stdin → stdout
|
# Format files / directories in place (recurses into *.nix, parallel)
# Check only (exit 1 if any file would change)
# Layout
# Debugging modes (match `nixfmt --ast` / `nixfmt --ir` exactly)
|
|
treefmt
The binary is a drop-in for nixfmt, so with treefmt-nix just override the
package:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
treefmt-nix.url = "github:numtide/treefmt-nix";
nixfmt-rs.url = "github:Mic92/nixfmt-rs";
};
outputs = { nixpkgs, treefmt-nix, nixfmt-rs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in {
formatter = forAllSystems (system:
treefmt-nix.lib.mkWrapper nixpkgs.legacyPackages.${system} {
programs.nixfmt = {
enable = true;
package = nixfmt-rs.packages.${system}.default;
};
});
};
}
Or in a plain treefmt.toml:
[]
= "nixfmt" # the nixfmt-rs binary is also called `nixfmt`
= ["*.nix"]
Without treefmt at all, point nix fmt straight at the binary (it recurses
into directories and formats *.nix in place):
outputs = { nixpkgs, nixfmt-rs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in {
formatter = forAllSystems (system: nixfmt-rs.packages.${system}.default);
};
Editor integration
The binary is named nixfmt and accepts the same flags and stdin/stdout
contract as upstream, so any existing nixfmt integration works unchanged once
this package is on $PATH (or pointed at explicitly).
VS Code (jnoortheen.nix-ide):
"nix.formatterPath": "nixfmt"
Neovim (conform.nvim):
require.
Helix (languages.toml):
[[]]
= "nix"
= { = "nixfmt" }
Emacs (apheleia): nixfmt
is built in; just ensure the binary resolves to this one.
Anything else: pipe the buffer through nixfmt (reads stdin, writes
stdout, exit 1 on parse error).
Error messages
Parse errors come with source snippets, related spans and fix-it hints:
Error[E001]: expected ';', found '='
┌─ config.nix:2:27
│
1 │ {
2 │ services.nginx.enable = true
│ ^^^^
3 │ networking.firewall.enable = false;
= note: missing semicolon after definition
= help: add a semicolon at the end of the previous line
Error[E002]: unclosed delimiter '{'
┌─ config.nix:5:1
│
3 │ bar = {
│ - unclosed delimiter opened here
4 │ baz = 2;
5 │ }
│ ^
= help: add closing '}'
Error[E005]: commas are not used to separate list elements in Nix
┌─ config.nix:1:4
│
1 │ [ 1, 2, 3 ]
│ ^
= help: use spaces to separate list elements: [1 2 3]
Run cargo run --example error_visualization to see the full catalogue
of diagnostics on intentionally broken inputs.
Benchmarks
--check over a full nixpkgs checkout (42 942 .nix files), AMD EPYC
7713P 64-core, nixfmt 1.2.0, treefmt 2.5.0. treefmt runs use --no-cache
so every file is actually processed:
| command | wall time | user time | vs nixfmt-rs |
|---|---|---|---|
nixfmt-rs --check . |
1.68 s | 9.34 s | 1.00× |
treefmt driving nixfmt-rs |
3.35 s | 10.14 s | 1.99× |
nixfmt-tree (treefmt + Haskell) |
38.89 s | 216.2 s | 23.2× |
nixfmt --check . (Haskell) |
220.67 s | 214.4 s | 131× |
Single large file (all-packages.nix, ~12 k lines): 36.8 ms vs 762.7 ms
(20.7×).
Reproduce with scripts/bench.sh; the dev shell
provides hyperfine and the script defaults to the nixpkgs revision
pinned in flake.lock, so nix develop -c scripts/bench.sh is
self-contained.
For parser micro-benchmarks via criterion (the bench feature gates the
criterion dependency so cargo test stays lean):
Library
The formatter is also usable as a library. Disable default features to skip
the CLI-only dependencies (ignore, mimalloc):
[]
= { = "0.1", = false }
let formatted = format?;
let mut opts = default;
opts.width = 80;
let formatted = format_with?;
On parse failure, render the returned ParseError with source context via
nixfmt_rs::format_error. See the API docs.
For JavaScript/TypeScript, the WebAssembly build is on npm as
nixfmt-rs — see
wasm/README.md.
Design goals
- Exact behavioural parity.
--ast,--irand formatted output are diffable byte-for-byte against the Haskell implementation, so any divergence can be bisected mechanically. - Hand-written recursive-descent parser. No parser-combinator or
grammar generator; the structure mirrors
Nixfmt/Parser.hsdirectly, which keeps error messages and trivia handling under our control. - Minimal dependencies. The library uses only
memchrandcompact_str; the binary addsignore(parallel.nixwalking) andmimalloc.
See docs/ARCHITECTURE.md for how the pieces fit
together.
Testing
# differential check vs. reference `nixfmt` over a nixpkgs checkout
# modes: format | ir | ast; env: NIXPKGS, LIMIT, JOBS, MAX_BYTES, REF, OUT
LIMIT=0
The test suite is layered (unit → regression → vendored fixtures →
properties); see tests/README.md for where to add
new cases.