|
3 | 3 | # SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors |
4 | 4 | # SPDX-License-Identifier: Apache-2.0 |
5 | 5 |
|
6 | | -################################################################################ |
7 | | - |
8 | | -# This script is a helper to evaluate flake outputs in github actions. |
9 | | - |
10 | | -set -e # exit immediately if a command fails |
11 | | -set -E # exit immediately if a command fails (subshells) |
12 | | -set -u # treat unset variables as an error and exit |
13 | | -set -o pipefail # exit if any pipeline command fails |
14 | | - |
15 | | -TMPDIR="$(mktemp -d --suffix .evaltmp)" |
16 | | -MYNAME=$(basename "$0") |
17 | | -DEF_FILTER="packages\." |
18 | | -RED='' NONE='' |
19 | | - |
20 | | -################################################################################ |
21 | | - |
22 | | -usage () { |
23 | | - echo "" |
24 | | - echo "Usage: $MYNAME [-h] [-v] [-t EVAL_TARGETS] [-j JOB_ID] [-m MAX_JOBS]" |
25 | | - echo "" |
26 | | - echo "Helper to evaluate flake targets in github actions" |
27 | | - echo "" |
28 | | - echo "Options:" |
29 | | - echo " -j Set the instance JOB_ID: integer value between 0 and MAX_JOBS-1" |
30 | | - echo " -m Set the MAX_JOBS count: how many instances of this script will " |
31 | | - echo " be executed on different hosts (runners)" |
32 | | - echo " -t Regex to filter evaluation targets (default='$DEF_FILTER')" |
33 | | - echo " -v Set the script verbosity to DEBUG" |
34 | | - echo " -h Print this help message" |
35 | | - echo "" |
36 | | - echo "Examples:" |
37 | | - echo "" |
38 | | - echo " Following four commands (combined) will evaluate flake outputs that " |
39 | | - echo " match the default filter '$DEF_FILTER' - each command should be" |
40 | | - echo " executed in its own github runner, thus splitting the eval work on " |
41 | | - echo " separate hosts allowing concurrent execution:" |
42 | | - echo "" |
43 | | - echo " $MYNAME -j 0 -m 4" |
44 | | - echo " $MYNAME -j 1 -m 4" |
45 | | - echo " $MYNAME -j 2 -m 4" |
46 | | - echo " $MYNAME -j 3 -m 4" |
47 | | - echo "" |
48 | | -} |
49 | | - |
50 | | -################################################################################ |
51 | | - |
52 | | -on_exit () { |
53 | | - rm -fr "$TMPDIR" # remove tmpdir |
54 | | -} |
55 | | - |
56 | | -print_err () { |
57 | | - printf "${RED}Error:${NONE} %b\n" "$1" >&2 |
58 | | -} |
59 | | - |
60 | | -argparse () { |
61 | | - # Colorize output if stdout is to a terminal (and not to pipe or file) |
62 | | - if [ -t 1 ]; then RED='\033[1;31m'; NONE='\033[0m'; fi |
63 | | - # Parse arguments |
64 | | - JOB_ID="0"; MAX_JOBS="1"; EVAL_TARGETS="$DEF_FILTER"; OPTIND=1 |
65 | | - while getopts "hvj:m:t:" copt; do |
66 | | - case "${copt}" in |
67 | | - h) |
68 | | - usage; exit 0 ;; |
69 | | - v) |
70 | | - set -x ;; |
71 | | - t) |
72 | | - EVAL_TARGETS="$OPTARG" ;; |
73 | | - j) |
74 | | - JOB_ID="$OPTARG" |
75 | | - if ! [[ "$JOB_ID" == +([0-9]) ]]; then |
76 | | - print_err "'-j' expects a non-negative integer (got: '$JOB_ID')" |
77 | | - usage |
78 | | - fi |
79 | | - ;; |
80 | | - m) |
81 | | - MAX_JOBS="$OPTARG" |
82 | | - if ! [[ "$MAX_JOBS" == +([0-9]) ]]; then |
83 | | - print_err "'-m' expects a non-negative integer (got: '$MAX_JOBS')" |
84 | | - usage |
85 | | - fi |
86 | | - ;; |
87 | | - *) |
88 | | - print_err "unrecognized option"; usage; exit 1 ;; |
89 | | - esac |
90 | | - done |
91 | | - shift $((OPTIND-1)) |
92 | | - if [ -n "$*" ]; then |
93 | | - print_err "unsupported positional argument(s): '$*'"; exit 1 |
94 | | - fi |
95 | | - if [ "$JOB_ID" -ge "$MAX_JOBS" ]; then |
96 | | - print_err "'-j' must be smaller than '-m'"; usage; exit 1 |
97 | | - fi |
98 | | -} |
99 | | - |
100 | | -exit_unless_command_exists () { |
101 | | - if ! command -v "$1" &>/dev/null; then |
102 | | - print_err "command '$1' is not installed (Hint: are you inside a nix-shell?)" |
103 | | - exit 1 |
104 | | - fi |
| 6 | +# Evaluate flake outputs using nix-eval-jobs with index-based sharding. |
| 7 | +# Uses --select for Nix-based attribute filtering and --no-instantiate for speed. |
| 8 | + |
| 9 | +set -euo pipefail |
| 10 | + |
| 11 | +if [ $# -ne 2 ]; then |
| 12 | + echo "Usage: $0 <job-id> <total-jobs>" >&2 |
| 13 | + exit 1 |
| 14 | +fi |
| 15 | + |
| 16 | +JOB_ID="$1" |
| 17 | +TOTAL_JOBS="$2" |
| 18 | + |
| 19 | +if ! [[ "$JOB_ID" =~ ^[0-9]+$ ]] || ! [[ "$TOTAL_JOBS" =~ ^[0-9]+$ ]]; then |
| 20 | + echo "Error: job-id and total-jobs must be non-negative integers" >&2 |
| 21 | + exit 1 |
| 22 | +fi |
| 23 | + |
| 24 | +if [ "$JOB_ID" -ge "$TOTAL_JOBS" ]; then |
| 25 | + echo "Error: job-id must be less than total-jobs" >&2 |
| 26 | + exit 1 |
| 27 | +fi |
| 28 | + |
| 29 | +# Nix expression for index-based sharding of flake outputs |
| 30 | +SELECT_EXPR=' |
| 31 | +flake: let |
| 32 | + lib = flake.inputs.nixpkgs.lib; |
| 33 | + jobId = '"$JOB_ID"'; |
| 34 | + totalJobs = '"$TOTAL_JOBS"'; |
| 35 | +
|
| 36 | + # Shard an attrset: keep attrs where (globalIdx + localIdx) mod totalJobs == jobId |
| 37 | + shardAttrs = globalIdx: attrs: |
| 38 | + let |
| 39 | + names = builtins.attrNames attrs; |
| 40 | + selected = lib.imap0 (i: name: |
| 41 | + if lib.mod (globalIdx + i) totalJobs == jobId then name else null |
| 42 | + ) names; |
| 43 | + in lib.getAttrs (builtins.filter (x: x != null) selected) attrs; |
| 44 | +
|
| 45 | + # Apply sharding across all systems for an output type |
| 46 | + shardOutput = outputName: |
| 47 | + let |
| 48 | + output = flake.${outputName} or {}; |
| 49 | + systems = builtins.attrNames output; |
| 50 | + offsets = builtins.foldl'"'"' (acc: sys: |
| 51 | + acc // { ${sys} = acc._idx; _idx = acc._idx + builtins.length (builtins.attrNames output.${sys}); } |
| 52 | + ) { _idx = 0; } systems; |
| 53 | + in builtins.mapAttrs (sys: attrs: shardAttrs offsets.${sys} attrs) output; |
| 54 | +
|
| 55 | +in { |
| 56 | + packages = shardOutput "packages"; |
| 57 | + devShells = shardOutput "devShells"; |
105 | 58 | } |
| 59 | +' |
| 60 | + |
| 61 | +echo "[+] Evaluating flake outputs (job $JOB_ID/$TOTAL_JOBS)" |
| 62 | + |
| 63 | +# Run nix-eval-jobs with: |
| 64 | +# --no-instantiate: Skip writing derivations to store (faster, no garbage) |
| 65 | +# --select: Use Nix expression for sharding |
| 66 | +# --force-recurse: Evaluate nested attribute sets |
| 67 | +nix run --inputs-from .# nixpkgs#nix-eval-jobs -- \ |
| 68 | + --flake ".#" \ |
| 69 | + --no-instantiate \ |
| 70 | + --select "$SELECT_EXPR" \ |
| 71 | + --force-recurse \ |
| 72 | + --accept-flake-config \ |
| 73 | + --option allow-import-from-derivation false \ |
| 74 | + | tee eval-output.json |
| 75 | + |
| 76 | +# Check for evaluation errors in output |
| 77 | +if grep -q '"error":' eval-output.json; then |
| 78 | + echo "" |
| 79 | + echo "Evaluation errors found:" |
| 80 | + grep '"error":' eval-output.json |
| 81 | + exit 1 |
| 82 | +fi |
106 | 83 |
|
107 | | -################################################################################ |
108 | | - |
109 | | -evaluate () { |
110 | | - job="$1" |
111 | | - max_jobs="$2" |
112 | | - filter="$3" |
113 | | - echo "[+] Using filter: '$filter'" |
114 | | - # Output all flake output names |
115 | | - nix flake show --all-systems --json |\ |
116 | | - jq '[paths(scalars) as $path | { ($path|join(".")): getpath($path) }] | add' \ |
117 | | - >"$TMPDIR/outs_all.txt" |
118 | | - # Apply the given filter |
119 | | - if ! grep -Po "${filter}\S*.name" "$TMPDIR/outs_all.txt" >"$TMPDIR/outs.txt"; then |
120 | | - print_err "No flake outputs match filter: '$filter'"; exit 1 |
121 | | - fi |
122 | | - # Remove the '.name' suffix |
123 | | - sed -i "s/.name//" "$TMPDIR/outs.txt" |
124 | | - # Read the attribute set names |
125 | | - grep -oP ".*(?=\.)" "$TMPDIR/outs.txt" | sort | uniq >"$TMPDIR/attrs.txt" |
126 | | - # Generate eval expression on the fly |
127 | | - printf '%s\n' \ |
128 | | - "let" \ |
129 | | - " flake = builtins.getFlake ("git+file://" + toString ./.);"\ |
130 | | - " lib = (import flake.inputs.nixpkgs { }).lib;"\ |
131 | | - "in {" >"$TMPDIR/eval.nix" |
132 | | - while read -r attrset; do |
133 | | - mapfile -t attrs < <(grep "$attrset" "$TMPDIR/outs.txt" | rev | cut -d '.' -f1 | rev | sort | uniq) |
134 | | - # Split the target attribute set so the evaluation work gets distributed |
135 | | - # somewhat evenly between 'max_jobs' runners |
136 | | - split_size=$(( ( ${#attrs[@]} + max_jobs - 1 ) / max_jobs )) |
137 | | - # Select the attributes that will be evaluated by this runner |
138 | | - start_index=$(( job * split_size )) |
139 | | - split_attrs=("${attrs[@]:$start_index:$split_size}") |
140 | | - if [ "${#split_attrs[@]}" -eq 0 ]; then |
141 | | - continue |
142 | | - fi |
143 | | - # shellcheck disable=SC2129 |
144 | | - printf " out_%s = lib.getAttrs [ " "$attrset" >>"$TMPDIR/eval.nix" |
145 | | - printf " \"%s\" " "${split_attrs[@]}" >>"$TMPDIR/eval.nix" |
146 | | - printf " ] flake.%s;\n" "$attrset" >>"$TMPDIR/eval.nix" |
147 | | - done < "$TMPDIR/attrs.txt" |
148 | | - printf "}\n" >>"$TMPDIR/eval.nix" |
149 | | - echo "[+] Evaluating nix expression:" |
150 | | - cat "$TMPDIR/eval.nix" |
151 | | - # Evaluate with nix-eval-jobs |
152 | | - gcroot="$TMPDIR/gcroot" |
153 | | - # nix-eval-jobs exits with success status even if the evaluation fails. |
154 | | - # It outputs the error to stdout, which we parse below |
155 | | - # converting possible eval errors to exit status: |
156 | | - nix-eval-jobs \ |
157 | | - --accept-flake-config \ |
158 | | - --gc-roots-dir "$gcroot" \ |
159 | | - --force-recurse \ |
160 | | - --option allow-import-from-derivation false \ |
161 | | - --expr "$(cat "$TMPDIR/eval.nix")" | tee "$TMPDIR/eval.out" |
162 | | - if grep --color "error:" "$TMPDIR/eval.out"; then |
163 | | - exit 1 |
164 | | - fi |
165 | | -} |
166 | | - |
167 | | -main () { |
168 | | - trap on_exit EXIT |
169 | | - echo "[+] Using tmpdir: '$TMPDIR'" |
170 | | - argparse "$@" |
171 | | - exit_unless_command_exists nix-eval-jobs |
172 | | - exit_unless_command_exists jq |
173 | | - exit_unless_command_exists sed |
174 | | - exit_unless_command_exists grep |
175 | | - exit_unless_command_exists mktemp |
176 | | - evaluate "$JOB_ID" "$MAX_JOBS" "$EVAL_TARGETS" |
177 | | -} |
178 | | - |
179 | | -main "$@" |
180 | | - |
181 | | -################################################################################ |
| 84 | +echo "[+] Evaluation completed successfully" |
0 commit comments