This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

.github.ci: list the shared derivations in the eval job

The shared job dry-ran every machine to find the derivations needed by
more than one, after a runner start and a prefetch of all drv closures
from hestia. In the benchmark run (PR 68, only neoprism uncached) it
built nothing and still cost the machine job 61 s: 5 s queueing, 6 s
setup, 21 s prefetch (mostly the 47 MiB nixpkgs source), 13 s dry-run,
then 6 s queueing of the machine job.

The eval job has the drvs already, so it dry-runs the machines itself,
in parallel, and only for two or more (one machine shares nothing). It
hands the list to the shared job as an artifact; with an empty list the
shared job is skipped, and the machine jobs wait for the manifest
version of the eval job instead of the shared job's.

author
Hermes
committer
lassulus
date (Oct 17, 2026, 11:40 AM CEST) commit 7d3f6b7c parent a6ac39e6
+73 -41
+18 -7
.github/ci-shared.sh
··· 1 1 #!/usr/bin/env bash 2 - # For the shared job of ci.yml: dry-run every matrix row and list the 3 - # derivations that more than one row would build (not substitutable from any 4 - # configured substituter). Usage: ci-shared.sh MATRIX_JSON OUTDIR 2 + # For the eval job of ci.yml: dry-run every matrix row, in parallel, and list 3 + # the derivations that more than one row would build (not substitutable from 4 + # any configured substituter). Usage: ci-shared.sh MATRIX_JSON OUTDIR 5 5 # Writes OUTDIR/<name>.drvs per row, OUTDIR/shared-all.drvs and the 6 6 # substitutable subset OUTDIR/shared.drvs. 7 7 set -euo pipefail ··· 9 9 out=$2 10 10 mkdir -p "$out" 11 11 12 - while IFS=$'\t' read -r name installables; do 13 - read -ra inst <<<"$installables" 14 - nix build --dry-run "${inst[@]}" 2>"$out/$name.plan" || { 12 + plan() { 13 + local name=$1 14 + shift 15 + nix build --dry-run "$@" 2>"$out/$name.plan" || { 15 16 cat "$out/$name.plan" 16 - exit 1 17 + return 1 17 18 } 18 19 # the drv lines below the "will be built:" header, up to the next header 19 20 awk '/^[^ ]/ { b = /will be built:$/; next } b && $1 ~ /^\/nix\/store\// { print $1 }' \ 20 21 "$out/$name.plan" | sort -u >"$out/$name.drvs" 22 + } 23 + 24 + pids=() 25 + while IFS=$'\t' read -r name installables; do 26 + read -ra inst <<<"$installables" 27 + plan "$name" "${inst[@]}" & 28 + pids+=("$!") 21 29 done < <(jq -r '.include[] | [.name, .installables] | @tsv' "$matrix") 30 + for pid in "${pids[@]}"; do 31 + wait "$pid" 32 + done 22 33 23 34 jq -r '.include[].name' "$matrix" | while read -r name; do cat "$out/$name.drvs"; done | 24 35 sort | uniq -d >"$out/shared-all.drvs"
+55 -34
.github/workflows/ci.yml
··· 70 70 # The NixOS machines, built on plain runners with hestia (the GitHub Actions 71 71 # cache) as binary cache next to cache.lassul.us: 72 72 # 73 - # - eval: evaluates ciToplevels once, uploads the drv closures to hestia 74 - # and lists the machines whose toplevel is in neither cache. 75 - # - shared: builds the derivations needed by more than one of them once, 76 - # so the machine jobs substitute them instead of each building them. 73 + # - eval: evaluates ciToplevels once, uploads the drv closures to hestia, 74 + # lists the machines whose toplevel is in neither cache and the 75 + # derivations needed by more than one of them (.github/ci-shared.sh). 76 + # - shared: builds those once, so the machine jobs substitute them instead 77 + # of each building them; skipped when there are none. 77 78 # - one job per listed machine, builds its toplevel by drv path on a lazy 78 79 # /nix/store (.github/lazy-store.sh): the paths it would substitute are 79 80 # only downloaded once a file in them is opened. ··· 90 91 matrix: ${{ steps.matrix.outputs.matrix }} 91 92 any-jobs: ${{ steps.matrix.outputs.any-jobs }} 92 93 manifest-version: ${{ steps.matrix.outputs.manifest-version }} 94 + shared: ${{ steps.plan.outputs.shared }} 93 95 niks3: ${{ steps.tools.outputs.niks3 }} 94 96 lazy-store: ${{ steps.tools.outputs.lazy-store }} 95 97 steps: ··· 129 131 run: | 130 132 echo "not cached: $(jq -r '[.include[].name] | join(" ")' <<<"$MATRIX")" | 131 133 tee -a "$GITHUB_STEP_SUMMARY" 134 + # the derivations the shared job builds, none for a single machine 135 + - id: plan 136 + if: steps.matrix.outputs.any-jobs == 'true' 137 + env: 138 + MATRIX: ${{ steps.matrix.outputs.matrix }} 139 + run: | 140 + matrix=$RUNNER_TEMP/matrix.json 141 + plans=$RUNNER_TEMP/plans 142 + printf '%s' "$MATRIX" > "$matrix" 143 + mkdir -p "$plans" 144 + : > "$plans/shared.drvs" 145 + if [ "$(jq '.include | length' "$matrix")" -ge 2 ]; then 146 + .github/ci-shared.sh "$matrix" "$plans" 147 + { 148 + for name in $(jq -r '.include[].name' "$matrix"); do 149 + echo "- $name: $(wc -l < "$plans/$name.drvs") derivations to build" 150 + done 151 + echo "- shared (needed by >= 2 machines): $(wc -l < "$plans/shared.drvs")" 152 + sed -E 's|^/nix/store/[^-]+-(.*)\.drv$| \1|' "$plans/shared.drvs" 153 + } | tee -a "$GITHUB_STEP_SUMMARY" 154 + fi 155 + if [ -s "$plans/shared.drvs" ]; then 156 + echo "shared=true" >> "$GITHUB_OUTPUT" 157 + fi 158 + - if: steps.plan.outputs.shared == 'true' 159 + uses: actions/upload-artifact@v4 160 + with: 161 + name: plans 162 + path: ${{ runner.temp }}/plans/shared.drvs 132 163 133 164 shared: 134 165 needs: eval 135 - if: needs.eval.outputs.any-jobs == 'true' 166 + if: needs.eval.outputs.shared == 'true' 136 167 runs-on: ubuntu-latest 137 168 permissions: 138 169 contents: read ··· 158 189 run: | 159 190 nix-store --realise "$NIKS3" 160 191 echo "$NIKS3/bin" >> "$GITHUB_PATH" 192 + - uses: actions/download-artifact@v4 193 + with: 194 + name: plans 195 + path: ${{ runner.temp }}/plans 161 196 - id: build 162 197 env: 163 - MATRIX: ${{ needs.eval.outputs.matrix }} 164 198 EVAL_MANIFEST_VERSION: ${{ needs.eval.outputs.manifest-version }} 165 199 run: | 166 - matrix=$RUNNER_TEMP/matrix.json 167 - plans=$RUNNER_TEMP/plans 168 - printf '%s' "$MATRIX" > "$matrix" 169 - read -ra installables <<<"$(jq -r '[.include[].installables] | join(" ")' "$matrix")" 170 - "$HESTIA_BIN" prefetch "${installables[@]}" 171 - .github/ci-shared.sh "$matrix" "$plans" 172 - mapfile -t shared < "$plans/shared.drvs" 173 - version=$EVAL_MANIFEST_VERSION 174 - if [ "${#shared[@]}" -gt 0 ]; then 175 - # a failed one fails the jobs of the machines that need it 176 - .github/ci-build.sh "${shared[@]/%/^*}" || 177 - echo "::warning::some shared derivations failed to build" 178 - # upload now instead of in the post step, so the machine jobs can 179 - # wait for the manifest version that contains the shared outputs 180 - "$HESTIA_BIN" drain --socket "$HESTIA_SOCKET" 2>&1 | tee "$RUNNER_TEMP/drain.txt" 181 - drained=$(sed -nE 's/.*; manifest m[0-9]+#([0-9]+)$/\1/p' "$RUNNER_TEMP/drain.txt") 182 - version=${drained:-$version} 183 - fi 184 - echo "manifest-version=$version" >> "$GITHUB_OUTPUT" 185 - { 186 - for name in $(jq -r '.include[].name' "$matrix"); do 187 - echo "- $name: $(wc -l < "$plans/$name.drvs") derivations to build" 188 - done 189 - echo "- shared (needed by >= 2 machines): ${#shared[@]}" 190 - printf '%s\n' "${shared[@]}" | sed -E 's|^/nix/store/[^-]+-(.*)\.drv$| \1|' 191 - } | tee -a "$GITHUB_STEP_SUMMARY" 200 + mapfile -t shared < "$RUNNER_TEMP/plans/shared.drvs" 201 + "$HESTIA_BIN" prefetch "${shared[@]/%/^*}" 202 + # a failed one fails the jobs of the machines that need it 203 + .github/ci-build.sh "${shared[@]/%/^*}" || 204 + echo "::warning::some shared derivations failed to build" 205 + # upload now instead of in the post step, so the machine jobs can 206 + # wait for the manifest version that contains the shared outputs 207 + "$HESTIA_BIN" drain --socket "$HESTIA_SOCKET" 2>&1 | tee "$RUNNER_TEMP/drain.txt" 208 + drained=$(sed -nE 's/.*; manifest m[0-9]+#([0-9]+)$/\1/p' "$RUNNER_TEMP/drain.txt") 209 + echo "manifest-version=${drained:-$EVAL_MANIFEST_VERSION}" >> "$GITHUB_OUTPUT" 192 210 193 211 build: 194 212 needs: [eval, shared] 195 - if: needs.eval.outputs.any-jobs == 'true' 213 + # also when the shared job was skipped: nothing to share 214 + if: >- 215 + !cancelled() && needs.eval.result == 'success' && needs.eval.outputs.any-jobs == 'true' 216 + && (needs.shared.result == 'success' || needs.shared.result == 'skipped') 196 217 name: ${{ matrix.name }} 197 218 runs-on: ${{ matrix.os }} 198 219 permissions: ··· 214 235 with: 215 236 upstream-cache-filter: true 216 237 upstream-cache-key-names: cache.nixos.org-1 cache.lassul.us-1 217 - wait-manifest-version: ${{ needs.shared.outputs.manifest-version }} 238 + wait-manifest-version: ${{ needs.shared.outputs.manifest-version || needs.eval.outputs.manifest-version }} 218 239 - env: 219 240 NIKS3: ${{ needs.eval.outputs.niks3 }} 220 241 run: |