1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-07-05 12:21:48 +02:00

Merge pull request #10089 from edolstra/relative-flakes

Improve support for relative path inputs
This commit is contained in:
Eelco Dolstra 2025-01-16 14:21:27 +01:00 committed by GitHub
commit 043df13f72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 358 additions and 114 deletions

View file

@ -0,0 +1,12 @@
---
synopsis: "Support for relative path inputs"
prs: [10089]
---
Flakes can now refer to other flakes in the same repository using relative paths, e.g.
```nix
inputs.foo.url = "path:./foo";
```
uses the flake in the `foo` subdirectory of the referring flake. For more information, see the documentation on [the `path` flake input type](@docroot@/command-ref/new-cli/nix3-flake.md#path-fetcher).
This feature required a change to the lock file format. Previous Nix versions will not be able to use lock files that have locks for relative path inputs in them.

View file

@ -41,10 +41,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.outPath + ("/" + node.locked.path);
}
else else
# FIXME: remove obsolete node.info. # FIXME: remove obsolete node.info.
# Note: lock file entries are always final. # Note: lock file entries are always final.

View file

@ -166,6 +166,12 @@ bool Input::isFinal() const
return maybeGetBoolAttr(attrs, "__final").value_or(false); return maybeGetBoolAttr(attrs, "__final").value_or(false);
} }
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

