Skip to content

Commit 01447eb

Browse files
committed
rewrite script to use nix-eval-jobs
Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
1 parent 84cf79a commit 01447eb

File tree

2 files changed

+84
-193
lines changed

2 files changed

+84
-193
lines changed

.github/eval.sh

Lines changed: 77 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -3,179 +3,82 @@
33
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
44
# SPDX-License-Identifier: Apache-2.0
55

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";
10558
}
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
10683

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"

.github/workflows/eval.yml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,11 @@ permissions:
1616
jobs:
1717
eval:
1818
runs-on: ubuntu-latest
19-
timeout-minutes: 360
19+
timeout-minutes: 120
2020
strategy:
21+
fail-fast: false
2122
matrix:
22-
include:
23-
- jobid: 0
24-
- jobid: 1
25-
- jobid: 2
26-
- jobid: 3
27-
- jobid: 4
28-
- jobid: 5
29-
- jobid: 6
30-
- jobid: 7
31-
- jobid: 8
32-
- jobid: 9
23+
jobid: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3324
concurrency:
3425
# Cancel any in-progress workflow runs from the same PR or branch,
3526
# allowing matrix jobs to run concurrently:
@@ -47,13 +38,10 @@ jobs:
4738
ref: ${{ github.event.pull_request.merge.sha || github.ref }}
4839
fetch-depth: 0
4940
persist-credentials: false
41+
5042
- name: Install nix
5143
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
52-
- name: Evaluate (jobid=${{ matrix.jobid }})
44+
45+
- name: Evaluate (job ${{ matrix.jobid }}/${{ strategy.job-total }})
5346
run: |
54-
echo "JOB_TOTAL: ${JOB_TOTAL}"
55-
echo "matrix.jobid: ${{ matrix.jobid }}"
56-
nix shell nixpkgs#nix-eval-jobs nixpkgs#jq \
57-
--command .github/eval.sh -t '(devShells\.|packages\.)' -j ${{ matrix.jobid }} -m "${JOB_TOTAL}"
58-
env:
59-
JOB_TOTAL: ${{ strategy.job-total }}
47+
.github/eval.sh ${{ matrix.jobid }} ${{ strategy.job-total }}

0 commit comments

Comments
 (0)