SchOS/Makefile
Nexoniarz 64e4835524 boot: a shared UI, and a graphical splash on UEFI
The first piece of the two loaders that is genuinely common code rather
than a contract they both honour. It can be, because the difference
between the firmwares is entirely in how a framebuffer is *obtained* --
VBE in real mode, or GOP -- and not in what you do with one afterwards.
boot/common/ui.c takes a base, a size and a stride and draws; it knows
nothing about disks, memory maps or mode setting, which is what lets it
compile into a 16/32/64-bit BIOS loader and a PE/COFF UEFI application
without either leaking into it.

The look is deliberately quiet: dark ground, the mark centred, one thin
bar beneath it, nothing on screen that is not load-bearing. The whole
group is laid out from the centre outwards rather than from the top down,
so it sits identically at 800x600 and at 1600x1200 instead of drifting up
the screen as the mode grows.

UEFI had no graphical splash at all -- it printed text to a console the
QEMU window is usually not even showing. It draws the same screen as the
BIOS path now, with the bar tracking bytes of kernel actually written
rather than a segment count, which says nothing about how long each takes.

Two ordering bugs found by looking at the pixels rather than the code:
setup_framebuffer ran *after* the kernel was read, so every progress tick
went to a framebuffer that did not exist yet and the splash then painted
over the result; and that also put the resolution prompt after the
slowest step instead of at the start. The display is set up first now.

Verified from the host with screendump: background is exactly SCH_BG, the
bar is 4px tall and width/4 wide at y=495 on a 1280x800 screen, which is
where centring the group puts it. UEFI still boots to cpu0; BIOS
bootcheck 27/27.

Not yet shared: the BIOS loader still draws with its own copy of this
code. Deleting that copy in favour of this one is the next step and the
point of the exercise -- until then this is one loader using shared code
and one duplicating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:26:54 +02:00

1068 lines
50 KiB
Makefile

