Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b2be6fe
Improve support for subflakes
edolstra Apr 23, 2024
49f592d
call-flake.nix: Fix relative path resolution
edolstra May 17, 2024
3180671
Allow the 'url' flake input attribute to be a path literal
edolstra May 17, 2024
21fc07c
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Sep 16, 2024
09d7197
shellcheck
edolstra Sep 16, 2024
71d4bb8
parentPath -> parentInputPath
edolstra Sep 16, 2024
91e7d49
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Sep 23, 2024
f206325
tests/functional/flakes/relative-paths.sh: Fix build failure in hydra…
edolstra Sep 25, 2024
0b00bf7
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Nov 22, 2024
00b99b8
Remove FIXME
edolstra Nov 22, 2024
985b2f9
Remove FIXME
edolstra Nov 27, 2024
8534c42
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Dec 18, 2024
9223d64
Remove dead code
edolstra Dec 23, 2024
75cda2d
Document path values in inputs
edolstra Jan 7, 2025
e8c7dd9
Rename allowRelative -> preserveRelativePaths
edolstra Jan 7, 2025
0792152
Rename Override -> OverrideTarget
edolstra Jan 7, 2025
ef2739b
Example of referencing parent directories
edolstra Jan 7, 2025
d329b26
Fix manual
edolstra Jan 7, 2025
cd0127f
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Jan 13, 2025
6cc5b48
Add release note
edolstra Jan 14, 2025
550fe88
Merge remote-tracking branch 'origin/master' into relative-flakes
edolstra Jan 16, 2025
521667e
Fix follow-paths test
edolstra Jan 16, 2025
5d03ef9
PathInputSchema::getAbsPath(): Return std::filesystem::path
edolstra Jan 16, 2025
8b1fb92
flakes.md: Fix indentation that broke the list
edolstra Jan 16, 2025
db46d40
Update release note
edolstra Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/manual/rl-next/relative-path-flakes.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions src/libexpr/call-flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +44 to +54
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces an unnecessary dependency from the current sourceInfo onto the result of outputs of the parent flake. It should only be strict in the fetchTree result of the parent, not the outputs.

The full chain (all WHNF) is something like sourceInfo (current) -> parentNode -> allNodes.${...} -> if (line 81) -> result -> outputs // ... -> outputs.

Reproducer:

mkdir -p ~/tmp/repro-10089/example
cd ~/tmp/repro-10089
git init
echo '{ outputs = _: { }; }' >flake.nix
cat >example/flake.nix <<EOF
{
  inputs.parent.url = ../.;
  outputs = { self, parent, nixpkgs, ... }: builtins.seq parent {};
}
EOF
git add -N .
cd example
# might succeed:
nix build
# fails:
nix build

->

       error: stack overflow; max-call-depth exceeded

The fact that it succeeds the first time suggests that a freshly locked lock is treated in a significantly different way regarding flake directories, which may be a separate problem if there's other differences than just this failing.
nix build --recreate-lock-file --no-write-lock-file always works.

else
# FIXME: remove obsolete node.info.
# Note: lock file entries are always final.
Expand Down
6 changes: 6 additions & 0 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions src/libfetchers/fetchers.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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().
*/
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the expr / flake layer.
Open issue?


/**
* Return whether this is a "final" input, meaning that fetching
* it will not add, remove or change any attributes. (See
Expand Down Expand Up @@ -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);
Expand Down
37 changes: 10 additions & 27 deletions src/libfetchers/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -108,44 +108,27 @@ 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());
}

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 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;
auto absPath = getAbsPath(input);

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);
Expand All @@ -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");
}
Expand All @@ -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 &) {
Expand Down
Loading