1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
|
# An implementation of Flakes in Nix language, useful for validating and
# specifying flake behaviour, evaluating flakes in unintended ways, and
# otherwise experimenting with them. It also allows evaluating flakes on
# non-flake-compatible Nix implementations.
#
# Given a source tree containing a 'flake.nix' and 'flake.lock' file,
# flake-compat fetches the flake inputs and calls the flake's 'outputs'
# function. It then returns an attrset containing 'defaultNix' (to be used in
# 'default.nix'), 'shellNix' (to be used in 'shell.nix').
{
src,
# Whether to (slowly) copy the entire source tree into the Nix store.
# Disabling this improves evaluation speed immensely at the cost of
# deleting the alleged purity gained by flakes: no longer obeying
# gitignores.
#
# It is recommended anyway to do explicit file filtering using e.g.
# lib.filesets in nixpkgs rather than copying entire directories, since it
# improves evaluation performance and reduces spurious rebuilds.
copySourceTreeToStore ? true,
# Whether to use native builtins.fetchTree. In the future, this *might*
# become `builtins ? fetchTree`, but Lix in versions prior to 2.93
# (https://gerrit.lix.systems/c/lix/+/2399) had a fetchTree that throws in an
# uncatchable way if flakes are disabled.
#
# At a broader level, we are *to some extent* documenting
# builtins.fetchTree's oddities by implementing it in this project and it
# would not be ideal to lose that.
useBuiltinsFetchTree ? false,
system ? builtins.currentSystem or "unknown-system",
}:
let
lockFilePath = src + "/flake.lock";
lockFile = builtins.fromJSON (builtins.readFile lockFilePath);
optionalAttrs = cond: attrs: if cond then attrs else { };
copyAttrIfPresent =
name: attrs: if builtins.hasAttr name attrs then { "${name}" = attrs."${name}"; } else { };
maybeNarHash = attrs: optionalAttrs (attrs ? narHash) { sha256 = attrs.narHash; };
# Note [fetchurl of tarball files in recursive hash mode]:
# We have to use a custom fetchurl function here so that we can specify
# outputHashMode. The hash we get from the lock file is using recursive
# ingestion even though it’s not unpacked. So builtins.fetchurl and import
# <nix/fetchurl.nix> are insufficient.
#
# See Note [Recursive hashing of file inputs] in lix/libfetchers/tarball.cc
# for details on hash modes of flake inputs and why they are always recursive.
# https://git.lix.systems/lix-project/lix/src/b22bee91f5a0360b93d1e1ad71fbfd2ff432bf6c/lix/libfetchers/tarball.cc#L18-L49
# See also: https://git.lix.systems/lix-project/lix/issues/750
#
# Note that the return value from fetchurlInner will be a derivation and not
# a path as builtins.fetchTree is, which will cause it to appear in inputDrvs
# rather than inputSrcs in derivations it appears in. We have to fix this
# string context to be a path with import-from-derivation using
# builtins.path.
fetchurl =
{ url, sha256 }:
# Force an input-from-derivation point to change the path from inputDrvs to
# inputSrcs: the context needs to be a path rather than a derivation to
# match fetchTree behaviour.
#
# This is, needless to say, sort of gross, but it does not have any real
# performance consequence to use builtins.path as an identity function.
builtins.path {
path = fetchurlInner { inherit url sha256; };
name = "source";
# This is the default, but let's be explicit.
recursive = true;
};
fetchurlInner =
{ url, sha256 }:
derivation {
builder = "builtin:fetchurl";
name = "source";
inherit url;
outputHash = sha256;
outputHashAlgo = "sha256";
outputHashMode = "recursive";
executable = false;
unpack = false;
system = "builtin";
# No need to double the amount of network traffic
preferLocalBuild = true;
impureEnvVars = [
# We borrow these environment variables from the caller to allow
# easy proxy configuration. This is impure, but a fixed-output
# derivation like fetchurl is allowed to do so since its result is
# by definition pure.
"http_proxy"
"https_proxy"
"ftp_proxy"
"all_proxy"
"no_proxy"
];
# To make "nix-prefetch-url" work.
urls = [ url ];
};
fetchTree =
info:
if useBuiltinsFetchTree then
builtins.fetchTree info
else
fetchTreeInner info // copyAttrIfPresent "narHash" info;
fetchTreeInner =
info:
if info.type == "github" then
{
outPath = fetchTarball (
{
url = "https://api.${info.host or "github.com"}/repos/${info.owner}/${info.repo}/tarball/${info.rev}";
}
// maybeNarHash info
);
rev = info.rev;
shortRev = builtins.substring 0 7 info.rev;
lastModified = info.lastModified;
lastModifiedDate = formatSecondsSinceEpoch info.lastModified;
}
else if info.type == "git" then
{
outPath = builtins.fetchGit (
{
url = info.url;
}
// copyAttrIfPresent "rev" info
// copyAttrIfPresent "ref" info
// copyAttrIfPresent "submodules" info
);
lastModified = info.lastModified;
lastModifiedDate = formatSecondsSinceEpoch info.lastModified;
revCount = info.revCount or 0;
}
// optionalAttrs (info ? rev) {
inherit (info) rev;
shortRev = builtins.substring 0 7 info.rev;
}
else if info.type == "path" then
{
outPath =
if copySourceTreeToStore then
builtins.path (
{
inherit (info) path;
name = "source";
}
// maybeNarHash info
)
else
info.path;
}
else if info.type == "tarball" then
{
outPath = fetchTarball ({ inherit (info) url; } // maybeNarHash info);
}
else if info.type == "gitlab" then
{
inherit (info) rev lastModified;
outPath = fetchTarball (
{
url = "https://${info.host or "gitlab.com"}/api/v4/projects/${info.owner}%2F${info.repo}/repository/archive.tar.gz?sha=${info.rev}";
}
// maybeNarHash info
);
shortRev = builtins.substring 0 7 info.rev;
}
else if info.type == "sourcehut" then
{
inherit (info) rev lastModified;
outPath = fetchTarball (
{
url = "https://${info.host or "git.sr.ht"}/${info.owner}/${info.repo}/archive/${info.rev}.tar.gz";
}
// maybeNarHash info
);
shortRev = builtins.substring 0 7 info.rev;
}
else if info.type == "file" then
{
outPath =
if
builtins.substring 0 7 info.url == "http://" || builtins.substring 0 8 info.url == "https://"
then
fetchurl {
inherit (info) url;
sha256 = info.narHash;
}
else if builtins.substring 0 7 info.url == "file://" then
builtins.path (
{
# FIXME(jade): this probably should be called source? needs a test
path = builtins.substring 7 (-1) info.url;
}
// maybeNarHash info
)
else
throw "can't support url scheme of flake input with url '${info.url}'";
}
else
# FIXME: add Mercurial inputs.
throw "flake input has unsupported input type '${info.type}'";
callFlake4 =
flakeSrc: locks:
let
flake = import (flakeSrc + "/flake.nix");
inputs = builtins.mapAttrs (
n: v:
if v.flake or true then
callFlake4 (fetchTree (v.locked // v.info)) v.inputs
else
fetchTree (v.locked // v.info)
) locks;
outputs = flakeSrc // (flake.outputs (inputs // { self = outputs; }));
in
assert flake.edition == 201909;
outputs;
callLocklessFlake =
flakeSrc:
let
flake = import (flakeSrc + "/flake.nix");
outputs = flakeSrc // (flake.outputs ({ self = outputs; }));
in
outputs;
rootTreeFromPathish =
tree:
let
# Try to clean the source tree by using fetchGit, if this source
# tree is a valid git repository.
tryFetchGit =
tree:
if isGit && !isShallow then
let
res = builtins.fetchGit tree;
in
if res.rev == "0000000000000000000000000000000000000000" then
removeAttrs res [
"rev"
"shortRev"
]
else
res
else
{
outPath =
# Massage `tree` into a store path.
if builtins.isPath tree then
if
dirOf (toString tree) == builtins.storeDir
# `builtins.storePath` is not available in pure-eval mode.
&& builtins ? currentSystem
then
# If it's already a store path, don't copy it again.
builtins.storePath tree
else
builtins.path {
path = tree;
name = "source";
}
else
tree;
};
# NB git worktrees have a file for .git, so we don't check the type of .git
isGit = builtins.pathExists (tree + "/.git");
isShallow = builtins.pathExists (tree + "/.git/shallow");
in
{
lastModified = 0;
lastModifiedDate = formatSecondsSinceEpoch 0;
}
// (if tree ? outPath then tree else tryFetchGit tree);
rootSrc = rootTreeFromPathish (
if copySourceTreeToStore then
src
else
# *hacker voice*: it's definitely a store path, I promise (actually a
# nixlang path value, likely not pointing at the store).
{ outPath = src; }
);
# Format number of seconds in the Unix epoch as %Y%m%d%H%M%S.
formatSecondsSinceEpoch =
t:
let
rem = x: y: x - x / y * y;
days = t / 86400;
secondsInDay = rem t 86400;
hours = secondsInDay / 3600;
minutes = (rem secondsInDay 3600) / 60;
seconds = rem t 60;
# Courtesy of https://stackoverflow.com/a/32158604.
z = days + 719468;
era = (if z >= 0 then z else z - 146096) / 146097;
doe = z - era * 146097;
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
y = yoe + era * 400;
doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
mp = (5 * doy + 2) / 153;
d = doy - (153 * mp + 2) / 5 + 1;
m = mp + (if mp < 10 then 3 else -9);
y' = y + (if m <= 2 then 1 else 0);
pad = s: if builtins.stringLength s < 2 then "0" + s else s;
in
"${toString y'}${pad (toString m)}${pad (toString d)}${pad (toString hours)}${pad (toString minutes)}${pad (toString seconds)}";
allNodes = builtins.mapAttrs (
key: node:
let
sourceInfo =
if key == lockFile.root then
rootSrc
else
fetchTree (node.info or { } // removeAttrs node.locked [ "dir" ]);
subdir = if key == lockFile.root then "" else node.locked.dir or "";
outPath = sourceInfo + ((if subdir == "" then "" else "/") + subdir);
flake = import (outPath + "/flake.nix");
inputs = builtins.mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}) (
node.inputs or { }
);
# Resolve a input spec into a node name. An input spec is
# either a node name, or a 'follows' path from the root
# node.
resolveInput =
inputSpec: if builtins.isList inputSpec then getInputByPath lockFile.root inputSpec else inputSpec;
# Follow an input path (e.g. ["dwarffs" "nixpkgs"]) from the
# root node, returning the final node.
getInputByPath =
nodeName: path:
if path == [ ] then
nodeName
else
getInputByPath
# Since this could be a 'follows' input, call resolveInput.
(resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path})
(builtins.tail path);
outputs = flake.outputs (inputs // { self = result; });
result =
outputs
# We add the sourceInfo attribute for its metadata, as they are
# relevant metadata for the flake. However, the outPath of the
# sourceInfo does not necessarily match the outPath of the flake,
# as the flake may be in a subdirectory of a source.
# This is shadowed in the next //
// sourceInfo
// {
# This shadows the sourceInfo.outPath
inherit outPath;
inherit inputs;
inherit outputs;
inherit sourceInfo;
_type = "flake";
};
in
if node.flake or true then
assert builtins.isFunction flake.outputs;
result
else
sourceInfo
) lockFile.nodes;
result =
if !(builtins.pathExists lockFilePath) then
callLocklessFlake rootSrc
else if lockFile.version == 4 then
callFlake4 rootSrc (lockFile.inputs)
else if lockFile.version >= 5 && lockFile.version <= 7 then
allNodes.${lockFile.root}
else
throw "lock file '${lockFilePath}' has unsupported version ${toString lockFile.version}";
in
rec {
inputs = result.inputs or { } // {
self = result;
};
outputs = result;
defaultNix =
(builtins.removeAttrs result [ "__functor" ])
// (
if result ? defaultPackage.${system} then { default = result.defaultPackage.${system}; } else { }
)
// (
if result ? packages.${system}.default then
{ default = result.packages.${system}.default; }
else
{ }
);
shellNix =
defaultNix
// (if result ? devShell.${system} then { default = result.devShell.${system}; } else { })
// (
if result ? devShells.${system}.default then
{ default = result.devShells.${system}.default; }
else
{ }
);
}
|