Some checks failed
build image / build (push) Failing after 44m21s
The first real run took 100 minutes. Roughly 60 of those were spent doing things that either did not need doing or did not need doing under emulation. dracut ran three times. The kernel's %posttrans runs kernel-install, which builds an initramfs, and dracut-config-rescue makes it build a second, rescue one — both before this build has written /etc/dracut.conf.d/10-c630.conf and before /proc is bind-mounted, so both are wrong as well as expensive. stage2 then builds the real one. Setting initrd_generator=none in the install root for the duration of the transaction suppresses both: 50-dracut.install and 51-dracut-rescue.install each bail when KERNEL_INSTALL_INITRD_GENERATOR is not "dracut". Excluding dracut-config-rescue also spares the laptop a rescue initramfs on every future kernel update, which on this hardware is not cheap. That file must not ship. With it in place the machine would boot fine and then fail to come back after its next kernel update — a bug that surfaces weeks later looking nothing like an image problem. It is removed from the working copy, and asserted absent again immediately before the root filesystem is built. Compression was running as an aarch64 binary under qemu-user for no reason; zstd does not care what architecture it runs on. It now runs on the host when the host has zstd, falling back to in-container otherwise so the build never depends on it. Measured ~20 minutes against ~2. Add BASE_RECIPE to the staged-base stamp. Excluding a weak dependency changes what the base contains without changing the package list it hashes, so without this the next build would happily reuse a stale base. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
183 lines
7.0 KiB
Bash
Executable File
183 lines
7.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}"
|
|
|
|
# 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
|