SchOS/tools/posix/mkiso.sh
Nexoniarz f4107177ea boot: the disc that could not be chainloaded, and the error nobody could see
Reported from a real laptop booting through Ventoy: the F8 menu appears,
selecting Boot SchOS gives a black screen and a white dash in the corner.
Both halves of that are bugs.

The disc could not be chainloaded. Booting it directly, firmware runs the
El Torito FAT image and the loader opens \schos\kernel on that volume,
which is where mkiso.sh put it. Ventoy does not use El Torito at all: it
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
-- the ISO tree -- and the kernel was only ever inside the FAT image. It
is now in both. The comment in mkiso.sh had even described this failure,
as the reason the kernel is in the FAT image, without noticing the ISO
tree had the same requirement.

The error was invisible. print() redirects to the serial port once the
splash is up, which is right for a development machine and useless on a
laptop: the loader died with a perfectly good explanation and sent it to
a wire nobody was holding. What is left on screen is the firmware's own
cursor -- the white dash. die() now draws the message on the framebuffer
too, over a cleared band so it does not land on the logo.

Verified by reproducing the exact failure: the ISO tree extracted and
booted as a plain volume, as a chainloader presents it. With the kernel
removed it dies visibly with "no kernel at \schos\kernel or \kernel, EFI
status 0x000E" -- EFI_NOT_FOUND, which is what the laptop was hitting
silently. With it present, that same volume boots through to a shell.

110 [ok] on the ESP path, the ISO check passes, no failures.
2026-08-11 11:54:47 +02:00

185 lines
8.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 ))
[ "$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"