Skip to content

Sparse lock files WIP - #579

Open
edolstra wants to merge 10 commits into
sync-2.35from
lockfile-ng
Open

Sparse lock files WIP#579
edolstra wants to merge 10 commits into
sync-2.35from
lockfile-ng

Conversation

@edolstra

Copy link
Copy Markdown
Collaborator

Motivation

Context

In preparation for the version 8 sparse lock file format, rename
LockFile to LockFileV7 and move it, together with the Node and
LockedNode data types, to a new header lockfile-v7.hh (implementation
in lockfile-v7.cc). lockfile.hh continues to contain the common types
like InputAttrPath and NonEmptyInputAttrPath.

No behaviour change.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3816a757-b1d2-4e28-983e-178102e7bf84

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lockfile-ng

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

@github-actions
github-actions Bot temporarily deployed to pull request July 28, 2026 15:11 Inactive
edolstra added 5 commits July 28, 2026 19:36
In preparation for the version 8 sparse lock file format, turn
`LockedFlake` into an abstract base class that doesn't expose the
version 7 lock file representation, and move that representation into
a new subclass `LockedFlakeV7`. The `lockFlake()` functions now return
a `std::unique_ptr<LockedFlake>`.

Code that needs to query a locked flake now goes through virtual
methods instead of inspecting the version 7 node graph directly:

* `isUnlocked()`: check for unlocked or non-final inputs. This allows
  `getFingerprint()` to be implemented entirely in terms of the
  abstract interface.

* `toJSON()`, with non-virtual `to_string()` and `operator <<` on top
  of it.

* `getInputNames()`: return the names of the inputs of the input
  denoted by an attribute path prefix.

* `findInput()`: look up an input by attribute path, resolving
  "follows" indirections, and return an `InputInfo` struct containing
  its locked reference. Used by `--inputs-from` and
  `InstallableFlake::nixpkgsFlakeRef()`.

* `visit()`: walk all transitive inputs in depth-first order, calling
  a callback with either an `InputInfo` or the target path of a
  "follows" input. The callback controls recursion, e.g. to skip
  build-time inputs. Used by `nix flake metadata`, `nix flake archive`
  and `nix flake prefetch-inputs`.

The only remaining uses of `LockedFlakeV7` are in `lockFlake()` and
`callFlake()` in libflake itself, which will become the dispatch
points between the version 7 and version 8 implementations.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
The `computeLocks` machinery is moved from the free function
`lockFlake()` in flake.cc into a new static member function
`LockedFlakeV7::lockFlake()` in lockfile-v7.cc. The free function now
only contains version-independent work, expressed in terms of the
abstract `LockedFlake` interface:

* Reading the old lock file into a `nlohmann::json`, and constructing
  a `LockedFlake` from it (via a new `LockedFlakeV7` constructor that
  takes the JSON). This is where we will dispatch on the lock file
  version in the future.

* Change detection, by comparing the virtual `toJSON()` serializations
  of the old and new locked flakes. This also does the right thing
  across future version migrations (a version change is always a
  change).

* Printing the diff, via a new abstract `LockedFlake::diff()` method
  that shows the differences relative to an older `LockedFlake`. The
  version 7 implementation diffs against an empty lock file if the old
  lock file is not version 7, so all inputs show up as added.

* Writing/committing the new lock file, using `to_string()` and the
  virtual `isUnlocked()`.

`LockedFlakeV7::lockFlake()` takes the old lock file as a `const
LockedFlake &` and downcasts it internally. The `LockFileV7` parsing
constructor now takes a `nlohmann::json` instead of the file contents,
and `getFlake()` with a lock root attribute path is exported from
flake.cc since the lock algorithm needs it from its new home.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
This abstract method returns the source path of the input denoted by
an input attribute path (or of the top-level flake if the path is
empty), fetching the input if necessary. The returned path is backed
by `EvalState::rootFS`, i.e. it's a store path - possibly a virtual
one with the input's accessor mounted on it if lazy trees are
enabled. `nix flake prefetch-inputs` now uses this method instead of
accessing the version 7 node graph directly, so when lazy trees are
disabled, inputs are copied to the store via `EvalState::mountInput()`
(which also verifies their NAR hashes).

