Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1ae081e
Rename LockFile to LockFileV7 and move it to lockfile-v7.hh
edolstra Jul 27, 2026
a23b4cc
maintainers/format.sh: Allow running just one hook
edolstra Jul 27, 2026
938e69b
Rename lockfile.{cc,hh} -> input-attr-path.{cc,hh}
edolstra Jul 28, 2026
2c02256
Start making LockedFlake an abstract class
edolstra Jul 28, 2026
05cf555
Move the version 7 lock algorithm out of the free lockFlake() function
edolstra Jul 28, 2026
7377422
Add LockedFlake::getSourcePath()
edolstra Jul 28, 2026
b06972a
Make call-flake.nix independent of the lock file format
edolstra Jul 29, 2026
16335ed
Move Node/LockedNode/LockFileV7/LockedFlakeV7 into lockfile-v7.cc
edolstra Jul 29, 2026
472b0f5
lockfile-v7.cc: Move method definitions into the classes
edolstra Jul 29, 2026
eb49bf0
Document the semantics of parentInputAttrPath
edolstra Jul 29, 2026
5606148
Make LockFlags::inputUpdates an optional set
edolstra Aug 1, 2026
ab8592c
libflake: Add lock-file-format setting
edolstra Aug 3, 2026
c74de77
Move unused override/update warnings from lockFlakeV7() into lockFlake()
edolstra Aug 3, 2026
e767770
libflake: Replace the per-version diff() methods with a generic diffL…
edolstra Aug 3, 2026
cb3daf5
Add 'nix flake diff-locks' command
edolstra Aug 3, 2026
6ae378c
libflake: Factor out the warnRegistry() function
edolstra Aug 3, 2026
51da5d1
Make LockedFlake::visit() non-virtual
edolstra Aug 3, 2026
20f20a6
LockedFlake: Require fully resolved paths in the query methods
edolstra Aug 3, 2026
1678bd4
Add an EvalState parameter to the LockedFlake query methods
edolstra Aug 3, 2026
98b0c43
LockedFlake::visit(): Work around a GCC internal compiler error
edolstra Aug 3, 2026
fbc2f7b
libflake: Add the version 8 (sparse) lock file data model
edolstra Aug 3, 2026
d050b86
libflake: Implement the version 8 lock algorithm
edolstra Aug 3, 2026
4061fb1
lockFlake(): Dispatch on the lock file version
edolstra Aug 3, 2026
9f747f8
lockfile-v8.cc: Clarify comment terminology
edolstra Aug 3, 2026
431b73d
libflake: Implement evaluation-time resolution for version 8 lock files
edolstra Aug 3, 2026
7c555a9
Add the lock-file-v8 experimental feature
edolstra Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Observe the following rules when contributing to this repository:

* Before committing, run ./maintainers/format.sh to detect/fix any formatting issues.
* Before committing, run `./maintainers/format.sh` to detect/fix any formatting issues. If you've only touched C++ files, run `run ./maintainers/format.sh clang-format` since it's a lot faster.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CLAUDE.md ---'
cat -n CLAUDE.md | sed -n '1,12p'

printf '%s\n' '--- maintainer files ---'
fd -t f 'format\.sh$|flake\.nix$|shell\.nix$|pre-commit' . | sort

printf '%s\n' '--- format.sh ---'
format_script="$(fd -t f '^format\.sh$' . | head -n 1)"
cat -n "$format_script"

printf '%s\n' '--- related references ---'
rg -n --hidden -g '!node_modules' '_NIX_PRE_COMMIT_HOOKS_CONFIG|nix develop|format\.sh|clang-format' .

Repository: DeterminateSystems/nix-src

Length of output: 13601


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CI invocation ---'
cat -n ci/gha/tests/pre-commit-checks | sed -n '1,28p'

printf '%s\n' '--- development-shell environment ---'
cat -n packaging/dev-shell.nix | sed -n '250,275p'

printf '%s\n' '--- nix develop command documentation ---'
cat -n src/nix/develop.md | sed -n '68,82p'

