Build and tools work on Linux, macOS and BSD as well as Windows

Developed from both Windows and Linux, so nothing in the build should
care which host it runs on. It did, in four places.

Toolchain: one path instead of two. clang is a cross-compiler by
construction, so a single LLVM install targets bare-metal ELF, 16/32-bit
code below 1 MB, and Linux/musl userland from any host -- which is how
the Windows build already worked. The GNU branch beside it had drifted
into passing --no-warn-rwx-segments, a hard error on lld, which is the
default linker on both FreeBSD and macOS; and it required a brewed
x86_64-elf-gcc before anything built on a Mac at all. GNU is kept as a
fallback (USE_GNU=1) but clang+lld is chosen whenever both are present.
Host tools use the host's own cc; only mingw needs naming explicitly.

tools/mkschfs.c included <sys/sysmacros.h> unconditionally on non-mingw
hosts. That header is glibc/musl only -- macOS and the BSDs have neither
the header nor a need for it, since major()/minor() come from
<sys/types.h> there. Now guarded on __linux__.

Ports verified tarballs with `sha256sum -c -`, which does not exist on
macOS (shasum), FreeBSD/NetBSD (sha256), or OpenBSD (cksum -a sha256),
and each spells its check mode differently. tools/sha256check.sh picks
whichever of the five the host has and compares digests itself.

nproc is coreutils, so absent on macOS and BSD; falls through to sysctl
-n hw.ncpu before defaulting to 4.

The PowerShell tools now run under pwsh on any OS: QEMU is found on PATH
(the scoop location is a Windows-only fallback), and path literals use
forward slashes -- Join-Path $root 'build\x' produces a *filename
containing a backslash* on a Unix host. The backslash sweep is limited
to known directory prefixes on purpose: bootcheck sends
printf 'caf\303\251' to the guest to test UTF-8, and a blanket
substitution would have quietly changed what that asserts.

Also fixes six tools left booting build/fs.img, which stopped being
bootable when SchFS became the root filesystem: crashshot, dispshot,
fbshot, splashshot, textshot and ttyshot all take build/schfs.img now,
and the -SchFS switches that used to select it are gone.

Verified on Windows: full build, and bootcheck green. The Linux, macOS
and BSD paths are written from their documented behaviour and are NOT
executed here -- this machine has no WSL and no second host.
This commit is contained in:
Nexoniarz 2026-08-09 14:24:53 +02:00
commit cda5908660
15 changed files with 427 additions and 105 deletions

140
Makefile
View file

@ -8,7 +8,10 @@ SYSROOT = sysroot
# 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 := $(shell nproc 2>/dev/null || echo 4)
# 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
@ -92,31 +95,29 @@ CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Windows, under Git Bash or MSYS2 — WINHOST.
# 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.
#
# The usual answer is a cross binutils+gcc, which is a day of work that then
# rots. clang does not need any of it: it is a cross-compiler by construction,
# so one ordinary LLVM install targets both the bare-metal kernel and Linux
# userland, and llvm-objcopy/objdump stand in for binutils. Everything else the
# build shells out to — perl, sed, dd, sha256sum — comes with Git Bash.
ifneq (,$(filter MINGW% MSYS% CYGWIN%,$(UNAME_S)))
WINHOST := 1
endif
ifeq ($(UNAME_S),Linux)
ifeq ($(UNAME_P),aarch64)
TOOLPREFIX = x86_64-linux-gnu-
else
TOOLPREFIX =
endif
else ifeq ($(UNAME_S),Darwin)
TOOLPREFIX = x86_64-elf-
ifeq ($(UNAME_S),Darwin)
MACHOST := 1
endif
ifneq (,$(filter FreeBSD OpenBSD NetBSD DragonFly,$(UNAME_S)))
BSDHOST := 1
endif
QEMU = qemu-system-x86_64
@ -133,10 +134,40 @@ QEMU = qemu-system-x86_64
# virtio-gpu wins the display class, stdvga carries the splash and text console.
QEMU_DISPLAY = -vga std -device virtio-gpu-pci
ifdef WINHOST
# An explicit --target is what makes clang emit ELF instead of the COFF it
# would default to on this host. Without it every object is silently the
# wrong format and the first thing to complain is the linker.
# -----------------------------------------------------------------------
# Cross-compilation: clang everywhere it can be had
#
# The kernel, the bootloader and the ports all need a compiler targeting
# something other than the host -- bare-metal ELF, 16/32-bit code below one
# megabyte, and Linux/musl userland respectively. The traditional answer is a
# cross binutils+gcc per host, which is a day of work per platform that then
# rots, and on macOS means brewing x86_64-elf-* before anything builds at all.
#
# clang needs none of it: it is a cross-compiler by construction, so one
# ordinary LLVM install produces every target from every host. That was already
# how the Windows build worked; using it on all four hosts collapses what used
# to be two divergent toolchain branches -- one of which had drifted into
# passing GNU-ld-only flags to what is nowadays lld on FreeBSD and macOS -- into
# a single path that gets exercised on every platform instead of one.
#
# GNU is kept as a fallback for hosts without clang, but it is the exception.
# -----------------------------------------------------------------------
HAVE_CLANG := $(shell command -v clang 2>/dev/null)
HAVE_LLD := $(shell command -v ld.lld 2>/dev/null)
ifeq ($(origin USE_GNU), undefined)
ifneq (,$(HAVE_CLANG))
ifneq (,$(HAVE_LLD))
USE_CLANG := 1
endif
endif
endif
ifdef USE_CLANG
# An explicit --target is what makes clang emit ELF rather than whatever the
# host defaults to -- COFF on Windows, Mach-O on macOS. Without it every
# object is silently the wrong format and the first thing to complain is the
# linker.
CC = clang --target=x86_64-elf
CC32 = clang --target=i386-elf
LD = ld.lld
@ -151,49 +182,66 @@ ifdef WINHOST
# 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
# Host tools (mkfs, mkschfs, the SchFS suites) are ordinary Windows programs.
# They must be built with mingw-w64 rather than clang's default MSVC target:
# tools/mkfs.c includes <unistd.h> and <dirent.h>, and the Windows SDK has
# neither.
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
# Compiler for ports/, which build *Linux userland* against musl -- a third
# target, distinct from both the kernel (bare-metal ELF) and the host tools
# (Windows PE). mingw's gcc cannot do it: it targets Windows, so linking it
# against musl gives undefined `stderr` and `__errno_location`.
# 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 do not exist on this host.
# reaches for gcc's crtbeginT.o and libgcc, which need not exist on any
# particular host.
MUSLPREFIX = $(abspath $(BUILD))/ports/musl/prefix
PORTCC = clang --target=x86_64-unknown-linux-musl -fuse-ld=lld
PORTCRT = -nostdlib $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o
PORTEND = $(MUSLPREFIX)/lib/crtn.o
else
# GNU fallback. Needs a cross toolchain anywhere the host's own gcc does not
# already emit x86_64 ELF -- notably macOS, where TOOLPREFIX has to name a
# brewed x86_64-elf-gcc.
ifdef MACHOST
TOOLPREFIX ?= x86_64-elf-
else ifeq ($(UNAME_P),aarch64)
TOOLPREFIX ?= x86_64-linux-gnu-
else
TOOLPREFIX ?=
endif
CC = $(TOOLPREFIX)gcc
CC32 = $(CC) -m32
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
LDFLAGS = -m elf_x86_64 -nostdlib --no-warn-rwx-segments
HOSTCC = gcc
HOSTWARN = -Werror -Wall
EXE =
PYTHON = python3
LD_LOWBASE =
PORTCC = gcc
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.