summaryrefslogtreecommitdiffstats
path: root/ci/eval/default.nix
blob: 2c3ea0ff0732638b54645e8cb0e3d9d8dd0aab1f (plain)
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
# 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,
  perf,
}:

{
  # 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/eval/pre-eval.nix"
          "ci/eval/chunk.nix"
          "ci/eval/outpaths.nix"
          "default.nix"
          "doc"
          "lib"
          "maintainers"
          "modules"
          "nixos"
          "pkgs"
        ]
      );
    };

  supportedSystems = builtins.fromJSON (
    builtins.readFile ../../pkgs/top-level/release-supported-systems.json
  );

  preEval =
    {
      evalSystem,
    }:
    runCommand "pre-eval"
      {
        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 "Pre-eval done [%MKB max resident, %Es elapsed] %C" \
          nix-instantiate --eval --strict --json --show-trace \
            "$src/ci/eval/pre-eval.nix" \
            -A result \
            -I "$src" \
            --argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
            --option restrict-eval true \
            --option allow-import-from-derivation false \
            --option eval-system "${evalSystem}" > $out/result.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 `result.json` file from `preEval`
      preEvalFile ? "${preEval { inherit evalSystem; }}/result.json",
      # Output the number of assembly instructions executed during evaluation
      countInstructions ? false,
    }:
    let
      singleChunk = writeShellScript "single-chunk" ''
        set -euo pipefail
        chunkSize=$1
        myChunk=$2
        system=$3
        outputDir=$4
        preEvalFile=$5

        # 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 preEvalFile "$preEvalFile" \
          --arg systems "[ \"$system\" ]" \
          --arg includeBroken ${lib.boolToString includeBroken} \
          --argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
          -I ${nixpkgs} \
          -I "$preEvalFile" \
          > "$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
          ]
          ++ lib.optionals countInstructions [ perf ]
        );
        env = {
          inherit evalSystem chunkSize countInstructions;
        };
        __structuredAttrs = true;
        unsafeDiscardReferences.out = true;
      }
      ''
        export NIX_STATE_DIR=$(mktemp -d)
        nix-store --init

        echo "System: $evalSystem"
        cores=$NIX_BUILD_CORES
        echo "Cores: $cores"

        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
        ) &

        chunkedEval() {
          local chunkOutputDir=$1
          local preEvalFile=$2

          local attrCount=$(jq '.paths | length' "$preEvalFile")
          echo "Attribute count: $attrCount"
          echo "Chunk size: $chunkSize"
          # Same as `attrCount / chunkSize` but rounded up
          local chunkCount=$(( (attrCount - 1) / chunkSize + 1 ))
          echo "Chunk count: $chunkCount"

          local seq_end=$(( chunkCount - 1 ))
          ${lib.optionalString quickTest ''
            seq_end=0
          ''}

          mkdir -p "$chunkOutputDir"/{result,stats,timestats,stderr}

          runAllChunks() {
            seq -w 0 "$seq_end" |
              xargs -I{} -P"$cores" \
              ${singleChunk} "$chunkSize" {} "$evalSystem" "$chunkOutputDir" "$preEvalFile"
          }

          if [[ -n "$countInstructions" ]]; then
            export seq_end cores chunkSize evalSystem chunkOutputDir preEvalFile
            export -f runAllChunks
            perf stat \
              --event instructions:u --field-separator , --output "$chunkOutputDir"/perf-output-file \
              bash -c runAllChunks
            cat "$chunkOutputDir"/perf-output-file | tail -n 1 | cut -d, -f1 > "$chunkOutputDir"/instructions
            rm "$chunkOutputDir"/perf-output-file
          else
            runAllChunks
          fi

          if (( chunkSize * chunkCount != attrCount )); then
            # A final incomplete chunk would mess up the stats, don't include it
            rm "$chunkOutputDir"/stats/"$seq_end"
          fi
        }

        chunkOutputDirs=$(mktemp -d)

        # Preparation for the second eval
        disallowedAttributesPreEvalFile=$(mktemp)
        jq '{
          paths: (.attrPathsDisallowedForInternalUse | map(.attrPath)),
          attrPathsDisallowedForInternalUse: []
        }' ${preEvalFile} > "$disallowedAttributesPreEvalFile"

        startEpoch=$(date +%s)

        # The first eval evaluates only attributes that are not disallowed for internal Nixpkgs use, ensuring that they don't depend on disallowed attributes
        # Because the first eval doesn't evaluate the disallowed attributes themselves, but we still want to check that they don't fail evaluation, we evaluate them separately in a second eval
        # The reason we need two evals is because we want disallowed attributes to be able to depend on other disallowed attributes, which inherently needs a separate Nixpkgs instantiation
        # And while we could interleave that instantiation into a single eval, that would ~double memory usage for all chunks, while doing it separately doesn't
        echo "Evaluating the internally allowed attributes"
        chunkedEval "$chunkOutputDirs"/allowed ${preEvalFile}
        echo "Evaluating the internally disallowed attributes"
        chunkedEval "$chunkOutputDirs"/disallowed "$disallowedAttributesPreEvalFile"

        echo $(( $(date +%s) - startEpoch )) > "$out/${evalSystem}/total-time"

        # We only use the stats from the allowed attrs eval, because the disallowed attrs are generally not even a full chunk
        cp -r "$chunkOutputDirs"/allowed/stats $out/${evalSystem}/stats-by-chunk
        if [[ -f "$chunkOutputDirs"/allowed/instructions ]]; then
          cp "$chunkOutputDirs"/allowed/instructions $out/${evalSystem}/instructions
        fi

        cat "$chunkOutputDirs"/*/result/* | jq -s 'add | map_values(.outputs)' > $out/${evalSystem}/paths.json
        cat "$chunkOutputDirs"/*/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 $out/before/instructions
        for d in ${diffDir}/before/*; do
          cp -r "$d"/stats-by-chunk $out/before/stats/$(basename "$d")
          if [[ -f "$d"/instructions ]]; then
            cp "$d"/instructions $out/before/instructions/$(basename "$d")
          fi
        done

        mkdir -p $out/after/stats $out/after/instructions
        for d in ${diffDir}/after/*; do
          cp -r "$d"/stats-by-chunk $out/after/stats/$(basename "$d")
          if [[ -f "$d"/instructions ]]; then
            cp "$d"/instructions $out/after/instructions/$(basename "$d")
          fi
        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,
      # Output the number of assembly instructions executed during evaluation on
      # each system
      countInstructions ? false,
    }:
    symlinkJoin {
      name = "nixpkgs-eval-baseline";
      paths = map (
        evalSystem:
        singleSystem {
          inherit evalSystem countInstructions;
        }
      ) 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" "[ ]",
      # The branch the local comparison is made against; matches the `master`
      # used in the touched-files expression above.
      baseBranch ? "master",
      # Output the number of assembly instructions executed during evaluation on
      # each system
      countInstructions ? false,
    }:
    let
      diffs = symlinkJoin {
        name = "nixpkgs-eval-diffs";
        paths = map (
          evalSystem:
          diff {
            inherit evalSystem;
            beforeDir = baseline;
            afterDir = singleSystem {
              inherit evalSystem countInstructions;
            };
          }
        ) evalSystems;
      };
      comparisonReport = compare {
        combinedDir = combine { diffDir = diffs; };
        inherit touchedFilesJson baseBranch;
      };
    in
    comparisonReport;

in
{
  inherit
    preEval
    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
    ;
}