BUILD = build
SYSROOT = sysroot
# -----------------------------------------------------------------------
# Parallelism
#
# Nothing here is order-dependent beyond what the rules already declare, so
# there is no reason to compile one file at a time. Override with `make -j1`
# when you want readable output from a failing build.
# -----------------------------------------------------------------------
# nproc is coreutils, so it exists on Linux and under Git Bash but not on
# macOS or the BSDs, where the same number comes from sysctl. Falling through
# to 4 is a working build either way, just a slower one.
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
MAKEFLAGS += -j$(NPROC)
# Kernel object files — organized by subsystem
KERN_ARCH = $(addprefix $(BUILD)/kernel/arch/x86_64/, \
acpi.o e820.o entry.o trapasm.o swtch.o \
trap.o vm.o lapic.o ioapic.o mp.o)
KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
main.o kalloc.o dma.o spinlock.o sleeplock.o string.o console.o \
panic.o uaccess.o qr.o time.o)
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \
pci.o text.o ttf.o uart.o vga.o ramdisk.o procfs.o)
# Drivers for hardware that has to be *found* rather than assumed. Anything
# discovered by walking a bus belongs here; the fixed-address legacy devices
# (console, keyboard, PIT, the IDE ports) stay in dev/.
#
# drivers/<class>/<driver>/ -- one directory per driver, so a driver that grows
# a register header, a second source file or a firmware blob has somewhere to
# put it. Classes match include/device.h's DEVCLASS_*. Everything flat was
# already eight files with four called usb_something, and each driver added made
# the directory harder to read. The compile rule uses `mkdir -p $(dir $@)`, so
# the depth costs nothing.
KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_pci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/e1000/e1000.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o
KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \
exec.o file.o fs.o log.o pipe.o procfs.o)
# SchFS. Every one of these is the very same source the host tools compile, so
# the kernel and mkschfs cannot disagree about the on-disk format: there is one
# implementation, found and fixed on the host where a test takes a second.
KERN_FS += $(addprefix $(BUILD)/kernel/fs/schfs/, \
btree.o space.o vol.o obj.o objw.o kmount.o vfs.o)
# Sockets. Unix domain only for now; an IP stack becomes more files here
# rather than a reshaping of what is already written.
KERN_NET = $(addprefix $(BUILD)/kernel/net/, \
unix.o)
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
proc.o)
KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
syscall.o sys_proc.o sys_fs.o sys_mem.o sys_net.o \
sys_time.o sys_sig.o sys_compat.o sys_poll.o linux_abi.o)
# vectors.o lives alongside trapasm in the arch dir
KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o
KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \
$(KERN_FS) $(KERN_NET) $(KERN_PROC) $(KERN_SYS)
# entry.o must be first in link order; remove it from KERNOBJS for the rule
KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
# -----------------------------------------------------------------------
# First-party userland
#
# lib/ startup glue + syscall stubs every cmd/ program links against
# cmd/ one directory per program: cmd/<name>/main.c -> sysroot/bin/<name>
#
# To add a program: mkdir cmd/foo, write cmd/foo/main.c, add "foo" to CMDS.
# Third-party software does NOT go here — see ports/.
# -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
CMDS = init schinit fbinfo shmtest socktest inputtest
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# -----------------------------------------------------------------------
# Toolchain
# -----------------------------------------------------------------------
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Host platform.
#
# WINHOST Windows, under Git Bash or MSYS2
# MACHOST macOS
# BSDHOST FreeBSD / OpenBSD / NetBSD / DragonFly
# (else) Linux
#
# All four are supported hosts: this is developed from both Windows and Linux,
# and nothing here should care which.
#
# Building natively on Windows rather than in WSL, because WSL2 is a real Linux
# kernel in a Hyper-V VM and so hard-requires virtualisation; turning that on
# puts the Hyper-V hypervisor underneath the whole machine and demotes VMware
# to running nested on top of it, which is a real and permanent cost to pay for
# a build environment.
ifneq (,$(filter MINGW% MSYS% CYGWIN%,$(UNAME_S)))
WINHOST := 1
endif
ifeq ($(UNAME_S),Darwin)
MACHOST := 1
endif
ifneq (,$(filter FreeBSD OpenBSD NetBSD DragonFly,$(UNAME_S)))
BSDHOST := 1
endif
QEMU = qemu-system-x86_64
# Display for every QEMU target.
#
# virtio-gpu is what the display class binds, and what a compositor should be
# developed against: presenting is explicit, so the host never shows a
# half-drawn frame.
#
# stdvga stays alongside it and is not optional. The bootloader draws the splash
# through VBE in 16-bit real mode and there is no virtio path that early, so a
# machine with only virtio-gpu boots blind until the kernel is up. Both devices:
# virtio-gpu wins the display class, stdvga carries the splash and text console.
QEMU_DISPLAY = -vga std -device virtio-gpu-pci
# An absolute pointing device, on an xHCI controller.
#
# A PS/2 mouse is relative, so inside a VM the guest cursor and the host's
# accumulate independently and drift apart -- they are simply never in the same
# place. usb-tablet reports where the pointer *is*, so the two coincide.
#
# EMPTY BY DEFAULT, and that is not an oversight. The driver binds the tablet
# and identifies it correctly, but no reports have yet been observed arriving
# on its interrupt endpoint -- so switching the default pointer to it would
# replace a cursor that drifts with one that does not move. Opt in while
# working on it:
#
# make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0'
#
# See kernel/drivers/usb/hid/usb_hid.c, tablet_report().
QEMU_INPUT ?=
# -----------------------------------------------------------------------
# Cross-compilation: clang everywhere it can be had
#
# The kernel, the bootloader and the ports all need a compiler targeting
# something other than the host -- bare-metal ELF, 16/32-bit code below one
# megabyte, and Linux/musl userland respectively. The traditional answer is a
# cross binutils+gcc per host, which is a day of work per platform that then
# rots, and on macOS means brewing x86_64-elf-* before anything builds at all.
#
# clang needs none of it: it is a cross-compiler by construction, so one
# ordinary LLVM install produces every target from every host. That was already
# how the Windows build worked; using it on all four hosts collapses what used
# to be two divergent toolchain branches -- one of which had drifted into
# passing GNU-ld-only flags to what is nowadays lld on FreeBSD and macOS -- into
# a single path that gets exercised on every platform instead of one.
#
# GNU is kept as a fallback for hosts without clang, but it is the exception.
# -----------------------------------------------------------------------
HAVE_CLANG := $(shell command -v clang 2>/dev/null)
HAVE_LLD := $(shell command -v ld.lld 2>/dev/null)
ifeq ($(origin USE_GNU), undefined)
ifneq (,$(HAVE_CLANG))
ifneq (,$(HAVE_LLD))
USE_CLANG := 1
endif
endif
endif
ifdef USE_CLANG
# An explicit --target is what makes clang emit ELF rather than whatever the
# host defaults to -- COFF on Windows, Mach-O on macOS. Without it every
# object is silently the wrong format and the first thing to complain is the
# linker.
CC = clang --target=x86_64-elf
CC32 = clang --target=i386-elf
LD = ld.lld
OBJCOPY = llvm-objcopy
OBJDUMP = llvm-objdump
# ld.lld has no --no-warn-rwx-segments: it does not emit that warning, so
# there is nothing to suppress and the flag is a hard error.
LDFLAGS = -m elf_x86_64 -nostdlib
# The bootloader, the AP trampoline, initcode and the cmd/ programs are all
# linked at addresses below one megabyte. GNU ld places a section wherever
# -Ttext says; lld first checks it against a default image base of 0x400000
# and refuses, because for a hosted program that would be a bug. Saying the
# base is zero is how you tell it this is not a hosted program.
LD_LOWBASE = --image-base=0
# ports/ build *Linux userland* against musl -- a third target, distinct from
# both the kernel (bare-metal ELF) and the host tools (native).
#
# -nostdlib with the crt files named explicitly, because clang otherwise
# reaches for gcc's crtbeginT.o and libgcc, which need not exist on any
# particular host.
MUSLPREFIX = $(abspath $(BUILD))/ports/musl/prefix
PORTCC = clang --target=x86_64-unknown-linux-musl -fuse-ld=lld
PORTCRT = -nostdlib $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o
PORTEND = $(MUSLPREFIX)/lib/crtn.o
else
# GNU fallback. Needs a cross toolchain anywhere the host's own gcc does not
# already emit x86_64 ELF -- notably macOS, where TOOLPREFIX has to name a
# brewed x86_64-elf-gcc.
ifdef MACHOST
TOOLPREFIX ?= x86_64-elf-
else ifeq ($(UNAME_P),aarch64)
TOOLPREFIX ?= x86_64-linux-gnu-
else
TOOLPREFIX ?=
endif
CC = $(TOOLPREFIX)gcc
CC32 = $(CC) -m32
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
LDFLAGS = -m elf_x86_64 -nostdlib --no-warn-rwx-segments
LD_LOWBASE =
PORTCC = $(TOOLPREFIX)gcc
PORTCRT =
PORTEND =
endif
# -----------------------------------------------------------------------
# Host tools: mkfs, mkschfs, the SchFS suites, qrtest. Ordinary programs for
# the machine doing the building, so they use its native compiler.
# -----------------------------------------------------------------------
HOSTCC ?= cc
ifdef WINHOST
# mingw-w64's gcc, not clang: clang defaults to an MSVC target here, and
# tools/mkfs.c includes <unistd.h> and <dirent.h>, which the Windows SDK
# does not have.
HOSTCC = gcc
# mingw-w64's gcc warns about things glibc's does not (notably %lld/%zu in
# printf against msvcrt). Those warnings are about the host build only and
# say nothing about the image, so they must not stop it.
HOSTWARN = -Wall
# MinGW gcc appends .exe when the -o name has no extension, so a rule whose
# target lacks it is never considered built -- mkfs was recompiled on every
# single invocation of make, forever, because of this.
EXE = .exe
# `python3` is not on PATH from the python.org installer; `python` is.
PYTHON = python
else
HOSTWARN = -Werror -Wall
EXE =
PYTHON = python3
endif
# Host tools are generated output and live under build/, per docs/layout.md.
# $(EXE) matters: MinGW appends it, and a rule whose target lacks it never
# looks satisfied.
MKFS = $(BUILD)/tools/mkfs$(EXE)
MKSCHFS = $(BUILD)/tools/mkschfs$(EXE)
SCHFS_TEST = $(BUILD)/tools/schfs_test$(EXE)
SCHFS_OBJT = $(BUILD)/tools/schfs_objtest$(EXE)
SCHFS_FSCK = $(BUILD)/tools/schfs_fsck$(EXE)
OPT ?= -O0
# -ftls-model=local-exec: the kernel is *the* executable, never a shared
# object, so its __thread variables (cpu, proc) are always at a fixed negative
# offset from the thread pointer. Saying so lets the compiler emit a direct
# `movq %rcx, %fs:-0x10` and never materialise the thread pointer as a value.
#
# GCC picked local-exec on its own for this code; clang did not, and generated
# `movq %fs:0x0, %rax; addq $-0x10, %rax; ...` instead -- which reads the TCB
# self-pointer at %fs:0, a word this kernel does not maintain and something
# else has apparently always been scribbling on. Harmless for fifteen years
# because nothing ever read it; instant triple fault the moment a compiler did.
XFLAGS = -m64 -DX64 -mcmodel=large -mtls-direct-seg-refs -mno-red-zone
XFLAGS += -ftls-model=local-exec
# Probed once, with :=, not on every expansion of CFLAGS. CFLAGS is a
# recursively-expanded variable, so a $(shell) appended to it re-ran the probe
# on every single compile -- one extra gcc process per object file, for an
# answer that cannot change during a build.
NO_SSP := $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -ggdb -fno-omit-frame-pointer
CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT)
CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
# sys/ also needs to find table.inc via a relative include
CFLAGS_SYS = $(CFLAGS) -I./kernel/sys
# -----------------------------------------------------------------------
# Ramdisk root (opt-in): make RAMROOT=1
#
# Links a complete SCHFS image into the kernel and boots from it instead of
# the IDE disk, so the physical disk can be reformatted from a running system.
# Off by default because it adds RAMDISK_BLOCKS KB to the kernel, and stage2
# loads the kernel by PIO one sector at a time -- a 6 MB kernel takes visibly
# longer to boot than a 350 KB one.
# -----------------------------------------------------------------------
RAMDISK_BLOCKS ?= 6000
# 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.
#
# Empty right now, and the machinery is kept anyway, because this is a licensing
# rule 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. Anything that arrives in sysroot/ under a licence that
# cannot be combined with Apache-2.0, or that simply is not ours to redistribute,
# belongs on this list rather than in someone's memory.
#
# This is not hypothetical: a 33 MB test payload once landed here and overflowed
# the 6 MB image, and mkfs's "increase FSSIZE" message then pointed at a constant
# that had nothing to do with it.
# usb.ids is 730 KB of a 6 MB image -- 12% of the ramdisk root -- and nothing
# needs it to boot. It is a lookup table for a userspace tool, so it stays on
# the disk image and out of the kernel binary.
RAMDISK_EXCLUDE = usr/share/usb.ids
RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p))
ifdef RAMROOT
CFLAGS += -DRAMDISK_ROOT
RAMDISK_OBJ = $(BUILD)/kernel/ramdisk_obj
else
RAMDISK_OBJ =
endif
# Drop the objects when RAMROOT is toggled, in *either* direction.
#
# RAMROOT changes CFLAGS, and make cannot see that: an object compiled one way
# is happily reused the other way. The `ramroot` target already wiped on the way
# in, and nothing wiped on the way out -- so a plain `make` after `make ramroot`
# kept every RAMDISK_ROOT object and linked the 24 MB ramdisk back in. The
# result still booted, from the ramdisk, and the only outward symptom was that
# stage2 now had 25 MB to load through PIO one sector at a time. Measured:
# 25,413,120 bytes against 836,840 for the same kernel built clean.
#
# Evaluated when the Makefile is read, so the wipe happens before anything is
# compiled against the wrong setting.
RAMROOT_STATE := $(if $(RAMROOT),on,off)
RAMROOT_STAMP := $(BUILD)/ramroot.state
$(shell mkdir -p $(BUILD); \
if [ "$$(cat $(RAMROOT_STAMP) 2>/dev/null)" != "$(RAMROOT_STATE)" ]; then \
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd; \
printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \
fi)
# -----------------------------------------------------------------------
# Top-level targets
# -----------------------------------------------------------------------
# usbstick.img is built here rather than on demand because bootcheck -Usb
# cannot start QEMU without it, and a missing test image looks exactly like a
# broken USB driver: QEMU refuses the -drive and the whole run produces nothing.
all: schos.img $(BUILD)/usbstick.img
schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img
dd if=/dev/zero of=schos.img count=10000
dd if=$(BUILD)/boot/bootblock of=schos.img conv=notrunc
dd if=$(BUILD)/boot/stage2 of=schos.img seek=1 conv=notrunc
# seek is 1 + STAGE2_SECTORS, not a literal. It was 25 against a 24-sector
# stage2; raising STAGE2_SECTORS for the boot logo without touching it wrote
# the kernel straight over stage2's own tail.
dd if=$(BUILD)/kernel/kernel of=schos.img seek=$$(( 1 + $(STAGE2_SECTORS) )) conv=notrunc
kernel: $(BUILD)/kernel/kernel
# -----------------------------------------------------------------------
# UEFI loader
#
# A PE/COFF executable, not ELF, so it needs a different target and a
# different linker driver -- but the same clang install. That is the whole
# reason the toolchain is clang: one compiler reaches every target here.
#
# -mno-red-zone is not optional. UEFI firmware may take an interrupt at any
# point, and the red zone is memory below RSP that a handler is entitled to
# clobber; leaving it enabled corrupts locals at random.
# -fshort-wchar makes L"..." UTF-16, which is the only string type UEFI has.
# -----------------------------------------------------------------------
UEFI_CFLAGS = --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h \
boot/common/ui.c boot/common/ui.h
@mkdir -p $(BUILD)/uefi
clang $(UEFI_CFLAGS) -c boot/uefi/main.c -o $(BUILD)/uefi/main.obj
clang $(UEFI_CFLAGS) -c boot/common/ui.c -o $(BUILD)/uefi/ui.obj
lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj $(BUILD)/uefi/ui.obj -out:$@
uefi: $(BUILD)/uefi/BOOTX64.EFI
# The EFI System Partition, as a directory. QEMU serves a directory as FAT
# with `-drive file=fat:rw:`, which is enough to boot and test without any
# image-building tools -- mtools is not present on every supported host.
# A real ESP image for `make iso` is still to come.
$(BUILD)/esp/EFI/BOOT/BOOTX64.EFI: $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/kernel/kernel
@mkdir -p $(BUILD)/esp/EFI/BOOT $(BUILD)/esp/schos
@cp $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
@cp $(BUILD)/kernel/kernel $(BUILD)/esp/schos/kernel
@cp boot/uefi/startup.nsh $(BUILD)/esp/startup.nsh
@echo "ESP staged in $(BUILD)/esp"
esp: $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
# Boot the UEFI path under OVMF. The firmware ships with QEMU; the varstore
# is copied because it must be writable.
#
# i440fx, not q35: the kernel's IDE driver speaks to the legacy ports at
# 0x1f0, and q35 has no legacy IDE at all -- only AHCI. The root volume would
# simply not be there.
OVMF_CODE = $(BUILD)/OVMF_CODE.fd
OVMF_VARS = $(BUILD)/OVMF_VARS.fd
# OVMF, wherever this host keeps it.
#
# There is no common location: QEMU's own Windows build puts it beside the
# binary as edk2-x86_64-code.fd, Fedora ships edk2-ovmf into
# /usr/share/edk2/ovmf/OVMF_CODE.fd, Debian and Arch each differ again. Search
# rather than assume, and say something useful when nothing is found -- an
# unhelpful "cp: no such file" is a poor way to learn that a package is
# missing.
QEMU_SHARE = $(dir $(shell command -v $(QEMU) 2>/dev/null))share
OVMF_CODE_CANDIDATES = $(QEMU_SHARE)/edk2-x86_64-code.fd /usr/share/edk2/ovmf/OVMF_CODE.fd /usr/share/OVMF/OVMF_CODE.fd /usr/share/ovmf/x64/OVMF_CODE.fd /usr/share/qemu/edk2-x86_64-code.fd
OVMF_VARS_CANDIDATES = $(QEMU_SHARE)/edk2-i386-vars.fd /usr/share/edk2/ovmf/OVMF_VARS.fd /usr/share/OVMF/OVMF_VARS.fd /usr/share/ovmf/x64/OVMF_VARS.fd /usr/share/qemu/edk2-i386-vars.fd
# Tested through the shell rather than with $(wildcard). On Windows, make is a
# native binary using Win32 file calls while `command -v` hands back an MSYS
# path like /c/Users/..., which $(wildcard) cannot see -- it silently returns
# nothing and the firmware looks missing on a machine that has it.
OVMF_CODE_SRC = $(shell for f in $(OVMF_CODE_CANDIDATES); do [ -f "$$f" ] && { echo "$$f"; break; }; done)
OVMF_VARS_SRC = $(shell for f in $(OVMF_VARS_CANDIDATES); do [ -f "$$f" ] && { echo "$$f"; break; }; done)
$(OVMF_CODE):
@mkdir -p $(BUILD)
@test -n "$(OVMF_CODE_SRC)" || { echo "no OVMF firmware found. Install it:"; echo " Fedora sudo dnf install edk2-ovmf"; echo " Debian sudo apt install ovmf"; echo " Arch sudo pacman -S edk2-ovmf"; exit 1; }
cp "$(OVMF_CODE_SRC)" $@
chmod u+w $@
$(OVMF_VARS):
@mkdir -p $(BUILD)
@test -n "$(OVMF_VARS_SRC)" || { echo "no OVMF varstore found -- install the OVMF package (see above)"; exit 1; }
cp "$(OVMF_VARS_SRC)" $@
chmod u+w $@
qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT)
# Build the ramdisk-root variant. This is a target rather than just
# "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT),
# and make has no way to notice that: objects compiled for one setting would be
# silently reused for the other, leaving ROOTDEV pointing at the wrong device.
# Dropping the compiled objects first makes the switch always correct.
ramroot:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1
cmd: $(CMDBINS)
ports:
$(MAKE) -C ports
# projects/ -- things that run *on* SchOS rather than things that *are* SchOS.
#
# Deliberately not a dependency of anything above: `make` never descends here,
# and the kernel build does not know these exist. They are ordinary programs
# built against the sysroot's libc, exactly as toybox is, and the boundary
# between them and the OS is syscalls and protocols rather than a shared header
# nobody meant to share. See projects/README.md.
#
# Staged into $(SYSROOT) so the next image build picks them up; run
# `make projects && make` to get them onto a disk.
PROJECTS ?= schcom
projects:
@test -d $(MUSLPREFIX)/include || { \
echo "projects: no musl in $(MUSLPREFIX) -- run 'make ports' first" >&2; \
exit 1; }
@for p in $(PROJECTS); do \
echo "==> projects/$$p"; \
$(MAKE) -C projects/$$p \
CC='$(PORTCC)' \
CFLAGS='-O2 -static -D__SCHOS__ -I$(MUSLPREFIX)/include -fno-stack-protector' \
LDFLAGS='-static -L$(MUSLPREFIX)/lib' \
DESTDIR='$(abspath $(SYSROOT))' install || exit 1; \
done
# -----------------------------------------------------------------------
# Bootable ISO
#
# Always a ramdisk-root build. An ISO that needed a second disk to find its
# root filesystem would not be an install medium, it would be a puzzle -- and
# read-only media cannot host a writable root anyway. Same object-wiping
# preamble as the `ramroot` target, for the same reason: RAMROOT changes
# CFLAGS and make cannot see that.
# -----------------------------------------------------------------------
iso:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1 schos.iso
# 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
# in someone's optical drive.
qemu-iso: schos.iso
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-cdrom schos.iso -boot d \
-m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Bootloader — two stages. Stage 1 (bootblock) is the actual 512-byte MBR
# sector: BIOS-imposed hard limit, so it only loads stage 2 off disk and
# jumps to it. Stage 2 does VBE/A20/protected-mode setup, draws the boot
# splash, and loads the kernel — not size-constrained the same way, just
# padded out to a fixed STAGE2_SECTORS*512 bytes so the kernel's starting
# sector on disk is predictable (kept in sync with STAGE2_SECTORS in
# boot/bios/bootasm.S and boot/bios/stage2main.c).
# -----------------------------------------------------------------------
# STAGE2_SECTORS used to be written out three times -- here, in boot/bios/bootasm.S
# and in boot/bios/stage2main.c -- and docs/layout.md carried a warning about keeping
# them in step. They are now all fed from this one variable via -D, so the
# warning describes a problem that can no longer happen. Both C/asm files keep
# an #ifndef fallback purely so they still read sensibly on their own.
#
# The ceiling is memory, not disk: stage2 is loaded at 0x7E00 and the multiboot
# kernel-search buffer begins at 0x10000, so 65 sectors is the hard maximum.
# stage2main.c static-asserts this. padstage2.pl fails the build if the compiled
# binary overflows the budget, rather than truncating it -- a short stage2 would
# shift the kernel's start sector and fail in a way that looks nothing like its
# cause.
STAGE2_SECTORS = 48
STAGE2_DEF = -DSTAGE2_SECTORS=$(STAGE2_SECTORS)
# Byte budget is computed in the recipe with shell arithmetic, not $(shell expr
# ... \* 512): make hands the backslash through untouched, sh then treats the
# `*` as a glob, and expr gets a directory listing. It "worked" to the extent
# that make carried on and padstage2.pl received nonsense.
STAGE2_BYTES = $$(( $(STAGE2_SECTORS) * 512 ))
$(BUILD)/boot/bootblock: boot/bios/bootasm.S
@mkdir -p $(BUILD)/boot
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/bootasm.S -o $(BUILD)/boot/bootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootblock
./tools/sign.pl $(BUILD)/boot/bootblock
# Stage 1 for optical media. Same job as bootblock, far less of it: El Torito
# has already loaded stage2 alongside this sector by the time it runs, so it
# only records that we came from a CD and jumps. See boot/bios/cdboot.S.
$(BUILD)/boot/cdboot: boot/bios/cdboot.S
@mkdir -p $(BUILD)/boot
$(CC32) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/cdboot.S -o $(BUILD)/boot/cdbootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/cdboottmp.o $(BUILD)/boot/cdbootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/cdboottmp.o $@
./tools/sign.pl $@
$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h
@mkdir -p $(BUILD)/boot
$(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/stage2.S -o $(BUILD)/boot/stage2asm.o
$(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o
$(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \
-o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o
$(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2
./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES)
# -----------------------------------------------------------------------
# Kernel binary
# -----------------------------------------------------------------------
$(BUILD)/kernel/kernel: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ) kernel/kernel.ld
@mkdir -p $(BUILD)/kernel
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ \
$(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
$(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ)
$(OBJDUMP) -S $@ > $(BUILD)/kernel/kernel.asm
# 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.
# 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 $@
$(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT)
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_fs_ram_img_start=_binary_ramdisk_start \
--redefine-sym _binary_build_fs_ram_img_end=_binary_ramdisk_end \
--redefine-sym _binary_build_fs_ram_img_size=_binary_ramdisk_size \
$@
# AP startup blob
$(BUILD)/kernel/entryother: kernel/arch/x86_64/entryother.S
@mkdir -p $(BUILD)/kernel
$(CC) $(CFLAGS) -fno-pic -nostdinc -I./include -c $< -o $(BUILD)/kernel/entryother.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e start -Ttext 0x7000 -o $(BUILD)/kernel/entryothertmp.o $(BUILD)/kernel/entryother.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/kernel/entryothertmp.o $@
# NOTE: ld -r -b binary derives its symbol names from the *path* of the input
# file, so these --redefine-sym pairs must be kept in sync with the build
# directory layout above. Move the file, and the left-hand names change.
$(BUILD)/kernel/entryother_obj: $(BUILD)/kernel/entryother
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_kernel_entryother_start=_binary_entryother_start \
--redefine-sym _binary_build_kernel_entryother_end=_binary_entryother_end \
--redefine-sym _binary_build_kernel_entryother_size=_binary_entryother_size \
$@
# Init stub (first user-space code, embedded in the kernel image)
$(BUILD)/lib/initcode: lib/initcode.S
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -fno-pic -nostdinc -I./include -c $< -o $(BUILD)/lib/initcode.o
$(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -N -e start -Ttext 0 -o $(BUILD)/lib/initcodetmp.o $(BUILD)/lib/initcode.o
$(OBJCOPY) -S -O binary $(BUILD)/lib/initcodetmp.o $@
$(BUILD)/kernel/initcode_obj: $(BUILD)/lib/initcode
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_lib_initcode_start=_binary_initcode_start \
--redefine-sym _binary_build_lib_initcode_end=_binary_initcode_end \
--redefine-sym _binary_build_lib_initcode_size=_binary_initcode_size \
$@
# -----------------------------------------------------------------------
# vectors.S — generated from perl script
# -----------------------------------------------------------------------
kernel/arch/x86_64/vectors.S: tools/vectors.pl
perl tools/vectors.pl > $@
# -----------------------------------------------------------------------
# Compile rules for kernel/
# -----------------------------------------------------------------------
$(BUILD)/kernel/arch/x86_64/%.o: kernel/arch/x86_64/%.c
@mkdir -p $(BUILD)/kernel/arch/x86_64
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/arch/x86_64/%.o: kernel/arch/x86_64/%.S
@mkdir -p $(BUILD)/kernel/arch/x86_64
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/core/%.o: kernel/core/%.c
@mkdir -p $(BUILD)/kernel/core
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/dev/%.o: kernel/dev/%.c
@mkdir -p $(BUILD)/kernel/dev
$(CC) $(CFLAGS) -c $< -o $@
# `$(dir $@)` rather than a fixed path: drivers are grouped into one directory
# per device class (gpu/, net/, bus/, usb/), so the output directory varies with
# the target and a hardcoded mkdir only creates the top one.
$(BUILD)/kernel/drivers/%.o: kernel/drivers/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/fs/%.o: kernel/fs/%.c
@mkdir -p $(BUILD)/kernel/fs
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/fs/schfs/%.o: kernel/fs/schfs/%.c
@mkdir -p $(BUILD)/kernel/fs/schfs
$(CC) $(CFLAGS) -I./kernel/fs/schfs -c $< -o $@
$(BUILD)/kernel/net/%.o: kernel/net/%.c
@mkdir -p $(BUILD)/kernel/net
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/proc/%.o: kernel/proc/%.c
@mkdir -p $(BUILD)/kernel/proc
$(CC) $(CFLAGS) -c $< -o $@
# sys/ gets an extra -I so syscall.c can do #include "table.inc"
$(BUILD)/kernel/sys/%.o: kernel/sys/%.c
@mkdir -p $(BUILD)/kernel/sys
$(CC) $(CFLAGS_SYS) -c $< -o $@
# -----------------------------------------------------------------------
# First-party userland: lib/ + cmd/
# -----------------------------------------------------------------------
$(BUILD)/lib/%.o: lib/%.c
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/lib/%.o: lib/%.S
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/cmd/%: cmd/%/main.c $(ULIB)
@mkdir -p $(BUILD)/cmd
$(CC) $(CFLAGS) -c $< -o $(BUILD)/cmd/$*.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ $(BUILD)/cmd/$*.o $(ULIB)
$(OBJCOPY) -S $@
$(SYSROOT)/bin/%: $(BUILD)/cmd/% | $(SYSROOT)/bin
cp $< $@
chmod 755 $@
$(SYSROOT)/bin:
mkdir -p $@
# -----------------------------------------------------------------------
# Filesystem image
# -----------------------------------------------------------------------
# -----------------------------------------------------------------------
# SchFS B-tree torture test (host program).
#
# The B-tree underpins every record type in SchFS, so it is developed and
# tested here -- against a reference model, with simulated crashes -- before
# it is linked into the kernel. Run it after any change to
# kernel/fs/schfs/btree.c.
# -----------------------------------------------------------------------
schfs-test: $(SCHFS_TEST)
$(SCHFS_TEST)
# Use ASan/UBSan when the runtime libraries are actually installed; they catch
# exactly the pointer-arithmetic mistakes this code is most exposed to, but not
# every machine has libasan, so fall back rather than failing the build.
SAN := $(shell echo 'int main(void){return 0;}' > /tmp/.santest.c 2>/dev/null && \
gcc -fsanitize=address,undefined -o /tmp/.santest /tmp/.santest.c \
>/dev/null 2>&1 && echo -fsanitize=address,undefined)
SCHFS_SRC = kernel/fs/schfs/btree.c kernel/fs/schfs/btree.h \
kernel/fs/schfs/space.c kernel/fs/schfs/vol.c \
kernel/fs/schfs/obj.c kernel/fs/schfs/objw.c \
kernel/fs/schfs/schfs_int.h include/schfs.h
# -iquote, not -I: include/ has its own errno.h, stat.h, fcntl.h and types.h,
# and -I would put them ahead of the system ones for a host program that needs
# the real thing. -iquote applies only to "quoted" includes, so btree.h still
# finds schfs.h while <errno.h> still means the C library's.
HOSTCFLAGS = -Wall -Wextra -O1 -g $(SAN) -iquote ./include
$(SCHFS_TEST): tools/schfs_test.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_test.c
# The object layer -- files, directories, and whether deleting things gives the
# space back. Runs against a memory-backed volume, so a full create/write/
# delete/remount cycle takes milliseconds instead of a rebuild and a boot.
$(SCHFS_OBJT): tools/schfs_objtest.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_objtest.c
schfs-objtest: $(SCHFS_OBJT)
$(SCHFS_OBJT)
# The image writer and checker. Both #include the real filesystem sources, so
# there is exactly one implementation of the on-disk format: a host tool and the
# kernel cannot drift apart because there is nothing to drift from.
$(MKSCHFS): tools/mkschfs.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/mkschfs.c
$(SCHFS_FSCK): tools/schfs_fsck.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_fsck.c
mkschfs: $(MKSCHFS)
schfs-fsck: $(SCHFS_FSCK)
# End-to-end filesystem test: build an image from a directory tree, read it
# back with schfs_fsck, and diff the result against the original.
#
# The oracle here is the host's own filesystem, which knows nothing about SchFS.
# schfs-test proves the B-tree matches a model written alongside it; this proves
# the *filesystem* matches a tree that exists independently of it, so a writer
# and reader that agree on something wrong are still caught.
schfs-image-test: $(MKSCHFS) $(SCHFS_FSCK)
@sh tools/posix/schfs_roundtrip.sh $(MKSCHFS) $(SCHFS_FSCK)
# Every SchFS suite. Run this after touching anything under kernel/fs/schfs.
schfs-check: schfs-test schfs-objtest schfs-image-test
# Crash the kernel on purpose and photograph the result.
#
# The crash screen is the one part of the system you cannot look at by using
# the system, which is exactly why it rots. This makes "does it still render"
# a question with a thirty-second answer. Needs a ramroot build first.
crashshot:
pwsh -File tools/windows/crashshot.ps1
# Read the QR code back off a captured crash screen. End-to-end on purpose: it
# reads the pixels that were painted, so payload construction in panic context,
# the encoder, the module scaling and the quiet zone are all covered by whether
# text comes back. Run `make crashshot` first. Needs the same Python packages
# as `qrcheck`.
crashqr:
$(PYTHON) tools/python/crashqr.py
$(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I. -o $(MKFS) tools/mkfs.c
# Stage everything a running system needs into sysroot/, then image it.
#
# ports/ is built via its own Makefile rather than per-port from here, because
# that is where the inter-port ordering lives (mksh and toybox both link
# against musl, whose install prefix is under build/ — so `make clean` wipes it
# and musl must be rebuildable as part of the normal chain, not out-of-band).
#
# On Windows the ports are NOT rebuilt. musl, mksh and toybox are Linux
# userland built by configure scripts and kconfig, and getting those to run
# under Git Bash against a clang cross-target is a separate project from
# building the OS. Their outputs are already staged in sysroot/ and change
# perhaps twice a year, so the cost of not rebuilding them here is close to
# zero -- and it is stated out loud rather than silently skipped, because a
# port that quietly does not rebuild is exactly the kind of thing that wastes
# an afternoon later.
# schinit's job manifests. Kept under etc/ in the repo and installed here,
# because sysroot/ is generated and gitignored -- anything only ever written
# straight into it is lost on a fresh clone, which is already true of
# /etc/passwd and /etc/motd and should not become true of the boot
# configuration as well.
SCHINIT_JOBS = $(wildcard etc/schinit/*.job) etc/schinit/README
ETC_FILES = $(wildcard etc/*)
ETC_PLAIN = $(filter-out etc/schinit,$(ETC_FILES))
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN)
@mkdir -p $(SYSROOT)/etc/schinit
@cp $(ETC_PLAIN) $(SYSROOT)/etc/
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
@echo "installed $(words $(ETC_PLAIN)) /etc file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install
ifdef WINHOST
@echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)"
@test -f $(SYSROOT)/bin/sh || { echo "ports: $(SYSROOT)/bin/sh is missing and cannot be built here; build ports on a Linux host once" >&2; exit 1; }
else
$(MAKE) -C ports CC=gcc
endif
# A stamp whose timestamp advances only when the staged tree actually changes.
#
# make cannot depend on "the contents of a directory", and sysroot/ is filled
# by two different builds (cmd/ from here, ports/ recursively), so the images
# below used to depend on the phony sysroot-install -- which meant every image
# was rebuilt on every invocation, whether or not a single byte had moved.
# That cost 100 seconds of a 108-second no-op build.
#
# Path, size and mtime rather than content hashes: reading every staged file
# would undo the point, and a build system that lies about a file whose mtime
# and size both stayed the same has bigger problems. The cmp-then-move keeps
# the stamp's mtime unchanged when the summary is unchanged, which is the
# whole trick -- writing it unconditionally would just move the problem.
$(BUILD)/sysroot.stamp: sysroot-install
@mkdir -p $(BUILD)
@find $(SYSROOT) -type f -printf '%p %s %T@\n' | sort > $@.new
@cmp -s $@.new $@ || mv -f $@.new $@
@rm -f $@.new
# A small block device for the USB test to read.
#
# bootcheck -Usb attaches this as a usb-storage device and has the driver read
# block 1 off it, which is the only check that separates "a device answered
# INQUIRY" from "the driver can actually transfer data".
#
# Its own image, not the root volume and not build/fs.img:
# - attaching the root volume twice (once as hdb, once over USB) broke the
# xHCI interrupt check as well as the storage ones;
# - build/fs.img is no longer built by anything, since SchFS became the root,
# so pointing at it meant QEMU refused to start at all;
# - at FSSIZE blocks it would now be ~600 MB, because BSIZE went from 1024 to
# 4096. 16 MB is plenty to read one block from.
USBSTICK = $(BUILD)/usbstick.img
$(USBSTICK): $(MKFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
$(MKFS) $@ --size 4096 --sysroot $(SYSROOT)
usbstick: $(USBSTICK)
$(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@echo "Creating SchOS File System Image..."
@# A running QEMU holds this file open, and on Windows that makes rm fail
@# with "Device or resource busy" -- which stops the build *after* the
@# kernel has already relinked, leaving schos.img stale. The next test run
@# then boots the previous kernel and the change appears to have done
@# nothing, which has cost several debugging cycles. Say so plainly.
@rm -f $@ || { \
echo ""; \
echo " $@ is in use -- a QEMU is still holding it."; \
echo " Close it, or: pwsh -Command \"Get-Process qemu-system-x86_64 |"; \
echo " Where-Object { -not \$$_.MainWindowTitle } | Stop-Process -Force\""; \
echo ""; \
exit 1; \
}
$(MKFS) $@ --sysroot $(SYSROOT)
# -----------------------------------------------------------------------
# QEMU
# -----------------------------------------------------------------------
# The same thing with hardware virtualisation. Opt-in, never the default: the
# test suite's timings were established under TCG and changing the execution
# engine underneath them is a variable to introduce on purpose.
#
# Worth knowing what it does and does not buy. Measured with the guest timing
# itself, `ls /bin | wc -l` goes from 2.9s to 1.1s. Boot does *not* improve --
# it gets worse -- because stage2 loads the kernel by PIO one sector at a time,
# and under KVM every one of those port accesses is a VM exit, where under TCG
# it is cheap.
# Schcom, end to end, checked from outside the machine.
#
# Boots, starts the compositor and a client, and then screendumps the
# framebuffer and asserts the pixels on the host. That last part is the whole
# value: the compositor saying "I composited it" proves nothing, and a
# screenshot nobody inspects proves less. Needs `make projects` first.
schcom-test: schos.img $(BUILD)/schfs.img
sh tools/posix/schcom_test.sh
qemu-kvm: schos.img $(BUILD)/schfs.img
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
qemu: schos.img $(BUILD)/schfs.img
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret().
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size 8000 --label schos --sysroot $(SYSROOT)
# Boot with a SchFS volume attached as the second disk.
#
# Needs the ramdisk-root kernel ("make ramroot" first): with RAMROOT the root
# filesystem comes out of memory and device 1 is free, which is exactly the
# arrangement docs/architecture/schfs.md asks for -- a new-format disk that
# fails to mount cannot stop the system booting. Without RAMROOT, device 1 is
# the live v1 root and the SchFS probe correctly finds nothing there.
#
# Once up: cat /proc/schfs
qemu-schfs: schos.img $(BUILD)/schfs.img
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Misc
# -----------------------------------------------------------------------
# schos.img is deliberately NOT here. It is a real file with real
# prerequisites; listing it as phony made make skip the timestamp check and
# re-run the four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed.
# include/logo.h is generated but committed, exactly as include/font.h is: it
# derives from art that changes about never, and making it a build dependency
# would put Python on the critical path of every clean build for no benefit.
# Run this by hand after changing the art.
# The mark, not the wordmark. "SchOS Text Logo.png" is the text form and would
# be illegible scaled to LOGO_PX; the small logo this used to name is gone,
# replaced by the full logo, which is the same mark slightly redrawn.
LOGO_SRC = Assets/SchOS Full Logo.png
LOGO_PX = 128
logo:
$(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/logo.h $(LOGO_PX)
# The USB vendor/product name database, from linux-usb.org.
#
# Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD
# option, which is what makes it usable at all. See NOTICE.
#
# Cached in dist/ and refreshed only when you ask, NOT on every build. That was
# the request, and it is the one thing here worth pushing back on: fetching on
# every compilation makes an offline build fail, makes the same source produce
# different images on different days, and puts a third-party web server on the
# critical path of `make`. dist/ exists precisely so a rebuild never needs the
# network -- see docs/layout.md. `make usb-ids` is a deliberate act; the file it
# writes is committed and dated, so what shipped is always knowable.
USB_IDS_URL = http://www.linux-usb.org/usb.ids
usb-ids:
@echo "==> fetching $(USB_IDS_URL)"
@curl -sfL --max-time 60 -o dist/usb.ids.new "$(USB_IDS_URL)" || \
{ echo " fetch failed; keeping the cached copy"; rm -f dist/usb.ids.new; exit 1; }
@# A truncated or error-page download would otherwise silently replace a
@# good database with rubbish. Check it looks like what it claims to be.
@grep -q "^# Version:" dist/usb.ids.new || \
{ echo " that is not a usb.ids file; keeping the cached copy"; \
rm -f dist/usb.ids.new; exit 1; }
@mv dist/usb.ids.new dist/usb.ids
@cp dist/usb.ids $(SYSROOT)/usr/share/usb.ids
@grep -m1 "^# Version:" dist/usb.ids
# Build the kernel's QR encoder on the host and check it.
#
# A QR symbol with a mistyped table entry still looks like a perfectly plausible
# grid of black squares, so the self-checks in qrtest.c cover what can be
# verified mechanically and the PNG covers the rest: point a phone at it.
QRTEST = $(BUILD)/tools/qrtest$(EXE)
$(QRTEST): tools/qrtest.c kernel/core/qr.c include/qr.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I./include -o $@ tools/qrtest.c
qrtest: $(QRTEST)
@mkdir -p $(BUILD)
./$(QRTEST) $(BUILD)/qr.png
# The check that actually decides whether the encoder works: build a symbol and
# read it back with a real decoder, plus a module-for-module comparison against
# a second encoder. Needs Python packages that are not part of the build, so it
# is not wired into `make` -- install them wherever you like and point
# PYTHONPATH at it:
#
# pip install --target build/pylibs opencv-python-headless numpy segno
# PYTHONPATH=build/pylibs make qrcheck
qrcheck: $(QRTEST)
$(PYTHON) tools/python/qrcheck.py
.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-kvm schcom-test \
schfs-test schfs-objtest schfs-image-test schfs-check mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids
.PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
# so a rebuild never needs the network.
clean:
rm -rf $(BUILD) schos.img schos.iso kernel/arch/x86_64/vectors.S
distclean: clean
rm -rf dist
# Header dependencies, from gcc -MD.
#
# This used to be a set of globs including `$(BUILD)/kernel/**/*.d`. make's
# wildcard has no recursive `**`: it behaved as a single `*` and matched
# exactly one level, so build/kernel/core/*.d was picked up while
# build/kernel/arch/x86_64/*.d and build/kernel/fs/schfs/*.d were not.
# Everything under arch/ and schfs/ therefore had NO header dependency
# tracking at all -- editing include/schfs.h or include/mmu.h left stale
# objects that linked without complaint. find actually recurses.
-include $(shell find $(BUILD) -name '*.d' 2>/dev/null)