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:
commit
043df13f72
14 changed files with 358 additions and 114 deletions
12
doc/manual/rl-next/relative-path-flakes.md
Normal file
12
doc/manual/rl-next/relative-path-flakes.md
Normal 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.
|
|
@ -41,10 +41,17 @@ let
|
|||
(key: node:
|
||||
let
|
||||
|
||||
parentNode = allNodes.${getInputByPath lockFile.root node.parent};
|
||||
|
||||
sourceInfo =
|
||||
if overrides ? ${key}
|
||||
then
|
||||
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
|
||||
# FIXME: remove obsolete node.info.
|
||||
# Note: lock file entries are always final.
|
||||
|
|
|
@ -166,6 +166,12 @@ bool Input::isFinal() const
|
|||
return maybeGetBoolAttr(attrs, "__final").value_or(false);
|
||||
}
|
||||
|
||||
std::optional<std::string> Input::isRelative() const
|
||||
{
|
||||
assert(scheme);
|
||||
return scheme->isRelative(*this);
|
||||
}
|
||||
|
||||
Attrs Input::toAttrs() const
|
||||
{
|
||||
return attrs;
|
||||
|
|
|
@ -41,11 +41,6 @@ struct Input
|
|||
std::shared_ptr<InputScheme> scheme; // note: can be null
|
||||
Attrs attrs;
|
||||
|
||||
/**
|
||||
* path of the parent of this input, used for relative path resolution
|
||||
*/
|
||||
std::optional<Path> parent;
|
||||
|
||||
/**
|
||||
* Cached result of getFingerprint().
|
||||
*/
|
||||
|
@ -104,6 +99,12 @@ public:
|
|||
bool isConsideredLocked(
|
||||
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
|
||||
* it will not add, remove or change any attributes. (See
|
||||
|
@ -269,6 +270,9 @@ struct InputScheme
|
|||
|
||||
virtual bool isLocked(const Input & input) const
|
||||
{ return false; }
|
||||
|
||||
virtual std::optional<std::string> isRelative(const Input & input) const
|
||||
{ return std::nullopt; }
|
||||
};
|
||||
|
||||
void registerInputScheme(std::shared_ptr<InputScheme> && fetcher);
|
||||
|
|
|
@ -91,10 +91,10 @@ struct PathInputScheme : InputScheme
|
|||
std::string_view contents,
|
||||
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");
|
||||
if (isAbsolute(path))
|
||||
|
@ -108,12 +108,12 @@ struct PathInputScheme : InputScheme
|
|||
return (bool) input.getNarHash();
|
||||
}
|
||||
|
||||
CanonPath getAbsPath(const Input & input) const
|
||||
std::filesystem::path getAbsPath(const Input & input) const
|
||||
{
|
||||
auto path = getStrAttr(input.attrs, "path");
|
||||
|
||||
if (path[0] == '/')
|
||||
return CanonPath(path);
|
||||
if (isAbsolute(path))
|
||||
return canonPath(path);
|
||||
|
||||
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
|
||||
{
|
||||
Input input(_input);
|
||||
std::string absPath;
|
||||
auto path = getStrAttr(input.attrs, "path");
|
||||
|
||||
if (path[0] != '/') {
|
||||
if (!input.parent)
|
||||
throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string());
|
||||
auto absPath = getAbsPath(input);
|
||||
|
||||
auto parent = canonPath(*input.parent);
|
||||
|
||||
// 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));
|
||||
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying '%s' to the store", absPath));
|
||||
|
||||
// FIXME: check whether access to 'path' is allowed.
|
||||
auto storePath = store->maybeParseStorePath(absPath);
|
||||
auto storePath = store->maybeParseStorePath(absPath.string());
|
||||
|
||||
if (storePath)
|
||||
store->addTempRoot(*storePath);
|
||||
|
@ -154,7 +137,7 @@ struct PathInputScheme : InputScheme
|
|||
if (!storePath || storePath->name() != "source" || !store->isValidPath(*storePath)) {
|
||||
// FIXME: try to substitute storePath.
|
||||
auto src = sinkToSource([&](Sink & sink) {
|
||||
mtime = dumpPathAndGetMtime(absPath, sink, defaultPathFilter);
|
||||
mtime = dumpPathAndGetMtime(absPath.string(), sink, defaultPathFilter);
|
||||
});
|
||||
storePath = store->addToStoreFromDump(*src, "source");
|
||||
}
|
||||
|
@ -176,7 +159,7 @@ struct PathInputScheme : InputScheme
|
|||
store object and the subpath. */
|
||||
auto path = getAbsPath(input);
|
||||
try {
|
||||
auto [storePath, subPath] = store->toStorePath(path.abs());
|
||||
auto [storePath, subPath] = store->toStorePath(path.string());
|
||||
auto info = store->queryPathInfo(storePath);
|
||||
return fmt("path:%s:%s", info->narHash.to_string(HashFormat::Base16, false), subPath);
|
||||
} catch (Error &) {
|
||||
|
|
|
@ -102,12 +102,19 @@ static void expectType(EvalState & state, ValueType type,
|
|||
}
|
||||
|
||||
static std::map<FlakeId, FlakeInput> parseFlakeInputs(
|
||||
EvalState & state, Value * value, const PosIdx pos,
|
||||
const std::optional<Path> & baseDir, InputPath lockRootPath);
|
||||
EvalState & state,
|
||||
Value * value,
|
||||
const PosIdx pos,
|
||||
InputPath lockRootPath,
|
||||
const SourcePath & flakeDir);
|
||||
|
||||
static FlakeInput parseFlakeInput(EvalState & state,
|
||||
std::string_view inputName, Value * value, const PosIdx pos,
|
||||
const std::optional<Path> & baseDir, InputPath lockRootPath)
|
||||
static FlakeInput parseFlakeInput(
|
||||
EvalState & state,
|
||||
std::string_view inputName,
|
||||
Value * value,
|
||||
const PosIdx pos,
|
||||
InputPath lockRootPath,
|
||||
const SourcePath & flakeDir)
|
||||
{
|
||||
expectType(state, nAttrs, *value, pos);
|
||||
|
||||
|
@ -124,14 +131,25 @@ static FlakeInput parseFlakeInput(EvalState & state,
|
|||
for (auto & attr : *value->attrs()) {
|
||||
try {
|
||||
if (attr.name == sUrl) {
|
||||
expectType(state, nString, *attr.value, attr.pos);
|
||||
url = attr.value->string_view();
|
||||
forceTrivialValue(state, *attr.value, pos);
|
||||
if (attr.value->type() == nString)
|
||||
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);
|
||||
} else if (attr.name == sFlake) {
|
||||
expectType(state, nBool, *attr.value, attr.pos);
|
||||
input.isFlake = attr.value->boolean();
|
||||
} 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) {
|
||||
expectType(state, nString, *attr.value, attr.pos);
|
||||
auto follows(parseInputPath(attr.value->c_str()));
|
||||
|
@ -189,7 +207,7 @@ static FlakeInput parseFlakeInput(EvalState & state,
|
|||
if (!attrs.empty())
|
||||
throw Error("unexpected flake input attribute '%s', at %s", attrs.begin()->first, state.positions[pos]);
|
||||
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)
|
||||
|
@ -199,8 +217,11 @@ static FlakeInput parseFlakeInput(EvalState & state,
|
|||
}
|
||||
|
||||
static std::map<FlakeId, FlakeInput> parseFlakeInputs(
|
||||
EvalState & state, Value * value, const PosIdx pos,
|
||||
const std::optional<Path> & baseDir, InputPath lockRootPath)
|
||||
EvalState & state,
|
||||
Value * value,
|
||||
const PosIdx pos,
|
||||
InputPath lockRootPath,
|
||||
const SourcePath & flakeDir)
|
||||
{
|
||||
std::map<FlakeId, FlakeInput> inputs;
|
||||
|
||||
|
@ -212,8 +233,8 @@ static std::map<FlakeId, FlakeInput> parseFlakeInputs(
|
|||
state.symbols[inputAttr.name],
|
||||
inputAttr.value,
|
||||
inputAttr.pos,
|
||||
baseDir,
|
||||
lockRootPath));
|
||||
lockRootPath,
|
||||
flakeDir));
|
||||
}
|
||||
|
||||
return inputs;
|
||||
|
@ -227,7 +248,8 @@ static Flake readFlake(
|
|||
const SourcePath & rootDir,
|
||||
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.
|
||||
Value vInfo;
|
||||
|
@ -248,7 +270,7 @@ static Flake readFlake(
|
|||
auto sInputs = state.symbols.create("inputs");
|
||||
|
||||
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");
|
||||
|
||||
|
@ -388,13 +410,29 @@ LockedFlake lockFlake(
|
|||
|
||||
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> overridesUsed, updatesUsed;
|
||||
std::map<ref<Node>, SourcePath> nodePaths;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -408,7 +446,7 @@ LockedFlake lockFlake(
|
|||
const InputPath & inputPathPrefix,
|
||||
std::shared_ptr<const Node> oldNode,
|
||||
const InputPath & lockRootPath,
|
||||
const Path & parentPath,
|
||||
const SourcePath & sourcePath,
|
||||
bool trustLock)>
|
||||
computeLocks;
|
||||
|
||||
|
@ -424,7 +462,8 @@ LockedFlake lockFlake(
|
|||
copied. */
|
||||
std::shared_ptr<const Node> oldNode,
|
||||
const InputPath & lockRootPath,
|
||||
const Path & parentPath,
|
||||
/* The source path of this node's flake. */
|
||||
const SourcePath & sourcePath,
|
||||
bool trustLock)
|
||||
{
|
||||
debug("computing lock file node '%s'", printInputPath(inputPathPrefix));
|
||||
|
@ -436,7 +475,12 @@ LockedFlake lockFlake(
|
|||
auto inputPath(inputPathPrefix);
|
||||
inputPath.push_back(id);
|
||||
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);
|
||||
bool hasOverride = i != overrides.end();
|
||||
bool hasCliOverride = explicitCliOverrides.contains(inputPath);
|
||||
if (hasOverride) {
|
||||
if (hasOverride)
|
||||
overridesUsed.insert(inputPath);
|
||||
// Respect the “flakeness” of the input even if we
|
||||
// override it
|
||||
i->second.isFlake = input2.isFlake;
|
||||
}
|
||||
auto & input = hasOverride ? i->second : input2;
|
||||
auto input = hasOverride ? i->second.input : input2;
|
||||
|
||||
/* Resolve relative 'path:' inputs relative to
|
||||
the source path of the overrider. */
|
||||
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
|
||||
path we haven't processed yet. */
|
||||
|
@ -490,6 +539,33 @@ LockedFlake lockFlake(
|
|||
|
||||
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?
|
||||
And the input is not in updateInputs? */
|
||||
std::shared_ptr<LockedNode> oldLock;
|
||||
|
@ -503,6 +579,7 @@ LockedFlake lockFlake(
|
|||
|
||||
if (oldLock
|
||||
&& oldLock->originalRef == *input.ref
|
||||
&& oldLock->parentPath == overridenParentPath
|
||||
&& !hasCliOverride)
|
||||
{
|
||||
debug("keeping existing input '%s'", inputPathS);
|
||||
|
@ -511,7 +588,10 @@ LockedFlake lockFlake(
|
|||
didn't change and there is no override from a
|
||||
higher level flake. */
|
||||
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);
|
||||
|
||||
|
@ -563,11 +643,11 @@ LockedFlake lockFlake(
|
|||
}
|
||||
|
||||
if (mustRefetch) {
|
||||
auto inputFlake = getFlake(state, oldLock->lockedRef, false, flakeCache, inputPath);
|
||||
auto inputFlake = getInputFlake();
|
||||
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 {
|
||||
computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, parentPath, true);
|
||||
computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, sourcePath, true);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -575,7 +655,9 @@ LockedFlake lockFlake(
|
|||
this input. */
|
||||
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);
|
||||
|
||||
/* 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;
|
||||
|
||||
if (input.isFlake) {
|
||||
Path localPath = parentPath;
|
||||
FlakeRef localRef = *input.ref;
|
||||
auto inputFlake = getInputFlake();
|
||||
|
||||
// If this input is a path, recurse it down.
|
||||
// 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);
|
||||
auto childNode = make_ref<LockedNode>(inputFlake.lockedRef, ref, true, overridenParentPath);
|
||||
|
||||
node->inputs.insert_or_assign(id, childNode);
|
||||
|
||||
|
@ -620,17 +694,26 @@ LockedFlake lockFlake(
|
|||
? std::dynamic_pointer_cast<const Node>(oldLock)
|
||||
: readLockFile(settings, state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(),
|
||||
oldLock ? lockRootPath : inputPath,
|
||||
localPath,
|
||||
inputFlake.path,
|
||||
false);
|
||||
}
|
||||
|
||||
else {
|
||||
auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree(
|
||||
state, *input.ref, useRegistries, flakeCache);
|
||||
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(
|
||||
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);
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
computeLocks(
|
||||
|
@ -654,7 +734,7 @@ LockedFlake lockFlake(
|
|||
{},
|
||||
lockFlags.recreateLockFile ? nullptr : oldLockFile.root.get_ptr(),
|
||||
{},
|
||||
parentPath,
|
||||
flake.path,
|
||||
false);
|
||||
|
||||
for (auto & i : lockFlags.inputOverrides)
|
||||
|
|
|
@ -48,9 +48,10 @@ FlakeRef parseFlakeRef(
|
|||
const std::string & url,
|
||||
const std::optional<Path> & baseDir,
|
||||
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 != "")
|
||||
throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url);
|
||||
return flakeRef;
|
||||
|
@ -87,7 +88,8 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
|
|||
const std::string & url,
|
||||
const std::optional<Path> & baseDir,
|
||||
bool allowMissing,
|
||||
bool isFlake)
|
||||
bool isFlake,
|
||||
bool preserveRelativePaths)
|
||||
{
|
||||
static std::regex pathFlakeRegex(
|
||||
R"(([^?#]*)(\?([^#]*))?(#(.*))?)",
|
||||
|
@ -178,9 +180,8 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
|
|||
}
|
||||
|
||||
} else {
|
||||
if (!isAbsolute(path))
|
||||
if (!preserveRelativePaths && !isAbsolute(path))
|
||||
throw BadURL("flake reference '%s' is not an absolute path", url);
|
||||
path = canonPath(path + "/" + getOr(query, "dir", ""));
|
||||
}
|
||||
|
||||
return fromParsedURL(fetchSettings, {
|
||||
|
@ -199,8 +200,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
|
|||
static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
|
||||
const fetchers::Settings & fetchSettings,
|
||||
const std::string & url,
|
||||
bool isFlake
|
||||
)
|
||||
bool isFlake)
|
||||
{
|
||||
std::smatch match;
|
||||
|
||||
|
@ -228,8 +228,7 @@ std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef(
|
|||
const fetchers::Settings & fetchSettings,
|
||||
const std::string & url,
|
||||
const std::optional<Path> & baseDir,
|
||||
bool isFlake
|
||||
)
|
||||
bool isFlake)
|
||||
{
|
||||
try {
|
||||
auto parsed = parseURL(url);
|
||||
|
@ -248,7 +247,8 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
|||
const std::string & url,
|
||||
const std::optional<Path> & baseDir,
|
||||
bool allowMissing,
|
||||
bool isFlake)
|
||||
bool isFlake,
|
||||
bool preserveRelativePaths)
|
||||
{
|
||||
using namespace fetchers;
|
||||
|
||||
|
@ -257,7 +257,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
|||
} else if (auto res = parseURLFlakeRef(fetchSettings, url, baseDir, isFlake)) {
|
||||
return *res;
|
||||
} else {
|
||||
return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake);
|
||||
return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake, preserveRelativePaths);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,8 @@ FlakeRef parseFlakeRef(
|
|||
const std::string & url,
|
||||
const std::optional<Path> & baseDir = {},
|
||||
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)
|
||||
|
@ -102,7 +103,8 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
|||
const std::string & url,
|
||||
const std::optional<Path> & baseDir = {},
|
||||
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)
|
||||
|
|
|
@ -43,8 +43,9 @@ LockedNode::LockedNode(
|
|||
: lockedRef(getFlakeRef(fetchSettings, json, "locked", "info")) // FIXME: remove "info"
|
||||
, originalRef(getFlakeRef(fetchSettings, json, "original", nullptr))
|
||||
, 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.",
|
||||
fetchers::attrsToJSON(lockedRef.input.toAttrs()));
|
||||
|
||||
|
@ -198,10 +199,12 @@ std::pair<nlohmann::json, LockFile::KeyMap> LockFile::toJSON() const
|
|||
/* For backward compatibility, omit the "__final"
|
||||
attribute. We never allow non-final inputs in lock files
|
||||
anyway. */
|
||||
assert(lockedNode->lockedRef.input.isFinal());
|
||||
assert(lockedNode->lockedRef.input.isFinal() || lockedNode->lockedRef.input.isRelative());
|
||||
n["locked"].erase("__final");
|
||||
if (!lockedNode->isFlake)
|
||||
n["flake"] = false;
|
||||
if (lockedNode->parentPath)
|
||||
n["parent"] = *lockedNode->parentPath;
|
||||
}
|
||||
|
||||
nodes[key] = std::move(n);
|
||||
|
@ -248,7 +251,10 @@ std::optional<FlakeRef> LockFile::isUnlocked(const fetchers::Settings & fetchSet
|
|||
for (auto & i : nodes) {
|
||||
if (i == ref<const Node>(root)) continue;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,11 +38,19 @@ struct LockedNode : Node
|
|||
FlakeRef lockedRef, originalRef;
|
||||
bool isFlake = true;
|
||||
|
||||
/* The node relative to which relative source paths
|
||||
(e.g. 'path:../foo') are interpreted. */
|
||||
std::optional<InputPath> parentPath;
|
||||
|
||||
LockedNode(
|
||||
const FlakeRef & lockedRef,
|
||||
const FlakeRef & originalRef,
|
||||
bool isFlake = true)
|
||||
: lockedRef(lockedRef), originalRef(originalRef), isFlake(isFlake)
|
||||
bool isFlake = true,
|
||||
std::optional<InputPath> parentPath = {})
|
||||
: lockedRef(lockedRef)
|
||||
, originalRef(originalRef)
|
||||
, isFlake(isFlake)
|
||||
, parentPath(parentPath)
|
||||
{ }
|
||||
|
||||
LockedNode(
|
||||
|
|
|
@ -187,7 +187,7 @@ Currently the `type` attribute can be one of the following:
|
|||
* `nixpkgs/nixos-unstable/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293`
|
||||
* `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
|
||||
|
||||
```
|
||||
|
@ -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:`
|
||||
prefix is implied and can be omitted.
|
||||
|
||||
*path* generally must be an absolute path. However, on the command
|
||||
line, it can be a relative path (e.g. `.` or `./foo`) which is
|
||||
interpreted as relative to the current directory. In this case, it
|
||||
must start with `.` to avoid ambiguity with registry lookups
|
||||
(e.g. `nixpkgs` is a registry lookup; `./nixpkgs` is a relative
|
||||
path).
|
||||
If *path* is a relative path (i.e. if it does not start with `/`),
|
||||
it is interpreted as follows:
|
||||
|
||||
- If *path* is a command line argument, it is interpreted relative
|
||||
to the current directory.
|
||||
|
||||
- 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:
|
||||
|
||||
* `path:/home/user/sub/dir`
|
||||
* `/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
|
||||
by the attribute `url`.
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
source ./common.sh
|
||||
|
||||
# FIXME: this test is disabled because relative path flakes are broken. Re-enable this in #10089.
|
||||
exit 0
|
||||
|
||||
requireGit
|
||||
|
||||
flakeFollowsA=$TEST_ROOT/follows/flakeA
|
||||
|
@ -120,7 +117,7 @@ nix flake lock $flakeFollowsA
|
|||
[[ $(jq -c .nodes.B.inputs.foobar $flakeFollowsA/flake.lock) = '"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
|
||||
{
|
||||
description = "Flake A";
|
||||
|
@ -133,7 +130,28 @@ EOF
|
|||
|
||||
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.
|
||||
cat >$flakeFollowsA/flake.nix <<EOF
|
||||
|
@ -338,6 +356,6 @@ json=$(nix flake metadata "$flakeFollowsCustomUrlA" --json)
|
|||
rm "$flakeFollowsCustomUrlA"/flake.lock
|
||||
|
||||
# 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 -r .locks.nodes.C.original.path) = './flakeC' ]]
|
||||
|
|
|
@ -27,6 +27,7 @@ suites += {
|
|||
'shebang.sh',
|
||||
'commit-lock-file-summary.sh',
|
||||
'non-flake-inputs.sh',
|
||||
'relative-paths.sh',
|
||||
],
|
||||
'workdir': meson.current_source_dir(),
|
||||
}
|
||||
|
|
97
tests/functional/flakes/relative-paths.sh
Normal file
97
tests/functional/flakes/relative-paths.sh
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue