Improve lock file generation

This is now done in a single pass. Also fixes some issues when
updating flakes with circular dependencies. Finally, when using
'--recreate-lock-file --commit-lock-file', the commit message now
correctly shows the differences.
This commit is contained in:
Eelco Dolstra 2020-03-27 21:08:41 +01:00
parent 3fa1e7dace
commit 015f8f1c13
2 changed files with 189 additions and 191 deletions

View file

@ -306,217 +306,215 @@ LockedFlake lockFlake(
auto flake = getFlake(state, topRef, {}, lockFlags.useRegistries, flakeCache);
LockFile oldLockFile;
if (!lockFlags.recreateLockFile) {
// FIXME: symlink attack
oldLockFile = LockFile::read(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock");
}
// FIXME: symlink attack
auto oldLockFile = LockFile::read(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock");
debug("old lock file: %s", oldLockFile);
LockFile newLockFile, prevLockFile;
std::vector<InputPath> prevUnresolved;
// FIXME: check whether all overrides are used.
std::map<InputPath, FlakeInput> overrides;
for (auto & i : lockFlags.inputOverrides)
overrides.insert_or_assign(i.first, FlakeInput { .ref = i.second });
/* Compute the new lock file. This is dones as a fixpoint
iteration: we repeat until the new lock file no longer changes
and there are no unresolved "follows" inputs. */
while (true) {
std::vector<InputPath> unresolved;
LockFile newLockFile;
/* Recurse into the flake inputs. */
std::function<void(
const FlakeInputs & flakeInputs,
std::shared_ptr<const Node> oldLocks,
std::shared_ptr<Node> newLocks,
const InputPath & inputPathPrefix)>
updateLocks;
std::vector<FlakeRef> parents;
std::map<InputPath, InputPath> follows;
std::vector<FlakeRef> parents;
std::function<void(
const FlakeInputs & flakeInputs,
std::shared_ptr<Node> node,
const InputPath & inputPathPrefix,
std::shared_ptr<const Node> oldNode)>
computeLocks;
updateLocks = [&](
const FlakeInputs & flakeInputs,
std::shared_ptr<const Node> oldLocks,
std::shared_ptr<Node> newLocks,
const InputPath & inputPathPrefix)
{
/* Get the overrides (i.e. attributes of the form
'inputs.nixops.inputs.nixpkgs.url = ...'). */
for (auto & [id, input] : flake.inputs) {
for (auto & [idOverride, inputOverride] : input.overrides) {
auto inputPath(inputPathPrefix);
inputPath.push_back(id);
inputPath.push_back(idOverride);
overrides.insert_or_assign(inputPath, inputOverride);
}
}
computeLocks = [&](
const FlakeInputs & flakeInputs,
std::shared_ptr<Node> node,
const InputPath & inputPathPrefix,
std::shared_ptr<const Node> oldNode)
{
debug("computing lock file node '%s'", concatStringsSep("/", inputPathPrefix));
/* Go over the flake inputs, resolve/fetch them if
necessary (i.e. if they're new or the flakeref changed
from what's in the lock file). */
for (auto & [id, input2] : flakeInputs) {
/* Get the overrides (i.e. attributes of the form
'inputs.nixops.inputs.nixpkgs.url = ...'). */
// FIXME: check this
for (auto & [id, input] : flake.inputs) {
for (auto & [idOverride, inputOverride] : input.overrides) {
auto inputPath(inputPathPrefix);
inputPath.push_back(id);
auto inputPathS = concatStringsSep("/", inputPath);
/* Do we have an override for this input from one of
the ancestors? */
auto i = overrides.find(inputPath);
bool hasOverride = i != overrides.end();
auto & input = hasOverride ? i->second : input2;
if (input.follows) {
/* This is a "follows" input
(i.e. 'inputs.nixpkgs.follows =
"dwarffs/nixpkgs"). Resolve the source and copy
its inputs. Note that the source is normally
relative to the current node of the lock file
(e.g. "dwarffs/nixpkgs" refers to the nixpkgs
input of the dwarffs input of the root flake),
but if it's from an override, it's relative to
the *root* of the lock file. */
auto follows = (hasOverride ? newLockFile.root : newLocks)->findInput(*input.follows);
if (follows)
newLocks->inputs.insert_or_assign(id, follows);
else
/* We haven't processed the source of the
"follows" yet (e.g. "dwarffs/nixpkgs"). So
we'll need another round of the fixpoint
iteration. */
// FIXME: now that LockFile is a graph, we
// could pre-create the missing node.
unresolved.push_back(inputPath);
continue;
}
/* Do we have an entry in the existing lock file? And
we don't have a --update-input flag for this
input? */
auto oldLockIt =
lockFlags.inputUpdates.count(inputPath)
? oldLocks->inputs.end()
: oldLocks->inputs.find(id);
std::shared_ptr<const LockedNode> oldLock;
if (oldLockIt != oldLocks->inputs.end()) {
oldLock = std::dynamic_pointer_cast<const LockedNode>(oldLockIt->second);
assert(oldLock);
}
if (oldLock
&& oldLock->originalRef == input.ref
&& !hasOverride)
{
/* Copy the input from the old lock file if its
flakeref didn't change and there is no override
from a higher level flake. */
newLocks->inputs.insert_or_assign(id, std::make_shared<LockedNode>(*oldLock));
/* If we have an --update-input flag for an input
of this input, then we must fetch the flake to
to update it. */
auto lb = lockFlags.inputUpdates.lower_bound(inputPath);
auto hasChildUpdate =
lb != lockFlags.inputUpdates.end()
&& lb->size() > inputPath.size()
&& std::equal(inputPath.begin(), inputPath.end(), lb->begin());
if (hasChildUpdate) {
auto inputFlake = getFlake(
state, oldLock->lockedRef, oldLock->info, false, flakeCache);
updateLocks(inputFlake.inputs,
oldLock,
newLocks->inputs.find(id)->second,
inputPath);
} else {
/* No need to fetch this flake, we can be
lazy. However there may be new overrides on
the inputs of this flake, so we need to
check those. */
FlakeInputs fakeInputs;
for (auto & i : oldLock->inputs)
fakeInputs.emplace(i.first, FlakeInput {
.ref = std::dynamic_pointer_cast<LockedNode>(i.second)->originalRef
});
updateLocks(fakeInputs,
oldLock,
newLocks->inputs.find(id)->second,
inputPath);
}
} else {
/* We need to update/create a new lock file
entry. So fetch the flake/non-flake. */
if (!lockFlags.allowMutable && !input.ref.input->isImmutable())
throw Error("cannot update flake input '%s' in pure mode", inputPathS);
if (input.isFlake) {
auto inputFlake = getFlake(state, input.ref, {}, lockFlags.useRegistries, flakeCache);
newLocks->inputs.insert_or_assign(id,
std::make_shared<LockedNode>(inputFlake.lockedRef, inputFlake.originalRef, inputFlake.sourceInfo->info));
/* Recursively process the inputs of this
flake. Also, unless we already have this
flake in the top-level lock file, use this
flake's own lock file. */
/* Guard against circular flake imports. */
for (auto & parent : parents)
if (parent == input.ref)
throw Error("found circular import of flake '%s'", parent);
parents.push_back(input.ref);
Finally cleanup([&]() { parents.pop_back(); });
updateLocks(inputFlake.inputs,
oldLock
? std::dynamic_pointer_cast<const Node>(oldLock)
: LockFile::read(
inputFlake.sourceInfo->actualPath + "/" + inputFlake.lockedRef.subdir + "/flake.lock").root,
newLocks->inputs.find(id)->second,
inputPath);
}
else {
auto [sourceInfo, lockedRef] = fetchOrSubstituteTree(
state, input.ref, {}, lockFlags.useRegistries, flakeCache);
newLocks->inputs.insert_or_assign(id,
std::make_shared<LockedNode>(lockedRef, input.ref, sourceInfo.info, false));
}
}
inputPath.push_back(idOverride);
overrides.insert_or_assign(inputPath, inputOverride);
}
};
updateLocks(flake.inputs, oldLockFile.root, newLockFile.root, {});
/* Check if there is a cycle in the "follows" inputs. */
if (!unresolved.empty() && unresolved == prevUnresolved) {
std::vector<std::string> ss;
for (auto & i : unresolved)
ss.push_back(concatStringsSep("/", i));
throw Error("cycle or missing input detected in flake inputs: %s", concatStringsSep(", ", ss));
}
std::swap(unresolved, prevUnresolved);
/* Go over the flake inputs, resolve/fetch them if
necessary (i.e. if they're new or the flakeref changed
from what's in the lock file). */
for (auto & [id, input2] : flakeInputs) {
auto inputPath(inputPathPrefix);
inputPath.push_back(id);
auto inputPathS = concatStringsSep("/", inputPath);
debug("computing input '%s'", concatStringsSep("/", inputPath));
/* Done with the fixpoint iteration? */
if (newLockFile == prevLockFile) break;
prevLockFile = newLockFile;
/* Do we have an override for this input from one of the
ancestors? */
auto i = overrides.find(inputPath);
bool hasOverride = i != overrides.end();
auto & input = hasOverride ? i->second : input2;
/* Resolve 'follows' later (since it may refer to an input
path we haven't processed yet. */
if (input.follows) {
if (hasOverride)
/* 'follows' from an override is relative to the
root of the graph. */
follows.insert_or_assign(inputPath, *input.follows);
else {
/* Otherwise, it's relative to the current flake. */
InputPath path(inputPathPrefix);
for (auto & i : *input.follows) path.push_back(i);
follows.insert_or_assign(inputPath, path);
}
continue;
}
/* Do we have an entry in the existing lock file? And we
don't have a --update-input flag for this input? */
std::shared_ptr<const LockedNode> oldLock;
if (oldNode && !lockFlags.inputUpdates.count(inputPath)) {
auto oldLockIt = oldNode->inputs.find(id);
if (oldLockIt != oldNode->inputs.end())
oldLock = std::dynamic_pointer_cast<const LockedNode>(oldLockIt->second);
}
if (oldLock
&& oldLock->originalRef == input.ref
&& !hasOverride)
{
debug("keeping existing input '%s'", inputPathS);
/* Copy the input from the old lock since its flakeref
didn't change and there is no override from a
higher level flake. */
auto childNode = std::make_shared<LockedNode>(
oldLock->lockedRef, oldLock->originalRef, oldLock->info, oldLock->isFlake);
node->inputs.insert_or_assign(id, childNode);
/* If we have an --update-input flag for an input
of this input, then we must fetch the flake to
to update it. */
auto lb = lockFlags.inputUpdates.lower_bound(inputPath);
auto hasChildUpdate =
lb != lockFlags.inputUpdates.end()
&& lb->size() > inputPath.size()
&& std::equal(inputPath.begin(), inputPath.end(), lb->begin());
if (hasChildUpdate) {
auto inputFlake = getFlake(
state, oldLock->lockedRef, oldLock->info, false, flakeCache);
computeLocks(inputFlake.inputs, childNode, inputPath, oldLock);
} else {
/* No need to fetch this flake, we can be
lazy. However there may be new overrides on the
inputs of this flake, so we need to check
those. */
FlakeInputs fakeInputs;
for (auto & i : oldLock->inputs) {
auto lockedNode = std::dynamic_pointer_cast<LockedNode>(i.second);
// Note: this node is not locked in case
// of a circular reference back to the root.
if (lockedNode)
fakeInputs.emplace(i.first, FlakeInput {
.ref = lockedNode->originalRef
});
else {
InputPath path(inputPath);
path.push_back(i.first);
follows.insert_or_assign(path, InputPath());
}
}
computeLocks(fakeInputs, childNode, inputPath, oldLock);
}
} else {
/* We need to create a new lock file entry. So fetch
this input. */
if (!lockFlags.allowMutable && !input.ref.input->isImmutable())
throw Error("cannot update flake input '%s' in pure mode", inputPathS);
if (input.isFlake) {
auto inputFlake = getFlake(state, input.ref, {}, lockFlags.useRegistries, flakeCache);
auto childNode = std::make_shared<LockedNode>(
inputFlake.lockedRef, inputFlake.originalRef, inputFlake.sourceInfo->info);
node->inputs.insert_or_assign(id, childNode);
/* Guard against circular flake imports. */
for (auto & parent : parents)
if (parent == input.ref)
throw Error("found circular import of flake '%s'", parent);
parents.push_back(input.ref);
Finally cleanup([&]() { parents.pop_back(); });
/* Recursively process the inputs of this
flake. Also, unless we already have this flake
in the top-level lock file, use this flake's
own lock file. */
computeLocks(
inputFlake.inputs, childNode, inputPath,
oldLock
? std::dynamic_pointer_cast<const Node>(oldLock)
: LockFile::read(
inputFlake.sourceInfo->actualPath + "/" + inputFlake.lockedRef.subdir + "/flake.lock").root);
}
else {
auto [sourceInfo, lockedRef] = fetchOrSubstituteTree(
state, input.ref, {}, lockFlags.useRegistries, flakeCache);
node->inputs.insert_or_assign(id,
std::make_shared<LockedNode>(lockedRef, input.ref, sourceInfo.info, false));
}
}
}
};
computeLocks(
flake.inputs, newLockFile.root, {},
lockFlags.recreateLockFile ? nullptr : oldLockFile.root);
/* Insert edges for 'follows' overrides. */
for (auto & [from, to] : follows) {
debug("adding 'follows' node from '%s' to '%s'",
concatStringsSep("/", from),
concatStringsSep("/", to));
assert(!from.empty());
InputPath fromParent(from);
fromParent.pop_back();
auto fromParentNode = newLockFile.root->findInput(fromParent);
assert(fromParentNode);
auto toNode = newLockFile.root->findInput(to);
if (!toNode)
throw Error("flake input '%s' follows non-existent flake input '%s'",
concatStringsSep("/", from),
concatStringsSep("/", to));
fromParentNode->inputs.insert_or_assign(from.back(), toNode);
}
debug("new lock file: %s", newLockFile);
/* Check whether we need to / can write the new lock file. */

View file

@ -518,7 +518,7 @@ cat > $flake3Dir/flake.nix <<EOF
EOF
nix flake update $flake3Dir
[[ $(jq .nodes.foo.locked $flake3Dir/flake.lock) = $(jq .nodes.bar.locked $flake3Dir/flake.lock) ]]
[[ $(jq .nodes.root.inputs.foo $flake3Dir/flake.lock) = $(jq .nodes.root.inputs.bar $flake3Dir/flake.lock) ]]
cat > $flake3Dir/flake.nix <<EOF
{