1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-25 19:01:16 +02:00

Improve support for subflakes

Subflakes are flakes in the same tree, accessed in flake inputs via
relative paths (e.g. `inputs.foo.url = "path:./subdir"`). Previously
these didn't work very well because they would be separately copied to
the store, which is inefficient and makes references to parent
directories tricky or impossible. Furthermore, they had their own NAR
hash in the lock file, which is superfluous since the parent is
already locked.

Now subflakes are accessed via the accessor of the calling flake. This
avoids the unnecessary copy and makes it possible for subflakes to
depend on flakes in a parent directory (so long as they're in the same
tree).

Lock file nodes for relative flake inputs now have a new `parent` field:

  {
    "locked": {
      "path": "./subdir",
      "type": "path"
    },
    "original": {
      "path": "./subdir",
      "type": "path"
    },
    "parent": [
      "foo",
      "bar"
    ]
  }

which denotes that `./subdir` is to be interpreted relative to the
directory of the `bar` input of the `foo` input of the root flake.

Extracted from the lazy-trees branch.
This commit is contained in:
Eelco Dolstra 2024-04-23 16:00:52 +02:00
parent bbe780b137
commit b2be6fed86
14 changed files with 332 additions and 120 deletions

View file

@ -38,10 +38,17 @@ let
(key: node: (key: node:
let let
parentNode = allNodes.${getInputByPath lockFile.root node.parent};
sourceInfo = sourceInfo =
if overrides ? ${key} if overrides ? ${key}
then then
overrides.${key}.sourceInfo overrides.${key}.sourceInfo
else if node.locked.type == "path" && builtins.substring 0 1 node.locked.path != "/"
then
parentNode.sourceInfo // {
outPath = parentNode.sourceInfo.outPath + ("/" + node.locked.path);
}
else else
# FIXME: remove obsolete node.info. # FIXME: remove obsolete node.info.
fetchTree (node.info or {} // removeAttrs node.locked ["dir"]); fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);

View file

@ -93,12 +93,17 @@ static void expectType(EvalState & state, ValueType type,
} }
static std::map<FlakeId, FlakeInput> parseFlakeInputs( static std::map<FlakeId, FlakeInput> parseFlakeInputs(
EvalState & state, Value * value, const PosIdx pos, EvalState & state,
const std::optional<Path> & baseDir, InputPath lockRootPath); Value * value,
const PosIdx pos,
InputPath lockRootPath);
static FlakeInput parseFlakeInput(EvalState & state, static FlakeInput parseFlakeInput(
const std::string & inputName, Value * value, const PosIdx pos, EvalState & state,
const std::optional<Path> & baseDir, InputPath lockRootPath) const std::string & inputName,
Value * value,
const PosIdx pos,
InputPath lockRootPath)
{ {
expectType(state, nAttrs, *value, pos); expectType(state, nAttrs, *value, pos);
@ -122,7 +127,7 @@ static FlakeInput parseFlakeInput(EvalState & state,
expectType(state, nBool, *attr.value, attr.pos); expectType(state, nBool, *attr.value, attr.pos);
input.isFlake = attr.value->boolean(); input.isFlake = attr.value->boolean();
} else if (attr.name == sInputs) { } else if (attr.name == sInputs) {
input.overrides = parseFlakeInputs(state, attr.value, attr.pos, baseDir, lockRootPath); input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootPath);
} else if (attr.name == sFollows) { } else if (attr.name == sFollows) {
expectType(state, nString, *attr.value, attr.pos); expectType(state, nString, *attr.value, attr.pos);
auto follows(parseInputPath(attr.value->c_str())); auto follows(parseInputPath(attr.value->c_str()));
@ -173,7 +178,7 @@ static FlakeInput parseFlakeInput(EvalState & state,
if (!attrs.empty()) if (!attrs.empty())
throw Error("unexpected flake input attribute '%s', at %s", attrs.begin()->first, state.positions[pos]); throw Error("unexpected flake input attribute '%s', at %s", attrs.begin()->first, state.positions[pos]);
if (url) if (url)
input.ref = parseFlakeRef(*url, baseDir, true, input.isFlake); input.ref = parseFlakeRef(*url, {}, true, input.isFlake, true);
} }
if (!input.follows && !input.ref) if (!input.follows && !input.ref)
@ -183,8 +188,10 @@ static FlakeInput parseFlakeInput(EvalState & state,
} }
static std::map<FlakeId, FlakeInput> parseFlakeInputs( static std::map<FlakeId, FlakeInput> parseFlakeInputs(
EvalState & state, Value * value, const PosIdx pos, EvalState & state,
const std::optional<Path> & baseDir, InputPath lockRootPath) Value * value,
const PosIdx pos,
InputPath lockRootPath)
{ {
std::map<FlakeId, FlakeInput> inputs; std::map<FlakeId, FlakeInput> inputs;
@ -196,7 +203,6 @@ static std::map<FlakeId, FlakeInput> parseFlakeInputs(
state.symbols[inputAttr.name], state.symbols[inputAttr.name],
inputAttr.value, inputAttr.value,
inputAttr.pos, inputAttr.pos,
baseDir,
lockRootPath)); lockRootPath));
} }
@ -232,7 +238,7 @@ static Flake readFlake(
auto sInputs = state.symbols.create("inputs"); auto sInputs = state.symbols.create("inputs");
if (auto inputs = vInfo.attrs()->get(sInputs)) if (auto inputs = vInfo.attrs()->get(sInputs))
flake.inputs = parseFlakeInputs(state, inputs->value, inputs->pos, flakePath.parent().path.abs(), lockRootPath); // FIXME flake.inputs = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootPath);
auto sOutputs = state.symbols.create("outputs"); auto sOutputs = state.symbols.create("outputs");
@ -366,13 +372,29 @@ LockedFlake lockFlake(
debug("old lock file: %s", oldLockFile); debug("old lock file: %s", oldLockFile);
std::map<InputPath, FlakeInput> overrides; struct Override
{
FlakeInput input;
SourcePath sourcePath;
std::optional<InputPath> parentPath; // FIXME: rename to inputPathPrefix?
};
std::map<InputPath, Override> overrides;
std::set<InputPath> explicitCliOverrides; std::set<InputPath> explicitCliOverrides;
std::set<InputPath> overridesUsed, updatesUsed; std::set<InputPath> overridesUsed, updatesUsed;
std::map<ref<Node>, SourcePath> nodePaths; std::map<ref<Node>, SourcePath> nodePaths;
for (auto & i : lockFlags.inputOverrides) { for (auto & i : lockFlags.inputOverrides) {
overrides.insert_or_assign(i.first, FlakeInput { .ref = i.second }); overrides.emplace(
i.first,
Override {
.input = FlakeInput { .ref = i.second },
/* Note: any relative overrides
(e.g. `--override-input B/C "path:./foo/bar"`)
are interpreted relative to the top-level
flake. */
.sourcePath = flake.path,
});
explicitCliOverrides.insert(i.first); explicitCliOverrides.insert(i.first);
} }
@ -386,7 +408,7 @@ LockedFlake lockFlake(
const InputPath & inputPathPrefix, const InputPath & inputPathPrefix,
std::shared_ptr<const Node> oldNode, std::shared_ptr<const Node> oldNode,
const InputPath & lockRootPath, const InputPath & lockRootPath,
const Path & parentPath, const SourcePath & sourcePath,
bool trustLock)> bool trustLock)>
computeLocks; computeLocks;
@ -402,7 +424,8 @@ LockedFlake lockFlake(
copied. */ copied. */
std::shared_ptr<const Node> oldNode, std::shared_ptr<const Node> oldNode,
const InputPath & lockRootPath, const InputPath & lockRootPath,
const Path & parentPath, /* 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));
@ -414,7 +437,12 @@ 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,
Override {
.input = inputOverride,
.sourcePath = sourcePath,
.parentPath = inputPathPrefix // FIXME: should this be inputPath?
});
} }
} }
@ -446,13 +474,18 @@ LockedFlake lockFlake(
auto i = overrides.find(inputPath); auto i = overrides.find(inputPath);
bool hasOverride = i != overrides.end(); bool hasOverride = i != overrides.end();
bool hasCliOverride = explicitCliOverrides.contains(inputPath); bool hasCliOverride = explicitCliOverrides.contains(inputPath);
if (hasOverride) { if (hasOverride)
overridesUsed.insert(inputPath); overridesUsed.insert(inputPath);
// Respect the “flakeness” of the input even if we auto input = hasOverride ? i->second.input : 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 ? i->second.sourcePath : 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. */
@ -468,6 +501,33 @@ LockedFlake lockFlake(
assert(input.ref); assert(input.ref);
auto overridenParentPath =
input.ref->input.isRelative()
? std::optional<InputPath>(hasOverride ? i->second.parentPath : inputPathPrefix)
: std::nullopt;
auto resolveRelativePath = [&]() -> std::optional<SourcePath>
{
if (auto relativePath = input.ref->input.isRelative()) {
return SourcePath {
overridenSourcePath.accessor,
CanonPath(*relativePath, overridenSourcePath.path.parent().value())
};
} else
return std::nullopt;
};
/* Get the input flake, resolve 'path:./...'
flakerefs relative to the parent flake. */
auto getInputFlake = [&]()
{
if (auto resolvedPath = resolveRelativePath()) {
return readFlake(state, *input.ref, *input.ref, *input.ref, *resolvedPath, inputPath);
} else {
return getFlake(state, *input.ref, useRegistries, flakeCache, inputPath);
}
};
/* Do we have an entry in the existing lock file? /* Do we have an entry in the existing lock file?
And the input is not in updateInputs? */ And the input is not in updateInputs? */
std::shared_ptr<LockedNode> oldLock; std::shared_ptr<LockedNode> oldLock;
@ -481,6 +541,7 @@ LockedFlake lockFlake(
if (oldLock if (oldLock
&& oldLock->originalRef == *input.ref && oldLock->originalRef == *input.ref
&& oldLock->parentPath == overridenParentPath
&& !hasCliOverride) && !hasCliOverride)
{ {
debug("keeping existing input '%s'", inputPathS); debug("keeping existing input '%s'", inputPathS);
@ -489,7 +550,10 @@ 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 = make_ref<LockedNode>( auto childNode = make_ref<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);
@ -541,11 +605,15 @@ LockedFlake lockFlake(
} }
if (mustRefetch) { if (mustRefetch) {
auto inputFlake = getFlake(state, oldLock->lockedRef, false, flakeCache, inputPath); auto inputFlake = getInputFlake();
nodePaths.emplace(childNode, inputFlake.path.parent()); nodePaths.emplace(childNode, inputFlake.path.parent());
computeLocks(inputFlake.inputs, childNode, inputPath, oldLock, lockRootPath, parentPath, false); computeLocks(inputFlake.inputs, childNode, inputPath, oldLock, lockRootPath, inputFlake.path, false);
} else { } else {
computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, parentPath, true); // FIXME: sourcePath is wrong here, we
// should pass a lambda that lazily
// fetches the parent flake if needed
// (i.e. getInputFlake()).
computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, sourcePath, true);
} }
} else { } else {
@ -553,7 +621,9 @@ LockedFlake lockFlake(
this input. */ this input. */
debug("creating new input '%s'", inputPathS); debug("creating new input '%s'", inputPathS);
if (!lockFlags.allowUnlocked && !input.ref->input.isLocked()) if (!lockFlags.allowUnlocked
&& !input.ref->input.isLocked()
&& !input.ref->input.isRelative())
throw Error("cannot update unlocked flake input '%s' in pure mode", inputPathS); throw Error("cannot update unlocked flake input '%s' in pure mode", inputPathS);
/* Note: in case of an --override-input, we use /* Note: in case of an --override-input, we use
@ -566,17 +636,9 @@ LockedFlake lockFlake(
auto ref = (input2.ref && explicitCliOverrides.contains(inputPath)) ? *input2.ref : *input.ref; auto ref = (input2.ref && explicitCliOverrides.contains(inputPath)) ? *input2.ref : *input.ref;
if (input.isFlake) { if (input.isFlake) {
Path localPath = parentPath; auto inputFlake = getInputFlake();
FlakeRef localRef = *input.ref;
// If this input is a path, recurse it down. auto childNode = make_ref<LockedNode>(inputFlake.lockedRef, ref, true, overridenParentPath);
// This allows us to resolve path inputs relative to the current flake.
if (localRef.input.getType() == "path")
localPath = absPath(*input.ref->input.getSourcePath(), parentPath);
auto inputFlake = getFlake(state, localRef, useRegistries, flakeCache, inputPath);
auto childNode = make_ref<LockedNode>(inputFlake.lockedRef, ref);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
@ -598,17 +660,26 @@ LockedFlake lockFlake(
? std::dynamic_pointer_cast<const Node>(oldLock) ? std::dynamic_pointer_cast<const Node>(oldLock)
: readLockFile(inputFlake.lockFilePath()).root.get_ptr(), : readLockFile(inputFlake.lockFilePath()).root.get_ptr(),
oldLock ? lockRootPath : inputPath, oldLock ? lockRootPath : inputPath,
localPath, inputFlake.path,
false); false);
} }
else { else {
auto [path, lockedRef] = [&]() -> std::tuple<SourcePath, FlakeRef>
{
// Handle non-flake 'path:./...' inputs.
if (auto resolvedPath = resolveRelativePath()) {
return {*resolvedPath, *input.ref};
} else {
auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree( auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree(
state, *input.ref, useRegistries, flakeCache); state, *input.ref, useRegistries, flakeCache);
return {state.rootPath(state.store->toRealPath(storePath)), lockedRef};
}
}();
auto childNode = make_ref<LockedNode>(lockedRef, ref, false); auto childNode = make_ref<LockedNode>(lockedRef, ref, false, overridenParentPath);
nodePaths.emplace(childNode, state.rootPath(state.store->toRealPath(storePath))); nodePaths.emplace(childNode, path);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
} }
@ -621,9 +692,6 @@ LockedFlake lockFlake(
} }
}; };
// Bring in the current ref for relative path resolution if we have it
auto parentPath = flake.path.parent().path.abs();
nodePaths.emplace(newLockFile.root, flake.path.parent()); nodePaths.emplace(newLockFile.root, flake.path.parent());
computeLocks( computeLocks(
@ -632,7 +700,7 @@ LockedFlake lockFlake(
{}, {},
lockFlags.recreateLockFile ? nullptr : oldLockFile.root.get_ptr(), lockFlags.recreateLockFile ? nullptr : oldLockFile.root.get_ptr(),
{}, {},
parentPath, flake.path,
false); false);
for (auto & i : lockFlags.inputOverrides) for (auto & i : lockFlags.inputOverrides)

View file

@ -51,9 +51,10 @@ FlakeRef parseFlakeRef(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir, const std::optional<Path> & baseDir,
bool allowMissing, bool allowMissing,
bool isFlake) bool isFlake,
bool allowRelative)
{ {
auto [flakeRef, fragment] = parseFlakeRefWithFragment(url, baseDir, allowMissing, isFlake); auto [flakeRef, fragment] = parseFlakeRefWithFragment(url, baseDir, allowMissing, isFlake, allowRelative);
if (fragment != "") if (fragment != "")
throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url); throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url);
return flakeRef; return flakeRef;
@ -69,11 +70,25 @@ std::optional<FlakeRef> maybeParseFlakeRef(
} }
} }
static std::pair<FlakeRef, std::string> fromParsedURL(
ParsedURL && parsedURL,
bool isFlake)
{
auto dir = getOr(parsedURL.query, "dir", "");
parsedURL.query.erase("dir");
std::string fragment;
std::swap(fragment, parsedURL.fragment);
return std::make_pair(FlakeRef(fetchers::Input::fromURL(parsedURL, isFlake), dir), fragment);
}
std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment( std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir, const std::optional<Path> & baseDir,
bool allowMissing, bool allowMissing,
bool isFlake) bool isFlake,
bool allowRelative)
{ {
std::string path = url; std::string path = url;
std::string fragment = ""; std::string fragment = "";
@ -90,7 +105,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
fragment = percentDecode(url.substr(fragmentStart+1)); fragment = percentDecode(url.substr(fragmentStart+1));
} }
if (pathEnd != std::string::npos && fragmentStart != std::string::npos) { if (pathEnd != std::string::npos && fragmentStart != std::string::npos) {
query = decodeQuery(url.substr(pathEnd+1, fragmentStart-pathEnd-1)); query = decodeQuery(url.substr(pathEnd + 1, fragmentStart - pathEnd - 1));
} }
if (baseDir) { if (baseDir) {
@ -154,6 +169,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
.authority = "", .authority = "",
.path = flakeRoot, .path = flakeRoot,
.query = query, .query = query,
.fragment = fragment,
}; };
if (subdir != "") { if (subdir != "") {
@ -165,9 +181,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
if (pathExists(flakeRoot + "/.git/shallow")) if (pathExists(flakeRoot + "/.git/shallow"))
parsedURL.query.insert_or_assign("shallow", "1"); parsedURL.query.insert_or_assign("shallow", "1");
return std::make_pair( return fromParsedURL(std::move(parsedURL), isFlake);
FlakeRef(fetchers::Input::fromURL(parsedURL), getOr(parsedURL.query, "dir", "")),
fragment);
} }
subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir); subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir);
@ -176,25 +190,30 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
} }
} else { } else {
if (!hasPrefix(path, "/")) if (!allowRelative && !hasPrefix(path, "/"))
throw BadURL("flake reference '%s' is not an absolute path", url); throw BadURL("flake reference '%s' is not an absolute path", url);
path = canonPath(path + "/" + getOr(query, "dir", ""));
} }
fetchers::Attrs attrs; fetchers::Attrs attrs;
attrs.insert_or_assign("type", "path"); attrs.insert_or_assign("type", "path");
attrs.insert_or_assign("path", path); attrs.insert_or_assign("path", path);
return std::make_pair(FlakeRef(fetchers::Input::fromAttrs(std::move(attrs)), ""), fragment); return fromParsedURL({
}; .url = path,
.base = path,
.scheme = "path",
.authority = "",
.path = path,
.query = query,
.fragment = fragment
}, isFlake);
}
/* Check if 'url' is a flake ID. This is an abbreviated syntax for /* Check if 'url' is a flake ID. This is an abbreviated syntax for
'flake:<flake-id>?ref=<ref>&rev=<rev>'. */ 'flake:<flake-id>?ref=<ref>&rev=<rev>'. */
std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef( std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
const std::string & url, const std::string & url,
bool isFlake bool isFlake)
)
{ {
std::smatch match; std::smatch match;
@ -223,32 +242,21 @@ std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef( std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir, const std::optional<Path> & baseDir,
bool isFlake bool isFlake)
)
{ {
ParsedURL parsedURL;
try { try {
parsedURL = parseURL(url); return fromParsedURL(parseURL(url), isFlake);
} catch (BadURL &) { } catch (BadURL &) {
return std::nullopt; return std::nullopt;
} }
std::string fragment;
std::swap(fragment, parsedURL.fragment);
auto input = fetchers::Input::fromURL(parsedURL, isFlake);
input.parent = baseDir;
return std::make_pair(
FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")),
fragment);
} }
std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir, const std::optional<Path> & baseDir,
bool allowMissing, bool allowMissing,
bool isFlake) bool isFlake,
bool allowRelative)
{ {
using namespace fetchers; using namespace fetchers;
@ -259,7 +267,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
} else if (auto res = parseURLFlakeRef(url, baseDir, isFlake)) { } else if (auto res = parseURLFlakeRef(url, baseDir, isFlake)) {
return *res; return *res;
} else { } else {
return parsePathFlakeRefWithFragment(url, baseDir, allowMissing, isFlake); return parsePathFlakeRefWithFragment(url, baseDir, allowMissing, isFlake, allowRelative);
} }
} }

