Improve subflake handling

Relative 'path:' inputs are now handled correctly in
call-flake.nix. This does require the lock file to record what the
'parent' is relative to which a node's source should be fetched.
This commit is contained in:
Eelco Dolstra 2022-06-30 14:22:35 +02:00
parent 0d14ffbcba
commit 5c2603d9c7
4 changed files with 107 additions and 53 deletions

View file

@ -4,24 +4,6 @@ let
lockFile = builtins.fromJSON lockFileStr; lockFile = builtins.fromJSON lockFileStr;
allNodes =
builtins.mapAttrs
(key: node:
let
sourceInfo =
if key == lockFile.root
then rootSrc
else fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
subdir = if key == lockFile.root then rootSubdir else node.locked.dir or "";
flake = import (sourceInfo + (if subdir != "" then "/" else "") + subdir + "/flake.nix");
inputs = builtins.mapAttrs
(inputName: inputSpec: allNodes.${resolveInput inputSpec})
(node.inputs or {});
# Resolve a input spec into a node name. An input spec is # Resolve a input spec into a node name. An input spec is
# either a node name, or a 'follows' path from the root # either a node name, or a 'follows' path from the root
# node. # node.
@ -41,6 +23,34 @@ let
(resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path}) (resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path})
(builtins.tail path); (builtins.tail path);
allNodes =
builtins.mapAttrs
(key: node:
let
sourceInfo =
if key == lockFile.root
then rootSrc
else if node.locked.type == "path" && builtins.substring 0 1 node.locked.path != "/"
then
let
parentNode = allNodes.${getInputByPath lockFile.root node.parent};
in parentNode.sourceInfo // {
outPath = parentNode.sourceInfo.outPath + ("/" + node.locked.path);
}
else
# FIXME: remove obsolete node.info.
fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
subdir = if key == lockFile.root then rootSubdir else node.locked.dir or "";
flake =
import (sourceInfo.outPath + ((if subdir != "" then "/" else "") + subdir + "/flake.nix"));
inputs = builtins.mapAttrs
(inputName: inputSpec: allNodes.${resolveInput inputSpec})
(node.inputs or {});
outputs = flake.outputs (inputs // { self = result; }); outputs = flake.outputs (inputs // { self = result; });
result = outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; }; result = outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; };

View file

