The WCN3990 has no burned-in Bluetooth address, so the kernel registers the controller unconfigured and bluetoothd never sees it. The factory address is on the machine all along: QCOM/BT.PROVISION on the DPP partition, a three-byte header followed by the six-byte address. c630-bt-addr reads it and hands it to btmgmt before bluetooth.service starts. Verified on hardware: controller configures, powers, scans, and streams A2DP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011XgGF5wfxLDAybVnNz6eNQ
536 lines
23 KiB
Bash
Executable File
536 lines
23 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 setfiles >/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 \
|
|
policycoreutils \
|
|
>/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
|
|
|
|
# The entry kernel-install wrote during the transaction had an empty options
|
|
# line, because nothing told it what the command line should be. That same gap
|
|
# would bite on the device: the first `dnf update kernel` would write an entry
|
|
# with no root= and no C630 quirks, and the machine would not come back. This is
|
|
# where kernel-install looks.
|
|
mkdir -p "$ROOTFS/etc/kernel"
|
|
printf 'root=UUID=%s ro %s\n' "$ROOT_UUID" "$DEVICE_CMDLINE" \
|
|
> "$ROOTFS/etc/kernel/cmdline"
|
|
|
|
# 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"
|
|
|
|
# SELinux labelling happens later, once the bind mounts are gone — see
|
|
# "Labelling the filesystem for SELinux" below.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
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
|
|
|
|
# rmtfs and tqftpserv are both shipped disabled by Fedora. They are inert
|
|
# without modem firmware and cost nothing to have running, so enable them here
|
|
# rather than leaving a step to rediscover later — onboard WiFi cannot work
|
|
# without tqftpserv, since the modem fetches the WLAN firmware over TFTP.
|
|
chroot "$ROOTFS" systemctl enable \
|
|
c630-growfs.service \
|
|
c630-bt-addr.service \
|
|
sshd.service \
|
|
NetworkManager.service \
|
|
systemd-resolved.service \
|
|
rmtfs.service \
|
|
tqftpserv.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"
|
|
# ---------------------------------------------------------------------------
|
|
mkdir -p "$ROOTFS/boot/loader/entries"
|
|
|
|
# kernel-install already wrote an entry during the dnf transaction, from inside
|
|
# the install root — where /boot is an ordinary directory, not a partition. It
|
|
# is wrong in three ways: its paths are /boot/vmlinuz-… which resolve nowhere
|
|
# once /boot is a filesystem in its own right; its options line is empty, so no
|
|
# root= and no command line; and it has no devicetree, so even if it loaded, the
|
|
# kernel would not know what machine it was on. Being named after the machine-id
|
|
# it also sorts before ours, so GRUB picks it. Delete anything we did not write.
|
|
rm -f "$ROOTFS/boot/loader/entries/"*.conf
|
|
|
|
# The bootstrap BLS entry. Subsequent kernels get theirs from kernel-install,
|
|
# with the devicetree line supplied by 95-c630-devicetree.install.
|
|
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
|
|
|
|
# blscfg turns each BLS entry into a menuentry that calls load_video before
|
|
# loading the kernel. Fedora's generated grub.cfg defines it; a hand-written one
|
|
# has to as well, or every entry fails with "can't find command 'load_video'".
|
|
function load_video {
|
|
if [ x\$feature_all_video_module = xy ]; then
|
|
insmod all_video
|
|
else
|
|
insmod efi_gop
|
|
insmod video_bochs
|
|
insmod video_cirrus
|
|
fi
|
|
}
|
|
|
|
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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
log "Labelling the filesystem for SELinux"
|
|
# ---------------------------------------------------------------------------
|
|
# mke2fs -d builds the filesystem from a directory tree and carries security.*
|
|
# xattrs across, but nothing has set them: the tree came out of dnf, not out of
|
|
# a running SELinux system. Boot an unlabelled root and systemd cannot set a
|
|
# context on anything under /dev, logs a screenful of "Permission denied", and
|
|
# dies with "Failed to allocate manager object". /.autorelabel does not rescue
|
|
# it — PID 1 never survives long enough to act on the flag.
|
|
#
|
|
# So label it here, after the bind mounts are gone (or setfiles would walk the
|
|
# builder's /proc) and before /boot is split out, so /boot's files are labelled
|
|
# with everything else.
|
|
FILE_CONTEXTS="$ROOTFS/etc/selinux/targeted/contexts/files/file_contexts"
|
|
LABELLED=0
|
|
if [ -f "$FILE_CONTEXTS" ] && command -v setfiles >/dev/null; then
|
|
if setfiles -F -r "$ROOTFS" "$FILE_CONTEXTS" "$ROOTFS"; then
|
|
LABELLED=1
|
|
echo "filesystem labelled"
|
|
else
|
|
echo "warning: setfiles failed — falling back to a first-boot relabel" >&2
|
|
fi
|
|
else
|
|
echo "warning: no SELinux policy or no setfiles in this container" >&2
|
|
fi
|
|
|
|
mkdir -p "$ROOTFS/etc/selinux"
|
|
cat > "$ROOTFS/etc/selinux/config" <<EOF
|
|
# SELINUXTYPE= can take one of these values: targeted, minimum, mls
|
|
SELINUXTYPE=targeted
|
|
SELINUX=${SELINUX_MODE}
|
|
EOF
|
|
|
|
if [ "$LABELLED" = 1 ]; then
|
|
# Already labelled, so skip the first-boot relabel — it is several minutes
|
|
# of USB-speed I/O to reproduce what we just did.
|
|
rm -f "$ROOTFS/.autorelabel"
|
|
else
|
|
: > "$ROOTFS/.autorelabel"
|
|
fi
|
|
|
|
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)"
|