Nix separates content identity from reproducible build recipes
Nix 2.8 experiments with fetching closures and building impure derivations into content-addressed paths. Its new formatting command applies the more familiar idea of declaring a project tool in the flake.
A result can have a precise identity even when its producer does not promise to reproduce it. That distinction is central to two experiments in Nix 2.8. One imports an existing store closure and can rewrite it into content-addressed form. The other permits an explicitly impure derivation to produce a different result on each build. The April 19 release notes describe both, alongside a flake-defined formatting command.
These additions need separate explanations because the word reproducible can hide different claims. Identifying the exact content being consumed is one claim. Saying that running a recipe again will produce that content is another. An impure build cannot acquire the second guarantee merely because Nix gives each result an address based on its content.
I value Nix because it makes build and machine inputs explicit. These experiments are interesting to me where they preserve a clear account of what is known. A content identity is useful evidence, but it should not become an unsupported claim about the process that produced it.
Fetching a closure during evaluation
The experimental builtins.fetchClosure imports a store closure from a binary cache at evaluation time. Its implementation and builtin documentation in 2.8 define the required source cache and source path. A destination path can specify the expected content-addressed result after rewriting.
builtins.fetchClosure {
fromStore = "https://cache.nixos.org";
fromPath = /nix/store/r2jd6ygnmirm2g803mksqqjm4y39yi6i-git-2.33.1;
toPath = /nix/store/ldbhlwhh39wha58rm61bkiiwm6j7211j-git-2.33.1;
}
These are the concrete example paths supplied by the versioned builtin documentation. The source path identifies the existing closure. The destination identifies its rewritten content-addressed form. Using this example requires enabling the experimental fetch-closure feature and access to the stated cache.
The expected destination is consequential. The implementation checks the result of rewriting against it and reports an error if they differ. That comparison prevents the expression from accepting an arbitrary rewritten result while pretending it obtained the expected one. The source location tells Nix where to retrieve the closure, while the destination records the result the expression expects to consume.
The documentation contrasts this with builtins.storePath, which can refer to an already built store path. Specifying a source cache makes the retrieval location part of the expression. Requiring a content-addressed final path also removes the need for users to configure that cache's public keys for this import mechanism.
This does not establish that the imported program has desirable behavior. A content check can identify a program whose behavior is undesirable just as precisely as any other program. The useful guarantee concerns which content the expression accepts. Deciding to use that content remains a separate application or maintainer decision.
Pure evaluation requires the final identity
The builtin's implementation requires a content-addressed final path in pure evaluation. When the source is already content-addressed, the destination may be omitted. The documentation also permits omission under impure evaluation. Those cases have different information available at the point where the expression is evaluated.
To discover the rewritten destination for an existing source closure, the documentation provides nix store make-content-addressed. That command can fetch from the source cache and print the mapping. An author can then record the expected destination in the expression instead of leaving the result unspecified.
The implementation also accepts an empty destination string as a discovery aid. It performs the rewrite and raises an error containing the resulting path, asking the author to place it in toPath. The error is informative because it distinguishes discovering an identity from completing an expression that already declares one.
The 2.8 implementation accepts HTTP and HTTPS stores and rejects URL query parameters for this builtin. It also rejects unsupported attributes in the argument set. Those limits make the actual interface narrower than a general promise to connect any store protocol to evaluation-time fetching.
For infrastructure declarations, this explicitness is useful. A file that says where an imported result comes from and which rewritten result it expects communicates more than an unexplained dependency on a store path already present on one workstation. Another evaluator can see the retrieval source instead of reconstructing that machine's history.
Impure derivations admit a different build contract
The other experiment marks a derivation with __impure = true. The release notes use a build command that writes the current date into its output. Building the expression again performs the impure build again and can produce a different content-addressed path because the output content has changed.
stdenv.mkDerivation {
name = "impure-date";
__impure = true;
buildCommand = "date > $out";
}
This fragment assumes a surrounding expression that supplies stdenv, as in the release-note example. It illustrates the build contract, not a way to obtain reproducible timestamps. The impure derivation experiment must be enabled to use it.
The release says these derivations have network access, like fixed-output derivations. That permits work whose inputs cannot all be treated as an ordinary immutable recipe. It also means the result must not be silently substituted into a dependency relationship that assumes those ordinary inputs determine the output.
Nix therefore restricts which derivations may depend directly on an impure one. The notes permit other impure derivations and fixed-output derivations. An ordinary input-addressed derivation cannot directly make that dependency. The rule gives impurity an explicit boundary in the build graph.
That boundary is more informative than treating every derivation as equally reproducible. A build description that deliberately admits changing results needs a different dependency contract. Its content-addressed outputs let Nix distinguish the results it actually obtained, while the derivation declaration explains why rebuilding can obtain another one.
The tests demonstrate where the boundary can sit
The 2.8 impure-derivation tests use a counter to check that a second build does not simply reuse the first result. They also test an impure derivation depending on another impure derivation and reject an ordinary input-addressed dependency on the impure result.
The matching expression fixture then introduces a fixed-output derivation between impure work and an ordinary dependent. Its fixed-output stage records an expected output hash. Rebuilding that completed result reuses it without rerunning the changing counter-backed work, as the test checks.
This demonstrates why a known result can provide a useful boundary after an impure process. The downstream ordinary derivation depends on a fixed result at that boundary. It does not thereby acquire a promise that the earlier impure operation always produces the same raw output.
My preference is to describe both parts when explaining such a build. The impure stage accounts for the uncontrolled acquisition or generation step. The fixed-output stage accounts for the exact result admitted downstream. Leaving either out would give an incomplete explanation of why reuse is appropriate.
Formatting declares a tool without declaring one universal style
Nix 2.8 also adds experimental nix fmt. Its command documentation selects a formatter through formatter.<system> in the flake outputs. The examples support different formatter packages. Nix supplies the command convention while the project chooses the formatter.
outputs = { self, nixpkgs }: {
formatter.x86_64-linux =
nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt;
};
This fragment exports a formatter for the stated system. The command implementation resolves the formatter from the local flake and executes it. With no arguments, it passes the current directory. With explicit arguments, it forwards them to the formatter.
That behavior matters for a project with a particular formatting policy. Contributors share an invocation and a declared tool instead of assuming a similarly named executable is installed globally. The result still depends on what the selected formatter does with its arguments. Nix does not replace the formatter's file-selection rules with an independent universal formatting engine.
Passing a specific filename is therefore a request to the formatter selected by the flake. It is not a request for Nix to infer a style from that file. Likewise, exporting a formatter for one system does not supply a formatter output for another system automatically. The platform-specific attribute is part of resolving the tool, just as a platform-specific package output is part of resolving a build. A project supporting several development platforms needs declarations that cover those platforms.
The release's changes consequently place different kinds of decisions in declarations. A formatter output identifies a tool to run. A fetched closure records content to consume. An impure derivation states that rerunning a producer can change its result. The useful property in each case is the specific contract a reader can inspect in the expression.