View file

@ -75,7 +75,8 @@ FlakeRef parseFlakeRef(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir = {}, const std::optional<Path> & baseDir = {},
bool allowMissing = false, bool allowMissing = false,
bool isFlake = true); bool isFlake = true,
bool allowRelative = false);
/** /**
* @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory)
@ -90,7 +91,8 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir = {}, const std::optional<Path> & baseDir = {},
bool allowMissing = false, bool allowMissing = false,
bool isFlake = true); bool isFlake = true,
bool allowRelative = false);
/** /**
* @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory)

View file

@ -36,8 +36,9 @@ LockedNode::LockedNode(const nlohmann::json & json)
: lockedRef(getFlakeRef(json, "locked", "info")) // FIXME: remove "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()) if (!lockedRef.input.isLocked() && !lockedRef.input.isRelative())
throw Error("lock file contains unlocked input '%s'", throw Error("lock file contains unlocked input '%s'",
fetchers::attrsToJSON(lockedRef.input.toAttrs())); fetchers::attrsToJSON(lockedRef.input.toAttrs()));
} }
@ -184,6 +185,8 @@ std::pair<nlohmann::json, LockFile::KeyMap> LockFile::toJSON() const
n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs()); n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs());
if (!lockedNode->isFlake) if (!lockedNode->isFlake)
n["flake"] = false; n["flake"] = false;
if (lockedNode->parentPath)
n["parent"] = *lockedNode->parentPath;
} }
nodes[key] = std::move(n); nodes[key] = std::move(n);
@ -230,7 +233,9 @@ std::optional<FlakeRef> LockFile::isUnlocked() const
for (auto & i : nodes) { for (auto & i : nodes) {
if (i == ref<const Node>(root)) continue; if (i == ref<const Node>(root)) continue;
auto node = i.dynamic_pointer_cast<const LockedNode>(); auto node = i.dynamic_pointer_cast<const LockedNode>();
if (node && !node->lockedRef.input.isLocked()) if (node
&& !node->lockedRef.input.isLocked()
&& !node->lockedRef.input.isRelative())
return node->lockedRef; return node->lockedRef;
} }

View file

@ -38,11 +38,19 @@ 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);

View file

@ -31,8 +31,7 @@ void emitTreeAttrs(
// FIXME: support arbitrary input attributes. // FIXME: support arbitrary input attributes.
auto narHash = input.getNarHash(); if (auto narHash = input.getNarHash())
assert(narHash);
attrs.alloc("narHash").mkString(narHash->to_string(HashFormat::SRI, true)); attrs.alloc("narHash").mkString(narHash->to_string(HashFormat::SRI, true));
if (input.getType() == "git") if (input.getType() == "git")

View file

@ -141,6 +141,12 @@ bool Input::isLocked() const
return scheme && scheme->isLocked(*this); return scheme && scheme->isLocked(*this);
} }
std::optional<std::string> Input::isRelative() const
{
assert(scheme);
return scheme->isRelative(*this);
}
Attrs Input::toAttrs() const Attrs Input::toAttrs() const
{ {
return attrs; return attrs;

View file

@ -31,11 +31,6 @@ struct Input
std::shared_ptr<InputScheme> scheme; // note: can be null std::shared_ptr<InputScheme> scheme; // note: can be null
Attrs attrs; Attrs attrs;
/**
* path of the parent of this input, used for relative path resolution
*/
std::optional<Path> parent;
public: public:
/** /**
* Create an `Input` from a URL. * Create an `Input` from a URL.
@ -73,6 +68,12 @@ public:
*/ */
bool isLocked() const; bool isLocked() const;
/**
* Only for relative path flakes, i.e. 'path:./foo', returns the
* relative path, i.e. './foo'.
*/
std::optional<std::string> isRelative() const;
bool operator ==(const Input & other) const; bool operator ==(const Input & other) const;
bool contains(const Input & other) const; bool contains(const Input & other) const;
@ -220,6 +221,9 @@ struct InputScheme
* if there is a mismatch. * if there is a mismatch.
*/ */
virtual void checkLocks(const Input & specified, const Input & final) const; virtual void checkLocks(const Input & specified, const Input & final) const;
virtual std::optional<std::string> isRelative(const Input & input) const
{ return std::nullopt; }
}; };
void registerInputScheme(std::shared_ptr<InputScheme> && fetcher); void registerInputScheme(std::shared_ptr<InputScheme> && fetcher);

