Skip to content

Commit 68976e9

Browse files
bqvThéophane Hufschmitt
authored andcommitted
fetchTree: Allow fetching plain files
Replace the `tarball` fetcher by a more generic `url` one, mostly similar except that it takes an extra `unpack` parameter (false by default) to specify whether the target is an archive to extract or a plain file. Also adds a few convolutions for backwards compatibility: - The `tarball` fetcher type is kept. It’s now an alias for `url`, but with `unpack` defaulting to `true` - Flake references that used to resolve to a `tarball` fetcher (i.e. `http(s)://` and `file://` urls whose path ends with a known archive extension) still do, meaning that 1. They are still extracted by default (which is both a backwards-compatibility measure, and very handy) 2. The generated lockfiles are still compatible with older Nix versions who don’t know of the `url` fetcher Fix #3785 Co-Authored-By: Tony Olagbaiye <[email protected]>
1 parent 78dc64e commit 68976e9

File tree

6 files changed

+181
-23
lines changed

6 files changed

+181
-23
lines changed

doc/manual/src/release-notes/rl-next.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@
2424

2525
Selecting derivation outputs using the attribute selection syntax
2626
(e.g. `nixpkgs#glibc.dev`) no longer works.
27+
28+
* `builtins.fetchTree` (and flake inputs) can now be used to fetch plain files
29+
over the `http(s)` and `file` protocols in addition to directory tarballs.

src/libfetchers/tarball.cc

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,23 +168,37 @@ std::pair<Tree, time_t> downloadTarball(
168168
};
169169
}
170170

171-
struct TarballInputScheme : InputScheme
171+
struct CurlInputScheme : InputScheme
172172
{
173+
const std::string inputType = "url";
174+
175+
// For backwards-compatibility.
176+
// An alias for `url`, except that setting type to `tarball` will cause
177+
// `unpack` to default to `true`
178+
const std::string tarballInputType = "tarball";
179+
180+
const std::string unpackParam = "unpack";
181+
173182
std::optional<Input> inputFromURL(const ParsedURL & url) override
174183
{
175184
if (url.scheme != "file" && url.scheme != "http" && url.scheme != "https") return {};
176185

177-
if (!hasSuffix(url.path, ".zip")
178-
&& !hasSuffix(url.path, ".tar")
179-
&& !hasSuffix(url.path, ".tgz")
180-
&& !hasSuffix(url.path, ".tar.gz")
181-
&& !hasSuffix(url.path, ".tar.xz")
182-
&& !hasSuffix(url.path, ".tar.bz2")
183-
&& !hasSuffix(url.path, ".tar.zst"))
184-
return {};
185-
186186
Input input;
187-
input.attrs.insert_or_assign("type", "tarball");
187+
188+
bool defaultUnpack = (hasSuffix(url.path, ".zip")
189+
|| hasSuffix(url.path, ".tar")
190+
|| hasSuffix(url.path, ".tgz")
191+
|| hasSuffix(url.path, ".tar.gz")
192+
|| hasSuffix(url.path, ".tar.xz")
193+
|| hasSuffix(url.path, ".tar.bz2")
194+
|| hasSuffix(url.path, ".tar.zst"));
195+
196+
if (auto unpack = url.query.find(unpackParam); unpack != url.query.end())
197+
input.attrs.insert_or_assign(unpackParam, Explicit<bool> { unpack->second == "1" });
198+
199+
// To keep things backwards-compatible use the "tarball" input type for
200+
// everything that used to be a "tarball" before.
201+
input.attrs.insert_or_assign("type", defaultUnpack ? tarballInputType : inputType);
188202
input.attrs.insert_or_assign("url", url.to_string());
189203
auto narHash = url.query.find("narHash");
190204
if (narHash != url.query.end())
@@ -194,14 +208,17 @@ struct TarballInputScheme : InputScheme
194208

195209
std::optional<Input> inputFromAttrs(const Attrs & attrs) override
196210
{
197-
if (maybeGetStrAttr(attrs, "type") != "tarball") return {};
211+
auto type = maybeGetStrAttr(attrs, "type");
212+
if (type != inputType && type != tarballInputType) return {};
198213

214+
std::set<std::string> allowedNames = {"type", "url", "narHash", "name", "unpack"};
199215
for (auto & [name, value] : attrs)
200-
if (name != "type" && name != "url" && /* name != "hash" && */ name != "narHash" && name != "name")
201-
throw Error("unsupported tarball input attribute '%s'", name);
216+
if (!allowedNames.count(name))
217+
throw Error("unsupported %s input attribute '%s'", *type, name);
202218

203219
Input input;
204220
input.attrs = attrs;
221+
205222
//input.locked = (bool) maybeGetStrAttr(input.attrs, "hash");
206223
return input;
207224
}
@@ -213,6 +230,8 @@ struct TarballInputScheme : InputScheme
213230
// don't have a canonical representation.
214231
if (auto narHash = input.getNarHash())
215232
url.query.insert_or_assign("narHash", narHash->to_string(SRI, true));
233+
if (auto unpack = maybeGetBoolAttr(input.attrs, unpackParam))
234+
url.query.insert_or_assign(unpackParam, unpack.value() ? "1" : "0");
216235
/*
217236
else if (auto hash = maybeGetStrAttr(input.attrs, "hash"))
218237
url.query.insert_or_assign("hash", Hash(*hash).to_string(SRI, true));
@@ -225,13 +244,32 @@ struct TarballInputScheme : InputScheme
225244
return true;
226245
}
227246

247+
/**
248+
* Whether this url input should be unpacked when we fetch it
249+
*/
250+
const bool shouldUnpack(const Input & input) const
251+
{
252+
auto type = getStrAttr(input.attrs, "type");
253+
// For backwards-compatibility, we unpack by default if the type is
254+
// set to "tarball"
255+
return maybeGetBoolAttr(input.attrs, unpackParam).value_or(type == "tarball");
256+
}
257+
228258
std::pair<StorePath, Input> fetch(ref<Store> store, const Input & input) override
229259
{
230-
auto tree = downloadTarball(store, getStrAttr(input.attrs, "url"), input.getName(), false).first;
231-
return {std::move(tree.storePath), input};
260+
StorePath outPath = [&]() {
261+
if (shouldUnpack(input)) {
262+
auto tree = downloadTarball(store, getStrAttr(input.attrs, "url"), input.getName(), false).first;
263+
return tree.storePath;
264+
} else {
265+
auto file = downloadFile(store, getStrAttr(input.attrs, "url"), input.getName(), false);
266+
return file.storePath;
267+
}
268+
}();
269+
return {std::move(outPath), input};
232270
}
233271
};
234272

235-
static auto rTarballInputScheme = OnStartup([] { registerInputScheme(std::make_unique<TarballInputScheme>()); });
273+
static auto rCurlInputScheme = OnStartup([] { registerInputScheme(std::make_unique<CurlInputScheme>()); });
236274

237275
}

