SCFF, the SchOS Compressed File Format (include/fs/scff.h), is a block image cut into 128 KB chunks, each its own zstd frame, behind an index. tools/mkscff writes it from fs-ram.img on every core (-19, 11 s) and checks each chunk decompresses to itself. The kernel carries the .scff as its ramdisk and unpacks a chunk the first time a block in it is read; zero blocks share one page. The kernel and the ISO are the compressed size, and the root costs memory only for what is touched. See docs/architecture/scff.md. zstd 1.5.7 comes from ports/zstd like uACPI: the decoder is compiled into the kernel at -O2, with a static context and a 64 KB stack of its own, and mkscff builds the whole library from the same release. world now has xorg-server, the fbdev and evdev drivers, xinit, Window Maker, xterm and DejaVu, and /root/.xinitrc runs wmaker. That root is 358 MB, 102 MB as SCFF. The ISO no longer carries the root a third time as an ISO 9660 tree, which nothing read. schos.iso: 1389 MB -> 318 MB. El Torito keeps its own copy of the EFI image, the layout real machines have booted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
198 lines
9.2 KiB
Shell
198 lines
9.2 KiB
Shell
#!/bin/sh
|
|
# Build a bootable SchOS ISO.
|
|
#
|
|
# tools/posix/mkiso.sh [output.iso]
|
|
#
|
|
# The disc carries one El Torito boot entry and the loader and kernel as files:
|
|
#
|
|
# /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
|
|
# /schos/kernel the same kernel, for chainloaders (below)
|
|
#
|
|
# The root filesystem is inside the kernel, as its ramdisk. The disc used to
|
|
# carry it a third time as an ISO 9660 tree, which nothing reads -- SchOS has
|
|
# no ISO 9660 driver -- and once Xorg was in the root that copy alone was
|
|
# 360 MB.
|
|
#
|
|
# --- 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"
|
|
|
|
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.
|
|
#
|
|
# 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
|
|
|
|
|
|
# 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"
|
|
# El Torito keeps its own copy of the image in the tree, the layout real
|
|
# machines have booted. Pointing it into the partition instead
|
|
# (--interval:appended_partition_2:all::, as Arch does) saves a copy, and
|
|
# an ASUS laptop stopped listing the stick the day that was tried. With
|
|
# the root compressed (SCFF) the copy is only the kernel's size.
|
|
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"
|