@ -307,11 +307,16 @@ LockedFlake lockFlake(
debug("old lock file: %s", oldLockFile); debug("old lock file: %s", oldLockFile);
// FIXME: check whether all overrides are used. // FIXME: check whether all overrides are used.
std::map<InputPath, FlakeInput> overrides; std::map<InputPath, std::tuple<FlakeInput, SourcePath, std::optional<InputPath>>> overrides;
std::set<InputPath> overridesUsed, updatesUsed; std::set<InputPath> overridesUsed, updatesUsed;
for (auto & i : lockFlags.inputOverrides) for (auto & i : lockFlags.inputOverrides)
overrides.insert_or_assign(i.first, FlakeInput { .ref = i.second }); overrides.emplace(
i.first,
std::make_tuple(
FlakeInput { .ref = i.second },
state.rootPath("/"),
std::nullopt));
LockFile newLockFile; LockFile newLockFile;
@ -322,18 +327,29 @@ LockedFlake lockFlake(
std::shared_ptr<Node> node, std::shared_ptr<Node> node,
const InputPath & inputPathPrefix, const InputPath & inputPathPrefix,
std::shared_ptr<const Node> oldNode, std::shared_ptr<const Node> oldNode,
const InputPath & lockRootPath, const InputPath & followsPrefix,
const SourcePath & parentPath, const SourcePath & sourcePath,
bool trustLock)> bool trustLock)>
computeLocks; computeLocks;
computeLocks = [&]( computeLocks = [&](
/* The inputs of this node, either from flake.nix or
flake.lock */
const FlakeInputs & flakeInputs, const FlakeInputs & flakeInputs,
/* The node whose locks are to be updated.*/
std::shared_ptr<Node> node, std::shared_ptr<Node> node,
/* The path to this node in the lock file graph. */
const InputPath & inputPathPrefix, const InputPath & inputPathPrefix,
/* The old node, if any, from which locks can be
copied. */
std::shared_ptr<const Node> oldNode, std::shared_ptr<const Node> oldNode,
const InputPath & lockRootPath, /* The prefix relative to which 'follows' should be
const SourcePath & parentPath, interpreted. When a node is initially locked, it's
relative to the node's flake; when it's already locked,
it's relative to the root of the lock file. */
const InputPath & followsPrefix,
/* The source path of this node's flake. */
const SourcePath & sourcePath,
bool trustLock) bool trustLock)
{ {
debug("computing lock file node '%s'", printInputPath(inputPathPrefix)); debug("computing lock file node '%s'", printInputPath(inputPathPrefix));
@ -345,7 +361,8 @@ LockedFlake lockFlake(
auto inputPath(inputPathPrefix); auto inputPath(inputPathPrefix);
inputPath.push_back(id); inputPath.push_back(id);
inputPath.push_back(idOverride); inputPath.push_back(idOverride);
overrides.insert_or_assign(inputPath, inputOverride); overrides.emplace(inputPath,
std::make_tuple(inputOverride, sourcePath, inputPathPrefix));
} }
} }
@ -364,13 +381,18 @@ LockedFlake lockFlake(
ancestors? */ ancestors? */
auto i = overrides.find(inputPath); auto i = overrides.find(inputPath);
bool hasOverride = i != overrides.end(); bool hasOverride = i != overrides.end();
if (hasOverride) { if (hasOverride)
overridesUsed.insert(inputPath); overridesUsed.insert(inputPath);
// Respect the “flakeness” of the input even if we auto input = hasOverride ? std::get<0>(i->second) : input2;
// override it
i->second.isFlake = input2.isFlake; /* Resolve relative 'path:' inputs relative to
} the source path of the overrider. */
auto & input = hasOverride ? i->second : input2; auto overridenSourcePath = hasOverride ? std::get<1>(i->second) : sourcePath;
/* Respect the "flakeness" of the input even if we
override it. */
if (hasOverride)
input.isFlake = input2.isFlake;
/* Resolve 'follows' later (since it may refer to an input /* Resolve 'follows' later (since it may refer to an input
path we haven't processed yet. */ path we haven't processed yet. */
@ -386,17 +408,20 @@ LockedFlake lockFlake(
assert(input.ref); assert(input.ref);
auto overridenParentPath =
input.ref->input.isRelative()
? std::optional<InputPath>(hasOverride ? std::get<2>(i->second) : inputPathPrefix)
: std::nullopt;
/* Get the input flake, resolve 'path:./...' /* Get the input flake, resolve 'path:./...'
flakerefs relative to the parent flake. */ flakerefs relative to the parent flake. */
auto getInputFlake = [&]() auto getInputFlake = [&]()
{ {
if (input.ref->input.isRelative()) { if (input.ref->input.isRelative()) {
SourcePath inputSourcePath { SourcePath inputSourcePath {
parentPath.accessor, overridenSourcePath.accessor,
CanonPath(*input.ref->input.getSourcePath(), *parentPath.path.parent()) CanonPath(*input.ref->input.getSourcePath(), *overridenSourcePath.path.parent())
}; };
// FIXME: we need to record in the lock
// file what the parent flake is.
return readFlake(state, *input.ref, *input.ref, *input.ref, inputSourcePath, inputPath); return readFlake(state, *input.ref, *input.ref, *input.ref, inputSourcePath, inputPath);
} else } else
return getFlake(state, *input.ref, useRegistries, inputPath); return getFlake(state, *input.ref, useRegistries, inputPath);
@ -415,6 +440,7 @@ LockedFlake lockFlake(
if (oldLock if (oldLock
&& oldLock->originalRef == *input.ref && oldLock->originalRef == *input.ref
&& oldLock->parentPath == overridenParentPath
&& !hasOverride) && !hasOverride)
{ {
debug("keeping existing input '%s'", inputPathS); debug("keeping existing input '%s'", inputPathS);
@ -423,7 +449,8 @@ LockedFlake lockFlake(
didn't change and there is no override from a didn't change and there is no override from a
higher level flake. */ higher level flake. */
auto childNode = std::make_shared<LockedNode>( auto childNode = std::make_shared<LockedNode>(
oldLock->lockedRef, oldLock->originalRef, oldLock->isFlake); oldLock->lockedRef, oldLock->originalRef, oldLock->isFlake,
oldLock->parentPath);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
@ -451,7 +478,7 @@ LockedFlake lockFlake(
.isFlake = (*lockedNode)->isFlake, .isFlake = (*lockedNode)->isFlake,
}); });
} else if (auto follows = std::get_if<1>(&i.second)) { } else if (auto follows = std::get_if<1>(&i.second)) {
if (! trustLock) { if (!trustLock) {
// It is possible that the flake has changed, // It is possible that the flake has changed,
// so we must confirm all the follows that are in the lockfile are also in the flake. // so we must confirm all the follows that are in the lockfile are also in the flake.
auto overridePath(inputPath); auto overridePath(inputPath);
@ -466,7 +493,7 @@ LockedFlake lockFlake(
break; break;
} }
} }
auto absoluteFollows(lockRootPath); auto absoluteFollows(followsPrefix);
absoluteFollows.insert(absoluteFollows.end(), follows->begin(), follows->end()); absoluteFollows.insert(absoluteFollows.end(), follows->begin(), follows->end());
fakeInputs.emplace(i.first, FlakeInput { fakeInputs.emplace(i.first, FlakeInput {
.follows = absoluteFollows, .follows = absoluteFollows,
@ -477,13 +504,14 @@ LockedFlake lockFlake(
if (mustRefetch) { if (mustRefetch) {
auto inputFlake = getInputFlake(); auto inputFlake = getInputFlake();
computeLocks(inputFlake.inputs, childNode, inputPath, oldLock, lockRootPath, inputFlake.path, !mustRefetch); computeLocks(inputFlake.inputs, childNode, inputPath, oldLock, followsPrefix,
inputFlake.path, !mustRefetch);
} else { } else {
// FIXME: parentPath is wrong here, we // FIXME: sourcePath is wrong here, we
// should pass a lambda that lazily // should pass a lambda that lazily
// fetches the parent flake if needed // fetches the parent flake if needed
// (i.e. getInputFlake()). // (i.e. getInputFlake()).
computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, parentPath, !mustRefetch); computeLocks(fakeInputs, childNode, inputPath, oldLock, followsPrefix, sourcePath, !mustRefetch);
} }
} else { } else {
@ -506,7 +534,9 @@ LockedFlake lockFlake(
if (input.isFlake) { if (input.isFlake) {
auto inputFlake = getInputFlake(); auto inputFlake = getInputFlake();
auto childNode = std::make_shared<LockedNode>(inputFlake.lockedRef, ref); auto childNode = std::make_shared<LockedNode>(
inputFlake.lockedRef, ref, true,
overridenParentPath);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
@ -526,7 +556,7 @@ LockedFlake lockFlake(
oldLock oldLock
? std::dynamic_pointer_cast<const Node>(oldLock) ? std::dynamic_pointer_cast<const Node>(oldLock)
: readLockFile(inputFlake).root, : readLockFile(inputFlake).root,
oldLock ? lockRootPath : inputPath, oldLock ? followsPrefix : inputPath,
inputFlake.path, inputFlake.path,
false); false);
} }
@ -537,7 +567,7 @@ LockedFlake lockFlake(
auto [accessor, lockedRef] = resolvedRef.lazyFetch(state.store); auto [accessor, lockedRef] = resolvedRef.lazyFetch(state.store);
node->inputs.insert_or_assign(id, node->inputs.insert_or_assign(id,
std::make_shared<LockedNode>(lockedRef, ref, false)); std::make_shared<LockedNode>(lockedRef, ref, false, overridenParentPath));
} }
} }
@ -549,8 +579,13 @@ LockedFlake lockFlake(
}; };
computeLocks( computeLocks(
flake->inputs, newLockFile.root, {}, flake->inputs,
lockFlags.recreateLockFile ? nullptr : oldLockFile.root, {}, flake->path, false); newLockFile.root,
{},
lockFlags.recreateLockFile ? nullptr : oldLockFile.root,
{},
flake->path,
false);
for (auto & i : lockFlags.inputOverrides) for (auto & i : lockFlags.inputOverrides)
if (!overridesUsed.count(i.first)) if (!overridesUsed.count(i.first))

View file

@ -31,9 +31,10 @@ FlakeRef getFlakeRef(
} }
LockedNode::LockedNode(const nlohmann::json & json) LockedNode::LockedNode(const nlohmann::json & json)
: lockedRef(getFlakeRef(json, "locked", "info")) : lockedRef(getFlakeRef(json, "locked", "info")) // FIXME: remove "info"
, originalRef(getFlakeRef(json, "original", nullptr)) , originalRef(getFlakeRef(json, "original", nullptr))
, isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true) , isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true)
, parentPath(json.find("parent") != json.end() ? (std::optional<InputPath>) json["parent"] : std::nullopt)
{ {
if (!lockedRef.input.isLocked() && !lockedRef.input.isRelative()) if (!lockedRef.input.isLocked() && !lockedRef.input.isRelative())
throw Error("lock file contains unlocked input '%s'", throw Error("lock file contains unlocked input '%s'",
@ -164,7 +165,10 @@ nlohmann::json LockFile::toJSON() const
if (auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(node)) { if (auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(node)) {
n["original"] = fetchers::attrsToJSON(lockedNode->originalRef.toAttrs()); n["original"] = fetchers::attrsToJSON(lockedNode->originalRef.toAttrs());
n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs()); n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs());
if (!lockedNode->isFlake) n["flake"] = false; if (!lockedNode->isFlake)
n["flake"] = false;
if (lockedNode->parentPath)
n["parent"] = *lockedNode->parentPath;
} }
nodes[key] = std::move(n); nodes[key] = std::move(n);

View file

@ -33,11 +33,16 @@ struct LockedNode : Node
FlakeRef lockedRef, originalRef; FlakeRef lockedRef, originalRef;
bool isFlake = true; bool isFlake = true;
/* The node relative to which relative source paths
(e.g. 'path:../foo') are interpreted. */
std::optional<InputPath> parentPath;
LockedNode( LockedNode(
const FlakeRef & lockedRef, const FlakeRef & lockedRef,
const FlakeRef & originalRef, const FlakeRef & originalRef,
bool isFlake = true) bool isFlake = true,
: lockedRef(lockedRef), originalRef(originalRef), isFlake(isFlake) std::optional<InputPath> parentPath = {})
: lockedRef(lockedRef), originalRef(originalRef), isFlake(isFlake), parentPath(parentPath)
{ } { }
LockedNode(const nlohmann::json & json); LockedNode(const nlohmann::json & json);