Some checks failed
build image / build (push) Failing after 25m28s
Build 18744 died at "Building filesystems" with:
mkfs.vfat: file /work/esp.img already exists
esp.img was left there by build 18700, which was cancelled during compression
and so never reached its end-of-build cleanup. Making $WORK persist so the
staged base could be reused made everything else in it persist as well, and
that turns out not to be inert: mkfs.vfat -C refuses to overwrite, and
`mv $ROOTFS/boot $WORK/boot` would have nested inside a surviving directory
rather than replacing it — a subtler failure that would have produced a /boot
filesystem containing a stray boot/ subdirectory.
Clear the work directory at the start of each run, keeping only the staged base
and its stamp, so the invariant is stated positively rather than depending on
the previous run having exited cleanly. Cleanup that only runs on success is
not cleanup.
Also record why 95-set-boot-entry.install exits 1 during the transaction, so
the next person to read a build log does not go hunting. It is a consequence of
suppressing initramfs generation, is confined to the build, and cannot occur on
the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
456 lines
19 KiB
Bash
Executable File
456 lines
19 KiB
Bash
Executable File
#!/usr/bin/bash
|
|
#
|
|
# Runs inside an aarch64 Fedora container (see build/build-image.sh).
|
|
#
|
|
# Stages a Fedora root filesystem with dnf, applies the C630 overlay, then
|
|
# assembles a GPT disk image. Deliberately avoids loop devices: filesystems are
|
|
# built from directory trees with `mke2fs -d` and `mcopy`, then dd'd into a
|
|
# partitioned sparse file. That keeps the whole thing working on CI runners
|
|
# where /dev/loop-control is not available to the job.
|
|
|
|
set -euo pipefail
|
|
|
|
SRC=/src
|
|
OUT=/out
|
|
WORK=/work # persists between builds; see --work in build-image.sh
|
|
BASE="$WORK/base" # pristine post-dnf tree, reused between builds
|
|
ROOTFS="$WORK/rootfs" # disposable working copy of the above
|
|
STAMP="$WORK/base.stamp"
|
|
DNF_CACHE=/var/cache/c630-dnf # bind-mounted from the host, survives the run
|
|
|
|
# shellcheck source=../config/device.env
|
|
source "$SRC/config/device.env"
|
|
|
|
: "${VARIANT:=minimal}"
|
|
: "${BUILD_REF:=unknown}"
|
|
: "${BUILD_DATE:=unknown}"
|
|
: "${KEEP_ROOTFS:=0}"
|
|
: "${FRESH:=0}"
|
|
: "${COMPRESS_IN_CONTAINER:=1}"
|
|
|
|
# Bump when the *procedure* for staging the base changes in a way that alters
|
|
# its contents. The stamp otherwise hashes only the package list, so a change
|
|
# like excluding a weak dependency would silently reuse a stale base.
|
|
BASE_RECIPE=2
|
|
|
|
# dracut-config-rescue arrives as a weak dependency of dracut and only costs us:
|
|
# it makes kernel-install build a second, rescue initramfs — another emulated
|
|
# dracut run — and leaves a rescue entry in the boot menu we never use. On a
|
|
# machine this slow it would also double the cost of every future kernel update.
|
|
DNF_EXCLUDE=(--exclude=dracut-config-rescue)
|
|
|
|
IMAGE_NAME="fedora-${FEDORA_RELEASE}-${VARIANT}-${DEVICE_NAME}-${BUILD_DATE}-${BUILD_REF}"
|
|
IMAGE_PATH="$OUT/${IMAGE_NAME}.img"
|
|
|
|
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
|
|
|
MOUNTED=()
|
|
unbind_all() {
|
|
local i
|
|
for (( i=${#MOUNTED[@]}-1; i>=0; i-- )); do
|
|
umount "${MOUNTED[i]}" 2>/dev/null || umount -l "${MOUNTED[i]}" 2>/dev/null || true
|
|
done
|
|
MOUNTED=()
|
|
}
|
|
trap unbind_all EXIT
|
|
|
|
bind() { mount --bind "$1" "$2" && MOUNTED+=("$2"); }
|
|
|
|
mkdir -p "$WORK" "$DNF_CACHE"
|
|
|
|
# $WORK persists between builds so the staged base can be reused — which means
|
|
# everything else in it persists too, including intermediates from a build that
|
|
# was cancelled or failed before its cleanup ran. That is not inert: mkfs.vfat
|
|
# -C refuses to overwrite an existing file, and `mv dir $WORK/boot` nests inside
|
|
# a surviving directory rather than replacing it. So state the invariant
|
|
# positively — only the staged base and its stamp survive a new run.
|
|
find "$WORK" -mindepth 1 -maxdepth 1 \
|
|
! -name base ! -name base.stamp -exec rm -rf {} +
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build tooling. Every dnf transaction in here runs emulated, so the prebuilt
|
|
# base image (gongfoo's build-fedora-44-aarch64) carries these already and this
|
|
# becomes a no-op. Only a stock Fedora image pays for it.
|
|
# ---------------------------------------------------------------------------
|
|
if command -v mke2fs >/dev/null && command -v mcopy >/dev/null \
|
|
&& command -v sgdisk >/dev/null && command -v zstd >/dev/null; then
|
|
echo "build tooling already present in the container image"
|
|
else
|
|
log "Installing build tooling into the container"
|
|
dnf -y install --setopt=install_weak_deps=False \
|
|
--setopt=cachedir="$DNF_CACHE" --setopt=keepcache=1 \
|
|
e2fsprogs dosfstools mtools gdisk util-linux rsync zstd findutils \
|
|
>/dev/null
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Resolving package list (base + ${VARIANT})"
|
|
# ---------------------------------------------------------------------------
|
|
read_pkgs() {
|
|
# Strip comments and blank lines. '@^env' is dnf4 spelling for an
|
|
# environment group; dnf5 wants a plain '@env'.
|
|
sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' \
|
|
-e 's/^@\^/@/' "$1"
|
|
}
|
|
|
|
mapfile -t PACKAGES < <(
|
|
read_pkgs "$SRC/config/packages/base.pkgs"
|
|
read_pkgs "$SRC/config/packages/${VARIANT}.pkgs"
|
|
)
|
|
echo "${#PACKAGES[@]} package specs"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The dnf transaction is the only genuinely expensive step — an hour or more of
|
|
# emulated rpm scriptlets. Everything after it is minutes. So stage it once into
|
|
# a pristine tree keyed on the inputs that would change it, and copy that tree
|
|
# per build. Iterating on the kernel command line or the overlay then costs a
|
|
# copy instead of a reinstall.
|
|
# ---------------------------------------------------------------------------
|
|
WANT_STAMP="$(printf '%s\n' "$FEDORA_RELEASE" "$TARGET_ARCH" "$VARIANT" \
|
|
"recipe=$BASE_RECIPE" "${DNF_EXCLUDE[@]}" "${PACKAGES[@]}" \
|
|
| sha256sum | cut -d' ' -f1)"
|
|
|
|
if [ "$FRESH" = 1 ]; then
|
|
log "Discarding the staged base (--fresh)"
|
|
rm -rf "$BASE" "$STAMP"
|
|
fi
|
|
|
|
if [ -d "$BASE" ] && [ "$(cat "$STAMP" 2>/dev/null || true)" = "$WANT_STAMP" ]; then
|
|
log "Reusing the staged base — package set is unchanged"
|
|
echo "pass --fresh to force a reinstall"
|
|
else
|
|
rm -rf "$BASE" "$STAMP"
|
|
mkdir -p "$BASE/etc/yum.repos.d" "$BASE/etc/pki/rpm-gpg" "$BASE/etc/dnf"
|
|
|
|
# dnf reads its repo definitions from inside --installroot. Seed them from
|
|
# the container (same release, same arch) so the first transaction has
|
|
# somewhere to fetch from and something to verify signatures against. The
|
|
# fedora-repos package overwrites these with its own during the transaction.
|
|
cp -a /etc/yum.repos.d/. "$BASE/etc/yum.repos.d/"
|
|
cp -a /etc/pki/rpm-gpg/. "$BASE/etc/pki/rpm-gpg/"
|
|
if [ -d /etc/dnf/vars ]; then cp -a /etc/dnf/vars "$BASE/etc/dnf/"; fi
|
|
|
|
# The kernel's %posttrans runs kernel-install, which runs dracut — before
|
|
# this build has written /etc/dracut.conf.d/10-c630.conf and before /proc is
|
|
# bind-mounted. The result is an initramfs that is both wrong and expensive:
|
|
# two emulated dracut runs (normal + rescue) costing roughly 40 minutes,
|
|
# immediately superseded by the one stage2 builds later with the right
|
|
# config. Both 50-dracut.install and 51-dracut-rescue.install bail out when
|
|
# KERNEL_INSTALL_INITRD_GENERATOR is anything other than "dracut".
|
|
#
|
|
# Expected side effect: with no initramfs present, 95-set-boot-entry.install
|
|
# logs "Error: /boot/initramfs-<kver>.img not found" and exits 1 during the
|
|
# transaction. rpm reports the scriptlet failure and carries on. It does not
|
|
# matter here — stage2 writes its own BLS entry below — and it cannot happen
|
|
# on the device, where this file is gone and dracut runs normally.
|
|
#
|
|
# This file must not survive into the image — see the removal after the
|
|
# working copy is made.
|
|
mkdir -p "$BASE/etc/kernel"
|
|
printf 'initrd_generator=none\n' > "$BASE/etc/kernel/install.conf"
|
|
|
|
log "Installing Fedora ${FEDORA_RELEASE} (${TARGET_ARCH}) — this is the slow part"
|
|
# keepcache=1 with a cachedir outside the install root: the downloaded rpms
|
|
# outlive both the transaction and the staged tree, so a --fresh rebuild
|
|
# re-runs the scriptlets but does not re-download 500-odd packages.
|
|
dnf -y \
|
|
--installroot="$BASE" \
|
|
--releasever="$FEDORA_RELEASE" \
|
|
--setopt=cachedir="$DNF_CACHE" \
|
|
--setopt=keepcache=1 \
|
|
--setopt=install_weak_deps=True \
|
|
"${DNF_EXCLUDE[@]}" \
|
|
install "${PACKAGES[@]}"
|
|
|
|
printf '%s\n' "$WANT_STAMP" > "$STAMP"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Copying the staged base into a working tree"
|
|
# ---------------------------------------------------------------------------
|
|
# --reflink=auto is near-instant on btrfs (Fedora's default) and degrades to a
|
|
# real copy elsewhere. The working tree gets mutated heavily below — accounts,
|
|
# initramfs, bootloader — so the base has to stay untouched.
|
|
rm -rf "$ROOTFS"
|
|
cp -a --reflink=auto "$BASE" "$ROOTFS"
|
|
mkdir -p "$WORK/esp"
|
|
|
|
# Undo the build-time suppression of initramfs generation. Shipping this would
|
|
# mean the laptop generates no initramfs on its next kernel update and does not
|
|
# come back up — the worst kind of bug, because it appears weeks later and looks
|
|
# nothing like an image problem. Nothing in Fedora owns this path, so removing
|
|
# it restores stock behaviour exactly.
|
|
rm -f "$ROOTFS/etc/kernel/install.conf"
|
|
|
|
KVER="$(rpm --root "$ROOTFS" -q kernel-core --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' \
|
|
| sort -V | tail -1)"
|
|
[ -n "$KVER" ] || { echo "could not determine installed kernel version" >&2; exit 1; }
|
|
echo "kernel: $KVER"
|
|
|
|
if [ ! -e "$ROOTFS/boot/dtb-${KVER}/${DEVICE_DTB}" ]; then
|
|
echo "error: ${DEVICE_DTB} is not in this kernel's device trees." >&2
|
|
echo " Check DEVICE_DTB in config/device.env against:" >&2
|
|
ls "$ROOTFS/boot/dtb-${KVER}/qcom/" | grep -i yoga >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Applying overlay"
|
|
# ---------------------------------------------------------------------------
|
|
render() {
|
|
sed -e "s|@DEVICE_DTB@|${DEVICE_DTB}|g" \
|
|
-e "s|@DEVICE_CMDLINE@|${DEVICE_CMDLINE}|g" \
|
|
-e "s|@DEVICE_DESC@|${DEVICE_DESC}|g" \
|
|
-e "s|@DEVICE_NAME@|${DEVICE_NAME}|g" \
|
|
-e "s|@FEDORA_RELEASE@|${FEDORA_RELEASE}|g" \
|
|
-e "s|@BUILD_REF@|${BUILD_REF}|g" \
|
|
-e "s|@BUILD_DATE@|${BUILD_DATE}|g"
|
|
}
|
|
|
|
while IFS= read -r rel; do
|
|
src="$SRC/overlay/$rel"
|
|
dst="$ROOTFS/$rel"
|
|
if [[ "$rel" == *.in ]]; then
|
|
dst="${dst%.in}"
|
|
mkdir -p "$(dirname "$dst")"
|
|
render < "$src" > "$dst"
|
|
else
|
|
mkdir -p "$(dirname "$dst")"
|
|
cp "$src" "$dst"
|
|
fi
|
|
chmod --reference="$src" "$dst"
|
|
done < <(cd "$SRC/overlay" && find . -type f -printf '%P\n')
|
|
|
|
# Locally-supplied firmware, if the operator dropped any in. Contents mirror
|
|
# /usr/lib/firmware/updates/ and are gitignored — see docs/firmware.md.
|
|
if compgen -G "$SRC/firmware/local/*" >/dev/null; then
|
|
log "Baking in firmware from firmware/local/"
|
|
mkdir -p "$ROOTFS/usr/lib/firmware/updates"
|
|
rsync -a --exclude=.gitkeep "$SRC/firmware/local/" \
|
|
"$ROOTFS/usr/lib/firmware/updates/"
|
|
find "$ROOTFS/usr/lib/firmware/updates" -type f -printf ' %P\n'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Generating identifiers and filesystem tables"
|
|
# ---------------------------------------------------------------------------
|
|
ROOT_UUID="$(uuidgen)"
|
|
BOOT_UUID="$(uuidgen)"
|
|
ESP_ID="$(od -An -tx1 -N4 /dev/urandom | tr -d ' \n' | tr 'a-f' 'A-F')"
|
|
ESP_UUID="${ESP_ID:0:4}-${ESP_ID:4:4}"
|
|
|
|
cat > "$ROOTFS/etc/fstab" <<EOF
|
|
UUID=${ROOT_UUID} / ext4 defaults 1 1
|
|
UUID=${BOOT_UUID} /boot ext4 defaults 1 2
|
|
UUID=${ESP_UUID} /boot/efi vfat umask=0077,shortname=winnt 0 2
|
|
EOF
|
|
|
|
# Empty (not missing) machine-id marks this as a first boot for systemd, which
|
|
# then generates a unique one per device rather than cloning the builder's.
|
|
: > "$ROOTFS/etc/machine-id"
|
|
|
|
ln -sf ../run/systemd/resolve/stub-resolv.conf "$ROOTFS/etc/resolv.conf"
|
|
echo "$DEVICE_NAME" > "$ROOTFS/etc/hostname"
|
|
|
|
# mke2fs -d does not reliably carry SELinux labels across, and the builder has
|
|
# no policy loaded anyway. Relabel on first boot.
|
|
: > "$ROOTFS/.autorelabel"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Configuring the target system"
|
|
# ---------------------------------------------------------------------------
|
|
bind /proc "$ROOTFS/proc"
|
|
bind /sys "$ROOTFS/sys"
|
|
bind /dev "$ROOTFS/dev"
|
|
bind /dev/pts "$ROOTFS/dev/pts"
|
|
|
|
chroot "$ROOTFS" useradd -m -G wheel -s /bin/bash "$DEFAULT_USER"
|
|
echo "${DEFAULT_USER}:${DEFAULT_PASSWORD}" | chroot "$ROOTFS" chpasswd
|
|
chroot "$ROOTFS" chage -d 0 "$DEFAULT_USER" # force a change at first login
|
|
chroot "$ROOTFS" passwd -l root
|
|
|
|
chroot "$ROOTFS" systemctl enable \
|
|
c630-growfs.service \
|
|
sshd.service \
|
|
NetworkManager.service \
|
|
systemd-resolved.service
|
|
|
|
if [ "$VARIANT" = workstation ]; then
|
|
chroot "$ROOTFS" systemctl set-default graphical.target
|
|
else
|
|
chroot "$ROOTFS" systemctl set-default multi-user.target
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Building initramfs for ${KVER}"
|
|
# ---------------------------------------------------------------------------
|
|
chroot "$ROOTFS" dracut --force --no-hostonly --no-hostonly-cmdline \
|
|
"/boot/initramfs-${KVER}.img" "$KVER"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Writing bootloader configuration"
|
|
# ---------------------------------------------------------------------------
|
|
# The bootstrap BLS entry. Subsequent kernels get theirs from kernel-install,
|
|
# with the devicetree line supplied by 95-c630-devicetree.install.
|
|
mkdir -p "$ROOTFS/boot/loader/entries"
|
|
cat > "$ROOTFS/boot/loader/entries/c630-${KVER}.conf" <<EOF
|
|
title Fedora Linux ${FEDORA_RELEASE} (${KVER}) — ${DEVICE_DESC}
|
|
version ${KVER}
|
|
linux /vmlinuz-${KVER}
|
|
initrd /initramfs-${KVER}.img
|
|
devicetree /dtb-${KVER}/${DEVICE_DTB}
|
|
options root=UUID=${ROOT_UUID} ro ${DEVICE_CMDLINE}
|
|
grub_users \$grub_users
|
|
grub_arg --unrestricted
|
|
grub_class fedora
|
|
EOF
|
|
|
|
# A deliberately small grub.cfg. grub2-mkconfig would want to probe the running
|
|
# system's block devices, which inside this container describe the builder, not
|
|
# the C630. blscfg reads the entries above, so there is nothing else to do.
|
|
mkdir -p "$ROOTFS/boot/grub2"
|
|
cat > "$ROOTFS/boot/grub2/grub.cfg" <<EOF
|
|
set timeout=5
|
|
set default=0
|
|
|
|
insmod part_gpt
|
|
insmod ext2
|
|
insmod fat
|
|
insmod all_video
|
|
insmod gzio
|
|
insmod blscfg
|
|
|
|
search --no-floppy --fs-uuid --set=root ${BOOT_UUID}
|
|
|
|
if [ -s \$prefix/grubenv ]; then
|
|
load_env
|
|
fi
|
|
|
|
blscfg
|
|
EOF
|
|
: > "$ROOTFS/boot/grub2/grubenv"
|
|
|
|
# ESP: GRUB at the removable-media path, because that is the only thing the
|
|
# C630's firmware will find on a freshly written USB stick or SD card, and a
|
|
# one-line stub telling it where the real configuration lives.
|
|
GRUB_EFI="$ROOTFS/boot/efi/EFI/fedora/grubaa64.efi"
|
|
[ -f "$GRUB_EFI" ] || { echo "grubaa64.efi missing from the install root" >&2; exit 1; }
|
|
|
|
mkdir -p "$WORK/esp/EFI/BOOT" "$WORK/esp/EFI/fedora"
|
|
cp "$GRUB_EFI" "$WORK/esp/EFI/BOOT/BOOTAA64.EFI"
|
|
cp "$GRUB_EFI" "$WORK/esp/EFI/fedora/grubaa64.efi"
|
|
if [ -f "$ROOTFS/boot/efi/EFI/fedora/shimaa64.efi" ]; then
|
|
cp "$ROOTFS/boot/efi/EFI/fedora/shimaa64.efi" "$WORK/esp/EFI/fedora/"
|
|
fi
|
|
|
|
# grubaa64.efi is built with a compiled-in prefix of /EFI/fedora, so this is
|
|
# the file it looks for regardless of which path it was launched from.
|
|
cat > "$WORK/esp/EFI/fedora/grub.cfg" <<EOF
|
|
search --no-floppy --fs-uuid --set=dev ${BOOT_UUID}
|
|
set root=\$dev
|
|
set prefix=(\$dev)/grub2
|
|
export prefix
|
|
configfile \$prefix/grub.cfg
|
|
EOF
|
|
cp "$WORK/esp/EFI/fedora/grub.cfg" "$WORK/esp/EFI/BOOT/grub.cfg"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Splitting /boot out of the root tree"
|
|
# ---------------------------------------------------------------------------
|
|
# Non-lazily, so the bind-mounted trees are genuinely gone before mke2fs walks
|
|
# the root filesystem — a lazy unmount would let it copy in the builder's /dev.
|
|
unbind_all
|
|
for d in dev/pts dev sys proc; do
|
|
if mountpoint -q "$ROOTFS/$d"; then
|
|
echo "error: $ROOTFS/$d is still mounted" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
mv "$ROOTFS/boot" "$WORK/boot"
|
|
mkdir -p "$ROOTFS/boot"
|
|
rm -rf "$WORK/boot/efi"
|
|
mkdir -p "$WORK/boot/efi"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Partitioning ${IMAGE_SIZE_MIB} MiB image"
|
|
# ---------------------------------------------------------------------------
|
|
rm -f "$IMAGE_PATH"
|
|
truncate -s "${IMAGE_SIZE_MIB}M" "$IMAGE_PATH"
|
|
|
|
sgdisk --zap-all "$IMAGE_PATH" >/dev/null
|
|
sgdisk \
|
|
--new "1:1M:+${ESP_SIZE_MIB}M" --typecode 1:ef00 --change-name 1:ESP \
|
|
--new "2:0:+${BOOT_SIZE_MIB}M" --typecode 2:8300 --change-name 2:boot \
|
|
--new "3:0:0" --typecode 3:8300 --change-name 3:root \
|
|
"$IMAGE_PATH" >/dev/null
|
|
sgdisk --print "$IMAGE_PATH"
|
|
|
|
part_first() { sgdisk --info="$1" "$IMAGE_PATH" | awk '/First sector/ {print $3}'; }
|
|
part_last() { sgdisk --info="$1" "$IMAGE_PATH" | awk '/Last sector/ {print $3}'; }
|
|
|
|
ESP_START=$(part_first 1); ESP_SECTORS=$(( $(part_last 1) - ESP_START + 1 ))
|
|
BOOT_START=$(part_first 2); BOOT_SECTORS=$(( $(part_last 2) - BOOT_START + 1 ))
|
|
ROOT_START=$(part_first 3); ROOT_SECTORS=$(( $(part_last 3) - ROOT_START + 1 ))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Building filesystems from the staged trees"
|
|
# ---------------------------------------------------------------------------
|
|
# orphan_file and metadata_csum_seed are recent ext4 features that older GRUB
|
|
# builds refuse to read. /boot has to be readable by whatever GRUB the firmware
|
|
# ends up running, so keep both filesystems conservative.
|
|
EXT4_OPTS="^orphan_file,^metadata_csum_seed"
|
|
|
|
mkfs.vfat -F 32 -n ESP -i "$ESP_ID" -C "$WORK/esp.img" $(( ESP_SECTORS / 2 )) >/dev/null
|
|
mcopy -i "$WORK/esp.img" -s "$WORK/esp/EFI" ::
|
|
|
|
mke2fs -q -t ext4 -b 4096 -O "$EXT4_OPTS" -L boot -U "$BOOT_UUID" \
|
|
-d "$WORK/boot" "$WORK/boot.img" $(( BOOT_SECTORS / 8 ))
|
|
|
|
# Last chance to catch the build-time initramfs suppression leaking into the
|
|
# image. If it shipped, the laptop would boot fine and then fail to come back
|
|
# after its next kernel update — far from here, and looking nothing like an
|
|
# image bug. Cheap to assert, so assert it.
|
|
if [ -e "$ROOTFS/etc/kernel/install.conf" ]; then
|
|
echo "error: /etc/kernel/install.conf is about to ship — it disables" >&2
|
|
echo " initramfs generation and would brick the next kernel update" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mke2fs -q -t ext4 -b 4096 -O "$EXT4_OPTS" -L fedora -U "$ROOT_UUID" \
|
|
-d "$ROOTFS" "$WORK/root.img" $(( ROOT_SECTORS / 8 ))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Assembling the disk image"
|
|
# ---------------------------------------------------------------------------
|
|
dd if="$WORK/esp.img" of="$IMAGE_PATH" bs=512 seek="$ESP_START" conv=notrunc,sparse status=none
|
|
dd if="$WORK/boot.img" of="$IMAGE_PATH" bs=512 seek="$BOOT_START" conv=notrunc,sparse status=none
|
|
dd if="$WORK/root.img" of="$IMAGE_PATH" bs=512 seek="$ROOT_START" conv=notrunc,sparse status=none
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Compression is architecture-independent, so running it here runs it under
|
|
# emulation — roughly twenty minutes for work the host does in two. When the
|
|
# host has zstd, build-image.sh handles it and this just hands over the name.
|
|
# ---------------------------------------------------------------------------
|
|
printf '%s\n' "$IMAGE_NAME" > "$OUT/.build-result"
|
|
|
|
if [ "$COMPRESS_IN_CONTAINER" = 1 ]; then
|
|
log "Compressing (in-container: the host had no zstd)"
|
|
zstd -12 -T0 --rm -f -o "${IMAGE_PATH}.zst" "$IMAGE_PATH"
|
|
( cd "$OUT" && sha256sum "${IMAGE_NAME}.img.zst" > "${IMAGE_NAME}.img.zst.sha256" )
|
|
fi
|
|
|
|
# The intermediate filesystem images are multi-gigabyte and worthless once
|
|
# they are inside the disk image. The staged base is the opposite: expensive to
|
|
# produce and the whole point of the cache, so it always stays.
|
|
rm -f "$WORK/esp.img" "$WORK/boot.img" "$WORK/root.img"
|
|
rm -rf "$WORK/esp"
|
|
if [ "$KEEP_ROOTFS" != "1" ]; then
|
|
rm -rf "$ROOTFS" "$WORK/boot"
|
|
fi
|
|
|
|
log "Done"
|
|
ls -lh "$OUT"
|
|
printf '\nstaged base kept (%s) — the next build reuses it unless the package set changes\n' \
|
|
"$(du -sh "$BASE" 2>/dev/null | cut -f1)"
|