Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for subflakes #10089

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/libexpr/flake/call-flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,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.
fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
Expand Down
184 changes: 134 additions & 50 deletions src/libexpr/flake/flake.cc

Large diffs are not rendered by default.

68 changes: 38 additions & 30 deletions src/libexpr/flake/flakeref.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ FlakeRef parseFlakeRef(
const std::string & url,
const std::optional<Path> & baseDir,
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 != "")
throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url);
return flakeRef;
Expand All @@ -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(
const std::string & url,
const std::optional<Path> & baseDir,
bool allowMissing,
bool isFlake)
bool isFlake,
bool allowRelative)
{
std::string path = url;
std::string fragment = "";
Expand All @@ -90,7 +105,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
fragment = percentDecode(url.substr(fragmentStart+1));
}
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) {
Expand Down Expand Up @@ -154,6 +169,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
.authority = "",
.path = flakeRoot,
.query = query,
.fragment = fragment,
};

if (subdir != "") {
Expand All @@ -165,9 +181,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
if (pathExists(flakeRoot + "/.git/shallow"))
parsedURL.query.insert_or_assign("shallow", "1");

return std::make_pair(
FlakeRef(fetchers::Input::fromURL(parsedURL), getOr(parsedURL.query, "dir", "")),
fragment);
return fromParsedURL(std::move(parsedURL), isFlake);
}

subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir);
Expand All @@ -176,25 +190,30 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
}

} else {
if (!hasPrefix(path, "/"))
if (!allowRelative && !hasPrefix(path, "/"))
throw BadURL("flake reference '%s' is not an absolute path", url);
path = canonPath(path + "/" + getOr(query, "dir", ""));
}

fetchers::Attrs attrs;
attrs.insert_or_assign("type", "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
'flake:<flake-id>?ref=<ref>&rev=<rev>'. */
std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
const std::string & url,
bool isFlake
)
bool isFlake)
{
std::smatch match;

Expand Down Expand Up @@ -223,32 +242,21 @@ std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef(
const std::string & url,
const std::optional<Path> & baseDir,
bool isFlake
)
bool isFlake)
{
ParsedURL parsedURL;
try {
parsedURL = parseURL(url);
return fromParsedURL(parseURL(url), isFlake);
} catch (BadURL &) {
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(
const std::string & url,
const std::optional<Path> & baseDir,
bool allowMissing,
bool isFlake)
bool isFlake,
bool allowRelative)
{
using namespace fetchers;

Expand All @@ -259,7 +267,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
} else if (auto res = parseURLFlakeRef(url, baseDir, isFlake)) {
return *res;
} else {
return parsePathFlakeRefWithFragment(url, baseDir, allowMissing, isFlake);
return parsePathFlakeRefWithFragment(url, baseDir, allowMissing, isFlake, allowRelative);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/libexpr/flake/flakeref.hh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ FlakeRef parseFlakeRef(
const std::string & url,
const std::optional<Path> & baseDir = {},
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)
Expand All @@ -90,7 +91,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 allowRelative = false);

/**
* @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory)
Expand Down
9 changes: 7 additions & 2 deletions src/libexpr/flake/lockfile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ LockedNode::LockedNode(const nlohmann::json & json)
: lockedRef(getFlakeRef(json, "locked", "info")) // FIXME: remove "info"
, originalRef(getFlakeRef(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.isLocked())
if (!lockedRef.input.isLocked() && !lockedRef.input.isRelative())
throw Error("lock file contains unlocked input '%s'",
fetchers::attrsToJSON(lockedRef.input.toAttrs()));
}
Expand Down Expand Up @@ -184,6 +185,8 @@ std::pair<nlohmann::json, LockFile::KeyMap> LockFile::toJSON() const
n["locked"] = fetchers::attrsToJSON(lockedNode->lockedRef.toAttrs());
if (!lockedNode->isFlake)
n["flake"] = false;
if (lockedNode->parentPath)
n["parent"] = *lockedNode->parentPath;
}

nodes[key] = std::move(n);
Expand Down Expand Up @@ -230,7 +233,9 @@ std::optional<FlakeRef> LockFile::isUnlocked() const
for (auto & i : nodes) {
if (i == ref<const Node>(root)) continue;
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;
}

Expand Down
12 changes: 10 additions & 2 deletions src/libexpr/flake/lockfile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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(const nlohmann::json & json);
Expand Down
5 changes: 2 additions & 3 deletions src/libexpr/primops/fetchTree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ void emitTreeAttrs(

// FIXME: support arbitrary input attributes.

auto narHash = input.getNarHash();
assert(narHash);
attrs.alloc("narHash").mkString(narHash->to_string(HashFormat::SRI, true));
if (auto narHash = input.getNarHash())
attrs.alloc("narHash").mkString(narHash->to_string(HashFormat::SRI, true));

if (input.getType() == "git")
attrs.alloc("submodules").mkBool(
Expand Down
6 changes: 6 additions & 0 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ bool Input::isLocked() const
return scheme && scheme->isLocked(*this);
}

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 @@ -31,11 +31,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;

public:
/**
* Create an `Input` from a URL.
Expand Down Expand Up @@ -73,6 +68,12 @@ public:
*/
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 contains(const Input & other) const;
Expand Down Expand Up @@ -220,6 +221,9 @@ struct InputScheme
* if there is a mismatch.
*/
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);
Expand Down
25 changes: 4 additions & 21 deletions src/libfetchers/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,31 +116,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.abs());

if (storePath)
store->addTempRoot(*storePath);
Expand All @@ -149,7 +132,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.abs(), sink, defaultPathFilter);
});
storePath = store->addToStoreFromDump(*src, "source");
}
Expand Down
25 changes: 18 additions & 7 deletions src/nix/flake.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:`
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.

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`.
Expand Down