#!/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 )) [ "$img_kb" -lt 512 ] && img_kb=512 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 -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" HYBRID="-isohybrid-gpt-basdat" # mkisofs proper does not know this one 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"