@ -41,11 +41,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;
/** /**
* Cached result of getFingerprint(). * Cached result of getFingerprint().
*/ */
@ -104,6 +99,12 @@ public:
bool isConsideredLocked( bool isConsideredLocked(
const Settings & settings) const; const Settings & settings) const;
/**
* Only for relative path flakes, i.e. 'path:./foo', returns the
* relative path, i.e. './foo'.
*/
std::optional<std::string> isRelative() const;
/** /**
* Return whether this is a "final" input, meaning that fetching * Return whether this is a "final" input, meaning that fetching
* it will not add, remove or change any attributes. (See * it will not add, remove or change any attributes. (See
@ -269,6 +270,9 @@ struct InputScheme
virtual bool isLocked(const Input & input) const virtual bool isLocked(const Input & input) const
{ return false; } { return false; }
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

@ -91,10 +91,10 @@ struct PathInputScheme : InputScheme
std::string_view contents, std::string_view contents,
std::optional<std::string> commitMsg) const override std::optional<std::string> commitMsg) const override
{ {
writeFile((CanonPath(getAbsPath(input)) / path).abs(), contents); writeFile(getAbsPath(input) / path.rel(), contents);
} }
std::optional<std::string> isRelative(const Input & input) const std::optional<std::string> isRelative(const Input & input) const override
{ {
auto path = getStrAttr(input.attrs, "path"); auto path = getStrAttr(input.attrs, "path");
if (isAbsolute(path)) if (isAbsolute(path))
@ -108,12 +108,12 @@ struct PathInputScheme : InputScheme
return (bool) input.getNarHash(); return (bool) input.getNarHash();
} }
CanonPath getAbsPath(const Input & input) const std::filesystem::path getAbsPath(const Input & input) const
{ {
auto path = getStrAttr(input.attrs, "path"); auto path = getStrAttr(input.attrs, "path");
if (path[0] == '/') if (isAbsolute(path))
return CanonPath(path); return canonPath(path);
throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string()); throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string());
} }
@ -121,31 +121,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.string());
if (storePath) if (storePath)
store->addTempRoot(*storePath); store->addTempRoot(*storePath);
@ -154,7 +137,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.string(), sink, defaultPathFilter);
}); });
storePath = store->addToStoreFromDump(*src, "source"); storePath = store->addToStoreFromDump(*src, "source");
} }
@ -176,7 +159,7 @@ struct PathInputScheme : InputScheme
store object and the subpath. */ store object and the subpath. */
auto path = getAbsPath(input); auto path = getAbsPath(input);
try { try {
auto [storePath, subPath] = store->toStorePath(path.abs()); auto [storePath, subPath] = store->toStorePath(path.string());
auto info = store->queryPathInfo(storePath); auto info = store->queryPathInfo(storePath);
return fmt("path:%s:%s", info->narHash.to_string(HashFormat::Base16, false), subPath); return fmt("path:%s:%s", info->narHash.to_string(HashFormat::Base16, false), subPath);
} catch (Error &) { } catch (Error &) {

View file

@ -102,12 +102,19 @@ 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,
const SourcePath & flakeDir);
static FlakeInput parseFlakeInput(EvalState & state, static FlakeInput parseFlakeInput(
std::string_view inputName, Value * value, const PosIdx pos, EvalState & state,
const std::optional<Path> & baseDir, InputPath lockRootPath) std::string_view inputName,
Value * value,
const PosIdx pos,
InputPath lockRootPath,
const SourcePath & flakeDir)
{ {
expectType(state, nAttrs, *value, pos); expectType(state, nAttrs, *value, pos);
@ -124,14 +131,25 @@ static FlakeInput parseFlakeInput(EvalState & state,
for (auto & attr : *value->attrs()) { for (auto & attr : *value->attrs()) {
try { try {
if (attr.name == sUrl) { if (attr.name == sUrl) {
expectType(state, nString, *attr.value, attr.pos); forceTrivialValue(state, *attr.value, pos);
if (attr.value->type() == nString)
url = attr.value->string_view(); url = attr.value->string_view();
else if (attr.value->type() == nPath) {
auto path = attr.value->path();
if (path.accessor != flakeDir.accessor)
throw Error("input path '%s' at %s must be in the same source tree as %s",
path, state.positions[attr.pos], flakeDir);
url = "path:" + flakeDir.path.makeRelative(path.path);
}
else
throw Error("expected a string or a path but got %s at %s",
showType(attr.value->type()), state.positions[attr.pos]);
attrs.emplace("url", *url); attrs.emplace("url", *url);
} else if (attr.name == sFlake) { } else if (attr.name == sFlake) {
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, flakeDir);
} 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()));
@ -189,7 +207,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(state.fetchSettings, *url, baseDir, true, input.isFlake); input.ref = parseFlakeRef(state.fetchSettings, *url, {}, true, input.isFlake, true);
} }
if (!input.follows && !input.ref) if (!input.follows && !input.ref)
@ -199,8 +217,11 @@ 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,
const SourcePath & flakeDir)
{ {
std::map<FlakeId, FlakeInput> inputs; std::map<FlakeId, FlakeInput> inputs;
@ -212,8 +233,8 @@ 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)); flakeDir));
} }
return inputs; return inputs;
@ -227,7 +248,8 @@ static Flake readFlake(
const SourcePath & rootDir, const SourcePath & rootDir,
const InputPath & lockRootPath) const InputPath & lockRootPath)
{ {
auto flakePath = rootDir / CanonPath(resolvedRef.subdir) / "flake.nix"; auto flakeDir = rootDir / CanonPath(resolvedRef.subdir);
auto flakePath = flakeDir / "flake.nix";
// NOTE evalFile forces vInfo to be an attrset because mustBeTrivial is true. // NOTE evalFile forces vInfo to be an attrset because mustBeTrivial is true.
Value vInfo; Value vInfo;
@ -248,7 +270,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, flakeDir);
auto sOutputs = state.symbols.create("outputs"); auto sOutputs = state.symbols.create("outputs");
@ -388,13 +410,29 @@ LockedFlake lockFlake(
debug("old lock file: %s", oldLockFile); debug("old lock file: %s", oldLockFile);
std::map<InputPath, FlakeInput> overrides; struct OverrideTarget
{
FlakeInput input;
SourcePath sourcePath;
std::optional<InputPath> parentInputPath; // FIXME: rename to inputPathPrefix?
};
std::map<InputPath, OverrideTarget> 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,
OverrideTarget {
.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);
} }
@ -408,7 +446,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;
@ -424,7 +462,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));
@ -436,7 +475,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,
OverrideTarget {
.input = inputOverride,
.sourcePath = sourcePath,
.parentInputPath = inputPathPrefix
});
} }
} }
@ -468,13 +512,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. */
@ -490,6 +539,33 @@ LockedFlake lockFlake(
assert(input.ref); assert(input.ref);
auto overridenParentPath =
input.ref->input.isRelative()
? std::optional<InputPath>(hasOverride ? i->second.parentInputPath : 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;
@ -503,6 +579,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);
@ -511,7 +588,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);
@ -563,11 +643,11 @@ 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); computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, sourcePath, true);
} }
} else { } else {
@ -575,7 +655,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
@ -588,17 +670,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);
@ -620,17 +694,26 @@ LockedFlake lockFlake(
? std::dynamic_pointer_cast<const Node>(oldLock) ? std::dynamic_pointer_cast<const Node>(oldLock)
: readLockFile(settings, state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(), : readLockFile(settings, state.fetchSettings, 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);
} }
@ -643,9 +726,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(
@ -654,7 +734,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

@ -48,9 +48,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 preserveRelativePaths)
{ {
auto [flakeRef, fragment] = parseFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake); auto [flakeRef, fragment] = parseFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake, preserveRelativePaths);
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;
@ -87,7 +88,8 @@ 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 preserveRelativePaths)
{ {
static std::regex pathFlakeRegex( static std::regex pathFlakeRegex(
R"(([^?#]*)(\?([^#]*))?(#(.*))?)", R"(([^?#]*)(\?([^#]*))?(#(.*))?)",
@ -178,9 +180,8 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
} }
} else { } else {
if (!isAbsolute(path)) if (!preserveRelativePaths && !isAbsolute(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", ""));
} }
return fromParsedURL(fetchSettings, { return fromParsedURL(fetchSettings, {
@ -199,8 +200,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef( static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
const fetchers::Settings & fetchSettings, const fetchers::Settings & fetchSettings,
const std::string & url, const std::string & url,
bool isFlake bool isFlake)
)
{ {
std::smatch match; std::smatch match;
@ -228,8 +228,7 @@ std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef(
const fetchers::Settings & fetchSettings, const fetchers::Settings & fetchSettings,
const std::string & url, const std::string & url,
const std::optional<Path> & baseDir, const std::optional<Path> & baseDir,
bool isFlake bool isFlake)
)
{ {
try { try {
auto parsed = parseURL(url); auto parsed = parseURL(url);
@ -248,7 +247,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, bool allowMissing,
bool isFlake) bool isFlake,
bool preserveRelativePaths)
{ {
using namespace fetchers; using namespace fetchers;
@ -257,7 +257,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
} else if (auto res = parseURLFlakeRef(fetchSettings, url, baseDir, isFlake)) { } else if (auto res = parseURLFlakeRef(fetchSettings, url, baseDir, isFlake)) {
return *res; return *res;
} else { } else {
return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake); return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake, preserveRelativePaths);
} }
} }

View file

@ -84,7 +84,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 preserveRelativePaths = 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)
@ -102,7 +103,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 preserveRelativePaths = 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

@ -43,8 +43,9 @@ LockedNode::LockedNode(
: lockedRef(getFlakeRef(fetchSettings, json, "locked", "info")) // FIXME: remove "info" : lockedRef(getFlakeRef(fetchSettings, json, "locked", "info")) // FIXME: remove "info"
, originalRef(getFlakeRef(fetchSettings, json, "original", nullptr)) , originalRef(getFlakeRef(fetchSettings, 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.isConsideredLocked(fetchSettings)) if (!lockedRef.input.isConsideredLocked(fetchSettings) && !lockedRef.input.isRelative())
throw Error("Lock file contains unlocked input '%s'. Use '--allow-dirty-locks' to accept this lock file.", throw Error("Lock file contains unlocked input '%s'. Use '--allow-dirty-locks' to accept this lock file.",
fetchers::attrsToJSON(lockedRef.input.toAttrs())); fetchers::attrsToJSON(lockedRef.input.toAttrs()));
@ -198,10 +199,12 @@ std::pair<nlohmann::json, LockFile::KeyMap> LockFile::toJSON() const
/* For backward compatibility, omit the "__final" /* For backward compatibility, omit the "__final"
attribute. We never allow non-final inputs in lock files attribute. We never allow non-final inputs in lock files
anyway. */ anyway. */
assert(lockedNode->lockedRef.input.isFinal()); assert(lockedNode->lockedRef.input.isFinal() || lockedNode->lockedRef.input.isRelative());
n["locked"].erase("__final"); n["locked"].erase("__final");
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);
@ -248,7 +251,10 @@ std::optional<FlakeRef> LockFile::isUnlocked(const fetchers::Settings & fetchSet
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.isConsideredLocked(fetchSettings) || !node->lockedRef.input.isFinal())) if (node
&& (!node->lockedRef.input.isConsideredLocked(fetchSettings)
|| !node->lockedRef.input.isFinal())
&& !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( LockedNode(

View file

@ -187,7 +187,7 @@ Currently the `type` attribute can be one of the following:
* `nixpkgs/nixos-unstable/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293` * `nixpkgs/nixos-unstable/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293`
* `sub/dir` (if a flake named `sub` is in the registry) * `sub/dir` (if a flake named `sub` is in the registry)
* `path`: arbitrary local directories. The required attribute `path` * <a name="path-fetcher"></a>`path`: arbitrary local directories. The required attribute `path`
specifies the path of the flake. The URL form is specifies the path of the flake. The URL form is
``` ```
@ -200,18 +200,38 @@ 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. On the other
hand, a flake in the `/foo` directory of a tree can use
`path:../bar` to refer to the flake in `/bar`.
Path inputs can be specified with path values in `flake.nix`. Path values are a syntax for `path` inputs, and they are converted by
1. resolving them into relative paths, relative to the base directory of `flake.nix`
2. escaping URL characters (refer to IETF RFC?)
3. prepending `path:`
Note that the allowed syntax for path values in flake `inputs` may be more restrictive than general Nix, so you may need to use `path:` if your path contains certain special characters. See [Path literals](@docroot@/language/syntax.md#path-literal)
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

@ -2,9 +2,6 @@
source ./common.sh source ./common.sh
# FIXME: this test is disabled because relative path flakes are broken. Re-enable this in #10089.
exit 0
requireGit requireGit
flakeFollowsA=$TEST_ROOT/follows/flakeA flakeFollowsA=$TEST_ROOT/follows/flakeA
@ -120,7 +117,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";
@ -133,7 +130,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
@ -338,6 +356,6 @@ json=$(nix flake metadata "$flakeFollowsCustomUrlA" --json)
rm "$flakeFollowsCustomUrlA"/flake.lock rm "$flakeFollowsCustomUrlA"/flake.lock
# if override-input is specified, lock "original" entry should contain original url # if override-input is specified, lock "original" entry should contain original url
json=$(nix flake metadata "$flakeFollowsCustomUrlA" --override-input B/C "path:./flakeB/flakeD" --json) json=$(nix flake metadata "$flakeFollowsCustomUrlA" --override-input B/C "$flakeFollowsCustomUrlD" --json)
echo "$json" | jq .locks.nodes.C.original echo "$json" | jq .locks.nodes.C.original
[[ $(echo "$json" | jq -r .locks.nodes.C.original.path) = './flakeC' ]] [[ $(echo "$json" | jq -r .locks.nodes.C.original.path) = './flakeC' ]]

View file

@ -27,6 +27,7 @@ suites += {
'shebang.sh', 'shebang.sh',
'commit-lock-file-summary.sh', 'commit-lock-file-summary.sh',
'non-flake-inputs.sh', 'non-flake-inputs.sh',
'relative-paths.sh',
], ],
'workdir': meson.current_source_dir(), 'workdir': meson.current_source_dir(),
} }

View file

@ -0,0 +1,97 @@
#!/usr/bin/env bash
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 that this still works after commiting the lock file.
git -C "$rootFlake" add sub2/flake.lock
[[ $(nix eval "$subflake2#y") = 15 ]]
# Make sure there are no content locks for relative path flakes.
(! grep "$TEST_ROOT" "$subflake2/flake.lock")
if ! isTestOnNixOS; then
(! grep "$NIX_STORE_DIR" "$subflake2/flake.lock")
fi
(! 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