printf '%s\n' '--- command-token verifier ---'
python3 - <<'PY'
import shlex

commands = [
    "./maintainers/format.sh",
    "run ./maintainers/format.sh clang-format",
    "nix develop -c ./maintainers/format.sh",
    "nix develop -c ./maintainers/format.sh clang-format",
]

for command in commands:
    print(f"{command!r} -> {shlex.split(command)!r}")
PY

Repository: DeterminateSystems/nix-src

Length of output: 2831


Use executable formatter commands.

From a normal shell, document nix develop -c ./maintainers/format.sh and nix develop -c ./maintainers/format.sh clang-format. Remove the extra run token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 3, Update the formatter command guidance in CLAUDE.md to
use executable commands through the Nix development shell: document `nix develop
-c ./maintainers/format.sh` generally and `nix develop -c
./maintainers/format.sh clang-format` for C++-only changes, removing the extra
`run` token.

Source: Learnings


* Use "Assisted-by:" instead of "Co-Authored-By:" for the Claude trailer in commits.
9 changes: 8 additions & 1 deletion maintainers/format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ if test -z "$_NIX_PRE_COMMIT_HOOKS_CONFIG"; then
exit 1;
fi;

while ! pre-commit run --config "$_NIX_PRE_COMMIT_HOOKS_CONFIG" --all-files; do
# The argument is either `--until-stable` or the ID of the single
# hook to run.
hook=""
if [ "${1:-}" != "--until-stable" ]; then
hook="${1:-}"
fi

while ! pre-commit run --config "$_NIX_PRE_COMMIT_HOOKS_CONFIG" --all-files ${hook:+"$hook"}; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- maintainers/format.sh ---'
cat -n maintainers/format.sh
printf '%s\n' '--- related references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'until-stable|pre-commit run|format\.sh' .

Repository: DeterminateSystems/nix-src

Length of output: 2403


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path("maintainers/format.sh")
text = p.read_text()
print("has_until_stable=", "--until-stable" in text)
for i, line in enumerate(text.splitlines(), 1):
    if 15 <= i <= 25:
        print(f"{i}: {line}")
PY

Repository: DeterminateSystems/nix-src

Length of output: 457


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
lines = Path("maintainers/format.sh").read_text().splitlines()
for n, line in enumerate(lines, 1):
    if 1 <= n <= 40:
        print(f"{n}: {line}")
PY

Repository: DeterminateSystems/nix-src

Length of output: 923


Stop retrying persistent pre-commit failures forever.

