Files
c630/build/build-image.sh
rob thijssen 4d1fd98683
Some checks failed
build image / prepare (push) Successful in 0s
build image / build (push) Failing after 29s
Cache the expensive half of the build so iteration is cheap
The dnf transaction is essentially the whole cost of a build — emulated rpm
scriptlets for 502 packages on minimal, 1929 on workstation. Everything after
it is minutes. Getting this laptop to boot will take several attempts at the
kernel command line and the dracut driver list, and paying for a reinstall each
time is not tenable.

Stage the post-dnf tree under <work>/base, keyed on a hash of the package
lists, release and variant, and copy it per build with --reflink=auto (a CoW
clone on btrfs). Config and overlay edits now reuse it; package list edits
invalidate it on their own, so --fresh is only needed to force the issue.

Keep downloaded rpms in a cachedir outside the install root, so even --fresh
re-runs the scriptlets without re-downloading. keepcache=0 was exactly the
wrong setting for a build meant to be run repeatedly.

Add --work so CI can put both outside the job workspace, which is wiped between
runs, and default the build container to the gongfoo aarch64 build base so the
assembly tooling is not installed under emulation every time. That image is a
speedup, not a dependency: fall back to stock Fedora when it is unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
2026-07-27 11:58:12 +03:00

156 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/bash
#
# Host-side driver. Runs on an x86_64 CI runner (or your workstation) and does
# the real work inside an aarch64 Fedora container under qemu-user emulation.
#
# ./build/build-image.sh --variant minimal
#
# Everything arch-specific happens in build/stage2.sh, which runs inside that
# container. This script's only jobs are checking that emulation is wired up
# and handing the container the right mounts.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
VARIANT=minimal
OUTPUT_DIR="$REPO_DIR/output"
CACHE_DIR="$REPO_DIR/.cache/dnf"
WORK_DIR=""
CONTAINER_IMAGE=""
KEEP_ROOTFS=0
FRESH=0
usage() {
cat <<EOF
Usage: $0 [options]
--variant NAME Package variant from config/packages/ (default: minimal)
--output DIR Where to write the image (default: ./output)
--size MIB Image size in MiB (default: from config/device.env)
--image REF Build container image (default: the gongfoo aarch64 build
base, falling back to registry.fedoraproject.org/fedora)
--cache DIR Persistent dnf package cache (default: ./.cache/dnf)
--work DIR Where the staged base lives (default: <output>/.work).
Point this at a path outside the checkout on CI so the
staged base survives between jobs.
--fresh Re-run the dnf transaction instead of reusing the staged
base. Needed after changing config/packages/.
--keep-rootfs Leave the working rootfs behind for inspection
-h, --help This message
The staged root filesystem is cached under <work>/base and reused when the
package set is unchanged, so edits to config/device.env or overlay/ rebuild in
minutes rather than hours.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--variant) VARIANT="$2"; shift 2 ;;
--output) OUTPUT_DIR="$2"; shift 2 ;;
--size) export IMAGE_SIZE_MIB="$2"; shift 2 ;;
--image) CONTAINER_IMAGE="$2"; shift 2 ;;
--cache) CACHE_DIR="$2"; shift 2 ;;
--work) WORK_DIR="$2"; shift 2 ;;
--fresh) FRESH=1; shift ;;
--keep-rootfs) KEEP_ROOTFS=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
# shellcheck source=../config/device.env
source "$REPO_DIR/config/device.env"
# The gongfoo build base carries the image-assembly tooling already, which
# saves an emulated dnf transaction on every build. It is only a speedup, so
# fall back to stock Fedora rather than failing when it is not reachable.
BASE_IMAGE="git.lair.cafe/gongfoo/build-fedora-${FEDORA_RELEASE}-aarch64:latest"
STOCK_IMAGE="registry.fedoraproject.org/fedora:${FEDORA_RELEASE}"
if [ -z "$CONTAINER_IMAGE" ]; then
if podman image exists "$BASE_IMAGE" || podman pull -q "$BASE_IMAGE" >/dev/null 2>&1; then
CONTAINER_IMAGE="$BASE_IMAGE"
else
echo "note: ${BASE_IMAGE} unavailable, falling back to ${STOCK_IMAGE}"
echo " (the build works either way, it just installs its tooling first)"
CONTAINER_IMAGE="$STOCK_IMAGE"
fi
fi
if [ ! -f "$REPO_DIR/config/packages/${VARIANT}.pkgs" ]; then
echo "no such variant: ${VARIANT}" >&2
echo "available: $(cd "$REPO_DIR/config/packages" && ls *.pkgs | sed 's/\.pkgs$//' | grep -v '^base$' | tr '\n' ' ')" >&2
exit 2
fi
# --- emulation check ----------------------------------------------------
#
# Building an aarch64 rootfs means running aarch64 rpm scriptlets, which needs
# a binfmt_misc handler registered in the *host* kernel. A container cannot
# provide that for itself.
if [ "$(uname -m)" != "$TARGET_ARCH" ]; then
handler=/proc/sys/fs/binfmt_misc/qemu-aarch64
if [ ! -e "$handler" ]; then
cat >&2 <<EOF
error: no binfmt_misc handler for aarch64.
This host is $(uname -m), so building an ${TARGET_ARCH} image needs qemu-user
emulation registered with the kernel. On a Fedora runner, one-time setup:
sudo dnf install -y qemu-user-static-aarch64
sudo systemctl restart systemd-binfmt
See docs/runner-setup.md.
EOF
exit 1
fi
# The handler must be flagged F (fix binary), or the interpreter is looked
# up inside the container's mount namespace, where it does not exist.
if ! grep -q '^flags:.*F' "$handler"; then
echo "error: $handler is registered without the 'F' flag; the qemu" >&2
echo " interpreter will not be visible inside the container." >&2
echo " Install qemu-user-static-aarch64 rather than qemu-user." >&2
exit 1
fi
fi
command -v podman >/dev/null || { echo "error: podman not found" >&2; exit 1; }
# The staged base is per-variant — two variants sharing one directory would
# thrash the stamp and reinstall on every alternating build.
: "${WORK_DIR:=${OUTPUT_DIR}/.work}"
WORK_DIR="${WORK_DIR}/${VARIANT}"
mkdir -p "$OUTPUT_DIR" "$CACHE_DIR" "$WORK_DIR"
BUILD_REF="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)"
BUILD_DATE="$(date -u +%Y-%m-%d)"
echo "==> variant=${VARIANT} release=${FEDORA_RELEASE} arch=${TARGET_ARCH} ref=${BUILD_REF}"
echo "==> build container: ${CONTAINER_IMAGE}"
echo "==> work=${WORK_DIR} cache=${CACHE_DIR}"
# --privileged is what lets stage2 bind-mount /proc and /sys into the staged
# rootfs so dracut can run in a chroot. Rootless podman grants only the caps
# the invoking user already has inside their user namespace, so this is not
# the escalation it looks like.
exec podman run --rm \
--arch arm64 \
--privileged \
--security-opt label=disable \
-v "$REPO_DIR:/src:ro" \
-v "$OUTPUT_DIR:/out" \
-v "$CACHE_DIR:/var/cache/c630-dnf" \
-v "$WORK_DIR:/work" \
-e VARIANT="$VARIANT" \
-e FEDORA_RELEASE="$FEDORA_RELEASE" \
-e IMAGE_SIZE_MIB="${IMAGE_SIZE_MIB}" \
-e BUILD_REF="$BUILD_REF" \
-e BUILD_DATE="$BUILD_DATE" \
-e KEEP_ROOTFS="$KEEP_ROOTFS" \
-e FRESH="$FRESH" \
"$CONTAINER_IMAGE" \
/bin/bash /src/build/stage2.sh