A native driver written from the NVMe 1.4 base specification. It resets the controller the firmware left running, then sets up an admin queue and one I/O queue pair. Completion is polled, with interrupts disabled at the queue, for the same reason AHCI polls: without AML, interrupt routing on real hardware cannot be trusted. Each active namespace with 512-byte to 4 KB sectors and no metadata is registered as nvme<C>n<N>, the name Linux uses, and its GPT is read like any other disk's. Tested under QEMU q35: disk.img on NVMe as the only disk boots through OVMF, roots on nvme0n1p2, and writes read back after sync. A 4 KB-sector namespace registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
1540 lines
74 KiB
Makefile
1540 lines
74 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)
|
|
|
|
# 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/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)
|
|
|
|
# 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)
|
|
|
|
# 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)
|
|
|
|
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 = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone
|
|
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)
|
|
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)
|
|
|
|
# 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.
|
|
#
|
|
# 8000 blocks of 4 KB is 32 MB. It was 6000, which the userland had quietly
|
|
# grown to fill: 5157 of 6000 used, 86%, after toybox gained its pending
|
|
# applets and an editor arrived. That still fit, and the *next* thing added
|
|
# would not have -- and what mkschfs says when an image is full is "try a
|
|
# larger --size", which is a poor way to find out that a build variant you were
|
|
# not using has been broken for a while.
|
|
# -----------------------------------------------------------------------
|
|
RAMDISK_BLOCKS ?= 8000
|
|
|
|
# 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 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.
|
|
# 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/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/schboot.h \
|
|
boot/common/ui.c boot/common/ui.h \
|
|
include/logo.h include/theme.h include/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.
|
|
# sysroot-install because the disc also carries the tree as a real hierarchy.
|
|
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install
|
|
SYSROOT=$(SYSROOT) 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, 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 \
|
|
$@
|
|
|
|
# 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 $@
|
|
|
|
# `$(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 $(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 $@
|
|
|
|
# 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
|
|
|
|
$(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 $@
|
|
|
|
$(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/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/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)
|
|
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.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_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/
|
|
@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
|
|
|
|
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install
|
|
@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.
|
|
SCHFS_BLOCKS ?= 25600
|
|
|
|
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
|
|
@mkdir -p $(BUILD)
|
|
@rm -f $@
|
|
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --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: 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/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 = docs/branding/shift-mask.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: 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 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/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)
|