1# Evaluates all the accessible paths in nixpkgs.
2# *This only builds on Linux* since it requires the Linux sandbox isolation to
3# be able to write in various places while evaluating inside the sandbox.
4#
5# This file is used by nixpkgs CI (see .github/workflows/eval.yml) as well as
6# being used directly as an entry point in Lix's CI (in `flake.nix` in the Lix
7# repo).
8#
9# If you know you are doing a breaking API change, please ping the nixpkgs CI
10# maintainers and the Lix maintainers (`nix eval -f . lib.teams.lix`).
11{
12 callPackage,
13 lib,
14 runCommand,
15 writeShellScript,
16 symlinkJoin,
17 busybox,
18 jq,
19 nix,
20}:
21
22let
23 nixpkgs =
24 with lib.fileset;
25 toSource {
26 root = ../..;
27 fileset = unions (
28 map (lib.path.append ../..) [
29 ".version"
30 "ci/supportedSystems.json"
31 "ci/eval/attrpaths.nix"
32 "ci/eval/chunk.nix"
33 "ci/eval/outpaths.nix"
34 "default.nix"
35 "doc"
36 "lib"
37 "maintainers"
38 "modules"
39 "nixos"
40 "pkgs"
41 ]
42 );
43 };
44
45 supportedSystems = builtins.fromJSON (builtins.readFile ../supportedSystems.json);
46
47 attrpathsSuperset =
48 {
49 evalSystem,
50 }:
51 runCommand "attrpaths-superset.json"
52 {
53 src = nixpkgs;
54 # Don't depend on -dev outputs to reduce closure size for CI.
55 nativeBuildInputs = map lib.getBin [
56 busybox
57 nix
58 ];
59 }
60 ''
61 export NIX_STATE_DIR=$(mktemp -d)
62 mkdir $out
63 export GC_INITIAL_HEAP_SIZE=4g
64 command time -f "Attribute eval done [%MKB max resident, %Es elapsed] %C" \
65 nix-instantiate --eval --strict --json --show-trace \
66 "$src/ci/eval/attrpaths.nix" \
67 -A paths \
68 -I "$src" \
69 --option restrict-eval true \
70 --option allow-import-from-derivation false \
71 --option eval-system "${evalSystem}" > $out/paths.json
72 '';
73
74 singleSystem =
75 {
76 # The system to evaluate.
77 # Note that this is intentionally not called `system`,
78 # because `--argstr system` would only be passed to the ci/default.nix file!
79 evalSystem ? builtins.currentSystem,
80 # The path to the `paths.json` file from `attrpathsSuperset`
81 attrpathFile ? "${attrpathsSuperset { inherit evalSystem; }}/paths.json",
82 # The number of attributes per chunk, see ./README.md for more info.
83 chunkSize ? 5000,
84
85 # Don't try to eval packages marked as broken.
86 includeBroken ? false,
87 # Whether to just evaluate a single chunk for quick testing
88 quickTest ? false,
89 }:
90 let
91 singleChunk = writeShellScript "single-chunk" ''
92 set -euo pipefail
93 chunkSize=$1
94 myChunk=$2
95 system=$3
96 outputDir=$4
97
98 export NIX_SHOW_STATS=1
99 export NIX_SHOW_STATS_PATH="$outputDir/stats/$myChunk"
100 echo "Chunk $myChunk on $system start"
101 set +e
102 command time -o "$outputDir/timestats/$myChunk" \
103 -f "Chunk $myChunk on $system done [%MKB max resident, %Es elapsed] %C" \
104 nix-env -f "${nixpkgs}/ci/eval/chunk.nix" \
105 --eval-system "$system" \
106 --option restrict-eval true \
107 --option allow-import-from-derivation false \
108 --query --available \
109 --out-path --json \
110 --show-trace \
111 --arg chunkSize "$chunkSize" \
112 --arg myChunk "$myChunk" \
113 --arg attrpathFile "${attrpathFile}" \
114 --arg systems "[ \"$system\" ]" \
115 --arg includeBroken ${lib.boolToString includeBroken} \
116 -I ${nixpkgs} \
117 -I ${attrpathFile} \
118 > "$outputDir/result/$myChunk" \
119 2> "$outputDir/stderr/$myChunk"
120 exitCode=$?
121 set -e
122 cat "$outputDir/stderr/$myChunk"
123 cat "$outputDir/timestats/$myChunk"
124 if (( exitCode != 0 )); then
125 echo "Evaluation failed with exit code $exitCode"
126 # This immediately halts all xargs processes
127 kill $PPID
128 elif [[ -s "$outputDir/stderr/$myChunk" ]]; then
129 echo "Nixpkgs on $system evaluated with warnings, aborting"
130 kill $PPID
131 fi
132 '';
133 in
134 runCommand "nixpkgs-eval-${evalSystem}"
135 {
136 # Don't depend on -dev outputs to reduce closure size for CI.
137 nativeBuildInputs = map lib.getBin [
138 busybox
139 jq
140 nix
141 ];
142 env = {
143 inherit evalSystem chunkSize;
144 };
145 __structuredAttrs = true;
146 unsafeDiscardReferences.out = true;
147 }
148 ''
149 export NIX_STATE_DIR=$(mktemp -d)
150 nix-store --init
151
152 echo "System: $evalSystem"
153 cores=$NIX_BUILD_CORES
154 echo "Cores: $cores"
155 attrCount=$(jq length "${attrpathFile}")
156 echo "Attribute count: $attrCount"
157 echo "Chunk size: $chunkSize"
158 # Same as `attrCount / chunkSize` but rounded up
159 chunkCount=$(( (attrCount - 1) / chunkSize + 1 ))
160 echo "Chunk count: $chunkCount"
161
162 mkdir -p $out/${evalSystem}
163
164 # Record and print stats on free memory and swap in the background
165 (
166 while true; do
167 availMemory=$(free -m | grep Mem | awk '{print $7}')
168 freeSwap=$(free -m | grep Swap | awk '{print $4}')
169 echo "Available memory: $(( availMemory )) MiB, free swap: $(( freeSwap )) MiB"
170
171 if [[ ! -f "$out/${evalSystem}/min-avail-memory" ]] || (( availMemory < $(<$out/${evalSystem}/min-avail-memory) )); then
172 echo "$availMemory" > $out/${evalSystem}/min-avail-memory
173 fi
174 if [[ ! -f $out/${evalSystem}/min-free-swap ]] || (( freeSwap < $(<$out/${evalSystem}/min-free-swap) )); then
175 echo "$freeSwap" > $out/${evalSystem}/min-free-swap
176 fi
177 sleep 4
178 done
179 ) &
180
181 seq_end=$(( chunkCount - 1 ))
182
183 ${lib.optionalString quickTest ''
184 seq_end=0
185 ''}
186
187 chunkOutputDir=$(mktemp -d)
188 mkdir "$chunkOutputDir"/{result,stats,timestats,stderr}
189
190 seq -w 0 "$seq_end" |
191 command time -f "%e" -o "$out/${evalSystem}/total-time" \
192 xargs -I{} -P"$cores" \
193 ${singleChunk} "$chunkSize" {} "$evalSystem" "$chunkOutputDir"
194
195 cp -r "$chunkOutputDir"/stats $out/${evalSystem}/stats-by-chunk
196
197 if (( chunkSize * chunkCount != attrCount )); then
198 # A final incomplete chunk would mess up the stats, don't include it
199 rm "$chunkOutputDir"/stats/"$seq_end"
200 fi
201
202 cat "$chunkOutputDir"/result/* | jq -s 'add | map_values(.outputs)' > $out/${evalSystem}/paths.json
203 '';
204
205 diff = callPackage ./diff.nix { };
206
207 combine =
208 {
209 diffDir,
210 }:
211 runCommand "combined-eval"
212 {
213 # Don't depend on -dev outputs to reduce closure size for CI.
214 nativeBuildInputs = map lib.getBin [
215 jq
216 ];
217 }
218 ''
219 mkdir -p $out
220
221 # Combine output paths from all systems
222 cat ${diffDir}/*/diff.json | jq -s '
223 reduce .[] as $item ({}; {
224 added: (.added + $item.added),
225 changed: (.changed + $item.changed),
226 removed: (.removed + $item.removed),
227 rebuilds: (.rebuilds + $item.rebuilds)
228 })
229 ' > $out/combined-diff.json
230
231 mkdir -p $out/before/stats
232 for d in ${diffDir}/before/*; do
233 cp -r "$d"/stats-by-chunk $out/before/stats/$(basename "$d")
234 done
235
236 mkdir -p $out/after/stats
237 for d in ${diffDir}/after/*; do
238 cp -r "$d"/stats-by-chunk $out/after/stats/$(basename "$d")
239 done
240 '';
241
242 compare = callPackage ./compare { };
243
244 baseline =
245 {
246 # Whether to evaluate on a specific set of systems, by default all are evaluated
247 evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
248 # The number of attributes per chunk, see ./README.md for more info.
249 chunkSize ? 5000,
250 quickTest ? false,
251 }:
252 symlinkJoin {
253 name = "nixpkgs-eval-baseline";
254 paths = map (
255 evalSystem:
256 singleSystem {
257 inherit quickTest evalSystem chunkSize;
258 }
259 ) evalSystems;
260 };
261
262 full =
263 {
264 # Whether to evaluate on a specific set of systems, by default all are evaluated
265 evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
266 # The number of attributes per chunk, see ./README.md for more info.
267 chunkSize ? 5000,
268 quickTest ? false,
269 baseline,
270 # Which maintainer should be considered the author?
271 # Defaults to nixpkgs-ci which is not a maintainer and skips the check.
272 githubAuthorId ? "nixpkgs-ci",
273 # What files have been touched? Defaults to none; use the expression below to calculate it.
274 # ```
275 # git diff --name-only --merge-base master HEAD \
276 # | jq --raw-input --slurp 'split("\n")[:-1]' > touched-files.json
277 # ```
278 touchedFilesJson ? builtins.toFile "touched-files.json" "[ ]",
279 }:
280 let
281 diffs = symlinkJoin {
282 name = "nixpkgs-eval-diffs";
283 paths = map (
284 evalSystem:
285 diff {
286 inherit evalSystem;
287 beforeDir = baseline;
288 afterDir = singleSystem {
289 inherit quickTest evalSystem chunkSize;
290 };
291 }
292 ) evalSystems;
293 };
294 comparisonReport = compare {
295 combinedDir = combine { diffDir = diffs; };
296 inherit touchedFilesJson githubAuthorId;
297 };
298 in
299 comparisonReport;
300
301in
302{
303 inherit
304 attrpathsSuperset
305 singleSystem
306 diff
307 combine
308 compare
309 # The above three are used by separate VMs in a GitHub workflow,
310 # while the below are intended for testing on a single local machine
311 baseline
312 full
313 ;
314}