July 15, 2019·6 min read·engineeringnixflakesreproducibility

The flakes RFC makes evaluation inputs explicit

RFC 49 proposes a standard structure and locked dependencies for Nix projects. Its central concern is reproducing evaluation before a build begins.

A reproducible build can still depend on an evaluation that selects different inputs on another machine. Eelco Dolstra's flakes RFC, opened on July 15, addresses that gap by proposing explicit project dependencies and pure evaluation. The initial draft identifies user configuration files, environment variables and repository lookups as ways an expression can depend on information outside its apparent source.

That is a distinct problem from isolating the builder. Before Nix can build a derivation, evaluation has to decide which derivation and inputs to produce. If that decision depends on a user's search path or local configuration, repeating the later build procedure does not reproduce the earlier choice.

This is why the proposal interests me as someone drawn to declarative systems. A complete description of desired state needs to identify the inputs used to compute it. Otherwise, the same checked-in expression can describe different systems depending on who evaluates it and when.

The proposal standardizes project entry points

The initial draft defines a flake as a Git repository with a flake.nix file at its root. That file carries metadata, dependencies and an outputs function. The outputs are Nix values that the project provides, including packages or modules.

The proposed structure addresses discoverability as well as dependency selection. Existing names such as default.nix and release.nix are conventions, and the draft argues that Nix projects lack a consistent way to explain what they provide. Well-known output names would let tools find packages and checks without requiring a separate project-specific explanation.

The draft's packages output is a flat set of derivations. It explicitly excludes nested sets there because discovering packages in an arbitrary nested structure would require evaluating attributes to determine what they contain. The restriction makes a tooling operation more predictable by limiting what that part of the project interface can represent.

Other proposed output categories serve different consumers. Checks are derivations intended for the flake check command. Hydra jobs can remain nested. A default package supplies the result when a command does not name one. These conventions do not make every output equivalent, and that is part of their usefulness to tools.

Dependencies become declared input references

The early draft represents dependencies as a list of flake references. A small fragment illustrates that proposed form.

inputs = [ "nixpkgs" ];

This fragment belongs inside the draft's flake metadata. It is an example of the proposal, not a statement that an ordinary released Nix installation already interprets every command and field in the RFC. The draft points to a prototype that users can try.

A reference can identify a repository directly or use an identifier resolved through a registry. The registry maps a convenient name such as nixpkgs to a location. That makes the command line easier to use, but the mapping alone does not pin the repository's contents.

The draft distinguishes global, local and command-line registry entries, with more local choices taking precedence. That is a convenience mechanism for resolving a name. Reproducibility still requires recording the particular result of that resolution when it becomes a project dependency.

This separation is important. A short identifier is useful when discovering or updating a dependency. An exact locked reference is useful when repeating an existing evaluation. Trying to make a convenient name perform both jobs without a recorded resolution would retain the ambiguity the proposal is intended to remove.

The lock file records a dependency tree

The draft proposes a generated flake.lock file containing mappings from input references to direct references with revisions. It also records content hashes for the fetched input. This gives evaluation a concrete dependency tree to use again.

A Git commit identifies a revision, while the additional content hash checks the material supplied to Nix. The draft discusses this especially in relation to GitHub archives, where Nix cannot directly establish the archive contents merely by treating their download as a Git operation. Recording both gives the lock entry a precise role.

The early design uses the top-level lock file and ignores lock files inside dependencies. That is a specific composition choice. The root project determines the resolved tree used for its evaluation, rather than independently accepting every nested project's current lock file as another authority.

Updating dependencies is also explicit work in the proposal. The draft describes recreating the lock file and checking for newer input versions. A project can therefore distinguish a repeat evaluation using existing locked inputs from a deliberate update that changes them.

The lock file does not make the dependency's source permanently available. The draft lists deletion of repositories or branches as an unresolved problem and calls for a way to gather dependencies elsewhere. Identity and availability remain different properties even when the chosen input is recorded accurately.

Pure evaluation closes other input channels

Locking repository references cannot prevent an expression from consulting a local file or the current time. The draft therefore pairs lock files with a pure evaluation mode. It restricts access to the flake's own source, declared inputs and appropriately pinned fetched paths.

The proposed mode also prevents ordinary access to environment variables and the current system type. Current time is unavailable too. These restrictions remove values that could change an evaluation result without a corresponding change to the locked project inputs.

The Nix search path is another restricted mechanism. Composition should use flake inputs or pinned fetching instead of an ambient search-path lookup. In particular, the draft says Nixpkgs would no longer read the user's configuration directory through its usual implicit route in pure mode.

Those restrictions impose compatibility costs. The draft identifies existing workflows that pass configuration through NIX_PATH or a user's Nixpkgs configuration file. Removing those ambient inputs requires another explicit way to provide the information, or an intentional choice to disable pure mode for a command.

I regard that tradeoff as central to the proposal. Reproducible evaluation needs a boundary that excludes unrecorded inputs. A workflow that depends on those inputs cannot retain the same meaning merely because its repository dependencies now have a lock file.

Evaluation caching depends on the same boundary

The draft connects pure evaluation to caching evaluated flake attributes. Its cache fingerprint includes the flake's store path and lock-file contents. If those identify all relevant inputs, a previously computed result can be reused for the same fingerprint.

The causal relationship matters. Caching is not safe merely because an expression lives in a Git repository. If it can inspect the environment or a changing local file, the repository revision is incomplete as an evaluation key. Restricting those reads is what makes the proposed key meaningful.

The early prototype caches top-level attributes. The draft treats caching other evaluated values as future work. That scope avoids turning one proposed optimization into a claim that every evaluation step is already memoized.

This is also distinct from reusing a built package. Evaluation caching reuses the computation that identifies the derivation. A binary cache reuses the resulting build artifact. The proposal addresses time spent before the latter decision can be made, while relying on the same discipline of identifying inputs.

Channel replacement raises publication questions

The RFC proposes replacing channels with flake references and registries. In a same-day response, samueldr asks how tested channel advances will be published and how users will identify the tested branch. The concern is that a convenient repository reference does not itself establish that the referenced state passed the channel's tests.

This is a useful distinction between pinning and selection policy. A locked commit can be repeated even if it was a poor choice. A channel's testing process helps select a usable revision. Replacing its distribution mechanism needs to preserve a clear account of that selection.

The initial draft also considers offline use. It proposes tolerating failure to refresh a registry or an already fetched reference, and retaining fetched flakes as garbage-collection roots. Those are availability measures for the new workflow, not changes to what makes a dependency revision exact.

Several interfaces remain undecided

The early draft leaves questions about system types, overlays and flake arguments unresolved. Its example hard-codes x86_64-linux, and the unresolved-questions section asks how platform selection should work without undermining pure evaluation or caching.

That is an important limit on the current proposal. A standard project structure has to serve multiple systems while keeping its evaluation inputs explicit. Supplying arguments through an undeclared local mechanism would reintroduce the problem the design is trying to solve.

The RFC provides a prototype and a concrete model for discussion. Its strongest contribution at this point is the connection between declared dependencies, a root lock file and restricted evaluation. Those pieces explain how two evaluations of a project could select the same derivation before the package manager begins repeating the build itself.