SchOS/Makefile
Nexoniarz f6c4b6377f GMMOS: USB keyboards on xHCI
The third micro driver: xhci.c with everything but finding a keyboard and
reading it taken out. It does not take over the live driver's state --
the kernel may have died inside that driver -- so each controller is
halted, reset and enumerated from nothing. Of the live driver it uses
the register mapping and the scratchpad pages the controller was lent
(DCBAA entry 0, read before the reset).

Root ports, and USB 2 hubs up to two deep (power, reset, route string,
transaction translator for a slow device behind a high-speed hub). A
boot-protocol keyboard interface gets Set Configuration, Set Protocol
boot, Set Idle, one interrupt IN endpoint and a Normal TRB, re-queued
after every report. No interrupts, no hot-plug, nothing that is not a
keyboard or a hub. A static 256 KB pool instead of the DMA allocator.

Checked under QEMU with the PS/2 controller switched off (i8042=off): a
USB keyboard on a root port, and one behind a hub, both type into the
GMMOS terminal. QEMU's hub is full speed, so the transaction-translator
path is not exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:23:14 +02:00

1791 lines
86 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 watch.o ioperm.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 ksym.o klog.o sbuf.o kmem.o)
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \
pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
dbg.o netuser.o audio.o dmi.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/dev/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/vga/vga_fb.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/net/r8169/r8169.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/ahci/ahci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/nvme/nvme.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 devfs.o sysfs.o lock.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 attr.o kmount.o vfs.o)
# FAT12/16/32 with long names, for the disks every other system can read.
KERN_FS += $(BUILD)/kernel/fs/fat/fat.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 net.o inet.o netlink.o)
# lwIP: the TCP/IP stack, BSD-3-clause, unpacked by ports/lwip into
# build/ports/lwip/src. It is compiled straight into the kernel rather than
# built as a library, because there is nothing to link it into otherwise -- and
# because the alternative, a second build system inside ports/, would need the
# kernel's exact flags anyway.
#
# The file list is explicit rather than a wildcard. lwIP ships IPv6, 6LoWPAN,
# PPP, SLIP, DNS-SD and a dozen apps in the same tree, and building what is not
# configured produces either dead code or a link error naming a port function
# that was never meant to exist here.
LWIPDIR = $(BUILD)/ports/lwip/src/src
KERN_NET += $(addprefix $(BUILD)/lwip/core/, \
init.o def.o mem.o memp.o netif.o pbuf.o raw.o stats.o sys.o \
tcp.o tcp_in.o tcp_out.o timeouts.o udp.o ip.o inet_chksum.o \
dns.o)
KERN_NET += $(addprefix $(BUILD)/lwip/core/ipv4/, \
acd.o dhcp.o etharp.o icmp.o ip4.o ip4_addr.o ip4_frag.o)
KERN_NET += $(addprefix $(BUILD)/lwip/netif/, ethernet.o)
KERN_NET += $(addprefix $(BUILD)/kernel/net/lwip/, sys_arch.o netif_eth.o)
# uACPI: the AML interpreter, MIT, unpacked by ports/uacpi into
# build/ports/uacpi/src and compiled into the kernel like lwIP. kernel/acpi/
# is the host side of it -- memory, locks, I/O, the SCI -- and what the kernel
# uses it for: PCI interrupt routing, the power button and S5.
UACPIDIR = $(BUILD)/ports/uacpi/src
KERN_ACPI = $(addprefix $(BUILD)/uacpi/, \
default_handlers.o event.o interpreter.o io.o mutex.o \
namespace.o notify.o opcodes.o opregion.o osi.o registers.o \
resources.o shareable.o sleep.o stdlib.o tables.o types.o \
uacpi.o utilities.o)
KERN_ACPI += $(addprefix $(BUILD)/kernel/acpi/, host.o aml.o i2c_hid.o)
# zstd's decoder, for SCFF -- the compressed live root (include/fs/scff.h).
# Unpacked by ports/zstd into build/ports/zstd/src and compiled into the kernel
# like uACPI; kernel/scff/ is the host side of it.
ZSTDDIR = $(BUILD)/ports/zstd/src/lib
KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \
common/entropy_common.o common/error_private.o \
common/fse_decompress.o common/zstd_common.o common/xxhash.o \
common/debug.o decompress/huf_decompress.o \
decompress/zstd_ddict.o decompress/zstd_decompress.o \
decompress/zstd_decompress_block.o)
KERN_ZSTD += $(BUILD)/kernel/scff/scff.o
# GMMOS, what runs after a panic, and its micro drivers. See include/gmmos/gmmos.h.
KERN_GMMOS = $(addprefix $(BUILD)/kernel/gmmos/, gmmos.o hidkeys.o ps2.o i2c.o xhci.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 sys_event.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_ACPI) $(KERN_ZSTD) $(KERN_GMMOS) \
$(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 = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest
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
SMP ?= 2
QEMU = qemu-system-x86_64
# Hardware virtualisation, when the host has it.
#
# Not a micro-optimisation. Without KVM, QEMU emulates every instruction, and
# measured on this tree that is where most of "the system feels slow" came from:
# `ls /bin` took 120 ms emulated against 20 ms with KVM, on identical code. Every
# guess about the kernel being at fault has to be made against the KVM number,
# or it is really a measurement of TCG.
#
# Probed rather than assumed, because /dev/kvm is absent on a machine without
# the extension, inside another VM, or when the user is not in the kvm group --
# and a hard -enable-kvm turns each of those into a QEMU that refuses to start.
# `-cpu host` only makes sense with KVM: it is the physical CPU, which an
# emulator cannot present.
ifneq ($(wildcard /dev/kvm),)
QEMU_ACCEL = -enable-kvm -cpu host
else
QEMU_ACCEL = -cpu qemu64,+rdtscp
endif
# 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.
# Two display devices means **two QEMU displays**, and a window can only show
# one at a time -- GTK puts them under View, SDL cycles with Ctrl-Alt-2. Which
# matters more than it sounds once vgpud runs, because the two then show
# completely different things:
#
# stdvga the boot splash, then the text console and your shell
# virtio-gpu the mark, and then the same console, mirrored there by vgpud
#
# So the two windows show the same thing once vgpud is up, which is the point:
# a head showing the running system is a head you can tell is working. Before
# vgpud starts, the virtio-gpu display is black -- nothing is wrong when that
# happens, you are looking at a device nobody has drawn on yet. `make
# qemu-console` below boots with one display and no GPU to hand over, which is
# the quickest way to tell that apart from an actual failure.
QEMU_DISPLAY = -vga std -device virtio-gpu-pci
# One display, carrying the console. No virtio-gpu, so vgpud finds nothing to
# claim and exits, and the single window is the thing you wanted to look at.
QEMU_DISPLAY_1 = -vga std
# Sound, on every QEMU target.
#
# `wav` rather than a host audio backend: a build machine may have no
# soundcard, no PulseAudio and no session bus, and a QEMU that fails to open
# one of those prints a warning per voice and carries on -- which is noise in
# every boot log for a feature most runs are not testing. Writing to a file
# works everywhere and is the only backend that can be *checked*: the audio
# test plays a tone and measures the zero crossings of what came out.
#
# To actually hear it: make qemu QEMU_AUDIO='-audiodev pa,id=snd0 -device AC97,audiodev=snd0'
QEMU_AUDIO ?= -audiodev wav,id=snd0,path=$(BUILD)/qemu-audio.wav -device AC97,audiodev=snd0
# A network card, on every QEMU target.
#
# Spelled out rather than left to QEMU's default NIC. The default exists and is
# an e1000 on this machine type today, which is exactly the sort of thing that
# is true until a QEMU release changes it -- and then the network silently stops
# being tested rather than failing loudly. Naming the device also means `make
# qemu` and `bootcheck -Net` are running the same hardware.
#
# User-mode networking: the guest gets 10.0.2.15 by DHCP, the host is 10.0.2.2,
# and no privileges are needed. A bridge or a tap would need root and would put
# the guest on the real network, which is not what a development boot wants.
#
# To point the driver at different hardware once there is a second one, override
# it: make qemu QEMU_NET='-netdev user,id=net0 -device rtl8139,netdev=net0'
# The guest's network, and a way in from the host.
#
# `-netdev user` is QEMU's NAT: the guest can reach the outside world and
# nothing outside can reach the guest, which is the safe default and exactly
# wrong when the thing you are testing is a server. SchOS has an httpd now, and
# a web server nobody can open is hard to believe in.
#
# hostfwd punches three holes, on the ports something is actually likely to be
# listening on. They bind 127.0.0.1 on purpose: the guest becomes reachable
# from *this* machine and not from the network this machine is on, which is a
# development convenience rather than a server somebody forgot about.
#
# http://localhost:8080 -> guest port 80 (httpd's default)
# http://localhost:8000 -> guest port 8000
# http://localhost:8081 -> guest port 8080
#
# Port 80 on the host is not used: binding it needs root, and `make qemu`
# should not. Override the whole variable for anything else --
# make qemu QEMU_NET='-netdev user,id=net0,hostfwd=tcp::9000-:9000 ...'
QEMU_HOSTFWD = hostfwd=tcp:127.0.0.1:8080-:80,hostfwd=tcp:127.0.0.1:8000-:8000,hostfwd=tcp:127.0.0.1:8081-:8080
QEMU_NET = -netdev user,id=net0,$(QEMU_HOSTFWD) -device e1000,netdev=net0
# 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
LD = ld.lld
OBJCOPY = llvm-objcopy
OBJDUMP = llvm-objdump
NM = llvm-nm
# 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
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
NM = $(TOOLPREFIX)nm
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)
MKSCFF = $(BUILD)/tools/mkscff$(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)
# -MP with -MD: emit a phony target for every header, so that *moving* or
# deleting one does not wedge the build. Without it the stale .d still names the
# old path and make stops with "No rule to make target 'include/e1000.h'" --
# which happened the day that header moved next to its driver, and the only
# cure was knowing to delete the .d by hand.
CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -MP -ggdb -fno-omit-frame-pointer
CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT)
CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
# The kernel keeps its hands off the FPU and SSE registers: they belong to
# whichever process is running, and are saved only when it is switched out
# (see "The FPU" in kernel/proc/proc.c). Without this GCC vectorises struct
# copies and memset into %xmm, and an interrupt then scribbles over the
# registers of the program it interrupted. Kernel objects only -- cmd/ is
# built with these CFLAGS too and is welcome to use SSE.
$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o $(BUILD)/zstd/%.o: CFLAGS += -mgeneral-regs-only
# lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip).
# Only the network files are compiled with these: putting them in the global
# CFLAGS would let any kernel file include lwIP by accident, and the stack is
# meant to be reachable through kernel/net/ and nowhere else.
# shim/ comes first so lwIP's <string.h> and friends resolve to the kernel's
# functions rather than the host's headers, which declare incompatible
# prototypes and would otherwise be found on the default include path.
LWIP_CFLAGS = -I./kernel/net/lwip/shim -I$(LWIPDIR)/include -I./kernel/net/lwip
# lwIP is third-party and warns differently from this tree. Its warnings are
# not actionable here -- patching them would mean maintaining a fork -- so they
# are silenced for its files only, never for ours.
LWIP_QUIET = -Wno-unused-parameter -Wno-sign-compare -Wno-unused-but-set-variable \
-Wno-address-of-packed-member -Wno-constant-conversion
# 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 the image is linked into the kernel binary, and stage2
# loads that by PIO one sector at a time -- a kernel with a userland inside it
# takes visibly longer to boot than a 350 KB one.
#
# Sized to fit, not fixed. The image is what the root uses plus RAMDISK_SLACK
# blocks and an eighth, measured by building it once large; the kernel then
# grows the volume into half of the free memory at boot (kernel/dev/ramdisk.c),
# which is where `apk add` on the live system writes. It was a fixed 96 MB,
# 60 of it zeros carried in the kernel and twice over on the ISO, and room for
# 60 MB of packages whatever the machine had.
# -----------------------------------------------------------------------
RAMDISK_SLACK ?= 1024
# 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 GPL-2.0-or-later, 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 and pci.ids used to be on it, as 2 MB of a 6 MB image. They are
# Alpine's hwdata packages now: apk owns them, and a root missing files its
# database lists would be one apk had to repair.
RAMDISK_EXCLUDE =
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)
# -----------------------------------------------------------------------
# The root filesystem is an Alpine root that apk manages, with SchOS installed
# into it as the package schos-base -- see docs/plan/alpine.md and "The
# apk-managed root" below. Both images are made from it: schfs.img, which
# `make qemu` boots, and the ramdisk inside the kernel, which `make iso`
# boots. sysroot/ is what schos-base is packaged from, not a root of its own.
# -----------------------------------------------------------------------
AROOT_DIR = $(BUILD)/aroot
ROOT_TREE = $(AROOT_DIR)/root
ROOT_STAMP = $(AROOT_DIR)/stamp
# -----------------------------------------------------------------------
# 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.
# OVMF is in this list because it is no longer optional: with the BIOS loader
# gone, nothing in this tree boots without firmware, and `make clean` wipes
# build/ including the copy fetched from the host. Leaving it out meant a clean
# followed by a build produced a tree where every run script threw.
all: esp $(BUILD)/schfs.img $(BUILD)/usbstick.img
# ...and the firmware, added to `all` further down. It cannot be listed here:
# make expands a rule's prerequisites when it reads the line, and $(OVMF_CODE)
# is defined below this point, so naming it here appends nothing at all and
# does so silently.
# schos.img is gone with the BIOS loader that gave it its shape. It was a raw
# disk with the boot sector at LBA 0, stage2 next and the kernel at a fixed
# offset -- a layout only a loader that seeks by sector number can use. UEFI
# firmware wants a filesystem, so what boots now is the staged ESP, offered to
# QEMU through its virtual FAT.
#
# That is a development arrangement, not an installable disk. A real GPT image
# with a real FAT partition is the installer's job.
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.
# -----------------------------------------------------------------------
# Debug builds.
#
# make DEBUG=1
#
# What it turns on is the *boot menu's* remote-debugging entry, and nothing
# else -- see SCHBOOT_DEBUG_FLAG in include/arch/schboot.h. A release build has no
# way to switch a network debugger on, because the code that would set the flag
# is not compiled into it. That is the point: a menu item that is present and
# refuses is an invitation to find out how to make it not refuse.
#
# It is deliberately not a kernel-wide -DDEBUG. Nothing in the kernel changes
# behaviour on it, so a debug kernel and a release kernel are the same kernel,
# and a bug that only reproduces on one of them cannot be this flag's fault.
ifeq ($(DEBUG),1)
UEFI_DEBUG_FLAGS = -DSCHOS_DEBUG
else
UEFI_DEBUG_FLAGS =
endif
UEFI_CFLAGS = $(UEFI_DEBUG_FLAGS) --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include
# The three headers at the end are the ones boot/common/ui.c actually includes,
# and leaving them out meant a new logo did not rebuild the loader: the splash
# kept drawing whatever mask was compiled in months earlier, `make` reported
# nothing to do, and the only symptom was a boot screen that disagreed with the
# kernel about what the logo is. The kernel half never showed it because those
# objects are built with -MD -MP and pick up header changes on their own.
#
# **The loader is the one thing here that GCC cannot build.** It is a PE/COFF
# binary for the Microsoft ABI, which clang cross-targets and gcc does not, so
# this rule needs clang and lld-link and no other rule does.
#
# That matters more than it reads, because of *where* it sits: `make` stops at
# the first failed rule, and this one runs before the sysroot is staged and the
# filesystem image is built. So on a machine without clang, touching the loader
# does not produce "the loader did not build" -- it produces a new kernel, an
# old userland, and a boot that behaves like a mix of the two, with the real
# cause forty lines up the screen. That happened, and it cost an afternoon of
# looking at the wrong thing.
#
# Hence the check and the message. It is worth the four lines.
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \
boot/common/ui.c boot/common/ui.h \
include/gfx/logo.h include/gfx/theme.h include/gfx/font.h
@mkdir -p $(BUILD)/uefi
@command -v clang >/dev/null 2>&1 || { \
echo ""; \
echo " The UEFI loader needs clang and lld-link, and nothing else here does."; \
echo " It is the only PE/COFF binary in the tree -- gcc cannot target it."; \
echo ""; \
echo " On NixOS: nix-shell --run 'NIX_HARDENING_ENABLE=\"\" make'"; \
echo " Elsewhere: install clang and lld, then make again"; \
echo ""; \
test -f $@ && { \
echo " There is an existing loader at $@."; \
echo " To keep it and build everything else: make UEFI_KEEP=1"; \
echo ""; }; \
exit 1; }
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:$@
# Keep the loader that is already built and carry on with the rest.
#
# For the case the message above describes: a machine with no clang, where the
# loader has not meaningfully changed and the thing being worked on is the
# kernel or the userland. It is opt-in and prints what it is doing, because a
# build that quietly ships a stale bootloader is how you spend an evening
# debugging code that is not running.
ifeq ($(UEFI_KEEP),1)
$(BUILD)/uefi/BOOTX64.EFI:
@test -f $@ || { echo "make UEFI_KEEP=1: there is no loader to keep"; exit 1; }
@echo "uefi: keeping the existing loader (UEFI_KEEP=1) -- it may be out of date"
endif
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.
#
# `$(dir ...)` of the binary is its *bin* directory, so the share directory is
# one level above it -- `$(dir)/share` asked for `/usr/bin/share`, which exists
# nowhere. On the distributions below that went unnoticed, because the absolute
# paths after it matched anyway; on one that keeps nothing in /usr it meant the
# firmware was never found on a machine that had it, and the only way to build
# was to copy the file in by hand.
QEMU_BIN = $(shell command -v $(QEMU) 2>/dev/null)
QEMU_PREFIX = $(patsubst %/bin/,%,$(dir $(QEMU_BIN)))
QEMU_SHARE = $(QEMU_PREFIX)/share
OVMF_CODE_CANDIDATES = $(QEMU_SHARE)/qemu/edk2-x86_64-code.fd $(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)/qemu/edk2-i386-vars.fd $(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 $@
ovmf: $(OVMF_CODE) $(OVMF_VARS)
# The other half of `all`, now that OVMF_CODE and OVMF_VARS have values.
all: $(OVMF_CODE) $(OVMF_VARS)
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,format=raw -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 $(QEMU_ACCEL) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) $(QEMU_AUDIO)
# 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
# One El Torito entry, pointing at a FAT image holding the loader and kernel.
# The root is in the kernel, as its ramdisk; the disc carries no other copy.
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI tools/posix/mkiso.sh $(ROOT_STAMP)
sh tools/posix/mkiso.sh $@
# A partitioned disk -- GPT, an ESP and a SchFS partition -- which is what a
# real machine boots from: dd it to a USB stick or a disk. See
# tools/posix/mkdisk.sh. `make qemu-disk` boots it on q35 as the only disk.
disk: $(BUILD)/disk.img
$(BUILD)/disk.img: esp $(BUILD)/schfs.img tools/posix/mkdisk.sh
sh tools/posix/mkdisk.sh $(BUILD)/esp $(BUILD)/schfs.img $@
# 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) $(QEMU_ACCEL) $(QEMU_NET) \
-cdrom schos.iso -boot d \
-m 1G -smp $(SMP) -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Kernel binary
# -----------------------------------------------------------------------
# The kernel is linked twice, and the second pass is the one that ships.
#
# Pass one exists to be read: tools/ksyms.pl turns its symbol table into a C
# file, which pass two compiles in, so the kernel can name its own addresses
# (docs/architecture/debugging.md, D0). kernel.ld places .ksym after .text and .rodata
# precisely so that carrying the table cannot move anything the table records
# -- which is what makes two passes sufficient instead of the three Linux needs.
#
# That property is checked, not assumed. If the two passes ever disagree about
# where a function is, the table is silently wrong for every address, and a
# debugger that lies is worse than one that says nothing -- so the build stops.
KERN_LINK = $(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
$(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj \
$(BUILD)/kernel/consfont_obj $(RAMDISK_OBJ)
$(BUILD)/kernel/kernel.pass1: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(BUILD)/kernel/consfont_obj $(RAMDISK_OBJ) kernel/kernel.ld
@mkdir -p $(BUILD)/kernel
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK)
$(BUILD)/kernel/ksym.c: $(BUILD)/kernel/kernel.pass1 tools/ksyms.pl
$(NM) -n $< | perl tools/ksyms.pl > $@
$(BUILD)/kernel/ksym.o: $(BUILD)/kernel/ksym.c
$(CC) $(CFLAGS) -c -o $@ $<
$(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK) $(BUILD)/kernel/ksym.o
@$(NM) -n $(BUILD)/kernel/kernel.pass1 | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass1.txt
@$(NM) -n $@ | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass2.txt
@cmp -s $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt || \
{ echo "ksyms: the two link passes disagree about where the code is;"; \
echo "ksyms: the symbol table would name every address wrongly."; \
diff -u $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt | head -20; \
exit 1; }
$(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, sized to fit (see RAMDISK_SLACK) --
# 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) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
@used=$$($(MKSCHFS) $@ --size 262144 --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \
sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \
test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \
rm -f $@; \
$(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
# The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks,
# each its own zstd frame, unpacked by the kernel the first time a block in it
# is read. -19 is slow to write and costs nothing to read -- zstd decodes at
# the same speed whatever level made the frame -- and mkscff uses every core.
SCFF_LEVEL ?= 19
SCFF_CHUNK ?= 128
$(BUILD)/fs-ram.scff: $(BUILD)/fs-ram.img $(MKSCFF)
$(MKSCFF) $< $@ --level $(SCFF_LEVEL) --chunk $(SCFF_CHUNK)
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.scff
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_fs_ram_scff_start=_binary_ramdisk_start \
--redefine-sym _binary_build_fs_ram_scff_end=_binary_ramdisk_end \
--redefine-sym _binary_build_fs_ram_scff_size=_binary_ramdisk_size \
$@
# The console font, linked into the kernel.
#
# In the kernel image rather than loaded from disk, because the console has to
# work before there is a disk: a panic during boot has to be readable, and the
# only alternative is keeping the 8x16 bitmap around for early messages and
# switching later, which means two fonts and a seam where they disagree.
#
# 115 KB of kernel for every glyph in a modern font is a trade this makes
# gladly. The 8x16 bitmap it replaces is CP437 -- 256 glyphs, no Ą, no Ę, no Ś,
# no Ω -- and they did not render as something wrong, they rendered as `?`,
# because a codepoint the grid could not hold was mapped to one.
#
# JetBrains Mono is SIL OFL 1.1, which permits this and requires the name be
# kept; see NOTICE. The regular static weight, not the variable font -- a
# variable font needs `fvar`/`gvar` interpolation, which ttf.c does not do and
# which would render every glyph at the wrong weight without saying so.
CONSFONT = ports/JetBrains_Mono/static/JetBrainsMono-Regular.ttf
$(BUILD)/kernel/consfont_obj: $(CONSFONT)
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_$(subst .,_,$(subst /,_,$(subst -,_,$(CONSFONT))))_start=_binary_consfont_start \
--redefine-sym _binary_$(subst .,_,$(subst /,_,$(subst -,_,$(CONSFONT))))_end=_binary_consfont_end \
$@
# 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 $@
$(BUILD)/kernel/gmmos/%.o: kernel/gmmos/%.c
@mkdir -p $(BUILD)/kernel/gmmos
$(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.
# -I on the driver's own directory, so a driver can keep its register
# definitions next to its code instead of in include/. include/ is for headers
# the rest of the kernel needs; a chip's register map is not one of those, and
# e1000.h sat there for a year being included by exactly one file.
$(BUILD)/kernel/drivers/%.o: kernel/drivers/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -I$(dir $<) -c $< -o $@
$(BUILD)/kernel/fs/%.o: kernel/fs/%.c
@mkdir -p $(dir $@)
$(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 $@
# kernel/net gets lwIP's headers: net.c and inet.c are the two files that talk
# to the stack directly, and unix.c is unaffected by the extra include path.
$(BUILD)/kernel/net/%.o: kernel/net/%.c | $(LWIPDIR)
@mkdir -p $(BUILD)/kernel/net
$(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@
# Our own lwIP port layer. Ours, so it is held to this tree's warnings.
$(BUILD)/kernel/net/lwip/%.o: kernel/net/lwip/%.c | $(LWIPDIR)
@mkdir -p $(BUILD)/kernel/net/lwip
$(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@
# lwIP's own sources, compiled from the unpacked tarball.
$(BUILD)/lwip/%.o: $(LWIPDIR)/%.c | $(LWIPDIR)
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) $(LWIP_CFLAGS) $(LWIP_QUIET) -c $< -o $@
# Unpack lwIP on demand. Order-only everywhere it is used, because the
# directory's mtime is not a reason to recompile anything.
$(LWIPDIR):
$(MAKE) -C ports lwip
# And say that unpacking is what makes lwIP's .c files. Without this, a clean
# build fails at once with "no rule to make build/lwip/core/init.o": make
# picks a pattern rule only if every prerequisite exists or can be made, and
# before the unpack init.c is neither -- the order-only $(LWIPDIR) above
# would have created it, but make never gets that far. A second `make`
# worked, because by then the first had unpacked it. PRECIOUS because make
# deletes what it thinks it made along a chain of pattern rules.
$(LWIPDIR)/%.c: | $(LWIPDIR)
@test -f $@
.PRECIOUS: $(LWIPDIR)/%.c
# uACPI's headers for its own files and for kernel/acpi/, and the same
# configuration for both: a define seen by one side and not the other changes
# the size of structures they pass between them. Builtin string functions,
# because the kernel's memcpy and friends are not declared the way <string.h>
# declares them.
UACPI_CFLAGS = -I$(UACPIDIR)/include -DUACPI_USE_BUILTIN_STRING
# Third-party, warned about differently from this tree; silenced for its files
# only. kernel/acpi/ is ours and is held to the usual warnings.
UACPI_QUIET = -Wno-unused-parameter -Wno-sign-compare -Wno-unused-function
$(BUILD)/kernel/acpi/%.o: kernel/acpi/%.c | $(UACPIDIR)
@mkdir -p $(BUILD)/kernel/acpi
$(CC) $(CFLAGS) $(UACPI_CFLAGS) -c $< -o $@
$(BUILD)/uacpi/%.o: $(UACPIDIR)/source/%.c | $(UACPIDIR)
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) $(UACPI_CFLAGS) $(UACPI_QUIET) -c $< -o $@
$(UACPIDIR):
$(MAKE) -C ports uacpi
# The sources exist once the tarball is unpacked. Saying so is what lets the
# pattern rule above apply on a fresh tree, where make would otherwise look
# for $(UACPIDIR)/source/foo.c, find nothing, and report no rule for foo.o.
$(patsubst $(BUILD)/uacpi/%.o,$(UACPIDIR)/source/%.c,$(filter $(BUILD)/uacpi/%,$(KERN_ACPI))): | $(UACPIDIR)
@:
# zstd at -O2 whatever OPT says: the decoder runs on every first read of a
# root block, and at -O0 it is several times slower. No assembly, no BMI2
# dispatch, no legacy formats, no tracing hooks -- the kernel has none of
# what those need. compat/ stands in for the libc headers it includes.
ZSTD_CFLAGS = -Ikernel/scff/compat -I$(ZSTDDIR) -I$(ZSTDDIR)/common -O2 \
-DZSTD_DISABLE_ASM -DDYNAMIC_BMI2=0 -DZSTD_LEGACY_SUPPORT=0 \
-DZSTD_TRACE=0 -DDEBUGLEVEL=0 -DZSTD_STRIP_ERROR_STRINGS \
-DXXH_NAMESPACE=ZSTD_
ZSTD_CC = $(CC) $(filter-out -I./include,$(CFLAGS)) $(ZSTD_CFLAGS)
$(BUILD)/zstd/%.o: $(ZSTDDIR)/%.c | $(ZSTDDIR)
@mkdir -p $(dir $@)
$(ZSTD_CC) -Wno-unused-function -c $< -o $@
$(BUILD)/kernel/scff/%.o: kernel/scff/%.c | $(ZSTDDIR)
@mkdir -p $(dir $@)
$(ZSTD_CC) -c $< -o $@
$(ZSTDDIR):
$(MAKE) -C ports zstd
$(patsubst $(BUILD)/zstd/%.o,$(ZSTDDIR)/%.c,$(filter $(BUILD)/zstd/%,$(KERN_ZSTD))): | $(ZSTDDIR)
@:
$(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 $@
# ldlsd links the LDLS core as well as its own main, so it needs a rule of its
# own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and
# stub.c are freestanding and build unchanged for either side, which is the
# whole point of having written them that way.
LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o)
$(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h
@mkdir -p $(BUILD)/ldls/guest
$(CC) $(CFLAGS) -c $< -o $@
# The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci
# and /dev/netdev, which exist on SchOS and nowhere else.
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h
@mkdir -p $(BUILD)/ldls/guest/kpi
$(CC) $(CFLAGS) -I./ldls -c $< -o $@
$(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST)
@mkdir -p $(BUILD)/cmd
$(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ \
$(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB)
$(OBJCOPY) -S $@
# dynhello is a musl program, and the only dynamically linked one: the test
# that the kernel loads an interpreter and the interpreter maps libraries.
# Not -static, against libc.so rather than libc.a, with musl's loader named as
# the interpreter, and with a shared library of its own beside it -- see
# cmd/dynhello/main.c.
DYNCC = $(PORTCC) -O2 -D__SCHOS__ -I$(MUSLPREFIX)/include -fno-stack-protector
DYNLD = -Wl,-dynamic-linker,/lib/ld-musl-x86_64.so.1 -Wl,-rpath,/lib
$(MUSLPREFIX)/lib/libc.so:
$(MAKE) -C ports musl CC=gcc
# <linux/*.h>, for programs that spell ioctls the way Linux does. See
# ports/linux-headers/Makefile.
LINUXHDR = $(MUSLPREFIX)/include/linux/input.h
$(LINUXHDR): $(MUSLPREFIX)/lib/libc.so
$(MAKE) -C ports linux-headers
# xready: the kernel calls Xorg makes before drawing, checked. Musl and
# dynamic, like evtest.
$(BUILD)/cmd/xready: cmd/xready/main.c $(MUSLPREFIX)/lib/libc.so $(LINUXHDR)
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/xready.o
$(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/xready.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(BUILD)/cmd/locktest: cmd/locktest/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/locktest.o
$(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/locktest.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(BUILD)/cmd/libdynhello.so: cmd/dynhello/lib.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fPIC -c $< -o $(BUILD)/cmd/libdynhello.o
$(DYNCC) -shared -nostdlib -Wl,-soname,libdynhello.so -o $@ \
$(BUILD)/cmd/libdynhello.o -L$(MUSLPREFIX)/lib -lc
$(BUILD)/cmd/dynhello: cmd/dynhello/main.c $(BUILD)/cmd/libdynhello.so
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/dynhello.o
$(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/dynhello.o \
-L$(BUILD)/cmd -ldynhello -L$(MUSLPREFIX)/lib -lc $(PORTEND)
# evtest: eventfd, timerfd, signalfd and epoll through musl's wrappers. A
# musl program for the same reason as dynhello, and dynamic because it may as
# well be.
$(BUILD)/cmd/evtest: cmd/evtest/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/evtest.o
$(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/evtest.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(SYSROOT)/lib/libdynhello.so: $(BUILD)/cmd/libdynhello.so
@mkdir -p $(SYSROOT)/lib
cp $< $@
$(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 $@
# rm before cp, and it is not tidiness.
#
# toybox installs its 236 applets as hard links to one multicall binary, and
# some of those names collide with first-party programs here -- /bin/ping is
# both. `cp` onto an existing hard link writes *through* it, so copying this
# program over the applet of the same name replaced the contents of the shared
# inode, and every one of the 236 links became this program. `cat` ran ping.
#
# It was invisible until the applets became links: while they were separate
# copies, overwriting one affected exactly one.
$(SYSROOT)/bin/%: $(BUILD)/cmd/% | $(SYSROOT)/bin
rm -f $@
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/attr.c \
kernel/fs/schfs/schfs_int.h include/fs/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 slab allocator, on the host. Same argument as schfs-objtest above:
# kernel/core/kmem.c is written to compile on the build machine, so its
# bookkeeping -- slab lists, the free list threaded through free objects, the
# page-to-cache lookup, and whether pages actually come back -- is asserted in
# a second rather than in a boot.
KMEMTEST = $(BUILD)/tools/kmemtest$(EXE)
$(KMEMTEST): tools/kmemtest.c kernel/core/kmem.c include/core/kmem.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTCFLAGS) -I include -o $@ tools/kmemtest.c
kmem-test: $(KMEMTEST)
$(KMEMTEST)
# 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)
# The SCFF writer, built from the same zstd release as the kernel's decoder.
MKSCFF_ZSTD = $(wildcard $(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c)
$(MKSCFF): tools/mkscff.c include/fs/scff.h | $(ZSTDDIR)
@mkdir -p $(BUILD)/tools
$(HOSTCC) -O2 -pthread -iquote ./include -I$(ZSTDDIR) -DZSTD_DISABLE_ASM \
-DZSTD_LEGACY_SUPPORT=0 -o $@ tools/mkscff.c \
$(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c
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
# -----------------------------------------------------------------------
# LDLS -- the Linux Driver Layer. docs/plan/ldls.md.
#
# ldls/ko.c is freestanding and builds on both sides: here as part of a host
# tool, and later as part of the SchOS-side host. Same discipline as SchFS,
# and for the same reason -- the failures at this layer are format failures,
# and one found on the host costs a second instead of a boot.
# -----------------------------------------------------------------------
LDLS_INSPECT = $(BUILD)/tools/ldls-inspect$(EXE)
LDLS_RUN = $(BUILD)/tools/ldls-run$(EXE)
LDLS_TYPES = $(BUILD)/tools/ldls-types$(EXE)
LDLS_REFMOD = $(BUILD)/ldls/refmod.o
$(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/inspect.c ldls/ko.c
# L1. Calls a module's init with every symbol stubbed, to read off the call
# order. -fno-strict-aliasing because this deliberately treats loaded bytes as
# code, and no optimiser assumption about that is one we want.
$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \
ldls/ko.h ldls/stub.h ldls/btf.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \
ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c
# The test object, compiled and never linked. Ours, because docs/plan/ldls.md
# rule 1 means no .ko is committed here -- see the header of refmod.c.
#
# -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is
# a thing the loader would have to build. A module is built -fno-pic for the
# same reason, so this matches what the real input looks like.
$(LDLS_REFMOD): ldls/test/refmod.c
@mkdir -p $(BUILD)/ldls
$(HOSTCC) -c -O1 -fno-pic -fno-asynchronous-unwind-tables -o $@ $<
# L2. Reads struct layouts out of a kernel's own BTF, so they are never in our
# source. See ldls/btf.h.
$(LDLS_TYPES): ldls/host/types.c ldls/btf.c ldls/btf.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/types.c ldls/btf.c
ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES)
# Put a module and its kernel's BTF into the *local* sysroot, for testing ldlsd
# in QEMU.
#
# make ldls-stage KO=/path/to/8139too.ko BTF=/sys/kernel/btf/vmlinux
#
# This is the model docs/plan/ldls.md describes, carried out on the machine
# that is doing the testing: the user obtains a module and loads it on their
# own machine. Deliberately **not** a dependency of `all`, so an ordinary build
# never picks it up, and `.gitignore` refuses `*.ko` so it cannot be committed
# by accident. An image built after this has somebody else's GPL code in it and
# must not be redistributed -- which is exactly what licensing.md §6 says about
# the combination, and why it is one command you have to type on purpose.
ldls-stage:
@test -n "$(KO)" || { echo "usage: make ldls-stage KO=<module.ko> [BTF=<vmlinux-btf>]"; exit 1; }
@mkdir -p $(SYSROOT)/lib/modules
cp "$(KO)" $(SYSROOT)/lib/modules/
@test -z "$(BTF)" || cp "$(BTF)" $(SYSROOT)/lib/modules/btf
@echo "staged into $(SYSROOT)/lib/modules -- this image is now yours alone"
ldls-unstage:
rm -rf $(SYSROOT)/lib/modules
@echo "removed"
# Load our own object and check the loader got it right. No network, no QEMU,
# no module from anywhere.
ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES) $(LDLS_REFMOD)
$(PYTHON) tools/python/ldls_check.py $(LDLS_INSPECT) $(LDLS_REFMOD) $(LDLS_RUN)
$(MKFS): tools/mkfs.c include/fs/fs.h include/uapi/stat.h include/core/param.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I. -iquote ./include -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_ZSH = $(wildcard etc/zsh/*)
ETC_FILES = $(wildcard etc/*)
ETC_PLAIN = $(filter-out etc/schinit etc/zsh,$(ETC_FILES))
# The mark as characters, for anything that draws a logo into a terminal --
# fastfetch takes it with --file-raw. Installed from docs/branding rather than
# copied into sysroot by hand, so there is one source and `make` keeps them in
# step; see docs/branding/README.md.
#
# The 50-column one, because that is what fits beside an info block on an
# 80-column console. The wider renderings are in the same directory for
# anything with more room.
LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
# What httpd serves, if nobody has said otherwise. One page, whose job is to
# prove the path works end to end -- a directory listing of an empty /srv
# proves the same thing far less clearly.
SRV_FILES = $(wildcard srv/*)
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES)
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos $(SYSROOT)/srv
@cp $(ETC_PLAIN) $(SYSROOT)/etc/
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
@cp $(SRV_FILES) $(SYSROOT)/srv/
@# The device-name databases, where lspci and lsusb look for them: the
@# hwdata package's directory. They were at /usr/share/usb.ids, which no
@# tool searches, so lsusb printed numbers only.
@mkdir -p $(SYSROOT)/usr/share/hwdata
@rm -f $(SYSROOT)/usr/share/usb.ids
@for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true
@echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
# The mount points. Created here rather than by init, because a mkdir on a
# fresh SchFS is a whole transaction -- a copy-on-write rewrite of the path to
# the root and an uberblock behind a cache flush -- and four of them were most
# of what remained of a 2.6 second first boot. Making them at image-build time
# costs nothing at all.
#
# Not committed as empty directories with a .gitkeep, because /sys and /run are
# not mounted over and the marker file would be visible in them.
SYSROOT_DIRS = dev proc sys run tmp root
# After lwIP is unpacked: `make -C ports` below unpacks it too, and the two
# unzips running at once under -j trip over each other.
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) $(SYSROOT)/lib/libdynhello.so etc-install | $(LWIPDIR)
@mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS))
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. The size is load-bearing: tools/windows/bootcheck.ps1 asserts on
# READ CAPACITY returning 32768 sectors of 512 bytes, which is this.
#
# The exclusions matter more than they look, because the size above cannot
# move to accommodate content. This image exists to prove the USB driver
# can read a block and for no other reason, so what is in it is irrelevant
# and only its total size is not.
#
# --exclude bin: mkfs writes each hard link as its own copy, so once
# toybox's applets became links to one binary the /bin tree expanded to
# more than 60 MB on the way in -- hundreds of copies of something
# nothing here will ever execute.
# --exclude lib: `make ldls-stage` puts a kernel module and its BTF here,
# and a vmlinux BTF is 5 MB on its own. That filled this image and broke
# `make` for anyone who had staged a module -- a build failure a long way
# from its cause. It also kept somebody else's GPL module out of a test
# artifact, which RAMDISK_EXCLUDE does for the ramdisk and nothing was
# doing for this.
# --exclude usr: 7 MB of fonts, for a test that reads one block.
USBSTICK = $(BUILD)/usbstick.img
$(USBSTICK): $(MKFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
$(MKFS) $@ --size 4096 --exclude bin --exclude lib --exclude usr \
--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 the staged ESP holding a kernel
@# from the previous build. The next test run then boots that one 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: esp $(BUILD)/schfs.img
sh tools/posix/schcom_test.sh
# The firmware and the disks, in one place. Every run target below boots the
# same way, because there is only one way to boot now: OVMF, the staged ESP as
# the first disk, the SchFS volume as the second. Keeping the argument list in
# a variable is not tidiness -- four copies of a pflash pair is how one of them
# ends up with a writable firmware image and nobody notices.
OVMF_DRIVES = -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,format=raw \
-drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk
# Kept as an explicit spelling, but `make qemu` now picks KVM up on its own
# whenever /dev/kvm exists -- see QEMU_ACCEL. This target differs only in
# insisting on it, which is what you want when the point is to find out why the
# host has no /dev/kvm rather than to quietly get a slow machine.
# Source-level debugging, which the tree had no way to do.
#
# QEMU carries a gdb stub and it costs nothing to expose: -S holds the machine
# before its first instruction, -gdb waits for a connection. Attach and you get
# breakpoints, single-stepping and symbols in kernel code -- including the
# early boot the crash screen cannot reach and the serial console has not
# started for.
#
# terminal 1: make qemu-gdb
# terminal 2: gdb build/kernel/kernel
# (gdb) target remote :26000
# (gdb) break kmain
# (gdb) continue
#
# .gdbinit in the repo root does the connecting, so `gdb` alone is enough if
# gdb is allowed to load it -- see the file for the one line that needs.
GDBPORT ?= 26000
qemu-gdb: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
@echo "waiting for gdb on port $(GDBPORT) -- attach, then 'continue'"
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
$(OVMF_DRIVES) -m 1G -smp 1 -machine pc-i440fx-9.2 \
-S -gdb tcp::$(GDBPORT)
qemu-kvm: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) $(QEMU_AUDIO) -cpu host \
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
qemu: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
# The same machine with one screen, which is the console. See QEMU_DISPLAY.
qemu-console: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY_1) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret().
#
# 25600 blocks of 4 KB is 100 MB. It was 8000 (32 MB), which was comfortable
# when the whole userland was one multicall binary and stopped being so once
# there was more than one thing in it -- and mkschfs's answer to a full image
# is "try a larger --size", which is a worse thing to read than a number here
# is to change. A raw disk image is sparse on any filesystem worth the name,
# so the unused part costs nothing until it is used.
#
# 262144 blocks, 1 GB, since the root became one apk installs into: Xorg alone
# is 270 MB. Sparse, so the empty part costs nothing on the host.
SCHFS_BLOCKS ?= 262144
$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE)
# --- The apk-managed root ------------------------------------------------------
#
# Alpine's minirootfs, the packages ports/alpine/world names, and SchOS as the
# apk package schos-base -- a root that `apk add` and `apk upgrade` manage.
# See tools/posix/mkaroot.sh for what goes where and why.
#
# make aroot the tree and build/schfs.img; `make qemu` boots it
# make alpine-lock re-resolve ports/alpine/world against Alpine's mirror;
# the one step that looks for newer packages
#
# The build itself is offline once dist/ has the files world.lock names.
AROOT_KEYS = $(BUILD)/apk-keys
APK_STATIC = $(BUILD)/ports/alpine/apk.static
ALPINE_MINI = dist/alpine-minirootfs-3.24.3-x86_64.tar.gz
SCHOS_PKGVER ?= 0.1_git$(shell git log -1 --format=%cd --date=format:%Y%m%d%H%M 2>/dev/null || echo 0)-r0
alpine-port:
$(MAKE) -C ports/alpine install
alpine-lock:
$(MAKE) -C ports/alpine lock
$(AROOT_DIR)/stamp: $(BUILD)/sysroot.stamp ports/alpine/world.lock tools/posix/mkaroot.sh | alpine-port
sh tools/posix/mkaroot.sh $(SYSROOT) $(AROOT_DIR) $(APK_STATIC) $(ALPINE_MINI) \
$(AROOT_KEYS) $(SCHOS_PKGVER) ports/alpine/world.lock dist/alpine
touch $@
aroot: $(BUILD)/schfs.img
.PHONY: alpine-port alpine-lock aroot
# 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: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_ACCEL) $(QEMU_NET) \
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Misc
# -----------------------------------------------------------------------
# Nothing that is a real file with real prerequisites belongs here. schos.img
# used to be listed, and being phony made make skip the timestamp check and
# re-run its four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed. The file is gone
# with the BIOS loader, but the rule it taught applies to whatever replaces it.
# include/gfx/logo.h is generated but committed, exactly as include/gfx/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 = docs/branding/shift-mask.png
LOGO_PX = 128
logo:
$(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/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.
#
# pci.ids is the same arrangement for PCI, from the PCI ID Repository, under
# the same two licences. `make hwdata` refreshes both.
USB_IDS_URL = http://www.linux-usb.org/usb.ids
PCI_IDS_URL = https://pci-ids.ucw.cz/v2.2/pci.ids
usb-ids pci-ids:
@url="$(if $(filter usb-ids,$@),$(USB_IDS_URL),$(PCI_IDS_URL))"; \
f=$(subst -,.,$@); \
echo "==> fetching $$url"; \
curl -sfL --max-time 120 -o dist/$$f.new "$$url" || \
{ echo " fetch failed; keeping the cached copy"; rm -f dist/$$f.new; exit 1; }; \
grep -q "^#.*Version:" dist/$$f.new || \
{ echo " that is not a $$f file; keeping the cached copy"; \
rm -f dist/$$f.new; exit 1; }; \
mv dist/$$f.new dist/$$f; \
mkdir -p $(SYSROOT)/usr/share/hwdata; cp dist/$$f $(SYSROOT)/usr/share/hwdata/; \
grep -m1 "^#.*Version:" dist/$$f
@# A truncated or error-page download would otherwise silently replace a
@# good database with rubbish, which is what the Version check is for.
hwdata: usb-ids pci-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/gfx/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: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
.PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
# so a rebuild never needs the network.
clean:
rm -rf $(BUILD) 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/fs/schfs.h or include/arch/mmu.h left stale
# objects that linked without complaint. find actually recurses.
# Files only, and not under the trees that are root filesystems rather than
# build output: Alpine's usr/lib/sysctl.d is a directory, and including it
# stopped every make with "Is a directory".
-include $(shell find $(BUILD) \( -path $(BUILD)/aroot -o -path $(BUILD)/iso -o -path $(BUILD)/ports \) -prune -o -type f -name '*.d' -print 2>/dev/null)