January 22, 2025·6 min read·engineeringnixflakesreproducibility

Nix 2.26 gives relative flake inputs a parent in the lock file

Relative flake inputs now resolve within the referring source tree. The lock-file change removes redundant source locking while introducing a compatibility boundary for older Nix clients.

A repository containing several flakes already has a versioned source tree, but earlier handling of relative inputs could treat a subdirectory as another independent source. Nix 2.26 changes that relationship. A relative flake input can refer to another flake within the same repository, with the lock file recording the parent that gives the path its meaning.

The 2.26 release notes date the release to January 22. They also state the compatibility cost explicitly. Older Nix versions cannot use lock files containing the new relative path locks. This is a format change that affects collaborators sharing the repository, even when its directory layout is unchanged.

Nix's declarative model is why I use it for desired-state management of my personal infrastructure. A configuration repository should explain what its inputs are and how they relate. Making an input within the same source tree retain that relationship is useful, because it avoids describing one committed tree as several unrelated snapshots.

A relative path needs a defined starting point

The release notes show the new declaration in its smallest form.

inputs.foo.url = "path:./foo";

This selects the flake in the referring flake's foo subdirectory. The important part is the base used to interpret the relative path. It is not a request to resolve the path against whichever shell directory happens to launch Nix. The input relationship supplies the base within the flake source tree.

The versioned flake reference documents path inputs, while the upstream change explains the problem being repaired. Subflakes previously incurred separate copying into the store and their own NAR hashes. That was redundant where the parent source was already locked, and it complicated references to parent directories.

A subdirectory's identity can be derived from the parent tree and its location. Treating it as an independent source adds another representation of information already available through that relationship. More importantly, copying the directory separately can remove the surrounding tree that another relative reference expects to exist.

The new implementation accesses a subflake through the calling flake's source accessor. Upstream describes this as avoiding the unnecessary copy and permitting references to parent directories as long as they remain within the same tree. The boundary is the source tree, so the new support should not be interpreted as permission to depend on arbitrary neighboring paths on a developer's machine.

The flake reference distinguishes a relative path passed on the command line from one declared in a file. A command-line path uses the current directory as its base. A path in flake.nix uses the directory containing that file, subject to the tree restriction. This distinction explains why testing a path interactively and declaring it as an input are related operations with different sources for their starting directory.

The lock file records the relationship

The upstream explanation introduces a parent field on a relative input's lock node. The field identifies a path through the input graph. That path determines the flake directory against which the node's relative path is interpreted.

This is different from placing an absolute checkout path in the lock. An absolute local path describes a location on one machine. A parent relationship describes how to find a location inside the locked input graph. It can therefore preserve meaning when another user obtains the repository at a different filesystem location.

The graph detail also matters when a nested input belongs to another input. A relative path is meaningful only with the correct referring source, which may not be the root flake. Recording the parent identifies that source instead of assuming every relative path starts at the root repository directory.

The result also explains why the format change is necessary. A consumer needs to understand the parent relationship to interpret the node correctly. An older implementation cannot recover those semantics merely because it recognizes a path string. Reading the bytes of a lock file is not the same as implementing the relationships the file expresses.

There is no need to hand-author this structure to use the feature. The public declaration identifies the relative input, while Nix's locking implementation records the relationship. The source-level input declaration and generated lock data have different jobs, and understanding the latter helps explain compatibility without making it normal application configuration.

One source revision can contain several flake interfaces

This arrangement is useful when repository organization and flake interfaces do not have the same granularity. Several directories may each expose a flake while their implementation changes are reviewed and committed together. A relative input expresses that they belong to one source tree without requiring each directory to become an independently fetched repository.

That does not mean every subdirectory needs a flake. The feature addresses dependencies between flakes that already have a reason to exist separately. Adding interfaces merely because relative inputs are easier would create more declarations to maintain without establishing a corresponding benefit.

The mechanism instead makes an existing relationship explicit. A child can depend on another flake in its tree, including a parent directory where supported by that boundary. Changes to the parent source determine the source content available to the child. The lock does not need a redundant independent content identity for every such directory.

I prefer this model when a repository intentionally versions its components together. Independent release cycles can justify independent sources, but a directory inside one committed tree should not need an artificial distribution process solely to be referenced by another directory. The source relationship is already present in the repository.

Registry handling changes the other side of input resolution

Nix 2.26 also changes lock generation for indirect references. The release notes say system and user flake registries no longer participate in that resolution. The global registry and explicit command-line overrides still apply. This prevents local registry choices from unexpectedly becoming shared lock data.

The notes describe a concrete failure mode. A user can locally map nixpkgs to a path: reference on their machine, then commit a lock file containing that local result. Other users receive a lock whose source location belongs to the first user's environment. The file is committed, but the intended input is not necessarily available to everyone reading it.

Removing local registries from lock generation limits that ambient influence. It also complements the relative-input change. A deliberate local relationship inside a source tree has a defined representation, while an accidental machine-local override should not silently become a shared dependency.

The release recommends explicit input URLs instead of relying on registry resolution. That advice makes the source declaration easier to inspect. A reader can see which upstream reference the project requests before examining the generated lock, and a local registry is less likely to be mistaken for the project's declared source policy.

The compatibility cost belongs to the shared workflow

The new lock representation is the main adoption constraint. A repository can continue to use inputs understood by older clients, but once its lock includes relative path nodes in the new form, those clients cannot consume that lock. The release notes make this a conditional boundary tied to the lock contents.

That is different from claiming every existing flake lock becomes unreadable as soon as one developer installs 2.26. The documented limitation concerns locks for relative path inputs. The feature's use creates the new requirement, so a repository needs to treat that decision as part of its client-version expectations.

For declarative infrastructure, client compatibility belongs alongside the configuration that clients evaluate. A committed lock file is useful only when the intended evaluator can interpret it. The parent field improves how the dependency graph describes source relationships, while requiring the participating Nix clients to understand the same graph format.

Another 2.26 change addresses a related lifetime issue after transfer. nix copy gains --profile and --out-link. The notes explain that updating a profile during the copy avoids a window in which a copied path has no garbage-collection root. This connects source reproducibility to retaining the realized result long enough to use it.

The relative-input change concerns which source a flake means, while profile creation concerns which store result remains retained. Both are explicit relationships that surrounding workflows previously had to coordinate. For relative flakes, the new lock format now records the source-tree parent that makes the path meaningful across checkouts.