In `LockedFlakeV7`, the `nodePaths` map is replaced by a `mutable
Sync<std::optional<SourcePath>>` field in `LockedNode`, i.e. the
source path of a fetched node is now stored in the node itself. It is
set by `LockedFlakeV7::lockFlake()` for nodes fetched during locking,
and cached by `getSourcePath()` for nodes fetched on demand (where
relative path inputs are resolved against the source path of their
parent flake, and the `subdir` of the locked flakeref is taken into
account). `callFlake()` reconstructs the node -> source path map by
walking the node graph.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pull request July 28, 2026 18:16 Inactive
@edolstra edolstra added the flake-regression-test Run the flake regressions test suite on this PR label Jul 29, 2026
Instead of the version 7 lock file JSON and an attrset of pre-fetched
source trees ("overrides"), call-flake.nix now receives an external
value wrapping the C++ `LockedFlake` object, along with two internal
primops (registered with `internal = true`, so they're not exposed to
the user):

* `listFlakeInputs lockedFlake inputAttrPath`: returns the inputs of
  the input denoted by `inputAttrPath` as an attrset mapping input
  names to either null (for a regular input) or the input attribute
  path of the target of a "follows" input. This only consults the
  lock data, so it doesn't fetch anything.

* `fetchFlakeInput lockedFlake inputAttrPath`: fetches an input (via
  `LockedFlake::getSourcePath()`) and returns its `sourceInfo`
  attributes and flake subdirectory; or, for build-time inputs, the
  locked input attributes (without fetching).

As a result, fetching, override handling, relative path handling and
the lock file node graph all disappear from call-flake.nix - it just
lazily constructs a tree of inputs keyed by input attribute path,
resolving "follows" by walking the edges of that tree from the
top-level flake. This preserves evaluation sharing (every distinct
input is constructed only once) and prepares for the version 8 lock
file format, whose `LockedFlake` implementation will only need to
provide the same abstract interface.

Also:

* `callFlake()` now takes a `std::shared_ptr<const LockedFlake>`
  since the thunks created by call-flake.nix reference the
  `LockedFlake` object, which therefore must be kept alive for the
  lifetime of the evaluator.

* `LockedFlake::getInputTargets()` is a new abstract method backing
  `listFlakeInputs`; `getInputNames()` is now a non-virtual wrapper
  around it.

* `LockedFlakeV7::findInput()` now returns the `buildTime` and
  `parentInputAttrPath` fields (the former was dropped before, but
  wasn't used).

* The `KeyMap` returned by `LockFileV7::toJSON()` is no longer
  needed, since lock file node keys were only used by callFlake().

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pull request July 29, 2026 14:42 Inactive
edolstra added 2 commits July 29, 2026 16:49
These types are no longer used outside of lockfile-v7.cc, so they can
be implementation details. The header now only exposes two free
functions returning `std::unique_ptr<LockedFlake>`:

* `parseLockFileV7()`: construct a `LockedFlake` from the JSON
  contents of a version 5-7 lock file.

* `lockFlakeV7()` (previously `LockedFlakeV7::lockFlake()`): compute a
  new lock file.

This also keeps the version dispatch in the free function `lockFlake()`
symmetrical for the future version 8 implementation: it will just pick
between `parseLockFileV{7,8}()` and `lockFlakeV{7,8}()` based on the
`version` field in the lock file JSON.

Also remove the unused `LockedNode::computeStorePath()`.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Now that these classes are local to this file, there is no need for
separate declarations and definitions.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pull request July 29, 2026 15:06 Inactive
It's the input attribute path, relative to the top-level flake, of
the flake that *declares* the relative path (i.e. the flake that
declares the override, in the case of overridden inputs), which is
not necessarily the input's parent in the lock file graph.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pull request July 29, 2026 17:38 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flake-regression-test Run the flake regressions test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant