January 24, 2022·4 min read·engineeringnixflakescli

Nix finds a flake from inside its project tree

Nix 2.6 searches parent directories for flake.nix within repository and filesystem boundaries. The change improves local commands while retaining the source-selection rules of Git-backed flakes.

The directory containing the code being edited is often below the directory containing its build definition. Requiring a developer to return to the root before every command makes that layout visible in work that does not otherwise depend on it. Nix 2.6 changes local flake discovery to search upward for flake.nix, stopping at the Git repository root or a filesystem boundary. The January 24 release notes document the change.

This makes the current directory a starting point for discovery. It does not make every enclosing directory part of the flake, and it does not remove the distinction between Git-backed sources and ordinary paths. Those source-selection rules still determine what Nix evaluates once it has located the build definition.

Discovery follows the enclosing project

The Nix 2.6 command documentation explains how raw path references resolve. Inside a Git repository, the resulting reference identifies the repository root and the relative directory containing the nearest enclosing flake.nix. If the repository contains no suitable enclosing flake, Nix reports an error.

For example, consider a repository with its flake at /work/project/flake.nix and source files under /work/project/src/parser. Starting in the parser directory, a raw local reference can resolve to the enclosing project flake. A nested flake closer to that directory would instead provide the relevant definition.

cd /work/project/src/parser
nix flake show .

This example assumes the experimental flake interface is enabled and the stated project layout exists. The command asks about the located flake's outputs. It does not depend on inventing a package name that every project must expose. The discovery change determines where the flake comes from, while the flake's own definition determines what outputs exist.

I find this consistent with the reason declarative build definitions appeal to me. The project should describe its desired build state, and routine navigation through source directories should not require restating that description's location. The repository boundary remains a useful limit on how much surrounding filesystem layout influences the command.

The boundary is part of the behavior

The reference parser in the 2.6 source makes the search concrete. When the supplied directory lacks flake.nix, it examines parent directories. Encountering the Git boundary without finding a flake produces an error. For non-Git paths, crossing a filesystem boundary also terminates discovery with an error.

Those stops prevent a missing project definition from turning into unrestricted searching through the machine. A directory's parent may belong to an unrelated project, or to another mounted filesystem. The rule gives discovery a defined scope instead of promising that Nix will find some flake somewhere above the current working directory.

The command manual distinguishes the resulting reference forms. A local Git source uses a git+file reference with a dir parameter for the flake subdirectory. A non-Git source becomes a path reference to the enclosing flake directory. Both can identify a flake below a larger directory tree, but they do not imply identical rules for selecting files.

This matters in a repository with multiple flakes. The flake documentation for the release explicitly supports a dir attribute for selecting a flake in a subdirectory. Parent discovery provides a convenient local route to that same general arrangement. A repository root and a flake root need not be the same directory.

Finding a file does not include every working-tree file

The command documentation states that Git-backed flake sources include files indexed by Git. Files ignored by Git or never added to its index are unavailable in that source. Locating the correct flake.nix therefore does not establish that an unrelated untracked input is part of the source tree Nix receives.

That distinction explains a common class of confusing local behavior. An editor can open a file that a Git-backed flake does not include. Moving the invocation to the project root does not change the source-selection rule. The discovery change solves where to find the definition, while the Git source controls which files belong to its evaluated tree.

The documentation also permits an explicit path: reference when ordinary path behavior is wanted. Choosing that form is a decision about source selection, not merely a workaround for directory navigation. It should be used with an understanding of which files the evaluation now receives.

My preference is for commands whose convenient behavior still has an explainable limit. Nix 2.6 improves the common case of working below the project root, while its documented source forms let a reader determine whether the command uses Git-indexed inputs or a path source.

The release also makes attribute grouping a builtin

Another change in 2.6 brings zipAttrsWith into builtins. The release notes identify it as the counterpart to Nixpkgs' library function. Its builtin documentation in the source defines the operation as transposing a list of attribute sets and applying a function to each resulting group.

builtins.zipAttrsWith
  (name: values: values)
  [ { worker = "a"; } { worker = "b"; client = "c"; } ]

The result groups the two worker values into a list and gives client a one-element list. Attribute names come from the union of the input sets. The supplied function receives the name and a nonempty list of encountered values, so it can preserve the grouping or compute another result from it.

This is useful when a declaration combines several sets of contributions with the same names. It does not assume that every input set contains every name. The operation belongs to the evaluator in this release, so an expression can use it without first obtaining the corresponding Nixpkgs helper.

The parent-search change has a similarly specific scope. It improves how a local command finds its declaration without changing what that declaration promises to build. After discovery, the flake's source and outputs still provide the information needed to explain the result.