src/nix/flake.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,20 @@ Currently the `type` attribute can be one of the following:
178178
`git` type, except that the URL schema must be one of `hg+http`,
179179
`hg+https`, `hg+ssh` or `hg+file`.
180180

181-
* `tarball`: Tarballs. The location of the tarball is specified by the
182-
attribute `url`.
181+
* `url`: Plain files or directory tarballs, either over http(s) or from the local
182+
disk.
183183

184-
In URL form, the schema must be `http://`, `https://` or `file://`
185-
URLs and the extension must be `.zip`, `.tar`, `.tgz`, `.tar.gz`,
186-
`.tar.xz`, `.tar.bz2` or `.tar.zst`.
184+
In URL form, the schema must be `http://`, `https://` or `file://` URLs.
185+
If the extension corresponds to a known archive format (`.zip`, `.tar`,
186+
`.tgz`, `.tar.gz`, `.tar.xz`, `.tar.bz2` or `.tar.zst`), the target will be
187+
interpreted as a directory tarball to unpack, otherwise it will be treated as
188+
a plain file.
189+
190+
The `unpack` parameter can be passed (both to the URL and the record form) to
191+
explicitely specify what to do with the target.
192+
193+
* `tarball`: Tarballs. An alias to `url`, but that will unpack its target as a
194+
directory tarball by default.
187195

188196
* `github`: A more efficient way to fetch repositories from
189197
GitHub. The following attributes are required:
@@ -393,7 +401,7 @@ inputs.nixpkgs = {
393401
};
394402
```
395403

396-
Repositories that don't contain a `flake.nix` can also be used as
404+
Repositories that don't contain a `flake.nix` or plain files can also be used as
397405
inputs, by setting the input's `flake` attribute to `false`:
398406

399407
```nix

