SchOS/tools/posix/mkiso.sh
Nexoniarz 125663929a iso: a real EFI system partition, so a stick boots on strict firmware
-isohybrid-gpt-basdat does nothing without an MBR template, so the ISO
had no partition table at all: written to a stick, it was a CD
filesystem at sector 0. AMI on a B550 (and OVMF) read El Torito off any
block device and booted it anyway. An HP all-in-one from 2012 looks for
an EFI system partition on a USB disk, as the spec says to, found none,
and listed the stick under Legacy only.

The FAT image now goes in a second time as MBR partition 2, type 0xEF,
and is FAT16 by cluster count rather than FAT12, the type old firmware
reads worst. Booted under OVMF both as a CD and as a USB disk. The ISO
also rebuilds when mkiso.sh changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 16:19:27 +02:00

201 lines
9.1 KiB
Shell

#!/bin/sh
# Build a bootable SchOS ISO.
#
# tools/posix/mkiso.sh [output.iso]
#
# The disc carries one El Torito boot entry and a real directory tree:
#
# /boot/efiboot.img a FAT image holding the loader and the kernel
# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc
# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy
#
# --- Why only one entry -----------------------------------------------------
#
# There was a second, for BIOS: cdboot and stage2 concatenated with the kernel
# and loaded by El Torito in no-emulation mode. The BIOS loader is gone, so the
# entry that started it is too.
#
# What UEFI wants is not that mechanism at all. Firmware does not run
# no-emulation boot code; it looks for a FAT filesystem containing
# EFI/BOOT/BOOTX64.EFI and hands that volume to the application it finds. So
# the entry is a FAT image built with mtools, and it carries the kernel as well
# as the loader -- BOOTX64.EFI opens \schos\kernel on the volume it was itself
# loaded from, and for a UEFI boot that volume is this FAT image, not the ISO
# 9660 tree around it. Without the kernel in here the loader comes up and halts
# with EFI_NOT_FOUND, which is what it did.
#
# The loader is also copied into the ISO tree at /EFI/BOOT/BOOTX64.EFI, because
# some firmware looks there directly when booting from a USB stick.
#
# --- Booting from a USB stick -----------------------------------------------
#
# -isohybrid-gpt-basdat adds a GPT that marks the FAT image as a basic data
# partition, so writing this .iso to a stick with dd produces something UEFI
# will boot. That was already the only arrangement that worked from a stick:
# the firmware presents the medium as a hard disk, while the BIOS loader read
# the kernel back over ATAPI, so it never booted this way even when it existed.
#
# --- Rock Ridge and Joliet --------------------------------------------------
#
# Plain ISO 9660 gives 8.3 uppercase names with no permissions, so the sysroot
# would arrive as /BIN/TOYBOX.;1 with every file marked the same. Rock Ridge
# restores POSIX names, permissions and symlinks for Unix readers; Joliet does
# long names for Windows. Both are additions to the same image -- a reader that
# understands neither still sees a valid, if ugly, ISO 9660 disc.
#
# -cache-inodes matters more than it looks: the sysroot is a toybox multicall
# binary with a couple of hundred hard links to it. Without inode caching each
# link becomes its own copy and the disc grows by two orders of magnitude.
set -e
# Run from anywhere: resolve the repo root from this script's own location
# rather than trusting the caller's working directory. Everything below is
# relative to it, and running from tools/posix/ used to fail with "missing
# build/kernel/kernel -- run make first" on a tree that was perfectly built.
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repo=$(CDPATH= cd -- "$script_dir/../.." && pwd)
# An output path given on the command line means where the *caller* is, not
# where the repo is, so it is made absolute before the cd below moves the goal
# posts under it. Without an argument the disc lands at the repo root, which is
# where `make iso` has always put it.
invoked_from=$(pwd)
OUT=${1:-schos.iso}
case $OUT in
/*) ;;
*) OUT=$invoked_from/$OUT ;;
esac
cd "$repo"
BUILD=${BUILD:-build}
STAGING="$BUILD/iso"
SYSROOT=${SYSROOT:-sysroot}
KERNEL="$BUILD/kernel/kernel"
EFIAPP="$BUILD/uefi/BOOTX64.EFI"
[ -f "$KERNEL" ] || { echo "mkiso: missing $KERNEL -- run make first" >&2; exit 1; }
[ -f "$EFIAPP" ] || { echo "mkiso: missing $EFIAPP -- run 'make uefi' first" >&2; exit 1; }
# mtools is not optional any more. It used to be: without it the disc fell back
# to being BIOS-only, which was a degraded disc rather than no disc. There is no
# other entry to fall back to now, so a missing mformat means the ISO would not
# boot anything, and failing here says so instead of shipping it.
if ! command -v mformat >/dev/null 2>&1 || ! command -v mcopy >/dev/null 2>&1; then
echo "mkiso: need mtools (mformat, mcopy) to build the EFI system partition" >&2
echo " Fedora: sudo dnf install mtools" >&2
echo " Debian/Ubuntu: apt install mtools" >&2
exit 1
fi
rm -rf "$STAGING"
mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT" "$STAGING/schos"
cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI"
# The kernel in the ISO 9660 tree as well as inside the FAT image.
#
# Not redundancy for its own sake: it depends on who loaded us. Booting the
# disc directly, firmware runs the El Torito FAT image and the loader opens
# \schos\kernel on that volume. But a chainloader -- Ventoy is the common one
# -- ignores El Torito entirely, mounts the ISO 9660 tree and runs
# \EFI\BOOT\BOOTX64.EFI from *there*. The loader then looks for the kernel on
# the volume it was itself loaded from, which is now the ISO tree, and a disc
# with the kernel only in the FAT image cannot answer.
#
# The symptom is precise and was reported from a real machine: the loader comes
# up, draws its menu, and then dies with EFI_NOT_FOUND.
cp "$KERNEL" "$STAGING/schos/kernel"
# The FAT image for the UEFI El Torito entry.
#
# It carries the kernel as well as the loader. BOOTX64.EFI opens \schos\kernel
# on the volume it was itself loaded from, and for a UEFI boot that volume is
# this FAT image -- not the ISO 9660 tree around it. Without the kernel in here
# the loader comes up and halts with EFI_NOT_FOUND, which is what it did.
#
# The ISO 9660 tree still carries the sysroot for a running system to read; the
# loader's own two files live in here because that is the volume it is handed.
#
# Sized from what actually goes in plus slack, rather than fixed: a hardcoded
# size is a thing that breaks quietly the first time the kernel grows.
EFI_IMG="$STAGING/boot/efiboot.img"
payload_kb=$(( ( $(wc -c < "$EFIAPP") + $(wc -c < "$KERNEL") + 1023 ) / 1024 ))
img_kb=$(( ( (payload_kb + 512 + 31) / 32 ) * 32 ))
# FAT16, chosen by cluster count: a firmware decides FAT12/16/32 from the
# number of clusters alone, as the spec says to, and FAT12 is the type older
# firmware is least likely to read on a removable disk. So at least 8 MB, and
# the smallest power-of-two cluster that keeps the count under FAT16's 65525.
[ "$img_kb" -lt 8192 ] && img_kb=8192
spc=1
while [ $(( img_kb * 2 / spc )) -gt 65000 ]; do spc=$(( spc * 2 )); done
rm -f "$EFI_IMG"
if true; then
# Create the file first and format it with an explicit geometry.
#
# Not `mformat -f`: that takes one of the standard floppy capacities
# (360, 720, 1440, 2880 KB) and rejects anything else -- "Bad size 512".
# Giving heads, sectors-per-track and a total sector count instead lets the
# image be whatever the loader actually needs, which is the point of sizing
# it from the file rather than hardcoding 1.44 MB.
dd if=/dev/zero of="$EFI_IMG" bs=1024 count="$img_kb" status=none
mformat -i "$EFI_IMG" -h 64 -s 32 -c $spc -T $((img_kb * 2)) ::
mmd -i "$EFI_IMG" ::/EFI ::/EFI/BOOT ::/schos
mcopy -i "$EFI_IMG" "$EFIAPP" ::/EFI/BOOT/BOOTX64.EFI
mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel
fi
# The sysroot, as an ordinary hierarchy. Copied rather than linked so that
# whatever ends up on the disc is a snapshot, not a view of a tree that may be
# rebuilt underneath it.
if [ -d "$SYSROOT" ]; then
for d in "$SYSROOT"/*; do
[ -e "$d" ] || continue
cp -a "$d" "$STAGING/"
done
fi
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
# argument-compatible with it. The fallback is not academic: there is no xorriso
# package for Windows in scoop, where this is developed, but cdrtools provides
# mkisofs.exe.
if command -v xorriso >/dev/null 2>&1; then
MKISOFS="xorriso -as mkisofs"
# The same FAT image again, as a partition of its own in an MBR.
#
# This used to be -isohybrid-gpt-basdat, which does nothing without an
# MBR template (-isohybrid-mbr) -- so the ISO had no partition table at
# all, and written to a stick it was a CD filesystem at sector 0. A
# B550's firmware reads that as though it were a disc and boots it; an
# HP all-in-one from 2012 looks for an EFI system partition on a USB
# disk, finds none, and lists the stick under Legacy only. A partition
# of type 0xEF is what every UEFI firmware looks for on a removable
# disk, MBR rather than GPT because the oldest ones read that best.
HYBRID="-append_partition 2 0xef $EFI_IMG"
elif command -v mkisofs >/dev/null 2>&1; then
MKISOFS="mkisofs"
HYBRID=""
else
echo "mkiso: need xorriso or mkisofs on PATH" >&2
echo " Windows: scoop install cdrtools" >&2
echo " Debian/Ubuntu: apt install xorriso" >&2
exit 1
fi
# shellcheck disable=SC2086 # EFI_ARGS is a deliberate word list
$MKISOFS \
-quiet \
-V SCHOS \
-A "SchOS" \
-R -J -joliet-long \
-cache-inodes \
-e boot/efiboot.img \
-no-emul-boot \
$HYBRID \
-o "$OUT" \
"$STAGING"
echo "==> $OUT ($(wc -c < "$OUT") bytes)"
echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT"
echo " USB: dd if=$OUT of=/dev/sdX bs=4M"