View file

@ -116,31 +116,14 @@ struct PathInputScheme : InputScheme
std::pair<ref<SourceAccessor>, Input> getAccessor(ref<Store> store, const Input & _input) const override std::pair<ref<SourceAccessor>, Input> getAccessor(ref<Store> store, const Input & _input) const override
{ {
Input input(_input); Input input(_input);
std::string absPath;
auto path = getStrAttr(input.attrs, "path"); auto path = getStrAttr(input.attrs, "path");
if (path[0] != '/') { auto absPath = getAbsPath(input);
if (!input.parent)
throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string());
auto parent = canonPath(*input.parent); Activity act(*logger, lvlTalkative, actUnknown, fmt("copying '%s' to the store", absPath));
// the path isn't relative, prefix it
absPath = nix::absPath(path, parent);
// for security, ensure that if the parent is a store path, it's inside it
if (store->isInStore(parent)) {
auto storePath = store->printStorePath(store->toStorePath(parent).first);
if (!isDirOrInDir(absPath, storePath))
throw BadStorePath("relative path '%s' points outside of its parent's store path '%s'", path, storePath);
}
} else
absPath = path;
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying '%s'", absPath));
// FIXME: check whether access to 'path' is allowed. // FIXME: check whether access to 'path' is allowed.
auto storePath = store->maybeParseStorePath(absPath); auto storePath = store->maybeParseStorePath(absPath.abs());
if (storePath) if (storePath)
store->addTempRoot(*storePath); store->addTempRoot(*storePath);
@ -149,7 +132,7 @@ struct PathInputScheme : InputScheme
if (!storePath || storePath->name() != "source" || !store->isValidPath(*storePath)) { if (!storePath || storePath->name() != "source" || !store->isValidPath(*storePath)) {
// FIXME: try to substitute storePath. // FIXME: try to substitute storePath.
auto src = sinkToSource([&](Sink & sink) { auto src = sinkToSource([&](Sink & sink) {
mtime = dumpPathAndGetMtime(absPath, sink, defaultPathFilter); mtime = dumpPathAndGetMtime(absPath.abs(), sink, defaultPathFilter);
}); });
storePath = store->addToStoreFromDump(*src, "source"); storePath = store->addToStoreFromDump(*src, "source");
} }

View file

@ -195,18 +195,29 @@ Currently the `type` attribute can be one of the following:
If the flake at *path* is not inside a git repository, the `path:` If the flake at *path* is not inside a git repository, the `path:`
prefix is implied and can be omitted. prefix is implied and can be omitted.
*path* generally must be an absolute path. However, on the command If *path* is a relative path (i.e. if it does not start with `/`),
line, it can be a relative path (e.g. `.` or `./foo`) which is it is interpreted as follows:
interpreted as relative to the current directory. In this case, it
must start with `.` to avoid ambiguity with registry lookups - If *path* is a command line argument, it is interpreted relative
(e.g. `nixpkgs` is a registry lookup; `./nixpkgs` is a relative to the current directory.
path).
- If *path* is used in a `flake.nix`, it is interpreted relative to
the directory containing that `flake.nix`. However, the resolved
path must be in the same tree. For instance, a `flake.nix` in the
root of a tree can use `path:./foo` to access the flake in
subdirectory `foo`, but `path:../bar` is illegal.
Note that if you omit `path:`, relative paths must start with `.` to
avoid ambiguity with registry lookups (e.g. `nixpkgs` is a registry
lookup; `./nixpkgs` is a relative path).
For example, these are valid path flake references: For example, these are valid path flake references:
* `path:/home/user/sub/dir` * `path:/home/user/sub/dir`
* `/home/user/sub/dir` (if `dir/flake.nix` is *not* in a git repository) * `/home/user/sub/dir` (if `dir/flake.nix` is *not* in a git repository)
* `./sub/dir` (when used on the command line and `dir/flake.nix` is *not* in a git repository) * `path:sub/dir`
* `./sub/dir`
* `path:../parent`
* `git`: Git repositories. The location of the repository is specified * `git`: Git repositories. The location of the repository is specified
by the attribute `url`. by the attribute `url`.

View file

@ -115,7 +115,7 @@ nix flake lock $flakeFollowsA
[[ $(jq -c .nodes.B.inputs.foobar $flakeFollowsA/flake.lock) = '"foobar"' ]] [[ $(jq -c .nodes.B.inputs.foobar $flakeFollowsA/flake.lock) = '"foobar"' ]]
jq -r -c '.nodes | keys | .[]' $flakeFollowsA/flake.lock | grep "^foobar$" jq -r -c '.nodes | keys | .[]' $flakeFollowsA/flake.lock | grep "^foobar$"
# Ensure a relative path is not allowed to go outside the store path # Check that path: inputs cannot escape from their root.
cat > $flakeFollowsA/flake.nix <<EOF cat > $flakeFollowsA/flake.nix <<EOF
{ {
description = "Flake A"; description = "Flake A";
@ -128,7 +128,28 @@ EOF
git -C $flakeFollowsA add flake.nix git -C $flakeFollowsA add flake.nix
expect 1 nix flake lock $flakeFollowsA 2>&1 | grep 'points outside' expect 1 nix flake lock $flakeFollowsA 2>&1 | grep '/flakeB.*is forbidden in pure evaluation mode'
expect 1 nix flake lock --impure $flakeFollowsA 2>&1 | grep '/flakeB.*does not exist'
# Test relative non-flake inputs.
cat > $flakeFollowsA/flake.nix <<EOF
{
description = "Flake A";
inputs = {
E.flake = false;
E.url = "./foo.nix"; # test relative paths without 'path:'
};
outputs = { E, ... }: { e = import E; };
}
EOF
echo 123 > $flakeFollowsA/foo.nix
git -C $flakeFollowsA add flake.nix foo.nix
nix flake lock $flakeFollowsA
[[ $(nix eval --json $flakeFollowsA#e) = 123 ]]
# Non-existant follows should print a warning. # Non-existant follows should print a warning.
cat >$flakeFollowsA/flake.nix <<EOF cat >$flakeFollowsA/flake.nix <<EOF

View file

@ -0,0 +1,89 @@
source ./common.sh
requireGit
rootFlake=$TEST_ROOT/flake1
subflake0=$rootFlake/sub0
subflake1=$rootFlake/sub1
subflake2=$rootFlake/sub2
rm -rf $rootFlake
mkdir -p $rootFlake $subflake0 $subflake1 $subflake2
cat > $rootFlake/flake.nix <<EOF
{
inputs.sub0.url = "./sub0";
outputs = { self, sub0 }: {
x = 2;
y = self.x * sub0.x;
};
}
EOF
cat > $subflake0/flake.nix <<EOF
{
outputs = { self }: {
x = 7;
};
}
EOF
[[ $(nix eval $rootFlake#x) = 2 ]]
[[ $(nix eval $rootFlake#y) = 14 ]]
cat > $subflake1/flake.nix <<EOF
{
inputs.root.url = "../";
outputs = { self, root }: {
x = 3;
y = self.x * root.x;
};
}
EOF
[[ $(nix eval $rootFlake?dir=sub1#y) = 6 ]]
git init $rootFlake
git -C $rootFlake add flake.nix sub0/flake.nix sub1/flake.nix
[[ $(nix eval $subflake1#y) = 6 ]]
cat > $subflake2/flake.nix <<EOF
{
inputs.root.url = "../";
inputs.sub1.url = "../sub1";
outputs = { self, root, sub1 }: {
x = 5;
y = self.x * sub1.x;
};
}
EOF
git -C $rootFlake add flake.nix sub2/flake.nix
[[ $(nix eval $subflake2#y) = 15 ]]
# Make sure there are no content locks for relative path flakes.
(! grep "$TEST_ROOT" $subflake2/flake.lock)
(! grep "$NIX_STORE_DIR" $subflake2/flake.lock)
(! grep narHash $subflake2/flake.lock)
# Test circular relative path flakes. FIXME: doesn't work at the moment.
if false; then
cat > $rootFlake/flake.nix <<EOF
{
inputs.sub1.url = "./sub1";
inputs.sub2.url = "./sub1";
outputs = { self, sub1, sub2 }: {
x = 2;
y = self.x * sub1.x * sub2.x;
z = sub1.y * sub2.y;
};
}
EOF
[[ $(nix eval $rootFlake#x) = 30 ]]
[[ $(nix eval $rootFlake#z) = 90 ]]
fi

View file

@ -7,6 +7,7 @@ nix_tests = \
flakes/circular.sh \ flakes/circular.sh \
flakes/init.sh \ flakes/init.sh \
flakes/inputs.sh \ flakes/inputs.sh \
flakes/relative-paths.sh \
flakes/follow-paths.sh \ flakes/follow-paths.sh \
flakes/bundle.sh \ flakes/bundle.sh \
flakes/check.sh \ flakes/check.sh \