# Gitea runner setup The build runs on runners labelled `metal`, executing directly on the host rather than in a runner-provided container, because it needs to drive podman itself. ## One-time host preparation On each runner host: ```sh sudo dnf install -y podman qemu-user-static-aarch64 sudo systemctl restart systemd-binfmt ``` Verify: ```sh cat /proc/sys/fs/binfmt_misc/qemu-aarch64 ``` The `flags:` line must contain `F`. That flag makes the kernel open the interpreter at registration time and keep the reference, so `qemu-aarch64-static` resolves even inside a container that does not have it. Without it, every aarch64 binary inside the build container fails with `exec format error`. `qemu-user-static-aarch64` registers it correctly; plain `qemu-user` does not. The workflow attempts this install itself, but that only works if the runner account has passwordless sudo. Run `probe-runner.yaml` to find out. ## Checking what a runner can do The `probe runner` workflow is manual-dispatch only and reports architecture, user, sudo, podman, emulation status and free disk. Run it once per runner before debugging a build failure. ## Disk space The build keeps persistent state in `/var/tmp/c630-build` on each runner: | Path | Contents | Rough size | |---|---|---| | `/var/tmp/c630-build/work//base` | Staged post-dnf root filesystem | 2 GiB minimal, 8 GiB workstation | | `/var/tmp/c630-build/dnf` | Downloaded rpms and repo metadata | 2 GiB minimal, 6 GiB workstation | Plus, transiently in the job workspace, the working rootfs copy, three filesystem images and the final disk image. Budget roughly 20 GiB for `minimal` and 45 GiB for `workstation` per runner. The workflow prunes cached rpms untouched for 30 days. The staged base is not pruned — it is invalidated by content hash, not age, so a stale one is harmless. Delete `/var/tmp/c630-build` to reclaim the space. ## Runtime Every aarch64 binary runs under qemu-user emulation, and rpm scriptlets are the worst case. A cold `minimal` build is roughly 40 minutes; `workstation` is several hours. `timeout-minutes` is set to 600 accordingly. The first measured run took 100 minutes, and about 60 of those were avoidable: | Cost | Cause | Fix | |---|---|---| | ~40 min | The kernel's `%posttrans` ran dracut twice (normal + rescue) before the C630 dracut config existed, producing an initramfs immediately thrown away | `initrd_generator=none` in the install root during the transaction; `dracut-config-rescue` excluded | | ~20 min | `zstd` running under emulation | Compression moved to the host | Both are worth remembering if either ever regresses: the symptom is a build that appears to hang with no output, because rpm scriptlet output is buffered until the transaction ends. A warm build — one where the package set has not changed since that runner last built — skips the dnf transaction entirely and finishes in minutes. Because the state is per-runner and jobs are scheduled across all nine, expect the first build on each runner to be cold. Pinning this workflow to a single runner with a dedicated label would make warm builds the norm at the cost of parallelism. If this becomes tiresome, the fix is a native aarch64 runner. Register one with an `aarch64` label and change `runs-on: metal` to `runs-on: aarch64` in `.gitea/workflows/build-image.yaml`; `build/build-image.sh` already skips the emulation check when the host is already the target architecture. ## Changing which runners are used All nine current runners carry `metal` and `podman`. `runs-on: metal` therefore matches any of them. To pin the build to a subset, give those runners a distinct label and use it — Gitea requires a runner to carry every label listed in `runs-on`.