#!/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 </.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 /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 <&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}" # Compressing an 8 GiB image is architecture-independent work. Doing it inside # the aarch64 container means doing it under qemu-user, which measured at ~20 # minutes against ~2 natively. Do it here when the host can. if command -v zstd >/dev/null; then COMPRESS_IN_CONTAINER=0 else echo "note: no zstd on this host, compressing inside the container instead" echo " (install zstd to save roughly 20 minutes per build)" COMPRESS_IN_CONTAINER=1 fi # --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. 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" \ -e COMPRESS_IN_CONTAINER="$COMPRESS_IN_CONTAINER" \ "$CONTAINER_IMAGE" \ /bin/bash /src/build/stage2.sh if [ "$COMPRESS_IN_CONTAINER" = 0 ]; then name="$(cat "$OUTPUT_DIR/.build-result")" img="$OUTPUT_DIR/${name}.img" [ -f "$img" ] || { echo "error: ${img} is missing" >&2; exit 1; } echo echo "==> Compressing $(du -h "$img" | cut -f1) natively" zstd -12 -T0 --rm -f -o "${img}.zst" "$img" ( cd "$OUTPUT_DIR" && sha256sum "${name}.img.zst" > "${name}.img.zst.sha256" ) fi rm -f "$OUTPUT_DIR/.build-result" echo ls -lh "$OUTPUT_DIR"/*.img.zst "$OUTPUT_DIR"/*.sha256 2>/dev/null