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
|
# Evaluates all the accessible paths in nixpkgs.
# *This only builds on Linux* since it requires the Linux sandbox isolation to
# be able to write in various places while evaluating inside the sandbox.
#
# This file is used by nixpkgs CI (see .github/workflows/eval.yml) as well as
# being used directly as an entry point in Lix's CI (in `flake.nix` in the Lix
# repo).
#
# If you know you are doing a breaking API change, please ping the nixpkgs CI
# maintainers and the Lix maintainers (`nix eval -f . lib.teams.lix`).
{
callPackage,
lib,
runCommand,
writeShellScript,
symlinkJoin,
busybox,
jq,
nix,
}:
{
# The number of attributes per chunk, see ./README.md for more info.
chunkSize ? 5000,
# Whether to just evaluate a single chunk for quick testing
quickTest ? false,
# Don't try to eval packages marked as broken.
includeBroken ? false,
# Customize the config used to evaluate nixpkgs
extraNixpkgsConfig ? { },
}:
let
nixpkgs =
with lib.fileset;
toSource {
root = ../..;
fileset = unions (
map (lib.path.append ../..) [
".version"
"ci/supportedSystems.json"
"ci/eval/attrpaths.nix"
"ci/eval/chunk.nix"
"ci/eval/outpaths.nix"
"default.nix"
"doc"
"lib"
"maintainers"
"modules"
"nixos"
"pkgs"
]
);
};
supportedSystems = builtins.fromJSON (builtins.readFile ../supportedSystems.json);
attrpathsSuperset =
{
evalSystem,
}:
runCommand "attrpaths-superset.json"
{
src = nixpkgs;
# Don't depend on -dev outputs to reduce closure size for CI.
nativeBuildInputs = map lib.getBin [
busybox
nix
];
}
''
export NIX_STATE_DIR=$(mktemp -d)
mkdir $out
export GC_INITIAL_HEAP_SIZE=4g
command time -f "Attribute eval done [%MKB max resident, %Es elapsed] %C" \
nix-instantiate --eval --strict --json --show-trace \
"$src/ci/eval/attrpaths.nix" \
-A paths \
-I "$src" \
--argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
--option restrict-eval true \
--option allow-import-from-derivation false \
--option eval-system "${evalSystem}" > $out/paths.json
'';
singleSystem =
{
# The system to evaluate.
# Note that this is intentionally not called `system`,
# because `--argstr system` would only be passed to the ci/default.nix file!
evalSystem ? builtins.currentSystem,
# The path to the `paths.json` file from `attrpathsSuperset`
attrpathFile ? "${attrpathsSuperset { inherit evalSystem; }}/paths.json",
}:
let
singleChunk = writeShellScript "single-chunk" ''
set -euo pipefail
chunkSize=$1
myChunk=$2
system=$3
outputDir=$4
# Default is 5, higher values effectively disable the warning.
# This randomly breaks Eval.
export GC_LARGE_ALLOC_WARN_INTERVAL=1000
export NIX_SHOW_STATS=1
export NIX_SHOW_STATS_PATH="$outputDir/stats/$myChunk"
echo "Chunk $myChunk on $system start"
set +e
command time -o "$outputDir/timestats/$myChunk" \
-f "Chunk $myChunk on $system done [%MKB max resident, %Es elapsed] %C" \
nix-env -f "${nixpkgs}/ci/eval/chunk.nix" \
--eval-system "$system" \
--option restrict-eval true \
--option allow-import-from-derivation false \
--query --available \
--out-path --json \
--meta \
--show-trace \
--arg chunkSize "$chunkSize" \
--arg myChunk "$myChunk" \
--arg attrpathFile "${attrpathFile}" \
--arg systems "[ \"$system\" ]" \
--arg includeBroken ${lib.boolToString includeBroken} \
--argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
-I ${nixpkgs} \
-I ${attrpathFile} \
> "$outputDir/result/$myChunk" \
2> "$outputDir/stderr/$myChunk"
exitCode=$?
set -e
cat "$outputDir/stderr/$myChunk"
cat "$outputDir/timestats/$myChunk"
if (( exitCode != 0 )); then
echo "Evaluation failed with exit code $exitCode"
# This immediately halts all xargs processes
kill $PPID
elif [[ -s "$outputDir/stderr/$myChunk" ]]; then
echo "Nixpkgs on $system evaluated with warnings, aborting"
kill $PPID
fi
'';
in
runCommand "nixpkgs-eval-${evalSystem}"
{
# Don't depend on -dev outputs to reduce closure size for CI.
nativeBuildInputs = map lib.getBin [
busybox
jq
nix
];
env = {
inherit evalSystem chunkSize;
};
__structuredAttrs = true;
unsafeDiscardReferences.out = true;
}
''
export NIX_STATE_DIR=$(mktemp -d)
nix-store --init
echo "System: $evalSystem"
cores=$NIX_BUILD_CORES
echo "Cores: $cores"
attrCount=$(jq length "${attrpathFile}")
echo "Attribute count: $attrCount"
echo "Chunk size: $chunkSize"
# Same as `attrCount / chunkSize` but rounded up
chunkCount=$(( (attrCount - 1) / chunkSize + 1 ))
echo "Chunk count: $chunkCount"
mkdir -p $out/${evalSystem}
# Record and print stats on free memory and swap in the background
(
while true; do
availMemory=$(free -m | grep Mem | awk '{print $7}')
freeSwap=$(free -m | grep Swap | awk '{print $4}')
echo "Available memory: $(( availMemory )) MiB, free swap: $(( freeSwap )) MiB"
if [[ ! -f "$out/${evalSystem}/min-avail-memory" ]] || (( availMemory < $(<$out/${evalSystem}/min-avail-memory) )); then
echo "$availMemory" > $out/${evalSystem}/min-avail-memory
fi
if [[ ! -f $out/${evalSystem}/min-free-swap ]] || (( freeSwap < $(<$out/${evalSystem}/min-free-swap) )); then
echo "$freeSwap" > $out/${evalSystem}/min-free-swap
fi
sleep 4
done
) &
seq_end=$(( chunkCount - 1 ))
${lib.optionalString quickTest ''
seq_end=0
''}
chunkOutputDir=$(mktemp -d)
mkdir "$chunkOutputDir"/{result,stats,timestats,stderr}
seq -w 0 "$seq_end" |
command time -f "%e" -o "$out/${evalSystem}/total-time" \
xargs -I{} -P"$cores" \
${singleChunk} "$chunkSize" {} "$evalSystem" "$chunkOutputDir"
cp -r "$chunkOutputDir"/stats $out/${evalSystem}/stats-by-chunk
if (( chunkSize * chunkCount != attrCount )); then
# A final incomplete chunk would mess up the stats, don't include it
rm "$chunkOutputDir"/stats/"$seq_end"
fi
cat "$chunkOutputDir"/result/* | jq -s 'add | map_values(.outputs)' > $out/${evalSystem}/paths.json
cat "$chunkOutputDir"/result/* | jq -s 'add | map_values(.meta)' > $out/${evalSystem}/meta.json
'';
diff = callPackage ./diff.nix { };
combine =
{
diffDir,
}:
runCommand "combined-eval"
{
# Don't depend on -dev outputs to reduce closure size for CI.
nativeBuildInputs = map lib.getBin [
jq
];
}
''
mkdir -p $out
# Combine output paths from all systems
cat ${diffDir}/*/diff.json | jq -s '
reduce .[] as $item ({}; {
added: (.added + $item.added),
changed: (.changed + $item.changed),
removed: (.removed + $item.removed),
rebuilds: (.rebuilds + $item.rebuilds)
})
' > $out/combined-diff.json
# Combine maintainers from all systems
cat ${diffDir}/*/maintainers.json | jq -s '
add | group_by(.package) | map({
key: .[0].package,
value: map(.maintainers) | flatten | unique
}) | from_entries
' > $out/maintainers.json
mkdir -p $out/before/stats
for d in ${diffDir}/before/*; do
cp -r "$d"/stats-by-chunk $out/before/stats/$(basename "$d")
done
mkdir -p $out/after/stats
for d in ${diffDir}/after/*; do
cp -r "$d"/stats-by-chunk $out/after/stats/$(basename "$d")
done
'';
compare = callPackage ./compare { };
baseline =
{
# Whether to evaluate on a specific set of systems, by default all are evaluated
evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
}:
symlinkJoin {
name = "nixpkgs-eval-baseline";
paths = map (
evalSystem:
singleSystem {
inherit evalSystem;
}
) evalSystems;
};
full =
{
# Whether to evaluate on a specific set of systems, by default all are evaluated
evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
baseline,
# What files have been touched? Defaults to none; use the expression below to calculate it.
# ```
# git diff --name-only --merge-base master HEAD \
# | jq --raw-input --slurp 'split("\n")[:-1]' > touched-files.json
# ```
touchedFilesJson ? builtins.toFile "touched-files.json" "[ ]",
}:
let
diffs = symlinkJoin {
name = "nixpkgs-eval-diffs";
paths = map (
evalSystem:
diff {
inherit evalSystem;
beforeDir = baseline;
afterDir = singleSystem {
inherit evalSystem;
};
}
) evalSystems;
};
comparisonReport = compare {
combinedDir = combine { diffDir = diffs; };
inherit touchedFilesJson;
};
in
comparisonReport;
in
{
inherit
attrpathsSuperset
singleSystem
diff
combine
compare
# The above three are used by separate VMs in a GitHub workflow,
# while the below are intended for testing on a single local machine
baseline
full
;
}
|