tests/fetchTree-file.sh

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
source common.sh
2+
3+
clearStore
4+
5+
cd "$TEST_ROOT"
6+
7+
test_fetch_file () {
8+
echo foo > test_input
9+
10+
input_hash="$(nix hash path test_input)"
11+
12+
nix eval --impure --file - <<EOF
13+
let
14+
tree1 = builtins.fetchTree { type = "url"; url = "file://$PWD/test_input"; };
15+
tree2 = builtins.fetchTree { type = "tarball"; url = "file://$PWD/test_input"; unpack = false; };
16+
in
17+
assert (tree1.narHash == "$input_hash");
18+
assert (tree2.narHash == "$input_hash");
19+
tree1
20+
EOF
21+
}
22+
23+
# Make sure that `http(s)` and `file` flake inputs are properly extracted when
24+
# they should be, and treated as opaque files when they should be
25+
test_file_flake_input () {
26+
rm -fr "$TEST_ROOT/testFlake";
27+
mkdir "$TEST_ROOT/testFlake";
28+
pushd testFlake
29+
30+
mkdir inputs
31+
echo foo > inputs/test_input_file
32+
tar cfa test_input.tar.gz inputs
33+
cp test_input.tar.gz test_input_no_ext
34+
input_tarball_hash="$(nix hash path test_input.tar.gz)"
35+
input_directory_hash="$(nix hash path inputs)"
36+
37+
cat <<EOF > flake.nix
38+
{
39+
inputs.no_ext_default_no_unpack = {
40+
url = "file://$PWD/test_input_no_ext";
41+
flake = false;
42+
};
43+
inputs.no_ext_explicit_unpack = {
44+
url = "file://$PWD/test_input_no_ext?unpack=1";
45+
flake = false;
46+
};
47+
inputs.tarball_default_unpack = {
48+
url = "file://$PWD/test_input.tar.gz";
49+
flake = false;
50+
};
51+
inputs.tarball_explicit_no_unpack = {
52+
url = "file://$PWD/test_input.tar.gz?unpack=0";
53+
flake = false;
54+
};
55+
outputs = { ... }: {};
56+
}
57+
EOF
58+
59+
nix flake update
60+
nix eval --file - <<EOF
61+
with (builtins.fromJSON (builtins.readFile ./flake.lock));
62+
63+
# Url inputs whose extension doesn’t match a know archive format should
64+
# not be unpacked by default
65+
assert (nodes.no_ext_default_no_unpack.locked.type == "url");
66+
assert (nodes.no_ext_default_no_unpack.locked.unpack or false == false);
67+
assert (nodes.no_ext_default_no_unpack.locked.narHash == "$input_tarball_hash");
68+
69+
# For backwards compatibility, flake inputs that correspond to the
70+
# old 'tarball' fetcher should still have their type set to 'tarball'
71+
assert (nodes.tarball_default_unpack.locked.type == "tarball");
72+
# Unless explicitely specified, the 'unpack' parameter shouldn’t appear here
73+
# because that would break older Nix versions
74+
assert (!nodes.tarball_default_unpack.locked ? unpack);
75+
assert (nodes.tarball_default_unpack.locked.narHash == "$input_directory_hash");
76+
77+
# Explicitely passing the unpack parameter should enforce the desired behavior
78+
assert (nodes.no_ext_explicit_unpack.locked.narHash == nodes.tarball_default_unpack.locked.narHash);
79+
assert (nodes.tarball_explicit_no_unpack.locked.narHash == nodes.no_ext_default_no_unpack.locked.narHash);
80+
true
81+
EOF
82+
popd
83+
84+
[[ -z "${NIX_DAEMON_PACKAGE}" ]] && return 0
85+
86+
# Ensure that a lockfile generated by the current Nix for tarball inputs
87+
# can still be read by an older Nix
88+
89+
cat <<EOF > flake.nix
90+
{
91+
inputs.tarball = {
92+
url = "file://$PWD/test_input.tar.gz";
93+
flake = false;
94+
};
95+
outputs = { self, tarball }: {
96+
foo = builtins.readFile "${tarball}/test_input_file";
97+
};
98+
}
99+
nix flake update
100+
101+
clearStore
102+
"$NIX_DAEMON_PACKAGE/bin/nix" eval .#foo
103+
EOF
104+
}
105+
106+
test_fetch_file
107+
test_file_flake_input

tests/local.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ nix_tests = \
2323
fetchGit.sh \
2424
fetchurl.sh \
2525
fetchPath.sh \
26+
fetchTree-file.sh \
2627
simple.sh \
2728
referrers.sh \
2829
optimise-store.sh \

tests/tarball.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test_tarball() {
3131

3232
nix-build -o $TEST_ROOT/result -E "import (fetchTree file://$tarball)"
3333
nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; })"
34+
nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"url\"; url = file://$tarball; unpack = true; })"
3435
nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })"
3536
# Do not re-fetch paths already present
3637
nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file:///does-not-exist/must-remain-unused/$tarball; narHash = \"$hash\"; })"

0 commit comments

Comments
 (0)