| 27 |
27 |
|
[ -d "$ROOTFS" ] || { echo "no $ROOTFS" >&2; exit 1; }
|
| 28 |
28 |
|
[ -d "$OUTPUT" ] || { echo "no $OUTPUT" >&2; exit 1; }
|
| 29 |
29 |
|
|
| 30 |
|
- |
KVER="$(ls "$ROOTFS/usr/lib/modules" | head -1)"
|
|
30 |
+ |
# Version-sorted, not `head -1`: `ls` collates 7.1.10 before 7.1.3, so the
|
|
31 |
+ |
# alphabetical pick is arbitrary rather than newest. A bootc image should carry
|
|
32 |
+ |
# exactly one kernel, so more than one means something layered a second and the
|
|
33 |
+ |
# choice stops being obvious, so say so rather than picking silently.
|
|
34 |
+ |
KVER="$(ls -1 "$ROOTFS/usr/lib/modules" | sort -V | tail -1)"
|
| 31 |
35 |
|
[ -n "$KVER" ] || { echo "no kernel in $ROOTFS/usr/lib/modules" >&2; exit 1; }
|
|
36 |
+ |
KCOUNT="$(ls -1 "$ROOTFS/usr/lib/modules" | wc -l)"
|
|
37 |
+ |
[ "$KCOUNT" -eq 1 ] || echo "warning: $KCOUNT kernels present, using newest ($KVER)" >&2
|
| 32 |
38 |
|
say "kernel $KVER"
|
| 33 |
39 |
|
|
| 34 |
40 |
|
rm -rf "$WORK"
|
| 44 |
50 |
|
# ---------------------------------------------------------------------
|
| 45 |
51 |
|
say "building rootfs.img"
|
| 46 |
52 |
|
SIZE_KB="$(du -sk "$ROOTFS" | cut -f1)"
|
| 47 |
|
- |
# Slack for the ext4 metadata plus room for the overlay's early writes.
|
|
53 |
+ |
# Slack for ext4 metadata. Not for the live session's writes: with
|
|
54 |
+ |
# rd.live.overlay.overlayfs=1 this image is the overlay's read-only lower dir
|
|
55 |
+ |
# and is never written to. The upper dir is a tmpfs under /run, so the real
|
|
56 |
+ |
# budget for the live session is RAM, not this number.
|
| 48 |
57 |
|
IMG_MB=$(( SIZE_KB / 1024 + 1536 ))
|
|
58 |
+ |
# Inodes are budgeted from the file count rather than left to the default
|
|
59 |
+ |
# bytes-per-inode ratio. A desktop rootfs is many small files, and the default
|
|
60 |
+ |
# runs out before the space does. That fails mkfs mid-populate, minutes into
|
|
61 |
+ |
# a build, with an error that reads as unrelated to file count.
|
|
62 |
+ |
INODES=$(( $(find "$ROOTFS" -xdev 2>/dev/null | wc -l) * 12 / 10 ))
|
|
63 |
+ |
say "rootfs.img ${IMG_MB}M, $INODES inodes"
|
| 49 |
64 |
|
# The LiveOS directory has to exist *inside* the squashfs, so mksquashfs is
|
| 50 |
65 |
|
# pointed at its parent: it takes the contents of the directory it is given
|
| 51 |
66 |
|
# as the squashfs root, so compressing LiveOS/ directly puts rootfs.img at
|
| 53 |
68 |
|
# .../squashfs.img" while looking for LiveOS/rootfs.img.
|
| 54 |
69 |
|
mkdir -p "$WORK/sqroot/LiveOS"
|
| 55 |
70 |
|
truncate -s "${IMG_MB}M" "$WORK/sqroot/LiveOS/rootfs.img"
|
| 56 |
|
- |
mkfs.ext4 -q -L Alloy -d "$ROOTFS" "$WORK/sqroot/LiveOS/rootfs.img"
|
|
71 |
+ |
mkfs.ext4 -q -L Alloy -N "$INODES" -d "$ROOTFS" "$WORK/sqroot/LiveOS/rootfs.img"
|
| 57 |
72 |
|
|
| 58 |
73 |
|
# zstd level 19 costs minutes of saturated CPU for a few percent of size,
|
| 59 |
74 |
|
# which is worth it for a release and pure waste when the question is
|
| 247 |
262 |
|
# rather than a round number, because a fixed size is a future failure the
|
| 248 |
263 |
|
# day the EFI binaries grow.
|
| 249 |
264 |
|
say "efiboot.img"
|
|
265 |
+ |
# FAT16, not whatever mkfs.fat picks. Sized to contents it picks FAT12, which
|
|
266 |
+ |
# UEFI permits on removable media but which no shipping distro relies on --
|
|
267 |
+ |
# Arch and Fedora both force FAT16 for exactly this image. Firmware that
|
|
268 |
+ |
# declines FAT12 produces no boot option at all, and the GPT still looks
|
|
269 |
+ |
# perfect to every tool you would inspect it with, so the failure arrives as
|
|
270 |
+ |
# "the stick is not bootable" with nothing to read.
|
|
271 |
+ |
#
|
|
272 |
+ |
# FAT16 needs at least 4085 clusters, which the contents-sized image is far
|
|
273 |
+ |
# below, so floor it at 16 MiB. That is noise against a 4.8 GB ISO.
|
| 250 |
274 |
|
EFI_KB=$(( $(du -sk "$WORK/iso/EFI" | cut -f1) + 2048 ))
|
|
275 |
+ |
[ "$EFI_KB" -lt 16384 ] && EFI_KB=16384
|
| 251 |
276 |
|
truncate -s "${EFI_KB}K" "$WORK/efiboot.img"
|
| 252 |
|
- |
mkfs.fat -n ALLOYEFI "$WORK/efiboot.img" >/dev/null
|
| 253 |
|
- |
mmd -i "$WORK/efiboot.img" ::/EFI ::/EFI/BOOT 2>/dev/null || {
|
| 254 |
|
- |
# mtools absent; fall back to a loop mount, which needs privileges the
|
| 255 |
|
- |
# build already has.
|
| 256 |
|
- |
mkdir -p "$WORK/efimnt"
|
| 257 |
|
- |
mount -o loop "$WORK/efiboot.img" "$WORK/efimnt"
|
| 258 |
|
- |
mkdir -p "$WORK/efimnt/EFI/BOOT"
|
| 259 |
|
- |
cp -a "$WORK/iso/EFI/BOOT/." "$WORK/efimnt/EFI/BOOT/"
|
| 260 |
|
- |
umount "$WORK/efimnt"
|
| 261 |
|
- |
}
|
| 262 |
|
- |
if command -v mcopy >/dev/null; then
|
| 263 |
|
- |
mcopy -i "$WORK/efiboot.img" -s "$WORK/iso/EFI/BOOT/"* ::/EFI/BOOT/ 2>/dev/null || true
|
| 264 |
|
- |
fi
|
|
277 |
+ |
mkfs.fat -F 16 -n ALLOYEFI "$WORK/efiboot.img" >/dev/null
|
|
278 |
+ |
|
|
279 |
+ |
# One code path, and it is allowed to fail.
|
|
280 |
+ |
#
|
|
281 |
+ |
# This used to try mmd/mcopy, fall back to a loop mount when mtools was
|
|
282 |
+ |
# absent, and then re-run mcopy under `|| true`. Three paths, of which one --
|
|
283 |
+ |
# mmd succeeding and mcopy failing, produced an ESP holding two empty
|
|
284 |
+ |
# directories and no bootloader, reported success, and shipped. mtools was
|
|
285 |
+ |
# not in the builder, so the loop-mount branch is what actually ran; the
|
|
286 |
+ |
# silent branch was live and simply never taken. mtools is installed now
|
|
287 |
+ |
# (build/Containerfile.iso) and this is the only path.
|
|
288 |
+ |
mmd -i "$WORK/efiboot.img" ::/EFI ::/EFI/BOOT
|
|
289 |
+ |
mcopy -i "$WORK/efiboot.img" -s "$WORK/iso/EFI/BOOT/"* ::/EFI/BOOT/
|
|
290 |
+ |
|
|
291 |
+ |
# Read it back out of the filesystem rather than trusting the exit codes: a
|
|
292 |
+ |
# bootloader that is not in here is the one defect this whole file exists to
|
|
293 |
+ |
# avoid, and it is invisible everywhere else.
|
|
294 |
+ |
mdir -i "$WORK/efiboot.img" ::/EFI/BOOT | grep -qi 'BOOTX64' \
|
|
295 |
+ |
|| { echo "BOOTX64.EFI is not in the ESP; the medium cannot boot" >&2; exit 1; }
|
|
296 |
+ |
say "ESP $(( EFI_KB / 1024 ))M FAT16, BOOTX64.EFI present"
|
| 265 |
297 |
|
# Deliberately not copied into $WORK/iso. It is appended to the medium as a
|
| 266 |
298 |
|
# real partition below, and El Torito is pointed at that partition rather
|
| 267 |
299 |
|
# than at a file in the tree, so a copy inside the ISO filesystem would be
|
| 321 |
353 |
|
# fails to appear fails the build instead of the stick.
|
| 322 |
354 |
|
# ---------------------------------------------------------------------
|
| 323 |
355 |
|
say "checking the partition table"
|
| 324 |
|
- |
at() { dd if="$OUTPUT/install.iso" bs=1 skip="$1" count="$2" 2>/dev/null | od -An -tx1 | tr -d ' \n'; }
|
|
356 |
+ |
# -v is load-bearing, not decoration. Without it od replaces runs of identical
|
|
357 |
+ |
# 16-byte lines with a single `*`, so the hex string is shorter than the bytes
|
|
358 |
+ |
# it describes and every offset past the first repeat is wrong. A GPT entry
|
|
359 |
+ |
# array is mostly zero padding, which is exactly the input that triggers it.
|
|
360 |
+ |
# Presence checks survive this; arithmetic on offsets does not.
|
|
361 |
+ |
at() { dd if="$OUTPUT/install.iso" bs=1 skip="$1" count="$2" 2>/dev/null | od -An -tx1 -v | tr -d ' \n'; }
|
| 325 |
362 |
|
|
| 326 |
363 |
|
[ "$(at 510 2)" = "55aa" ] \
|
| 327 |
364 |
|
|| { echo "no MBR signature at offset 510; the ISO has no partition table" >&2; exit 1; }
|
| 345 |
382 |
|
ENTRY_COUNT=$(le "$(at $((512 + 80)) 4)")
|
| 346 |
383 |
|
ENTRY_SIZE=$(le "$(at $((512 + 84)) 4)")
|
| 347 |
384 |
|
ARRAY=$(dd if="$OUTPUT/install.iso" bs=512 skip="$ENTRY_LBA" \
|
| 348 |
|
- |
count=$(( (ENTRY_COUNT * ENTRY_SIZE + 511) / 512 )) 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
|
385 |
+ |
count=$(( (ENTRY_COUNT * ENTRY_SIZE + 511) / 512 )) 2>/dev/null | od -An -tx1 -v | tr -d ' \n')
|
| 349 |
386 |
|
case "$ARRAY" in
|
| 350 |
387 |
|
*28732ac11ff8d211ba4b00a0c93ec93b*) ;;
|
| 351 |
388 |
|
*) echo "the GPT describes no EFI system partition" >&2; exit 1 ;;
|
| 352 |
389 |
|
esac
|
| 353 |
390 |
|
say "protective MBR, GPT at LBA 1, ESP appended as partition 2"
|
| 354 |
391 |
|
|
|
392 |
+ |
# The checks above all pass on an image whose El Torito entry points somewhere
|
|
393 |
+ |
# other than the ESP, because they never compare the two. That is the optical boot
|
|
394 |
+ |
# path, and it is the one a partition-table change can silently break while
|
|
395 |
+ |
# every GPT check stays green. Read both numbers and require them to agree.
|
|
396 |
+ |
#
|
|
397 |
+ |
# The GUID match above found the ESP somewhere in the entry array; recover
|
|
398 |
+ |
# which entry it was, so its start LBA can be read rather than assumed.
|
|
399 |
+ |
ESP_PREFIX="${ARRAY%%28732ac11ff8d211ba4b00a0c93ec93b*}"
|
|
400 |
+ |
ESP_INDEX=$(( ${#ESP_PREFIX} / 2 / ENTRY_SIZE ))
|
|
401 |
+ |
ESP_LBA=$(le "${ARRAY:$(( (ESP_INDEX * ENTRY_SIZE + 32) * 2 )):16}")
|
|
402 |
+ |
|
|
403 |
+ |
# El Torito: the boot record volume descriptor at LBA 17 points at the boot
|
|
404 |
+ |
# catalog; the default entry sits 32 bytes into it, with the sector count at
|
|
405 |
+ |
# +6 and the load RBA at +8. The RBA is in 2048-byte blocks, the partition in
|
|
406 |
+ |
# 512-byte sectors.
|
|
407 |
+ |
CAT_LBA=$(le "$(at $((17 * 2048 + 71)) 4)")
|
|
408 |
+ |
ENTRY_OFF=$(( CAT_LBA * 2048 + 32 ))
|
|
409 |
+ |
BOOT_COUNT=$(le "$(at $((ENTRY_OFF + 6)) 2)")
|
|
410 |
+ |
BOOT_RBA=$(le "$(at $((ENTRY_OFF + 8)) 4)")
|
|
411 |
+ |
|
|
412 |
+ |
[ "$(( BOOT_RBA * 2048 ))" = "$(( ESP_LBA * 512 ))" ] \
|
|
413 |
+ |
|| { echo "El Torito load RBA ($(( BOOT_RBA * 2048 ))) is not the ESP ($(( ESP_LBA * 512 ))); optical boot would read the wrong bytes" >&2; exit 1; }
|
|
414 |
+ |
|
|
415 |
+ |
ISO_BYTES=$(stat -c %s "$OUTPUT/install.iso")
|
|
416 |
+ |
[ "$(( BOOT_RBA * 2048 + BOOT_COUNT * 512 ))" -le "$ISO_BYTES" ] \
|
|
417 |
+ |
|| { echo "El Torito extent runs past the end of the image" >&2; exit 1; }
|
|
418 |
+ |
|
|
419 |
+ |
say "El Torito entry points at the ESP, $BOOT_COUNT sectors, within the image"
|
|
420 |
+ |
|
| 355 |
421 |
|
chmod 0644 "$OUTPUT/install.iso"
|
| 356 |
422 |
|
say "built $(du -h "$OUTPUT/install.iso" | cut -f1) at $OUTPUT/install.iso"
|