Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 26 additions & 46 deletions src/libexpr/primops/fetchTree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ void emitTreeAttrs(

auto attrs = state.buildBindings(10);


state.mkStorePathString(tree.storePath, attrs.alloc(state.sOutPath));

// FIXME: support arbitrary input attributes.
Expand Down Expand Up @@ -71,48 +70,23 @@ void emitTreeAttrs(
v.mkAttrs(attrs);
}

std::string fixURI(std::string uri, EvalState & state, const std::string & defaultScheme = "file")
{
state.checkURI(uri);
if (uri.find("://") == std::string::npos) {
const auto p = ParsedURL {
.scheme = defaultScheme,
.authority = "",
.path = uri
};
return p.to_string();
} else {
return uri;
}
}

std::string fixURIForGit(std::string uri, EvalState & state)
{
/* Detects scp-style uris (e.g. git@github.com:NixOS/nix) and fixes
* them by removing the `:` and assuming a scheme of `ssh://`
* */
static std::regex scp_uri("([^/]*)@(.*):(.*)");
if (uri[0] != '/' && std::regex_match(uri, scp_uri))
return fixURI(std::regex_replace(uri, scp_uri, "$1@$2/$3"), state, "ssh");
else
return fixURI(uri, state);
}

struct FetchTreeParams {
bool emptyRevFallback = false;
bool allowNameArgument = false;
bool isFetchGit = false;
};

static void fetchTree(
EvalState & state,
const PosIdx pos,
Value * * args,
Value & v,
std::optional<std::string> type,
const FetchTreeParams & params = FetchTreeParams{}
) {
fetchers::Input input;
NixStringContext context;
std::optional<std::string> type;
if (params.isFetchGit) type = "git";

state.forceValue(*args[0], pos);

Expand Down Expand Up @@ -142,10 +116,8 @@ static void fetchTree(
if (attr.value->type() == nPath || attr.value->type() == nString) {
auto s = state.coerceToString(attr.pos, *attr.value, context, "", false, false).toOwned();
attrs.emplace(state.symbols[attr.name],
state.symbols[attr.name] == "url"
? type == "git"
? fixURIForGit(s, state)
: fixURI(s, state)
params.isFetchGit && state.symbols[attr.name] == "url"
? fixGitURL(s)
: s);
}
else if (attr.value->type() == nBool)
Expand All @@ -170,22 +142,24 @@ static void fetchTree(
"while evaluating the first argument passed to the fetcher",
false, false).toOwned();

if (type == "git") {
if (params.isFetchGit) {
fetchers::Attrs attrs;
attrs.emplace("type", "git");
attrs.emplace("url", fixURIForGit(url, state));
attrs.emplace("url", fixGitURL(url));
input = fetchers::Input::fromAttrs(std::move(attrs));
} else {
input = fetchers::Input::fromURL(fixURI(url, state));
input = fetchers::Input::fromURL(url);
}
}

if (!evalSettings.pureEval && !input.isDirect())
if (!evalSettings.pureEval && !input.isDirect() && experimentalFeatureSettings.isEnabled(Xp::Flakes))
input = lookupInRegistries(state.store, input).first;

if (evalSettings.pureEval && !input.isLocked())
state.debugThrowLastTrace(EvalError("in pure evaluation mode, 'fetchTree' requires a locked input, at %s", state.positions[pos]));

state.checkURI(input.toURLString());

auto [tree, input2] = input.fetch(state.store);

state.allowPath(tree.storePath);
Expand All @@ -195,20 +169,20 @@ static void fetchTree(

static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
fetchTree(state, pos, args, v, std::nullopt, FetchTreeParams { .allowNameArgument = false });
fetchTree(state, pos, args, v, { });
}

static RegisterPrimOp primop_fetchTree({
.name = "fetchTree",
.args = {"input"},
.doc = R"(
Fetch a source tree or a plain file using one of the supported backends.
*input* can be an attribute set representation of [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references) or a URL.
The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is allowed.
*input* must be a [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references), either in attribute set representation or in the URL-like syntax.
The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is enabled.

Here are some examples of how to use `fetchTree`:

- Fetch a GitHub repository:
- Fetch a GitHub repository using the attribute set representation:

```nix
builtins.fetchTree {
Expand All @@ -219,7 +193,7 @@ static RegisterPrimOp primop_fetchTree({
}
```

This evaluates to attribute set:
This evaluates to the following attribute set:

```
{
Expand All @@ -231,10 +205,11 @@ static RegisterPrimOp primop_fetchTree({
shortRev = "ae2e6b3";
}
```
- Fetch a single file from a URL:

```nix
builtins.fetchTree "https://example.com/"
- Fetch the same GitHub repository using the URL-like syntax:

```
builtins.fetchTree "github:NixOS/nixpkgs/ae2e6b3958682513d28f7d633734571fb18285dd"
```
)",
.fun = prim_fetchTree,
Expand Down Expand Up @@ -388,7 +363,12 @@ static RegisterPrimOp primop_fetchTarball({

static void prim_fetchGit(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
fetchTree(state, pos, args, v, "git", FetchTreeParams { .emptyRevFallback = true, .allowNameArgument = true });
fetchTree(state, pos, args, v,
FetchTreeParams {
.emptyRevFallback = true,
.allowNameArgument = true,
.isFetchGit = true
});
}

static RegisterPrimOp primop_fetchGit({
Expand Down
4 changes: 3 additions & 1 deletion src/libfetchers/git.cc
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ struct GitInputScheme : InputScheme
if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow" && name != "submodules" && name != "lastModified" && name != "revCount" && name != "narHash" && name != "allRefs" && name != "name" && name != "dirtyRev" && name != "dirtyShortRev")
throw Error("unsupported Git input attribute '%s'", name);

parseURL(getStrAttr(attrs, "url"));
maybeGetBoolAttr(attrs, "shallow");
maybeGetBoolAttr(attrs, "submodules");
maybeGetBoolAttr(attrs, "allRefs");
Expand All @@ -305,6 +304,9 @@ struct GitInputScheme : InputScheme

Input input;
input.attrs = attrs;
auto url = fixGitURL(getStrAttr(attrs, "url"));
parseURL(url);
input.attrs["url"] = url;
return input;
}

Expand Down
17 changes: 17 additions & 0 deletions src/libutil/url.cc
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,21 @@ ParsedUrlScheme parseUrlScheme(std::string_view scheme)
};
}

std::string fixGitURL(const std::string & url)
{
std::regex scpRegex("([^/]*)@(.*):(.*)");
if (!hasPrefix(url, "/") && std::regex_match(url, scpRegex))
return std::regex_replace(url, scpRegex, "ssh://$1@$2/$3");
else {
if (url.find("://") == std::string::npos) {
return (ParsedURL {
.scheme = "file",
.authority = "",
.path = url
}).to_string();
} else
return url;
}
}

}
5 changes: 5 additions & 0 deletions src/libutil/url.hh
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ struct ParsedUrlScheme {

ParsedUrlScheme parseUrlScheme(std::string_view scheme);

/* Detects scp-style uris (e.g. git@github.com:NixOS/nix) and fixes
them by removing the `:` and assuming a scheme of `ssh://`. Also
changes absolute paths into file:// URLs. */
std::string fixGitURL(const std::string & url);

}
6 changes: 6 additions & 0 deletions src/nix/flake.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ Currently the `type` attribute can be one of the following:
git(+http|+https|+ssh|+git|+file|):(//<server>)?<path>(\?<params>)?
```

or

```
<user>@<server>:<path>
```

The `ref` attribute defaults to resolving the `HEAD` reference.

The `rev` attribute must denote a commit that exists in the branch
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/fetchGit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ unset _NIX_FORCE_HTTP
path0=$(nix eval --impure --raw --expr "(builtins.fetchGit file://$TEST_ROOT/worktree).outPath")
path0_=$(nix eval --impure --raw --expr "(builtins.fetchTree { type = \"git\"; url = file://$TEST_ROOT/worktree; }).outPath")
[[ $path0 = $path0_ ]]
path0_=$(nix eval --impure --raw --expr "(builtins.fetchTree git+file://$TEST_ROOT/worktree).outPath")
[[ $path0 = $path0_ ]]
export _NIX_FORCE_HTTP=1
[[ $(tail -n 1 $path0/hello) = "hello" ]]

Expand Down
4 changes: 4 additions & 0 deletions tests/nixos/github-flakes.nix
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ in
client.succeed("nix registry pin nixpkgs")
client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2")

# Test fetchTree on a github URL.
hash = client.succeed(f"nix eval --raw --expr '(fetchTree {info['url']}).narHash'")
assert hash == info['locked']['narHash']

# Shut down the web server. The flake should be cached on the client.
github.succeed("systemctl stop httpd.service")

Expand Down