diff --git a/Makefile b/Makefile index f82b9e7..478a217 100644 --- a/Makefile +++ b/Makefile @@ -291,11 +291,9 @@ CFLAGS_SYS = $(CFLAGS) -I./kernel/sys # ----------------------------------------------------------------------- RAMDISK_BLOCKS ?= 6000 -# Inodes for the ramdisk root. The inode table is preallocated, so mkfs's -# default of 12000 cost 1501 of those 6000 blocks -- a quarter of the image, to -# describe about 260 files. 2000 is still an order of magnitude of headroom and -# costs 251 blocks. -RAMDISK_NINODES ?= 2000 +# RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table. +# The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates +# object ids as it goes, so there is no table to size and nothing to tune. # Kept out of the ramdisk root. # @@ -444,8 +442,12 @@ iso: rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd $(MAKE) RAMROOT=1 schos.iso -schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel - STAGE2_SECTORS=$(STAGE2_SECTORS) sh tools/posix/mkiso.sh $@ +# The UEFI loader is a dependency now: the disc carries a second El Torito +# entry pointing at a FAT image that holds it, so one disc boots both firmwares. +# sysroot-install because the disc also carries the tree as a real hierarchy. +schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel \ + $(BUILD)/uefi/BOOTX64.EFI sysroot-install + STAGE2_SECTORS=$(STAGE2_SECTORS) SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ # Boot the ISO the way a machine booting off a disc would: no hard disk at # all, so a kernel that still secretly depends on one fails here rather than @@ -525,10 +527,17 @@ $(BUILD)/kernel/kernel: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel # The ramdisk root image, and the object wrapping it for the linker. Built from # the same staged sysroot as the main image, just capped at RAMDISK_BLOCKS -- # mkfs's content dedup means the whole userland still fits in a few MB. -$(BUILD)/fs-ram.img: $(MKFS) $(BUILD)/sysroot.stamp +# Built with mkschfs, not mkfs. +# +# SchFS is the only root the kernel will mount -- it panics in forkret() when +# handed anything else -- so a v1 image here produced "schfs root: no SchFS +# volume on the root device" and made `make iso` and `make ramroot` unbootable. +# RAMDISK_NINODES is gone with it: SchFS has no preallocated inode table, which +# is what that knob existed to size. +$(BUILD)/fs-ram.img: $(MKSCHFS) $(BUILD)/sysroot.stamp @mkdir -p $(BUILD) @rm -f $@ - $(MKFS) $@ --size $(RAMDISK_BLOCKS) --ninodes $(RAMDISK_NINODES) \ + $(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \ $(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT) $(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img diff --git a/tools/mkschfs.c b/tools/mkschfs.c index b6d929f..be24376 100644 --- a/tools/mkschfs.c +++ b/tools/mkschfs.c @@ -464,6 +464,34 @@ nl_cmp(const void *a, const void *b) static void add_tree(uint64_t dirino, const char *hostdir); +// Paths kept out of the image, sysroot-relative. +// +// Same rule and same spelling as mkfs.c, because the two tools build the same +// tree and a path excluded from one must be excluded from the other. This is a +// licensing mechanism before it is a size one: the ramdisk image is linked into +// the kernel binary, so anything staged in sysroot/ when it is built becomes +// part of a distributable artifact. +#define MAXEXCLUDE 32 +static const char *excludes[MAXEXCLUDE]; +static int nexcludes; +// strlen(sysroot) + 1, so a host path can be turned back into the +// sysroot-relative one the exclude patterns are written against. +static size_t sysroot_prefix; + +// Matches exactly, or as a directory prefix so that "usr/share/misc" also drops +// "usr/share/misc/big.dat". +static int +is_excluded(const char *relpath) +{ + for (int i = 0; i < nexcludes; i++) { + size_t n = strlen(excludes[i]); + if (strncmp(relpath, excludes[i], n) == 0 && + (relpath[n] == '\0' || relpath[n] == '/')) + return 1; + } + return 0; +} + // Returns 1 if the entry was a directory, so the caller can total up the link // count without walking the directory a second time. static int @@ -472,6 +500,15 @@ add_entry(uint64_t dirino, const char *hostdir, const char *name) char path[8192]; snprintf(path, sizeof(path), "%s/%s", hostdir, name); + // Checked before the entry is dispatched, so an excluded directory is never + // opened and never gets an inode -- an empty directory left behind would + // still describe what was removed. + if (nexcludes && strlen(path) > sysroot_prefix && + is_excluded(path + sysroot_prefix)) { + fprintf(stderr, "mkschfs: excluding %s\n", path + sysroot_prefix); + return 0; + } + struct stat st; // lstat, not stat: a symlink must be stored as a symlink rather than // silently turned into a second copy of whatever it points at. @@ -562,6 +599,14 @@ main(int argc, char **argv) nblocks = strtoull(argv[++i], NULL, 0); else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc) label = argv[++i]; + else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) { + if (nexcludes >= MAXEXCLUDE) { + fprintf(stderr, "mkschfs: too many --exclude paths (max %d)\n", + MAXEXCLUDE); + return 1; + } + excludes[nexcludes++] = argv[++i]; + } else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) { // Testing only: shrink the name hash so collisions are the norm // rather than a once-in-a-lifetime event. schfs_fsck takes the @@ -583,7 +628,9 @@ main(int argc, char **argv) if (!out || !sysroot) { fprintf(stderr, "usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n" - " BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n", + " [--exclude PATH]...\n" + " BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n" + " --exclude omits a sysroot-relative path and its subtree\n", SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024))); return 1; } @@ -630,6 +677,10 @@ main(int argc, char **argv) if (r != 0) die("writing the root inode", r); ndirs++; + // add_entry() builds host paths as "/", so this is where the + // sysroot-relative part of such a path begins. + sysroot_prefix = strlen(sysroot) + 1; + add_tree(VOL.root_objectid, sysroot); // Publish. Everything above this line is invisible to a reader; if the tool diff --git a/tools/posix/mkiso.sh b/tools/posix/mkiso.sh index 98b7b48..c4b1fbc 100644 --- a/tools/posix/mkiso.sh +++ b/tools/posix/mkiso.sh @@ -3,8 +3,17 @@ # # tools/posix/mkiso.sh [output.iso] # -# The disc boots through El Torito's "no emulation" mode, which is the only -# one of the three that fits how SchOS actually loads itself. +# The disc carries two El Torito boot entries and a real directory tree: +# +# /boot/schos.bin BIOS: cdboot + stage2 + kernel, loaded no-emulation +# /boot/efiboot.img UEFI: a FAT image holding EFI/BOOT/BOOTX64.EFI +# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc +# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy +# +# --- BIOS entry ------------------------------------------------------------- +# +# El Torito's "no emulation" mode, which is the only one of the three that fits +# how SchOS actually loads itself. # # floppy emulation caps the image at 2.88 MB; the ramdisk-root kernel # alone is bigger than that. @@ -30,6 +39,38 @@ # NOT using -boot-info-table on purpose: it patches 56 bytes over offset 8 of # the boot image, which is the middle of cdboot's code. stage2main.c finds # where it was loaded from by reading the El Torito boot catalog instead. +# +# --- UEFI entry ------------------------------------------------------------- +# +# UEFI firmware does not run El Torito no-emulation code. It wants a FAT +# filesystem containing EFI/BOOT/BOOTX64.EFI, offered as a second boot entry. +# So one is built with mtools and added with -eltorito-alt-boot. The same +# 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 covers UEFI only, and deliberately. The classic BIOS isohybrid trick +# needs syslinux's isohdpfx.bin, and even with it the BIOS path here would not +# work: on a stick the firmware presents the medium as a hard disk, while +# stage2main.c reads the kernel back over ATAPI. Booting BIOS off USB is what +# build/usbstick.img is for. +# +# --- 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 @@ -39,14 +80,17 @@ STAGE2_SECTORS=${STAGE2_SECTORS:-48} STAGE2_BYTES=$((STAGE2_SECTORS * 512)) LOAD_SIZE=$((1 + STAGE2_SECTORS)) STAGING="$BUILD/iso" +SYSROOT=${SYSROOT:-sysroot} CDBOOT="$BUILD/boot/cdboot" STAGE2="$BUILD/boot/stage2" KERNEL="$BUILD/kernel/kernel" +EFIAPP="$BUILD/uefi/BOOTX64.EFI" for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do [ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; } done +[ -f "$EFIAPP" ] || { echo "mkiso: missing $EFIAPP -- run 'make uefi' first" >&2; exit 1; } # The three pieces must be contiguous and at these exact sizes: stage2main.c's # readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image, @@ -57,9 +101,61 @@ stage2_sz=$(wc -c < "$STAGE2") [ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; } rm -rf "$STAGING" -mkdir -p "$STAGING/boot" +mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT" cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin" +cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI" + +# 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. +# +# That means the kernel is stored twice on the disc: once inside boot/schos.bin +# for the BIOS entry, once here for the UEFI one. The alternative is teaching +# the loader to go looking for other volumes, which is a real feature and not a +# packaging detail -- worth doing deliberately rather than to save a copy. +# +# 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 + +if command -v mformat >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1; then + rm -f "$EFI_IMG" + # 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 + HAVE_EFI=1 +else + echo "mkiso: warning: mtools not found, building a BIOS-only disc" >&2 + echo " Fedora: sudo dnf install mtools" >&2 + rm -f "$EFI_IMG" + HAVE_EFI=0 +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 @@ -68,8 +164,10 @@ cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin" # 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 @@ -77,15 +175,31 @@ else exit 1 fi +if [ "$HAVE_EFI" = 1 ]; then + EFI_ARGS="-eltorito-alt-boot -e boot/efiboot.img -no-emul-boot $HYBRID" +else + EFI_ARGS="" +fi + +# shellcheck disable=SC2086 # EFI_ARGS is a deliberate word list $MKISOFS \ -quiet \ -V SCHOS \ -A "SchOS" \ + -R -J -joliet-long \ + -cache-inodes \ -b boot/schos.bin \ -no-emul-boot \ -boot-load-size "$LOAD_SIZE" \ + $EFI_ARGS \ -o "$OUT" \ "$STAGING" echo "==> $OUT ($(wc -c < "$OUT") bytes)" -echo " qemu-system-x86_64 -cdrom $OUT -boot d" +if [ "$HAVE_EFI" = 1 ]; then + echo " BIOS: qemu-system-x86_64 -cdrom $OUT -boot d" + echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT" + echo " USB: dd if=$OUT of=/dev/sdX bs=4M (UEFI only -- see the header)" +else + echo " qemu-system-x86_64 -cdrom $OUT -boot d" +fi