With --until-stable, every non-zero pre-commit result triggers another run. An unchanged worktree or configuration error causes an infinite loop. Retry only when the failed run changes files; otherwise exit with the failure status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@maintainers/format.sh` at line 19, Update the retry loop around the
pre-commit invocation in the format script to record the worktree state before
each run and retry only if the failed run modifies files. When the run fails
without changing the worktree, exit with its failure status instead of looping
indefinitely; preserve the existing until-stable behavior for failures that do
change files.

if [ "${1:-}" != "--until-stable" ]; then
exit 1
fi
Expand Down
12 changes: 6 additions & 6 deletions src/libcmd/flake-schemas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace nix::flake_schemas {
using namespace eval_cache;
using namespace flake;

static LockedFlake getBuiltinDefaultSchemasFlake(EvalState & state)
static std::unique_ptr<LockedFlake> getBuiltinDefaultSchemasFlake(EvalState & state)
{
auto accessor = make_ref<MemorySourceAccessor>();

Expand Down Expand Up @@ -49,11 +49,11 @@ ref<EvalCache> call(
#include "call-flake-schemas.nix.gen.hh"
;

auto lockedDefaultSchemasFlake = defaultSchemasFlake
? flake::lockFlake(flakeSettings, state, *defaultSchemasFlake, {})
: getBuiltinDefaultSchemasFlake(state);
std::shared_ptr<LockedFlake> lockedDefaultSchemasFlake =
defaultSchemasFlake ? flake::lockFlake(flakeSettings, state, *defaultSchemasFlake, {})
: getBuiltinDefaultSchemasFlake(state);
auto lockedDefaultSchemasFlakeFingerprint =
lockedDefaultSchemasFlake.getFingerprint(*state.store, state.fetchSettings);
lockedDefaultSchemasFlake->getFingerprint(*state.store, state.fetchSettings);

std::optional<Fingerprint> fingerprint2;
if (allowEvalCache && evalSettings.useEvalCache && evalSettings.pureEval && fingerprint
Expand All @@ -78,7 +78,7 @@ ref<EvalCache> call(
state.parseExprFromString(callFlakeSchemasNix, state.rootPath(CanonPath::root)), *vCallFlakeSchemas);

auto vFlake = state.allocValue();
flake::callFlake(state, *lockedFlake, *vFlake);
flake::callFlake(state, lockedFlake, *vFlake);

auto vDefaultSchemasFlake = state.allocValue();
if (vFlake->type() == nAttrs && vFlake->attrs()->get(state.symbols.create("schemas")))
Expand Down
1 change: 0 additions & 1 deletion src/libcmd/include/nix/cmd/command.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include "nix/cmd/common-eval-args.hh"
#include "nix/store/path.hh"
#include "nix/store/store-reference.hh"
#include "nix/flake/lockfile.hh"

#include <optional>

Expand Down
13 changes: 6 additions & 7 deletions src/libcmd/installable-flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ ref<flake::LockedFlake> InstallableFlake::getLockedFlake() const
flake::LockFlags lockFlagsApplyConfig = lockFlags;
// FIXME why this side effect?
lockFlagsApplyConfig.applyNixConfig = true;
_lockedFlake = make_ref<flake::LockedFlake>(lockFlake(flakeSettings, *state, flakeRef, lockFlagsApplyConfig));
_lockedFlake =
std::shared_ptr<flake::LockedFlake>(lockFlake(flakeSettings, *state, flakeRef, lockFlagsApplyConfig));
}
// _lockedFlake is now non-null but still just a shared_ptr
return ref<flake::LockedFlake>(_lockedFlake);
Expand All @@ -328,12 +329,10 @@ FlakeRef InstallableFlake::nixpkgsFlakeRef() const
{
auto lockedFlake = getLockedFlake();

if (auto nixpkgsInput = lockedFlake->lockFile.findInput({"nixpkgs"})) {
if (auto lockedNode = std::dynamic_pointer_cast<const flake::LockedNode>(nixpkgsInput)) {
if (lockedNode->isFlake) {
debug("using nixpkgs flake '%s'", lockedNode->lockedRef);
return std::move(lockedNode->lockedRef);
}
if (auto nixpkgsInput = lockedFlake->findInput(*state, lockedFlake->resolveFollows(*state, {"nixpkgs"}))) {
if (nixpkgsInput->isFlake) {
debug("using nixpkgs flake '%s'", nixpkgsInput->lockedRef);
return std::move(nixpkgsInput->lockedRef);
}
}

Expand Down
31 changes: 11 additions & 20 deletions src/libcmd/installables.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,9 @@ MixFlakeOptions::MixFlakeOptions()

addFlag({
.longName = "recreate-lock-file",
.description = R"(
Recreate the flake's lock file from scratch.

> **DEPRECATED**
>
> Use [`nix flake update`](@docroot@/command-ref/new-cli/nix3-flake-update.md) instead.
)",
.description = "Recreate the flake's lock file from scratch.",
.category = category,
.handler = {[&]() {
lockFlags.recreateLockFile = true;
warn(
"'--recreate-lock-file' is deprecated and will be removed in a future version; use 'nix flake update' instead.");
}},
.handler = {&lockFlags.recreateLockFile, true},
});

addFlag({
Expand Down Expand Up @@ -117,7 +107,7 @@ MixFlakeOptions::MixFlakeOptions()
if (!path)
throw UsageError(
"--update-input was passed a zero-length input path, which would refer to the flake itself, not an input");
lockFlags.inputUpdates.insert(*path);
lockFlags.inputUpdates->insert(*path);
}},
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
completeFlakeInputAttrPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix);
Expand Down Expand Up @@ -175,23 +165,24 @@ MixFlakeOptions::MixFlakeOptions()
.labels = {"flake-url"},
.handler = {[&](std::string flakeRef) {
auto evalState = getEvalState();
auto flake = flake::lockFlake(
auto lockedFlake = flake::lockFlake(
flakeSettings,
*evalState,
parseFlakeRef(fetchSettings, flakeRef, absPath(getCommandBaseDir()).string()),
{.writeLockFile = false});
for (auto & [inputName, input] : flake.lockFile.root->inputs) {
auto input2 = flake.lockFile.findInput({inputName}); // resolve 'follows' nodes
if (auto input3 = std::dynamic_pointer_cast<const flake::LockedNode>(input2)) {

for (auto & inputName : lockedFlake->getInputNames(*evalState, {})) {
if (auto input =
lockedFlake->findInput(*evalState, lockedFlake->resolveFollows(*evalState, {inputName}))) {
fetchers::Attrs extraAttrs;

if (!input3->lockedRef.subdir.empty()) {
extraAttrs["dir"] = input3->lockedRef.subdir;
if (!input->lockedRef.subdir.empty()) {
extraAttrs["dir"] = input->lockedRef.subdir;
}

overrideRegistry(
fetchers::Input::fromAttrs(fetchSettings, {{"type", "indirect"}, {"id", inputName}}),
input3->lockedRef.input,
input->lockedRef.input,
extraAttrs);
}
}
Expand Down
1 change: 0 additions & 1 deletion src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
#include "nix/store/outputs-query.hh"
#include "nix/store/globals.hh"
#include "nix/flake/flake.hh"
#include "nix/flake/lockfile.hh"
#include "nix/util/users.hh"
#include "nix/cmd/editor-for.hh"
#include "nix/util/finally.hh"
Expand Down
9 changes: 5 additions & 4 deletions src/libflake-c/nix_api_flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,10 @@ nix_locked_flake * nix_flake_lock(
nix_clear_err(context);
try {
eval_state->state.resetFileCache();
auto lockedFlake = nix::make_ref<nix::flake::LockedFlake>(nix::flake::lockFlake(
*flakeSettings->settings, eval_state->state, *flakeReference->flakeRef, *flags->lockFlags));
return new nix_locked_flake{lockedFlake};
std::shared_ptr<nix::flake::LockedFlake> lockedFlake(
nix::flake::lockFlake(
*flakeSettings->settings, eval_state->state, *flakeReference->flakeRef, *flags->lockFlags));
return new nix_locked_flake{nix::ref(lockedFlake)};
}
NIXC_CATCH_ERRS_NULL
}
Expand All @@ -203,7 +204,7 @@ nix_value * nix_locked_flake_get_output_attrs(
nix_clear_err(context);
try {
auto v = nix_alloc_value(context, evalState);
nix::flake::callFlake(evalState->state, *lockedFlake->lockedFlake, *v->value);
nix::flake::callFlake(evalState->state, lockedFlake->lockedFlake.get_ptr(), *v->value);
return v;
}
NIXC_CATCH_ERRS_NULL
Expand Down
112 changes: 51 additions & 61 deletions src/libflake/call-flake.nix
Original file line number Diff line number Diff line change
@@ -1,79 +1,64 @@
# This is a helper to callFlake() to lazily fetch flake inputs.

# The contents of the lock file, in JSON format.
lockFileStr:
# An external value wrapping the C++ `LockedFlake` object.
lockedFlake:

# A mapping of lock file node IDs to { sourceInfo, subdir } attrsets,
# with sourceInfo.outPath providing an SourceAccessor to a previously
# fetched tree. This is necessary for possibly unlocked inputs, in
# particular the root input, but also --override-inputs pointing to
# unlocked trees.
overrides:
# A primop that, given the locked flake and the input attribute path
# of an input, returns an attribute set mapping the names of its
# inputs to either null (for a regular input) or the input attribute
# path of the target of a "follows" input.
listFlakeInputs:

# A primop that, given the locked flake and the input attribute path
# of an input, fetches that input and returns an attribute set
# describing it.
fetchFlakeInput:

let
inherit (builtins) mapAttrs;

lockFile = builtins.fromJSON lockFileStr;

# Resolve a input spec into a node name. An input spec is
# either a node name, or a 'follows' path from the root
# node.
resolveInput =
inputSpec: if builtins.isList inputSpec then getInputByPath lockFile.root inputSpec else inputSpec;

# Follow an input attrpath (e.g. ["dwarffs" "nixpkgs"]) from the
# root node, returning the final node.
getInputByPath =
nodeName: path:
if path == [ ] then
nodeName
else
getInputByPath
# Since this could be a 'follows' input, call resolveInput.
(resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path})
(builtins.tail path);

allNodes = mapAttrs (
key: node:
inherit (builtins) mapAttrs foldl';

# Construct the input denoted by the input attribute path
# `inputAttrPath` (where `[ ]` denotes the top-level flake). This returns `edges`
# (mapping each input name of this input to the input it denotes,
# following "follows" indirections) and `result` (the value of this
# input, i.e. what ends up in the `inputs` attribute of a flake).
mkInput =
inputAttrPath:
let
hasOverride = overrides ? ${key};
isRelative = node.locked.type or null == "path" && builtins.substring 0 1 node.locked.path != "/";

parentNode = allNodes.${getInputByPath lockFile.root node.parent};
info = fetchFlakeInput lockedFlake inputAttrPath;

sourceInfo =
if node.buildTime or false then
if info.buildTime then
derivation {
name = "source";
builder = "builtin:fetch-tree";
system = "builtin";
__structuredAttrs = true;
input = node.locked;
input = info.locked;
outputHashMode = "recursive";
outputHash = node.locked.narHash;
outputHash = info.locked.narHash;
}
else if hasOverride then
overrides.${key}.sourceInfo
else if isRelative then
parentNode.sourceInfo
else
# FIXME: remove obsolete node.info.
# Note: lock file entries are always final.
builtins.fetchTree (node.info or { } // removeAttrs node.locked [ "dir" ]);
info.sourceInfo;

subdir = overrides.${key}.dir or node.locked.dir or "";
subdir = if info.buildTime then info.locked.dir or "" else info.dir;

outPath =
if !hasOverride && isRelative then
parentNode.outPath + (if node.locked.path == "" then "" else "/" + node.locked.path)
else
sourceInfo.outPath + (if subdir == "" then "" else "/" + subdir);
outPath = sourceInfo.outPath + (if subdir == "" then "" else "/" + subdir);

flake = import (outPath + "/flake.nix");

inputs = mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}.result) (
node.inputs or { }
);
# Note: constructing `edges` only consults the lock data (via
# `listFlakeInputs`), so it never causes anything to be
# fetched. A regular input is constructed in place; a "follows"
# input is resolved by walking the edges from the top-level
# flake, so every distinct input is constructed (and evaluated)
# only once.
edges = mapAttrs (
name: target:
if target == null then mkInput (inputAttrPath ++ [ name ]) else getInputByAttrPath target
) (listFlakeInputs lockedFlake inputAttrPath);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

inputs = mapAttrs (name: input: input.result) edges;

outputs = flake.outputs (inputs // { self = result; });

Expand All @@ -97,17 +82,22 @@ let

in
{
inherit edges;

result =
if node.flake or true then
if info.flake then
assert builtins.isFunction flake.outputs;
assert !(node.buildTime or false);
assert !info.buildTime;
result
else
sourceInfo // { inherit sourceInfo outPath; };
};

# Follow an input attribute path (e.g. ["dwarffs" "nixpkgs"]) from
# the top-level flake, returning the final input.
getInputByAttrPath = inputAttrPath: foldl' (input: name: input.edges.${name}) root inputAttrPath;

inherit outPath sourceInfo;
}
) lockFile.nodes;
root = mkInput [ ];

in
allNodes.${lockFile.root}.result
root.result
Loading
Loading