PROJECT_ROOT := $(abspath $(CURDIR))
override TOOLCHAIN_DIR := $(PROJECT_ROOT)/toolchain
override CROSS := $(TOOLCHAIN_DIR)/cross/bin/ia16-elf-

# Load user selections before assigning defaults or deriving feature flags.
# In particular, sound and network helper variables below must be computed
# from the selected .config values, not from their fallback defaults.  GNU
# make command-line assignments still take precedence over this include.
-include .config

ROOT_BLOCK_BACKEND ?= xtide
# Hybrid disk model: with ROOT_BLOCK_BACKEND=bios13 the nucleus bootstraps the
# init image through firmware INT 13h, but KERNEL_BLOCK_XTIDE=1 makes the running
# kernel (driverd + runtime archive reader) reach the disk exclusively through
# the in-kernel XTIDE PIO driver, never firmware, after the IVT quarantine.
KERNEL_BLOCK_XTIDE ?= 0
XTIDE_IO ?= 0x0300
ANX_PLATFORM_RAM_KB ?= 640
ANTLER_LPT_BASE ?= 0x0378u
BOOT_DEVICE ?= /dev/boot
ROOT_DEVICE ?= $(if $(filter mfm,$(ROOT_BLOCK_BACKEND)),/dev/mfm0p1,/dev/hda1)
ROOT_FS_TYPE ?= ext2
ROOT_FLAGS ?= rw
ifneq ($(ROOT_FS_TYPE),ext2)
$(error ROOT_FS_TYPE=$(ROOT_FS_TYPE) is unsupported; Antler supports ext2 only)
endif
BOOT_OPTIONS ?=
SYSINIT_EXTRA_LINE ?=
GEM_RUNLEVEL ?= 3
ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR ?= 8192
HD_GEOM_MODE ?= xtelks
HD_IMAGE_MB ?= 20
MACHINE_PROFILE ?= pc1640

# XTIDE revision 1 decodes a complete sixteen-port window, even though the
# current transfer loop uses only offsets 0 through 14.  Parse the student's
# setting on the host before putting it into C, assembly, or an emulator file.
# The helper accepts a single unsigned C99-style literal and normalizes it to
# an assembler-safe hexadecimal word.  BASE may be at most fff0h, making the
# inclusive final advertised port BASE + 15 no greater than ffffh.  Port zero
# is reserved as the resident driver's uninitialized state, not a controller.
XTIDE_PROFILE_VALIDATOR := scripts/validate-xtide-geometry.py
XTIDE_IO_PROFILE := $(shell python3 $(XTIDE_PROFILE_VALIDATOR) io '$(XTIDE_IO)' 2>/dev/null || printf '%s\n' invalid)
ifeq ($(firstword $(XTIDE_IO_PROFILE)),invalid)
$(error XTIDE_IO=$(XTIDE_IO) must be one unsigned C-style integer literal from 1 through 0xfff0; zero is uninitialized and the 16-port XTIDE window must not wrap)
endif
XTIDE_IO_LITERAL := $(word 1,$(XTIDE_IO_PROFILE))
XTIDE_IO_DECIMAL := $(word 2,$(XTIDE_IO_PROFILE))
XTIDE_IO_LAST_PORT := $(word 3,$(XTIDE_IO_PROFILE))

# The direct loader cannot ask firmware how much RAM exists after handoff.
# Validate one decimal machine-profile value on the build host, then derive
# the segment at the start of its final 64 KiB window.  Both the VBR and the
# nucleus bootstrap stack use this same result, so a nominal 192 KiB build can
# never inherit the old hidden 320 KiB stack requirement.  The helper prints
# canonical decimal values; target assembly performs no division or wide math.
PCXT_RAM_PROFILE_VALIDATOR := $(PROJECT_ROOT)/scripts/validate-pcxt-ram.py
PCXT_RAM_PROFILE_INPUT := $(ANX_PLATFORM_RAM_KB)
PCXT_RAM_PROFILE := $(shell python3 $(PCXT_RAM_PROFILE_VALIDATOR) '$(PCXT_RAM_PROFILE_INPUT)' 2>/dev/null || printf '%s\n' invalid)
ifeq ($(firstword $(PCXT_RAM_PROFILE)),invalid)
$(error ANX_PLATFORM_RAM_KB must be one unsigned decimal value in the range 192..640)
endif
override ANX_PLATFORM_RAM_KB := $(word 1,$(PCXT_RAM_PROFILE))
ANX_KERNEL_BOOT_STACK_SEG := $(word 2,$(PCXT_RAM_PROFILE))

# Antler deliberately exposes only the two graphical local-session policies.
# Level 3 starts GEM Login and the normal Desktop/File Manager session.  Level
# 1 is the small graphical fallback and starts GEM Terminal directly.  Neither
# level enables a character console or getty.
ifeq ($(filter $(GEM_RUNLEVEL),1 3),)
$(error unsupported GEM_RUNLEVEL=$(GEM_RUNLEVEL); use 3 for GEM Login or 1 for GEM Terminal)
endif
ifneq ($(words $(GEM_RUNLEVEL)),1)
$(error GEM_RUNLEVEL must be exactly one level: 1 or 3)
endif
# Keep the emulated CPU clock a closed host-profile setting.  These values
# never cross into 16-bit guest C.  The profile, integer frequency in hertz,
# and acceptance scope form one provenance tuple; accepting only the two
# literal tuples below prevents an accelerated debug image from being labeled
# as final PC/XT acceptance.
86BOX_PCXT_CPU_PROFILE ?= final-xt
86BOX_PCXT_CPU_SPEED ?= 4772728
86BOX_PCXT_ACCEPTANCE_SCOPE ?= final
override 86BOX_PCXT_CLOCK_PROVENANCE := $(86BOX_PCXT_CPU_PROFILE):$(86BOX_PCXT_CPU_SPEED):$(86BOX_PCXT_ACCEPTANCE_SCOPE)
ifneq ($(86BOX_PCXT_CLOCK_PROVENANCE),final-xt:4772728:final)
ifneq ($(86BOX_PCXT_CLOCK_PROVENANCE),debug-8mhz:8000000:debug-only)
$(error unsupported 86Box PC/XT clock provenance $(86BOX_PCXT_CLOCK_PROVENANCE); use final-xt:4772728:final or debug-8mhz:8000000:debug-only)
endif
endif
86BOX_PC1640_CPU_SPEED ?= 7159092
86BOX_DISPLAY_SCALE ?= 1
86BOX_VIDEO_RENDERER ?= qt_software
86BOX_VIDEO_FILTER ?= 0
86BOX_FORCE_10MS ?= 1
86BOX_FORCE_CONSTANT_MOUSE ?= 1

# Keep the emulator device and the guest's VDI policy on the same mouse
# backend.  These are host-side strings only; no shell integer or pathname is
# compiled into the 16-bit target.  A command-line 86BOX_MOUSE_TYPE or
# 86BOX_MOUSE_PORT may still override the generated VM for hardware testing.
GEM_VDI_DEFAULTS ?= userland/root/etc/default/gem-vdi
GEM_VDI_MOUSE_BACKEND ?= $(shell if [ -r "$(GEM_VDI_DEFAULTS)" ]; then awk -F= 'BEGIN { v="serial" } $$1 == "GEM_VDI_MOUSE_BACKEND" { v=tolower($$2) } END { print v }' "$(GEM_VDI_DEFAULTS)"; else printf serial; fi)
GEM_VDI_MOUSE_PORT_NAME ?= $(shell if [ -r "$(GEM_VDI_DEFAULTS)" ]; then awk -F= 'BEGIN { v="com1" } $$1 == "GEM_VDI_MOUSE_PORT" { v=tolower($$2) } END { print v }' "$(GEM_VDI_DEFAULTS)"; else printf com1; fi)
ifeq ($(GEM_VDI_MOUSE_BACKEND),serial)
86BOX_MOUSE_TYPE ?= msserial
else ifeq ($(GEM_VDI_MOUSE_BACKEND),amstrad)
86BOX_MOUSE_TYPE ?= internal
else ifneq ($(filter $(GEM_VDI_MOUSE_BACKEND),pc1512 pc1640),)
86BOX_MOUSE_TYPE ?= internal
else
$(error unsupported GEM_VDI_MOUSE_BACKEND=$(GEM_VDI_MOUSE_BACKEND); use serial or amstrad)
endif
# VDISYS opens its workstation before FSYS mounts the ext2 root, so its
# /etc/default/gem-vdi read always falls back to the compiled default.  Make
# that fallback match the selected backend so an Amstrad build actually uses
# the motherboard pointer instead of a nonexistent serial mouse.
ifneq ($(filter $(GEM_VDI_MOUSE_BACKEND),amstrad pc1512 pc1640),)
CPPFLAGS += -DANTLER_GEM_VDI_MOUSE_DEFAULT_AMSTRAD=1
endif
ifneq ($(filter $(GEM_VDI_MOUSE_PORT_NAME),com1 ttys0),)
86BOX_MOUSE_PORT ?= 0
else ifneq ($(filter $(GEM_VDI_MOUSE_PORT_NAME),com2 ttys1),)
86BOX_MOUSE_PORT ?= 1
else ifneq ($(filter $(GEM_VDI_MOUSE_PORT_NAME),com3 ttys2),)
86BOX_MOUSE_PORT ?= 2
else ifneq ($(filter $(GEM_VDI_MOUSE_PORT_NAME),com4 ttys3),)
86BOX_MOUSE_PORT ?= 3
else
$(error unsupported GEM_VDI_MOUSE_PORT=$(GEM_VDI_MOUSE_PORT_NAME); use COM1 through COM4 or ttyS0 through ttyS3)
endif
# The in-nucleus VDI world has no vdisys to parse /etc/default/gem-vdi, so
# pointersys self-selects the serial mouse lease from this compiled index
# (0 = COM1 .. 3 = COM4).  Amstrad builds ignore it (nucleus polls 78h/7Ah).
CPPFLAGS += -DANTLER_GEM_VDI_MOUSE_PORT_INDEX=$(86BOX_MOUSE_PORT)
86BOX_GEM_VIDEO ?= pega
86BOX_VM_DIR := $(abspath build)
86BOX_UUID ?= $(shell python3 -c 'import os, uuid; p = os.path.realpath("$(86BOX_VM_DIR)"); p = p.rstrip("/") + "/"; print(uuid.uuid5(uuid.UUID(int=0), p))')

ENTRY_TRACE ?= 0
KERNEL_SELFTEST ?= 0
KERNEL_SELFTEST_EXTENDED ?= 0
RAM_PROFILE ?= speed
ENABLE_ANX2_SERVICE_API ?= 1
BOOT_USERLAND_DEBUG ?= 0
PROCESS_DEBUG ?= 0
USER_PROC_DEBUG ?= $(PROCESS_DEBUG)
SPAWN_TRACE_PORT ?= 0
USER_TIME_SLICE_TICKS ?= 55
# Keep the compact exec handoff markers enabled on the normal XT image.  They
# are bounded integer records and are the first evidence available when an
# image replacement fails before the new program can print anything.
NATIVE_EXEC_DEBUG ?= 1
NATIVE_FORK_DEBUG ?= $(PROCESS_DEBUG)
MOUNTED_EXEC_TRACE ?= 0
INIT_DEBUG ?= $(PROCESS_DEBUG)
SERSYS_DEBUG ?= $(PROCESS_DEBUG)
DEVICE_INIT_DEBUG ?= 0
DRIVER_LOG_VERBOSITY ?= 0
# Direct COM2 tracing is an explicit diagnostic build option.  The normal GEM
# image leaves it off so sersys can be the sole interrupt-driven UART owner.
SERVER_TRACE_COM2 ?= 0
SERVER_TRACE_COM2_BASE ?= 0x02f8
SERVER_TRACE_COM2_DIVISOR ?= 12
BOOT_DEBUG_UART_BASE ?= $(SERVER_TRACE_COM2_BASE)
BOOT_DEBUG_UART_DIVISOR ?= $(SERVER_TRACE_COM2_DIVISOR)
SERVER_TRACE_DEBUG_PORT ?= 0
SERVER_TRACE_DEBUG_PORT_IO ?= 0x00e9
NFS_SERVER ?= hulk
NFS_SERVER_IP ?=
NFS_EXPORT ?= /mnt/srv/Antler
NFS_MOUNTPOINT ?= /mnt/nfs
NFS_TEST_ROOT ?= $(NFS_MOUNTPOINT)
NFS_DEVICE ?= /dev/eth0
NFS_PORT ?= 2049
NFS_MOUNT_PORT ?= 0
NFS_TIMEOUT_TICKS ?= 200
NFS_RETRIES ?= 3
INIT_BIN ?=
INIT_PATH ?= /sbin/init
ROOT_ARCHIVE_EXCLUDE ?=
USERLAND_MEMMODEL ?= small
USERLAND_SERVER_MEMMODEL ?= small
# Kernel uname(2) data exposed to the stock ELKS POSIX userland.
KERNEL_UTS_SYSNAME ?= Antler
KERNEL_UTS_NODENAME ?= antler86
KERNEL_UTS_RELEASE ?= 0.1
KERNEL_UTS_VERSION ?= Antler/86-0.1-GEM-POSIX-core
KERNEL_UTS_MACHINE ?= i8086
# Keep standalone tools visible under the command names users type.
# userland/Makefile already builds vi, but historical aliases only exposed
# elvis/ex/view.  Inject these aliases from the kernel/root build without
# editing userland sources.
USERLAND_VISIBLE_COMMAND_ALIASES ?= \
	bin/vi=../build/userland/bin/vi
override ARCHIVE_EXTRA_ARGS := $(USERLAND_VISIBLE_COMMAND_ALIASES) $(ARCHIVE_EXTRA_ARGS)
FSYS_DEBUG ?= 0
MFM_DEBUG ?= 0
MFM_TRACE_SECTORS ?= 0
EXT2_DEBUG ?= 0
ENABLE_NETWORK ?= 0
ENABLE_HTTPS ?= 0
START_NETWORK_ON_BOOT ?= 0
START_EXT2FS_ON_BOOT ?= 1
NET_DRIVER ?= ne2k
NET_PROBE_SELF_TEST ?= 0
NET_NE2K_IO_DEFAULT := 0x0300
# The PC1640 (and -dd) integrate an XTIDE controller at 0x0300; qemu likewise
# places the root XTIDE there.  Default the NE2000 to 0x0340 on those machines
# so the NIC does not shadow the boot disk controller and leave the XTIDE BIOS
# reporting "Master at 300h: not found".  pcxt keeps 0x0300 because its default
# storage is the MFM/Xebec controller, which does not decode that port.
ifneq ($(filter $(MACHINE_PROFILE),pc1640 pc1640-dd qemu),)
NET_NE2K_IO_DEFAULT := 0x0340
endif
NET_NE2K_IO ?= $(NET_NE2K_IO_DEFAULT)
# Keep both traditional serial IRQ groups available: IRQ3 serves COM2/COM4
# and IRQ4 serves COM1/COM3.  On an XT-class single 8259A, IRQ2 is the normal
# remaining edge-triggered line for the default NE2000 adapter.
NET_NE2K_IRQ ?= 2
NET_NE2K_BUS_DEFAULT := auto
ifneq ($(filter $(MACHINE_PROFILE),pcxt pc1640 pc1640-dd tandy video7 qemu),)
NET_NE2K_BUS_DEFAULT := 8
endif
NET_NE2K_BUS ?= $(NET_NE2K_BUS_DEFAULT)
NET_NE2K_DEBUG_TX ?= 0
NET_PACKET_PATH ?= 1
NET_COM2_TRACE ?= $(SERVER_TRACE_COM2)
NET_WD80X3_IO ?= 0x0300
NET_WD80X3_IRQ ?= 3
NET_WD80X3_RAM ?= 0xd000
NET_WD80X3_FLAGS ?= 0x90
NET_ULTRA_IO ?= 0x0280
NET_ULTRA_IRQ_DEFAULT := 3
ifeq ($(MACHINE_PROFILE),pc1640-dd)
NET_ULTRA_IRQ_DEFAULT := 2
endif
NET_ULTRA_IRQ ?= $(NET_ULTRA_IRQ_DEFAULT)
NET_ULTRA_RAM ?= 0xd000
NET_ULTRA_FLAGS ?= 0x80
NET_IPMODE ?= static
NET_DHCP_ENABLE ?= $(if $(filter dhcp,$(NET_IPMODE)),yes,no)
ENABLE_DHCP_CLIENT ?= $(if $(filter 1 yes YES true TRUE,$(NET_DHCP_ENABLE)),1,0)
NET_DHCP_WAIT ?= 15
NET_LOCALIP ?= 192.168.10.2
NET_GATEWAY ?= 192.168.10.254
NET_NETMASK ?= 255.255.255.0
NET_DNSSERVER ?= 192.168.10.254
NET_EXTRA_HOST_NAME ?=
NET_EXTRA_HOST_IP ?=
NET_LINK_ne2k := ne0
NET_LINK_wd80x3 := wd0
NET_LINK_el3 := 3c0
NET_LINK_ultra := ul0
NET_LINK_83c790qf := ul0
NET_DEVICE_ne2k := /dev/ne0
NET_DEVICE_wd80x3 := /dev/wd0
NET_DEVICE_el3 := /dev/3c0
NET_DEVICE_ultra := /dev/smc0
NET_DEVICE_83c790qf := /dev/smc0
NET_LINK_NAME := $(or $(NET_LINK_$(NET_DRIVER)),ul0)
NET_DEVICE_NAME := $(or $(NET_DEVICE_$(NET_DRIVER)),/dev/eth0)
NTP_ENABLE ?= 0
NTP_SERVER ?= pool.ntp.org
NTP_TIMEOUT ?= 6
NTP_TRIES ?= 3
NTP_LOG ?= /var/log/ntpdate.log
# Ipserial is an optional on-demand login transport (/etc/init.d/ipserial).
# Keeping it out of the boot set leaves netsys demand-started, which the
# 640KB GEM desktop fit depends on; enable it explicitly when wanted.
IPSERIAL_ENABLE ?= 0
IPSERIAL_MODE ?= tcp
IPSERIAL_PORT ?= 23
ENABLE_TCP ?= 1
ENABLE_NFS ?= 0
NFS_START_NETWORK_ON_BOOT ?= $(ENABLE_NFS)
ENABLE_DNS_RESOLVER ?= $(if $(filter 1 yes,$(ENABLE_NFS) $(NTP_ENABLE)),1,0)
ENABLE_MFM_DRIVER ?= $(if $(filter mfm,$(ROOT_BLOCK_BACKEND)),1,0)
# KERNEL_BLOCK_XTIDE routes runtime disk through driverd's direct XTIDE PIO
# (the nucleus only does firmware INT 13h for the pre-quarantine bootstrap), so
# driverd must compile + link the XTIDE PIO backend even when the nucleus
# bootstrap backend is bios13.
ENABLE_ATA_DRIVER ?= $(if $(filter xtide athd,$(ROOT_BLOCK_BACKEND))$(filter 1,$(KERNEL_BLOCK_XTIDE)),1,0)
ENABLE_BIOS13_DRIVER ?= $(if $(filter bios13,$(ROOT_BLOCK_BACKEND)),1,0)
ENABLE_DIRECTFD_DRIVER ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "DRIVER_DIRECTFD" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on") v=1 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 0; fi)
ENABLE_GAMEPORT_DRIVER ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "DRIVER_GAMEPORT" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on") v=1 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 0; fi)
ENABLE_RAMDISK_DRIVER ?= 0
ENABLE_LOADABLE_DRIVER_MODULES ?= 1
ENABLE_DRIVER_MODULE_COMPRESSION ?= 0
# The VDI (video system) and its video driver can be hosted directly inside the
# nucleus instead of the separate vdisys server, so clients reach it through a
# kernel path (no per-primitive IPC round trip) and the VDI calls its driver by
# direct far call.  VDI_VIDEO_DRIVER selects the raster driver linked into it.
# VDI is always hosted in the nucleus now; the classic userland vdisys server was
# retired (see docs/ARCHITECTURE.md), so there is no longer a KERNEL_VDI switch.
VDI_VIDEO_DRIVER ?= pcvideo
ENABLE_KERNEL_COMPRESSION ?= 0
FS_ENABLE_FAT_RUNTIME ?= 1
FS_FAT_ENABLE_WRITE ?= 1
# Non-root ext2 mounts remain part of the general system.  An exact low-memory
# hardware profile may compile out only that adapter while retaining the
# resident ext2 root and the standalone ext2fs service in the distribution.
FS_ENABLE_EXT2_MOUNT_RUNTIME ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=1 } $$1 == "FS_ENABLE_EXT2_MOUNT_RUNTIME" { if ($$2 == "0" || $$2 == "no" || $$2 == "off" || $$2 == "false") v=0; else v=1 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 1; fi)
# POSIX message queues and named semaphores are enabled unless one explicit
# driver profile opts out.  Reading this byte-sized policy from the selected
# driver defaults keeps the full MFM profile unchanged while allowing the FAT
# profile to keep fsys below one 16-bit text segment.
FS_POSIX_IPC_ENABLE ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=1 } $$1 == "FS_POSIX_IPC_ENABLE" { if ($$2 == "0" || $$2 == "no" || $$2 == "off" || $$2 == "false") v=0; else v=1 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 1; fi)
FSYS_PTYS ?= 4
FSYS_PTY_BUF_BYTES ?= 128
# The ELKS-complete ANX2 syscall core includes mount/fsctl/service/klog in the
# active kernel.  Keep the tight profile bounded, but allow the complete
# syscall table to fit without disabling required syscalls.
KERNEL_TIGHT_SIZE_LIMIT ?= 163840
EXOMIZER ?= exomizer
ENABLE_SOUND ?= 0
SOUND_SERVICE_ENABLE ?= 0
START_SOUND_ON_BOOT ?= 0
ESD_ENABLE ?= 0
DRIVERD_SOUND_ENABLE ?= $(if $(filter 1 yes,$(ENABLE_SOUND) $(SOUND_SERVICE_ENABLE) $(START_SOUND_ON_BOOT) $(ESD_ENABLE)),1,0)
ESD_PORT ?= 16001
ESD_RATE ?= 22222
ESD_LOG ?= /var/log/esd.log
SOUND_RATE ?= $(ESD_RATE)
ENABLE_YORI ?= 0
ENABLE_KERNEL_ADMIN_CORE ?= $(ENABLE_ANX2_SERVICE_API)
ifneq ($(origin ENABLE_ANX2_ADMIN_CORE),undefined)
ENABLE_KERNEL_ADMIN_CORE := $(ENABLE_ANX2_ADMIN_CORE)
endif
SB_PORT ?= 0x220
SB_IRQ ?= 5
SB_DMA ?= 1
SB_BOUNCE ?= 4096
SOUND_DRIVER ?= sb
ifeq ($(SOUND_DRIVER),pcspk)
SOUND_DRIVER := pcspeaker
endif
ifeq ($(filter $(SOUND_DRIVER),sb pcspeaker),)
$(error unsupported SOUND_DRIVER=$(SOUND_DRIVER); use sb or pcspeaker)
endif
SOUND_DRIVER_SB := $(if $(filter sb,$(SOUND_DRIVER)),1,0)
SOUND_DRIVER_PCSPEAKER := $(if $(filter pcspeaker,$(SOUND_DRIVER)),1,0)
SOUND_OPTI_SB_MODE ?= off
SOUND_OPTI_SB_PORT ?=
SOUND_OPTI_SB_IRQ ?=
SOUND_OPTI_SB_DMA ?=
SOUND_MAD16_ENABLE ?= 0
SOUND_DEFAULTS ?= userland/root/etc/default/sound
DRIVER_DEFAULTS ?= userland/root/etc/default/drivers
SOUND_IRQ_AWARE ?= $(shell if [ -r "$(SOUND_DEFAULTS)" ]; then awk -F= 'BEGIN { v=1 } $$1 == "SOUND_DRIVER_MODE" { if ($$2 == "poll" || $$2 == "polling") v=0; else v=1 } END { print v }' "$(SOUND_DEFAULTS)"; else printf 1; fi)
MFM_ST506_IRQ ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=5 } $$1 == "MFM_ST506_IRQ" { v=$$2 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 5; fi)
MFM_ST506_DMA ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=3 } $$1 == "MFM_ST506_DMA" { v=$$2 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 3; fi)
MFM_IRQ_AWARE ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "MFM_ST506_MODE" { if ($$2 == "irq" || $$2 == "interrupt" || $$2 == "interrupts") v=1; else if ($$2 == "poll" || $$2 == "polling") v=0 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 0; fi)
MFM_DMA_MODE ?= $(shell if [ -r "$(DRIVER_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "MFM_ST506_DMA_MODE" || $$1 == "MFM_ST506_TRANSFER" { if ($$2 == "yes" || $$2 == "on" || $$2 == "1" || $$2 == "dma") v=1; else if ($$2 == "no" || $$2 == "off" || $$2 == "0" || $$2 == "pio") v=0 } END { print v }' "$(DRIVER_DEFAULTS)"; else printf 0; fi)

KLOG_BUFFER_BYTES ?= 2048
KERNEL_DEBUG_TTY ?= 0
SERIAL_DEBUG ?= 0
LEGACY_8250_TRACE ?= 0
LEGACY_8250_TRACE_BASE ?= 0x02f8
TTY_DEFAULTS ?= userland/root/etc/default/tty
UART_DEFAULTS ?= userland/root/etc/default/uart
86BOX_COM1_DEVICE ?= stdio
86BOX_COM1_PATH ?= $(abspath build/86box-com1)
86BOX_COM1_MODE ?= 1
86BOX_COM2_DEVICE ?= stdio
86BOX_COM2_PATH ?= $(abspath build/86box-com2)
86BOX_COM2_MODE ?= 0
86BOX_LPT1_PATH ?= $(abspath build/86box-lpt1.prn)
86BOX_IPSERIAL_HOST_PORT ?= 2323
86BOX_MFM_SECOND_DRIVE ?= 0
86BOX_MFM_IMAGE ?= build/mfm-data.img
86BOX_MFM_IMAGE_CFG ?= mfm-data.img
86BOX_MFM_IMAGE_BYTES ?= 21307392
86BOX_MFM_CONTROLLER ?= st506_xt
86BOX_MFM_CHANNEL ?= 1
86BOX_MFM_SECT ?= 17
86BOX_MFM_HEAD ?= 4
86BOX_MFM_CYL ?= 612
SERSYS_TTYS0 ?= $(shell if [ -r "$(TTY_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "TTY_ttyS0" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on" || $$2 == "y") v=1 } END { print v }' "$(TTY_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS1 ?= $(shell if [ -r "$(TTY_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "TTY_ttyS1" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on" || $$2 == "y") v=1 } END { print v }' "$(TTY_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS2 ?= $(shell if [ -r "$(TTY_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "TTY_ttyS2" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on" || $$2 == "y") v=1 } END { print v }' "$(TTY_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS3 ?= $(shell if [ -r "$(TTY_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "TTY_ttyS3" { if ($$2 == "yes" || $$2 == "1" || $$2 == "on" || $$2 == "y") v=1 } END { print v }' "$(TTY_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS0_BASE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v="0x3f8" } $$1 == "UART_ttyS0_BASE" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 0x3f8; fi)
SERSYS_TTYS1_BASE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v="0x2f8" } $$1 == "UART_ttyS1_BASE" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 0x2f8; fi)
SERSYS_TTYS2_BASE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v="0x3e8" } $$1 == "UART_ttyS2_BASE" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 0x3e8; fi)
SERSYS_TTYS3_BASE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v="0x2e8" } $$1 == "UART_ttyS3_BASE" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 0x2e8; fi)
SERSYS_TTYS0_TYPE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=255 } $$1 == "UART_ttyS0_TYPE" { if ($$2 == "8250") v=0; else if ($$2 == "16450") v=1; else if ($$2 == "16550") v=2; else if ($$2 == "16550A" || $$2 == "16550a") v=3; else if ($$2 == "16750") v=4; else v=255 } END { print v }' "$(UART_DEFAULTS)"; else printf 255; fi)
SERSYS_TTYS1_TYPE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=255 } $$1 == "UART_ttyS1_TYPE" { if ($$2 == "8250") v=0; else if ($$2 == "16450") v=1; else if ($$2 == "16550") v=2; else if ($$2 == "16550A" || $$2 == "16550a") v=3; else if ($$2 == "16750") v=4; else v=255 } END { print v }' "$(UART_DEFAULTS)"; else printf 255; fi)
SERSYS_TTYS2_TYPE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=255 } $$1 == "UART_ttyS2_TYPE" { if ($$2 == "8250") v=0; else if ($$2 == "16450") v=1; else if ($$2 == "16550") v=2; else if ($$2 == "16550A" || $$2 == "16550a") v=3; else if ($$2 == "16750") v=4; else v=255 } END { print v }' "$(UART_DEFAULTS)"; else printf 255; fi)
SERSYS_TTYS3_TYPE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=255 } $$1 == "UART_ttyS3_TYPE" { if ($$2 == "8250") v=0; else if ($$2 == "16450") v=1; else if ($$2 == "16550") v=2; else if ($$2 == "16550A" || $$2 == "16550a") v=3; else if ($$2 == "16750") v=4; else v=255 } END { print v }' "$(UART_DEFAULTS)"; else printf 255; fi)
SERSYS_IRQ_AWARE ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=1 } $$1 == "UART_DRIVER_MODE" { if ($$2 == "poll" || $$2 == "polling") v=0; else v=1 } END { print v }' "$(UART_DEFAULTS)"; else printf 1; fi)
SERSYS_TTYS0_IRQ ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=4 } $$1 == "UART_ttyS0_IRQ" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 4; fi)
SERSYS_TTYS1_IRQ ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=3 } $$1 == "UART_ttyS1_IRQ" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 3; fi)
SERSYS_TTYS2_IRQ ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=4 } $$1 == "UART_ttyS2_IRQ" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 4; fi)
SERSYS_TTYS3_IRQ ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=3 } $$1 == "UART_ttyS3_IRQ" { v=$$2 } END { print v }' "$(UART_DEFAULTS)"; else printf 3; fi)
SERSYS_TTYS0_FLOW ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "UART_ttyS0_FLOW" { if ($$2 == "rtscts" || $$2 == "hardware" || $$2 == "yes" || $$2 == "1") v=1 } END { print v }' "$(UART_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS1_FLOW ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "UART_ttyS1_FLOW" { if ($$2 == "rtscts" || $$2 == "hardware" || $$2 == "yes" || $$2 == "1") v=1 } END { print v }' "$(UART_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS2_FLOW ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "UART_ttyS2_FLOW" { if ($$2 == "rtscts" || $$2 == "hardware" || $$2 == "yes" || $$2 == "1") v=1 } END { print v }' "$(UART_DEFAULTS)"; else printf 0; fi)
SERSYS_TTYS3_FLOW ?= $(shell if [ -r "$(UART_DEFAULTS)" ]; then awk -F= 'BEGIN { v=0 } $$1 == "UART_ttyS3_FLOW" { if ($$2 == "rtscts" || $$2 == "hardware" || $$2 == "yes" || $$2 == "1") v=1 } END { print v }' "$(UART_DEFAULTS)"; else printf 0; fi)
BOOTSTRAP_FS_MAX_SIZE ?= 512
KERNEL_CFG_BUF_BYTES ?= 1408
NETSTACK_RX_BYTES ?= 512
NFS_FRAME_BYTES ?= 512
KERNEL_NFS_DATA_BYTES ?= 320
SERIAL_RX_BUF_BYTES ?= 128
PROC_BUFFER_BYTES ?= 1024
NETDEV_NDB_WRITE_BYTES ?= 320
NETDEV_INBUFFER_BYTES ?= 512
NETDEV_UDP_DATA_BYTES ?= 512
KERNEL_MAX_SOCK_ADDR ?= 110
KERNEL_UNIX_PATH_MAX ?= 20
KERNEL_MMEM_SIZE ?= 64
KERNEL_NETSTACK_IP_MTU ?= 576
KERNEL_NETSTACK_TCP_MSS ?= 256
SERVICE_ARENA_NATIVE_PARAGRAPHS ?= 0x0fffu
KERNEL_MAX_PORTS ?= 34
KERNEL_MAX_TASKS ?= 36
KERNEL_MAX_USER_PROCS ?= 14
KERNEL_USER_PROC_POLL_BUDGET ?= 2
KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE ?= 32
KERNEL_NETSTACK_SOCKETS ?= 5
KERNEL_NETSTACK_ARP_ENTRIES ?= 4
KERNEL_MAX_SERIAL ?= 4
KERNEL_MAX_CHRDEV ?= 13
KERNEL_MAX_BLKDEV ?= 8
KERNEL_MAX_ETHS ?= 4
KERNEL_FS_MAX_TYPES ?= 4
KERNEL_NATIVE_MAX_NODES ?= 40
KERNEL_NATIVE_MOUNT_MAX ?= 8
KERNEL_SEGMENT_MAP_ENTRIES ?= 40
KERNEL_ANX2_TASKCTL_RECORDS ?= 11
KERNEL_ANX2_CODE_SEGMENTS ?= 6
KERNEL_SERVICE_ARENA_CORE ?= 16
KERNEL_SERVICE_ARENA_CHAR ?= 32
KERNEL_SERVICE_ARENA_BLOCK ?= 64
KERNEL_SERVICE_ARENA_SOUND ?= 384
KERNEL_SERVICE_ARENA_RAMDISK ?= 192
KERNEL_SERVICE_ARENA_FS ?= 64
KERNEL_SERVICE_ARENA_USER ?= 128
KERNEL_HEAP_BLOCKS ?= 7
KERNEL_HEAP_BLOCK_BYTES ?= 64
#
# The kernel runs with one real-mode near data segment.  Keep the fixed stacks
# compact so the POSIX core, ANX2 service tables, block cache, and process
# tables all remain addressable without segment conversion code.
KERNEL_MAIN_STACK_BYTES ?= 0x1000
KERNEL_SYSCALL_STACK_BYTES ?= 0x1000
# Hard IRQs keep IF clear and run only bounded device capture/doorbell code.
# The current linked stack-usage/disassembly audit bounds the deepest legacy
# hard-IRQ C chain at 30 bytes.  A 192-byte independent stack therefore leaves
# at least 162 bytes of headroom while keeping C frames off untrusted user
# SS:SP.  Re-run that audit before adding another call to a hard-IRQ path.
KERNEL_IRQ_STACK_BYTES ?= 0x00c0
KERNEL_BLOCK_CACHE_SECTORS ?= 16
KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS ?= 16

ifeq ($(ENABLE_NETWORK),1)
ifneq ($(NET_DRIVER),ne2k)
$(error NET_DRIVER=$(NET_DRIVER) is disabled: its user-service driver has no hard-IRQ relay; use ne2k or disable networking)
endif
endif

ifeq ($(ROOT_BLOCK_BACKEND),ide)
override ROOT_BLOCK_BACKEND := xtide
endif

# A disk chosen as the root transport is not an optional peripheral.  A stale
# .config may still say ENABLE_MFM_DRIVER=0 after a command-line backend switch;
# force the resident/user-service MFM path on so the native Xebec loader does
# not hand a mounted root device to a kernel image that omitted its driver.
ifeq ($(ROOT_BLOCK_BACKEND),mfm)
override ENABLE_MFM_DRIVER := 1
ifeq ($(ROOT_DEVICE),/dev/hda1)
override ROOT_DEVICE := /dev/mfm0p1
endif
endif

MACHINE_PROFILES := pcxt pc1640 pc1640-dd tandy video7 qemu
ifeq ($(filter $(MACHINE_PROFILE),$(MACHINE_PROFILES)),)
$(error unsupported MACHINE_PROFILE=$(MACHINE_PROFILE); use pcxt, pc1640, pc1640-dd, tandy, video7, or qemu)
endif
# A NIC decoding the root disk controller's ISA base makes the machine
# unbootable: the XTIDE BIOS probes 0x0300 and reports "not found" while the
# NE2000 answers in its place.  Reject that image at build time instead of
# producing a disk that black-screens before the kernel loads.
ifeq ($(ENABLE_NETWORK),1)
ifeq ($(ROOT_BLOCK_BACKEND),xtide)
ifeq ($(NET_NE2K_IO),$(XTIDE_IO))
$(error NET_NE2K_IO=$(NET_NE2K_IO) collides with the XTIDE root disk controller at XTIDE_IO=$(XTIDE_IO); move the NIC to a free ISA base such as 0x0340)
endif
endif
endif
ifeq ($(filter $(86BOX_GEM_VIDEO),vga ega cga hercules pega),)
$(error unsupported 86BOX_GEM_VIDEO=$(86BOX_GEM_VIDEO); use vga, ega, cga, hercules, or pega)
endif
ifneq ($(filter $(MACHINE_PROFILE),pc1640 pc1640-dd),)
ifneq ($(86BOX_GEM_VIDEO),pega)
$(error MACHINE_PROFILE=$(MACHINE_PROFILE) has internal PEGA, so 86BOX_GEM_VIDEO must be pega)
endif
else ifeq ($(86BOX_GEM_VIDEO),pega)
$(error 86BOX_GEM_VIDEO=pega requires MACHINE_PROFILE=pc1640 or pc1640-dd)
endif

ifeq ($(BOOT_USERLAND_DEBUG),1)
PROCESS_DEBUG := 0
USER_PROC_DEBUG := 0
NATIVE_EXEC_DEBUG := 0
NATIVE_FORK_DEBUG := 0
INIT_DEBUG := 1
SERSYS_DEBUG := 1
KERNEL_DEBUG_TTY := 0
endif

ifeq ($(RAM_PROFILE),small)
KERNEL_BLOCK_CACHE_SECTORS := 8
KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 8
KLOG_BUFFER_BYTES := 1024
BOOTSTRAP_FS_MAX_SIZE := 256
KERNEL_CFG_BUF_BYTES := 1408
NETSTACK_RX_BYTES := 256
NFS_FRAME_BYTES := 320
KERNEL_NFS_DATA_BYTES := 128
SERIAL_RX_BUF_BYTES := 64
PROC_BUFFER_BYTES := 512
NETDEV_NDB_WRITE_BYTES := 192
NETDEV_INBUFFER_BYTES := 320
NETDEV_UDP_DATA_BYTES := 320
KERNEL_MAX_SOCK_ADDR := 96
KERNEL_UNIX_PATH_MAX := 16
KERNEL_MMEM_SIZE := 32
KERNEL_NETSTACK_IP_MTU := 576
KERNEL_NETSTACK_TCP_MSS := 192
SERVICE_ARENA_NATIVE_PARAGRAPHS := 0x0800
KERNEL_MAX_PORTS := 32
KERNEL_MAX_TASKS := 34
KERNEL_MAX_USER_PROCS := 14
KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE := 16
KERNEL_NETSTACK_SOCKETS := 3
KERNEL_NETSTACK_ARP_ENTRIES := 2
KERNEL_MAX_SERIAL := 4
KERNEL_MAX_CHRDEV := 13
KERNEL_MAX_BLKDEV := 4
KERNEL_MAX_ETHS := 2
KERNEL_FS_MAX_TYPES := 2
KERNEL_NATIVE_MAX_NODES := 36
# fsys serves the ext2 root; its node table caches one row per resolved path
# component.  Built-in device/pty/proc rows consume nearly all of a 36-row
# table, leaving too few mutable rows to hold a directory node while resolving
# its child -- so any depth>=2 open (e.g. /etc/passwd) fails with ENOSPC and
# the GEM login can never read the password file.  Give fsys alone a larger
# table (the payload pool is capped separately, so the growth is the cheap
# ~72-byte node rows, not the 160-byte payloads) without enlarging nativesys
# or the kernel node tables.
FSYS_MAX_NODES := 64
KERNEL_NATIVE_MOUNT_MAX := 4
KERNEL_SEGMENT_MAP_ENTRIES := 40
KERNEL_ANX2_TASKCTL_RECORDS := 9
KERNEL_ANX2_CODE_SEGMENTS := 6
KERNEL_SERVICE_ARENA_CORE := 16
KERNEL_SERVICE_ARENA_CHAR := 24
KERNEL_SERVICE_ARENA_BLOCK := 48
KERNEL_SERVICE_ARENA_SOUND := 320
KERNEL_SERVICE_ARENA_RAMDISK := 128
KERNEL_SERVICE_ARENA_FS := 48
KERNEL_SERVICE_ARENA_USER := 96
KERNEL_HEAP_BLOCKS := 4
KERNEL_HEAP_BLOCK_BYTES := 64
endif

ifneq ($(filter mfm xtide athd,$(ROOT_BLOCK_BACKEND)),)
KERNEL_MAX_BLKDEV := 8
endif

ifeq ($(RAM_PROFILE),speed)
KERNEL_BLOCK_CACHE_SECTORS := 32
KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 32
KLOG_BUFFER_BYTES := 128
BOOTSTRAP_FS_MAX_SIZE := 512
KERNEL_CFG_BUF_BYTES := 1408
NETSTACK_RX_BYTES := 256
NFS_FRAME_BYTES := 320
KERNEL_NFS_DATA_BYTES := 128
SERIAL_RX_BUF_BYTES := 16
PROC_BUFFER_BYTES := 256
NETDEV_NDB_WRITE_BYTES := 320
NETDEV_INBUFFER_BYTES := 512
NETDEV_UDP_DATA_BYTES := 512
KERNEL_MAX_SOCK_ADDR := 80
KERNEL_UNIX_PATH_MAX := 16
KERNEL_MMEM_SIZE := 24
KERNEL_NETSTACK_IP_MTU := 576
KERNEL_NETSTACK_TCP_MSS := 256
SERVICE_ARENA_NATIVE_PARAGRAPHS := 0x0600
KERNEL_MAX_PORTS := 32
KERNEL_MAX_TASKS := 32
# Fifteen foreground process slots are the current fmem-owner packing limit:
# slots 0..14 fit in the four-bit owner id after the +1 encoding.  The normal
# XT service set leaves only one spare slot at fourteen, which makes a
# two-stage POSIX pipeline fail before memory is exhausted.
KERNEL_MAX_USER_PROCS := 15
KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE := 4
# Keep enough fixed TCP sockets for two resident listeners, such as ipserial
# and a small daemon, plus several accepted children.  The table stays static
# and byte-indexed, so this raises network capacity without adding heap work
# or another scheduler poll path on 8088/8086 systems.
KERNEL_NETSTACK_SOCKETS := 10
KERNEL_NETSTACK_ARP_ENTRIES := 2
KERNEL_MAX_SERIAL := 4
KERNEL_MAX_CHRDEV := 13
KERNEL_MAX_BLKDEV := 4
KERNEL_MAX_ETHS := 2
KERNEL_FS_MAX_TYPES := 2
KERNEL_NATIVE_MAX_NODES := 36
# fsys serves the ext2 root; its node table caches one row per resolved path
# component.  Built-in device/pty/proc rows consume nearly all of a 36-row
# table, leaving too few mutable rows to hold a directory node while resolving
# its child -- so any depth>=2 open (e.g. /etc/passwd) fails with ENOSPC and
# the GEM login can never read the password file.  Give fsys alone a larger
# table (the payload pool is capped separately, so the growth is the cheap
# ~72-byte node rows, not the 160-byte payloads) without enlarging nativesys
# or the kernel node tables.
FSYS_MAX_NODES := 64
KERNEL_NATIVE_MOUNT_MAX := 4
KERNEL_SEGMENT_MAP_ENTRIES := 40
KERNEL_ANX2_TASKCTL_RECORDS := 4
KERNEL_ANX2_CODE_SEGMENTS := 4
KERNEL_SERVICE_ARENA_CORE := 12
KERNEL_SERVICE_ARENA_CHAR := 20
KERNEL_SERVICE_ARENA_BLOCK := 48
# 4096-byte SB DMA bounce buffer plus up to 63 paragraphs of 1 KiB alignment.
KERNEL_SERVICE_ARENA_SOUND := 320
KERNEL_SERVICE_ARENA_RAMDISK := 96
KERNEL_SERVICE_ARENA_FS := 48
KERNEL_SERVICE_ARENA_USER := 96
KERNEL_HEAP_BLOCKS := 4
KERNEL_HEAP_BLOCK_BYTES := 64
KERNEL_MAIN_STACK_BYTES := 0x1000
# The speed-profile syscall trap stack is used only while the nucleus handles
# one 8086 user trap.  Three KiB keeps the normal IPC entry path intact while
# preserving enough near-segment headroom for nucleus-owned input systems.
KERNEL_SYSCALL_STACK_BYTES := 0x0c00
endif

ifneq ($(filter mfm xtide athd,$(ROOT_BLOCK_BACKEND)),)
KERNEL_MAX_BLKDEV := 8
endif

# RAM-tight profile targets constrained 4.77/4.77-era memory budgets with more
# aggressive static-buffer shrinkage than "small".  Use only when those limits are
# acceptable for your workload.
ifeq ($(RAM_PROFILE),tight)
KERNEL_BLOCK_CACHE_SECTORS := 4
KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 4
KLOG_BUFFER_BYTES := 512
BOOTSTRAP_FS_MAX_SIZE := 256
KERNEL_CFG_BUF_BYTES := 1408
NETSTACK_RX_BYTES := 256
NFS_FRAME_BYTES := 320
KERNEL_NFS_DATA_BYTES := 128
SERIAL_RX_BUF_BYTES := 32
PROC_BUFFER_BYTES := 256
NETDEV_NDB_WRITE_BYTES := 320
NETDEV_INBUFFER_BYTES := 512
NETDEV_UDP_DATA_BYTES := 512
KERNEL_MAX_SOCK_ADDR := 80
KERNEL_UNIX_PATH_MAX := 16
KERNEL_MMEM_SIZE := 24
KERNEL_NETSTACK_IP_MTU := 576
KERNEL_NETSTACK_TCP_MSS := 256
SERVICE_ARENA_NATIVE_PARAGRAPHS := 0x0600
KERNEL_MAX_PORTS := 32
KERNEL_MAX_TASKS := 36
# Keep one more native process slot than the standard small profile for
# fork/exec smoke tests while preserving the four-bit fmem owner encoding.
KERNEL_MAX_USER_PROCS := 15
KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE := 16
KERNEL_NETSTACK_SOCKETS := 3
KERNEL_NETSTACK_ARP_ENTRIES := 2
KERNEL_MAX_SERIAL := 4
KERNEL_MAX_CHRDEV := 13
KERNEL_MAX_BLKDEV := 4
KERNEL_MAX_ETHS := 2
KERNEL_FS_MAX_TYPES := 2
KERNEL_NATIVE_MOUNT_MAX := 4
KERNEL_SEGMENT_MAP_ENTRIES := 40
KERNEL_ANX2_TASKCTL_RECORDS := 11
KERNEL_ANX2_CODE_SEGMENTS := 6
KERNEL_SERVICE_ARENA_CORE := 12
KERNEL_SERVICE_ARENA_CHAR := 20
KERNEL_SERVICE_ARENA_BLOCK := 48
# 4096-byte SB DMA bounce buffer plus up to 63 paragraphs of 1 KiB alignment.
KERNEL_SERVICE_ARENA_SOUND := 320
KERNEL_SERVICE_ARENA_RAMDISK := 96
KERNEL_SERVICE_ARENA_FS := 48
KERNEL_SERVICE_ARENA_USER := 96
KERNEL_HEAP_BLOCKS := 4
KERNEL_HEAP_BLOCK_BYTES := 64
KERNEL_MAIN_STACK_BYTES := 0x1000
KERNEL_SYSCALL_STACK_BYTES := 0x1000
endif

ifeq ($(RAM_PROFILE),tight)
ifeq ($(ENABLE_NETWORK),1)
#
# The tight network image carries resident NE2K, netsys, socket state, and
# driver buffers in the same 8086 near-data budget as the block cache.  Two
# sectors still cache one 1 KiB ext2 block pair, while saving 1 KiB versus the
# default tight cache.  This keeps QEMU NE2K boot smoke stable without removing
# the block-cache read fast path from networked XT-class builds.
#
KERNEL_BLOCK_CACHE_SECTORS := 2
endif
endif

ifneq ($(filter mfm xtide athd,$(ROOT_BLOCK_BACKEND)),)
KERNEL_MAX_BLKDEV := 8
endif

#
# Networking pulls in fixed socket, packet, and NE2K state.  The speed profile
# keeps the 32-sector block cache for disk-only builds, but a network kernel
# has to fit inside one 8086 near-data segment.  Sixteen sectors still avoids
# repeated physical I/O while leaving room for TCP/UDP service state.
#
ifeq ($(RAM_PROFILE),speed)
ifeq ($(ENABLE_NETWORK),1)
ifeq ($(ENABLE_NFS),1)
#
# NFS brings in the UDP/RPC client, DNS resolver, and resident filesystem
# service.  The deployment image leaves NFS manual and sound disabled, so the
# cache/tables below keep the kernel under the PC/XT boot image limit while
	# still allowing root, one NFS mount, and one removable filesystem mount.
	#
	KERNEL_BLOCK_CACHE_SECTORS := 4
	KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 4
	KERNEL_NATIVE_MOUNT_MAX := 3
	KERNEL_MAX_USER_PROCS := 15
	KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE := 12
	KERNEL_SEGMENT_MAP_ENTRIES := 40
	KERNEL_ANX2_TASKCTL_RECORDS := 4
	KERNEL_ANX2_CODE_SEGMENTS := 5
	KLOG_BUFFER_BYTES := 128
	KERNEL_CFG_BUF_BYTES := 1280
else
		# Network + sound + the full POSIX core no longer fit the 8086 near
		# segment with the old 16-sector cache.  Eight sectors keeps the
		# speed profile buildable while preserving the cache and archive lookup
		# acceleration.
		KERNEL_BLOCK_CACHE_SECTORS := 8
		KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 8
		# Permanent unified tracing retains a 32-byte bounded history here;
		# live readers still receive the complete stream through native klog.
		KLOG_BUFFER_BYTES := 32
		#
		# Both stacks below live in the kernel's single 8086 near-data
		# segment.  The networked, non-NFS speed image otherwise exceeds
		# that segment even though only one native syscall trap can run at
		# a time.  Keep the proved 2800-byte main scheduler stack: reducing it to
		# 2400 bytes prevented the graphical login input path from progressing on
		# the exact PC1640 runtime.  The separate native-syscall trap stack is 1216
		# bytes.  This leaves room for the resident graphics and input services
		# without reducing process, task, port, signal, or socket capacity.
		# Both even-sized stacks retain independent canary/high-water scans.
		#
		KERNEL_MAIN_STACK_BYTES := 0x0af0
		KERNEL_SYSCALL_STACK_BYTES := 0x04c0
endif
endif
endif

#
# Native fixed-disk services need their own fixed state.  Xebec MFM, XTIDE,
# and ATHD images keep the cache at the same bounded budget used by networked
# speed builds so the 8086 near-data segment remains below 64 KiB.  Four
# sectors retain two complete 1 KiB ext2 blocks while leaving room for the
# nucleus stacks and process tables in the single near-data segment.
#
ifeq ($(RAM_PROFILE),speed)
ifneq ($(filter mfm xtide athd,$(ROOT_BLOCK_BACKEND)),)
KERNEL_BLOCK_CACHE_SECTORS := 4
KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS := 4
	# Every disk-only speed build keeps the same stack bounds already proved by
	# the graphical network speed profile.  A 2800-byte scheduler stack is the
	# measured lower bound for reliable GEM login input; the independent
	# 1216-byte native-syscall stack handles one real-mode trap at a time.  The
	# stack depth is set by the GEM/POSIX code paths, not by which fixed-disk
	# controller booted, so the XT-IDE and ATHD backends share these bounds and
	# stay within the one 8086 near-data segment instead of overflowing it with
	# the generic 4 KiB defaults.  Separate canary and high-water accounting is
	# retained for students.
	KERNEL_MAIN_STACK_BYTES := 0x0af0
	KERNEL_SYSCALL_STACK_BYTES := 0x04c0
ifeq ($(ENABLE_NETWORK),1)
ifeq ($(ENABLE_NFS),1)
NETSTACK_RX_BYTES := 256
NFS_FRAME_BYTES := 320
KERNEL_NFS_DATA_BYTES := 128
KERNEL_MAX_USER_PROCS := 15
KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE := 12
KERNEL_SEGMENT_MAP_ENTRIES := 40
KERNEL_ANX2_TASKCTL_RECORDS := 4
KERNEL_ANX2_CODE_SEGMENTS := 5
else
NETSTACK_RX_BYTES := 128
endif
NETDEV_INBUFFER_BYTES := 320
NETDEV_UDP_DATA_BYTES := 256
KERNEL_NETSTACK_IP_MTU := 320
KERNEL_NETSTACK_TCP_MSS := 128
KERNEL_NETSTACK_SOCKETS := 3
endif
endif
endif

ifeq ($(ENABLE_NETWORK),1)
ifeq ($(NET_DRIVER),ultra)
ULTRA_DRIVER_DEBUG ?= 0
ifeq ($(ULTRA_DRIVER_DEBUG),1)
DRIVER_LOG_VERBOSITY := 5
endif
ifneq ($(filter speed tight,$(RAM_PROFILE)),)
KLOG_BUFFER_BYTES := 112
endif
endif
endif

#
# Non-network builds keep only tiny network-capability constants so static RAM
# used by network nulls and headers is minimized automatically.
#
ifeq ($(ENABLE_NETWORK),0)
# Disk-only XT smoke images do not need a large resident kernel log ring.  Keep
# the buffer at 64 bytes so the 8086 kernel image stays under one segment while
# preserving the same low-frequency logging path.
KLOG_BUFFER_BYTES := 64
KERNEL_SEGMENT_MAP_ENTRIES := 34
KERNEL_NFS_DATA_BYTES := 64
NFS_FRAME_BYTES := 128
NETSTACK_RX_BYTES := 32
NETDEV_NDB_WRITE_BYTES := 48
NETDEV_INBUFFER_BYTES := 128
NETDEV_UDP_DATA_BYTES := 64
KERNEL_NETSTACK_IP_MTU := 240
KERNEL_NETSTACK_TCP_MSS := 32
KERNEL_NETSTACK_SOCKETS := 1
KERNEL_NETSTACK_ARP_ENTRIES := 1
KERNEL_MAX_ETHS := 1
endif

#
# KERNEL_SELFTEST is a diagnostic boot image, not the normal deployed kernel.
# It deliberately links extra test text into the resident 8086 image, so keep
# the fixed boot/syscall stacks and log ring tighter for that profile only.
# The self-test runs before user programs are dispatched and uses only bounded
# klog, heap, segment-ledger, brk-policy, and scheduler checks.  A two-KiB
# stack reserve for each entry path leaves room for those bounded calls while
# the reduced diagnostic process tables preserve a link-budget margin.  This
# avoids a 32-bit or protected-mode escape from the PC/XT near-segment budget.
#
KERNEL_SELFTEST_STACK_BYTES ?= 0x0800
ifeq ($(KERNEL_SELFTEST),1)
KLOG_BUFFER_BYTES := 64
KERNEL_SEGMENT_MAP_ENTRIES := 34
# The compact test exits before native user handoff.  Keep the mandatory two
# process rows and enough task slots for the fixed native-server test slot;
# larger production process tables would consume scarce near data without
# adding self-test coverage.
KERNEL_MAX_USER_PROCS := 2
KERNEL_MAX_TASKS := 20
# The compact diagnostic stops before user handoff and does not run the
# extended task-control suite.  Keep one bounded row of each task-control table
# so the normal syscall surface still links, without reserving metadata for
# user processes that this pre-handoff image deliberately never starts.
KERNEL_ANX2_TASKCTL_RECORDS := 1
KERNEL_ANX2_CODE_SEGMENTS := 1
KERNEL_MAIN_STACK_BYTES := $(KERNEL_SELFTEST_STACK_BYTES)
KERNEL_SYSCALL_STACK_BYTES := $(KERNEL_SELFTEST_STACK_BYTES)
endif

#
# fsys is a userland service and does not need to mirror every native-kernel
# table count.  On an XTIDE-root, no-network, no-MFM XT image the built-in fsys
# nodes stop at /proc, so 28 rows still leaves eight reclaimable ext2 cache
# nodes while saving resident service RAM and shortening bounded node scans.
# Driver-heavy and networked builds keep the wider defaults unless explicitly
# overridden on the make command line.
#
ifeq ($(origin FSYS_MAX_NODES),undefined)
FSYS_MAX_NODES := $(KERNEL_NATIVE_MAX_NODES)
# The MartyPC Xebec profile exposes eight MFM partition nodes, both NEC765
# floppy nodes, and the gameport before the four PTY pairs and /proc.  Those
# fixed entries place the first mutable ext2 cache row at index 38, so retain
# two bounded mutable rows in the 40-entry byte-indexed table.  Smaller
# profiles keep their existing 28- or 36-row limits.
ifeq ($(ENABLE_MFM_DRIVER),1)
ifeq ($(ENABLE_DIRECTFD_DRIVER),1)
ifeq ($(ENABLE_GAMEPORT_DRIVER),1)
FSYS_MAX_NODES := 40
endif
endif
endif
ifeq ($(ENABLE_NETWORK),0)
ifeq ($(ENABLE_MFM_DRIVER),0)
ifeq ($(ROOT_BLOCK_BACKEND),xtide)
ifneq ($(filter pcxt pc1640 pc1640-dd,$(MACHINE_PROFILE)),)
FSYS_MAX_NODES := 28
endif
endif
endif
endif
endif

ifeq ($(origin FSYS_MAX_MOUNTS),undefined)
FSYS_MAX_MOUNTS := $(KERNEL_NATIVE_MOUNT_MAX)
ifeq ($(ENABLE_NETWORK),0)
ifeq ($(ENABLE_MFM_DRIVER),0)
ifeq ($(ROOT_BLOCK_BACKEND),xtide)
ifneq ($(filter pcxt pc1640 pc1640-dd,$(MACHINE_PROFILE)),)
FSYS_MAX_MOUNTS := 3
endif
endif
endif
endif
endif

ifeq ($(origin FSYS_MAX_FDS),undefined)
FSYS_MAX_FDS := 32
endif

ifeq ($(BOOT_USERLAND_DEBUG),1)
KLOG_BUFFER_BYTES := 128
endif

override TOOLCHAIN_DIR := $(PROJECT_ROOT)/toolchain
override CROSS := $(TOOLCHAIN_DIR)/cross/bin/ia16-elf-

ROOT_BLOCK_BACKENDS := mfm xtide athd bios13
ROOT_BLOCK_BACKEND_UPPER_mfm := MFM
ROOT_BLOCK_BACKEND_UPPER_xtide := XTIDE
ROOT_BLOCK_BACKEND_UPPER_athd := ATHD
ROOT_BLOCK_BACKEND_UPPER_bios13 := BIOS13
ROOT_BLOCK_BACKEND_UPPER := $(ROOT_BLOCK_BACKEND_UPPER_$(ROOT_BLOCK_BACKEND))
ifeq ($(filter $(ROOT_BLOCK_BACKEND),$(ROOT_BLOCK_BACKENDS)),)
$(error unsupported ROOT_BLOCK_BACKEND=$(ROOT_BLOCK_BACKEND); use mfm, ide/xtide, athd, or bios13)
endif

# The bios13 backend reaches the disk through firmware INT 13h at every layer
# (MBR, ext2 VBR, nucleus archive reader, and driverd).  It is the portable
# path for stock QEMU/SeaBIOS and any real BIOS, and is the ONE backend that is
# deliberately exempt from the no-BIOS audit -- xtide/mfm/athd stay pure PIO.
ROOT_BLOCK_BACKEND_USES_BIOS := $(filter bios13,$(ROOT_BLOCK_BACKEND))
IA16_MEMMODELS := tiny small medium compact large huge
ifeq ($(filter $(USERLAND_MEMMODEL),$(IA16_MEMMODELS)),)
$(error unsupported USERLAND_MEMMODEL=$(USERLAND_MEMMODEL); use tiny, small, medium, compact, large, or huge)
endif
ifeq ($(filter $(USERLAND_SERVER_MEMMODEL),$(IA16_MEMMODELS)),)
$(error unsupported USERLAND_SERVER_MEMMODEL=$(USERLAND_SERVER_MEMMODEL); use tiny, small, medium, compact, large, or huge)
endif
USERLAND_MEMMODEL_CODE_tiny := T
USERLAND_MEMMODEL_CODE_small := S
USERLAND_MEMMODEL_CODE_medium := M
USERLAND_MEMMODEL_CODE_compact := C
USERLAND_MEMMODEL_CODE_large := L
USERLAND_MEMMODEL_CODE_huge := H
USERLAND_MEMMODEL_CODE := $(USERLAND_MEMMODEL_CODE_$(USERLAND_MEMMODEL))

CC := $(CROSS)gcc
AS := $(CROSS)as
LD := $(CROSS)ld
OBJCOPY := $(CROSS)objcopy
OBJDUMP := $(CROSS)objdump
SIZE := $(CROSS)size
NO_BIOS_AUDIT := scripts/audit-no-bios-runtime.py
NO_BIOS_SOURCE_AUDIT := scripts/audit-no-bios-source.py
NO_BIOS_KERNEL_STAMP := build/antler.no-bios
NO_BIOS_MBR_AUDIT_ELF := build/kern/boot/pcxt/mbr.audit.elf
NO_BIOS_EXT2_AUDIT_ELF := build/kern/boot/pcxt/ext2.audit.elf
NO_BIOS_MBR_STAMP := build/kern/boot/pcxt/mbr.no-bios
NO_BIOS_EXT2_STAMP := build/kern/boot/pcxt/ext2.no-bios

# The bios13 backend is the one path that intentionally calls firmware, so its
# kernel image and boot sectors must NOT be gated on the no-BIOS audit.  Every
# other backend keeps the audit as a hard build dependency.
ifeq ($(ROOT_BLOCK_BACKEND),bios13)
# Skip the no-BIOS *scan* (the stamp recipe runs the audit script), but still
# build the audited ELF so the boot sectors keep their raw-vs-ELF determinism
# cmp.  The kernel image has no such cmp, so it needs no dependency at all.
NO_BIOS_KERNEL_DEP :=
NO_BIOS_MBR_DEP := $(NO_BIOS_MBR_AUDIT_ELF)
NO_BIOS_EXT2_DEP := $(NO_BIOS_EXT2_AUDIT_ELF)
else
NO_BIOS_KERNEL_DEP := $(NO_BIOS_KERNEL_STAMP)
NO_BIOS_MBR_DEP := $(NO_BIOS_MBR_STAMP)
NO_BIOS_EXT2_DEP := $(NO_BIOS_EXT2_STAMP)
endif

CPPFLAGS := -Ikern -Ikern/system -Ikern/system/service -I.
CPPFLAGS += -DANTLER_KERNEL_MEMMODEL_NAME=\"small\"
CPPFLAGS += -DANTLER_KERNEL_MEMMODEL_CODE=\"S\"
CPPFLAGS += -DANTLER_NATIVE_MEMMODEL_NAME=\"$(USERLAND_MEMMODEL)\"
CPPFLAGS += -DANTLER_NATIVE_MEMMODEL_CODE=\"$(USERLAND_MEMMODEL_CODE)\"
CPPFLAGS += -DANTLER_ROOT_BLOCK_BACKEND_NAME=\"$(ROOT_BLOCK_BACKEND)\"
CPPFLAGS += -DANTLER_ROOT_BLOCK_BACKEND_$(ROOT_BLOCK_BACKEND_UPPER)=1
CPPFLAGS += -DANTLER_XTIDE_IO=$(XTIDE_IO_LITERAL)
CPPFLAGS += -DANX_PLATFORM_RAM_KB=$(ANX_PLATFORM_RAM_KB)u
CPPFLAGS += -DANX_KERNEL_BOOT_STACK_SEG=$(ANX_KERNEL_BOOT_STACK_SEG)
CPPFLAGS += -DANTLER_MACHINE_PROFILE_NAME=\"$(MACHINE_PROFILE)\"
CPPFLAGS += -DANTLER_BOOT_DEVICE=\"$(BOOT_DEVICE)\"
CPPFLAGS += -DANTLER_ROOT_DEVICE=\"$(ROOT_DEVICE)\"
CPPFLAGS += -DANTLER_ROOT_FS_TYPE=\"$(ROOT_FS_TYPE)\"
CPPFLAGS += -DANTLER_ROOT_FLAGS=\"$(ROOT_FLAGS)\"
CPPFLAGS += -DANTLER_INIT_PATH=\"$(INIT_PATH)\"
CPPFLAGS += -DANTLER_BOOT_ARCHIVE_BASE_SECTOR=$(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR)u
CPPFLAGS += -DANTLER_KERNEL_SELFTEST_DEFAULT=$(KERNEL_SELFTEST)
CPPFLAGS += -DANTLER_BOOT_USERLAND_DEBUG=$(BOOT_USERLAND_DEBUG)
CPPFLAGS += -DANTLER_PROCESS_DEBUG=$(PROCESS_DEBUG)
CPPFLAGS += -DANTLER_USER_PROC_DEBUG=$(USER_PROC_DEBUG)
CPPFLAGS += -DANX_SPAWN_TRACE_PORT=$(SPAWN_TRACE_PORT)
CPPFLAGS += -DANTLER_NATIVE_EXEC_DEBUG=$(NATIVE_EXEC_DEBUG)
CPPFLAGS += -DANTLER_NATIVE_FORK_DEBUG=$(NATIVE_FORK_DEBUG)
CPPFLAGS += -DANTLER_MOUNTED_EXEC_TRACE=$(MOUNTED_EXEC_TRACE)
CPPFLAGS += -DANTLER_DRIVER_LOG_VERBOSITY=$(DRIVER_LOG_VERBOSITY)
CPPFLAGS += -DANTLER_SERVER_TRACE_COM2=$(SERVER_TRACE_COM2)
CPPFLAGS += -DANTLER_SERVER_TRACE_COM2_BASE=$(SERVER_TRACE_COM2_BASE)u
CPPFLAGS += -DANTLER_SERVER_TRACE_COM2_DIVISOR=$(SERVER_TRACE_COM2_DIVISOR)u
CPPFLAGS += -DANTLER_BOOT_DEBUG_UART_BASE=$(BOOT_DEBUG_UART_BASE)u
CPPFLAGS += -DANTLER_BOOT_DEBUG_UART_DIVISOR=$(BOOT_DEBUG_UART_DIVISOR)u
CPPFLAGS += -DANTLER_SERVER_TRACE_DEBUG_PORT=$(SERVER_TRACE_DEBUG_PORT)
CPPFLAGS += -DANTLER_SERVER_TRACE_DEBUG_PORT_IO=$(SERVER_TRACE_DEBUG_PORT_IO)u
CPPFLAGS += -DANTLER_KLOG_BUFFER_BYTES=$(KLOG_BUFFER_BYTES)
CPPFLAGS += -DANTLER_KERNEL_DEBUG_TTY=$(KERNEL_DEBUG_TTY)
CPPFLAGS += -DANTLER_SERIAL_DEBUG=$(SERIAL_DEBUG)
CPPFLAGS += -DANTLER_LEGACY_8250_TRACE=$(LEGACY_8250_TRACE)
CPPFLAGS += -DANTLER_LEGACY_8250_TRACE_BASE=$(LEGACY_8250_TRACE_BASE)
CPPFLAGS += -DANTLER_SERSYS_TTYS0=$(SERSYS_TTYS0)
CPPFLAGS += -DANTLER_SERSYS_TTYS1=$(SERSYS_TTYS1)
CPPFLAGS += -DANTLER_SERSYS_TTYS2=$(SERSYS_TTYS2)
CPPFLAGS += -DANTLER_SERSYS_TTYS3=$(SERSYS_TTYS3)
CPPFLAGS += -DANTLER_SERSYS_TTYS0_BASE=$(SERSYS_TTYS0_BASE)
CPPFLAGS += -DANTLER_SERSYS_TTYS1_BASE=$(SERSYS_TTYS1_BASE)
CPPFLAGS += -DANTLER_SERSYS_TTYS2_BASE=$(SERSYS_TTYS2_BASE)
CPPFLAGS += -DANTLER_SERSYS_TTYS3_BASE=$(SERSYS_TTYS3_BASE)
CPPFLAGS += -DANTLER_SERSYS_TTYS0_TYPE=$(SERSYS_TTYS0_TYPE)
CPPFLAGS += -DANTLER_SERSYS_TTYS1_TYPE=$(SERSYS_TTYS1_TYPE)
CPPFLAGS += -DANTLER_SERSYS_TTYS2_TYPE=$(SERSYS_TTYS2_TYPE)
CPPFLAGS += -DANTLER_SERSYS_TTYS3_TYPE=$(SERSYS_TTYS3_TYPE)
CPPFLAGS += -DANTLER_SERSYS_IRQ_AWARE=$(SERSYS_IRQ_AWARE)
CPPFLAGS += -DANTLER_SERSYS_TTYS0_IRQ=$(SERSYS_TTYS0_IRQ)
CPPFLAGS += -DANTLER_SERSYS_TTYS1_IRQ=$(SERSYS_TTYS1_IRQ)
CPPFLAGS += -DANTLER_SERSYS_TTYS2_IRQ=$(SERSYS_TTYS2_IRQ)
CPPFLAGS += -DANTLER_SERSYS_TTYS3_IRQ=$(SERSYS_TTYS3_IRQ)
CPPFLAGS += -DANTLER_SERSYS_TTYS0_FLOW=$(SERSYS_TTYS0_FLOW)
CPPFLAGS += -DANTLER_SERSYS_TTYS1_FLOW=$(SERSYS_TTYS1_FLOW)
CPPFLAGS += -DANTLER_SERSYS_TTYS2_FLOW=$(SERSYS_TTYS2_FLOW)
CPPFLAGS += -DANTLER_SERSYS_TTYS3_FLOW=$(SERSYS_TTYS3_FLOW)
CPPFLAGS += -DANTLER_BOOTSTRAP_FS_MAX_SIZE=$(BOOTSTRAP_FS_MAX_SIZE)
CPPFLAGS += -DANTLER_CFG_BUF_BYTES=$(KERNEL_CFG_BUF_BYTES)
CPPFLAGS += -DANTLER_NETSTACK_RX_BYTES=$(NETSTACK_RX_BYTES)
CPPFLAGS += -DANTLER_NFS_FRAME_BYTES=$(NFS_FRAME_BYTES)
CPPFLAGS += -DANTLER_NFS_DATA_BYTES=$(KERNEL_NFS_DATA_BYTES)
CPPFLAGS += -DANTLER_SERIAL_RX_BUF=$(SERIAL_RX_BUF_BYTES)
CPPFLAGS += -DANTLER_PROC_BUFFER_BYTES=$(PROC_BUFFER_BYTES)
CPPFLAGS += -DANTLER_NETDEV_NDB_WRITE_BYTES=$(NETDEV_NDB_WRITE_BYTES)
CPPFLAGS += -DANTLER_NETDEV_INBUFFER_BYTES=$(NETDEV_INBUFFER_BYTES)
CPPFLAGS += -DANTLER_NET_UDP_DATA_BYTES=$(NETDEV_UDP_DATA_BYTES)
CPPFLAGS += -DANTLER_MAX_SOCK_ADDR=$(KERNEL_MAX_SOCK_ADDR)
CPPFLAGS += -DANTLER_UNIX_PATH_MAX=$(KERNEL_UNIX_PATH_MAX)
CPPFLAGS += -DANTLER_MMEM_SIZE=$(KERNEL_MMEM_SIZE)
CPPFLAGS += -DANTLER_NETSTACK_IP_MTU=$(KERNEL_NETSTACK_IP_MTU)
CPPFLAGS += -DANTLER_NETSTACK_TCP_MSS=$(KERNEL_NETSTACK_TCP_MSS)
CPPFLAGS += -DANTLER_SERVICE_ARENA_NATIVE_PARAGRAPHS=$(SERVICE_ARENA_NATIVE_PARAGRAPHS)
CPPFLAGS += -DANTLER_MAX_PORTS=$(KERNEL_MAX_PORTS)
CPPFLAGS += -DANTLER_MAX_TASKS=$(KERNEL_MAX_TASKS)
CPPFLAGS += -DANTLER_MAX_USER_PROCS=$(KERNEL_MAX_USER_PROCS)
CPPFLAGS += -DANTLER_USER_PROC_POLL_BUDGET=$(KERNEL_USER_PROC_POLL_BUDGET)u
CPPFLAGS += -DANTLER_USER_TIME_SLICE_TICKS=$(USER_TIME_SLICE_TICKS)u
CPPFLAGS += -DANTLER_TASK_EVENT_QUEUE=$(KERNEL_LIGHTCYCLE_TASK_EVENT_QUEUE)
CPPFLAGS += -DANTLER_NETSTACK_SOCKETS=$(KERNEL_NETSTACK_SOCKETS)
CPPFLAGS += -DANTLER_NETSTACK_ARP_ENTRIES=$(KERNEL_NETSTACK_ARP_ENTRIES)
CPPFLAGS += -DANTLER_SOUND_SB_PORT=$(SB_PORT)
CPPFLAGS += -DANTLER_SOUND_SB_IRQ=$(SB_IRQ)
CPPFLAGS += -DANTLER_SOUND_SB_DMA=$(SB_DMA)
CPPFLAGS += -DANTLER_ENABLE_SOUND_DRIVER=$(DRIVERD_SOUND_ENABLE)
CPPFLAGS += -DANTLER_ENABLE_DIRECTFD_DRIVER=$(if $(filter 1 yes,$(ENABLE_DIRECTFD_DRIVER)),1,0)
CPPFLAGS += -DANTLER_ENABLE_GAMEPORT_DRIVER=$(if $(filter 1 yes,$(ENABLE_GAMEPORT_DRIVER)),1,0)
CPPFLAGS += -DANTLER_SOUND_IRQ_AWARE=$(SOUND_IRQ_AWARE)
CPPFLAGS += -DANTLER_SOUND_DRIVER_SB=$(SOUND_DRIVER_SB)
CPPFLAGS += -DANTLER_SOUND_DRIVER_PCSPEAKER=$(SOUND_DRIVER_PCSPEAKER)
CPPFLAGS += -DCONFIG_SB_PORT=$(SB_PORT)
CPPFLAGS += -DCONFIG_SB_IRQ=$(SB_IRQ)
CPPFLAGS += -DCONFIG_SB_DMA=$(SB_DMA)
CPPFLAGS += -DANTLER_MFM_ST506_IRQ=$(MFM_ST506_IRQ)
CPPFLAGS += -DANTLER_MFM_ST506_DMA=$(MFM_ST506_DMA)
CPPFLAGS += -DANTLER_MFM_IRQ_AWARE=$(MFM_IRQ_AWARE)
CPPFLAGS += -DANTLER_MFM_DMA_MODE=$(MFM_DMA_MODE)
CPPFLAGS += -DANTLER_MAX_SERIAL=$(KERNEL_MAX_SERIAL)
CPPFLAGS += -DANTLER_MAX_CHRDEV=$(KERNEL_MAX_CHRDEV)
CPPFLAGS += -DANTLER_MAX_BLKDEV=$(KERNEL_MAX_BLKDEV)
CPPFLAGS += -DANTLER_MAX_ETHS=$(KERNEL_MAX_ETHS)
CPPFLAGS += -DANTLER_FILE_SYSTEM_MAX_TYPES=$(KERNEL_FS_MAX_TYPES)
CPPFLAGS += -DANTLER_NATIVE_MAX_NODES=$(KERNEL_NATIVE_MAX_NODES)
CPPFLAGS += -DANTLER_FSYS_MAX_FDS=$(FSYS_MAX_FDS)
CPPFLAGS += -DANTLER_FSYS_MAX_NODES=$(FSYS_MAX_NODES)
CPPFLAGS += -DANTLER_FSYS_MAX_MOUNTS=$(FSYS_MAX_MOUNTS)
CPPFLAGS += -DANTLER_FSYS_PTYS=$(FSYS_PTYS)
CPPFLAGS += -DANTLER_FSYS_PTY_BUF_BYTES=$(FSYS_PTY_BUF_BYTES)
CPPFLAGS += -DANTLER_SEGMENT_MAP_ENTRIES=$(KERNEL_SEGMENT_MAP_ENTRIES)
CPPFLAGS += -DANTLER_ANX2_TASKCTL_RECORDS=$(KERNEL_ANX2_TASKCTL_RECORDS)
CPPFLAGS += -DANTLER_ANX2_CODE_SEGMENTS=$(KERNEL_ANX2_CODE_SEGMENTS)
CPPFLAGS += -DANTLER_SERVICE_ARENA_CORE=$(KERNEL_SERVICE_ARENA_CORE)
CPPFLAGS += -DANTLER_SERVICE_ARENA_CHAR=$(KERNEL_SERVICE_ARENA_CHAR)
CPPFLAGS += -DANTLER_SERVICE_ARENA_BLOCK=$(KERNEL_SERVICE_ARENA_BLOCK)
CPPFLAGS += -DANTLER_SERVICE_ARENA_SOUND=$(KERNEL_SERVICE_ARENA_SOUND)
CPPFLAGS += -DANTLER_SERVICE_ARENA_RAMDISK=$(KERNEL_SERVICE_ARENA_RAMDISK)
CPPFLAGS += -DANTLER_SERVICE_ARENA_FS=$(KERNEL_SERVICE_ARENA_FS)
CPPFLAGS += -DANTLER_SERVICE_ARENA_USER=$(KERNEL_SERVICE_ARENA_USER)
CPPFLAGS += -DANTLER_HEAP_BLOCKS=$(KERNEL_HEAP_BLOCKS)
CPPFLAGS += -DANTLER_HEAP_BLOCK_BYTES=$(KERNEL_HEAP_BLOCK_BYTES)
CPPFLAGS += -DANTLER_KERNEL_STACK_BYTES=$(KERNEL_MAIN_STACK_BYTES)
CPPFLAGS += -DANTLER_NATIVE_SYSCALL_STACK_BYTES=$(KERNEL_SYSCALL_STACK_BYTES)
CPPFLAGS += -DANTLER_IRQ_STACK_BYTES=$(KERNEL_IRQ_STACK_BYTES)
CPPFLAGS += -DANTLER_BLOCK_CACHE_SECTORS=$(KERNEL_BLOCK_CACHE_SECTORS)
CPPFLAGS += -DANTLER_ARCHIVE_ENTRY_CACHE_SLOTS=$(KERNEL_ARCHIVE_ENTRY_CACHE_SLOTS)
CPPFLAGS += -DANTLER_UTS_SYSNAME=\"$(KERNEL_UTS_SYSNAME)\"
CPPFLAGS += -DANTLER_UTS_NODENAME=\"$(KERNEL_UTS_NODENAME)\"
CPPFLAGS += -DANTLER_UTS_RELEASE=\"$(KERNEL_UTS_RELEASE)\"
CPPFLAGS += -DANTLER_UTS_VERSION=\"$(KERNEL_UTS_VERSION)\"
CPPFLAGS += -DANTLER_UTS_MACHINE=\"$(KERNEL_UTS_MACHINE)\"
ifeq ($(KERNEL_SELFTEST),1)
CPPFLAGS += -DANTLER_KERNEL_SELFTEST_BUILD=1 -DANX_KERNEL_SELFTEST_BUILD=1
# The extended nucleus selftest has never fit the 8086 kernel budget: with
# its taskctl runtime records compiled in, the image passes the 1340h native
# init boundary and then the 64 KiB near-data segment.  Fail here with the
# real reason instead of a cryptic linker error; the KERNEL_SELFTEST=1
# diagnostic image is the supported selftest configuration.
ifeq ($(KERNEL_SELFTEST_EXTENDED),1)
$(error KERNEL_SELFTEST_EXTENDED=1 exceeds the 8086 kernel layout (1340h init boundary and 64K near data); split the extended suite before enabling)
endif
CPPFLAGS += -DANX_SELF_TEST_EXTENDED_NUCLEUS=$(KERNEL_SELFTEST_EXTENDED)
endif
CPPFLAGS += -DANX_ENTRY_TRACE=$(ENTRY_TRACE)
CPPFLAGS += -DANTLER_DEVICE_INIT_DEBUG=$(DEVICE_INIT_DEBUG)
ifeq ($(MFM_DEBUG),1)
CPPFLAGS += -DANTLER_MFM_VERBOSE_DEBUG=1
endif
ifeq ($(MFM_TRACE_SECTORS),1)
CPPFLAGS += -DANTLER_MFM_TRACE_SECTORS=1
endif
ifeq ($(EXT2_DEBUG),1)
CPPFLAGS += -DANTLER_EXT2_VERBOSE_DEBUG=1
endif
ifeq ($(ENABLE_NETWORK),1)
ifeq ($(NET_PACKET_PATH),1)
CPPFLAGS += -DANTLER_NET_PACKET_PATH=1
endif
CPPFLAGS += -DENABLE_NETWORK=1
CPPFLAGS += -DANTLER_NET_DRIVER_NAME=\"$(NET_DRIVER)\"
CPPFLAGS += -DANTLER_NET_LINK_NAME=\"$(NET_LINK_NAME)\"
CPPFLAGS += -DANTLER_NET_DEVICE_NAME=\"$(NET_DEVICE_NAME)\"
CPPFLAGS += -DANTLER_NET_HOSTNAME=\"$(KERNEL_UTS_NODENAME)\"
CPPFLAGS += -DANTLER_NET_DHCP_WAIT=$(NET_DHCP_WAIT)
CPPFLAGS += -DANTLER_NET_COM2_TRACE=$(NET_COM2_TRACE)
CPPFLAGS += -DANTLER_NET_NE2K_IO=$(NET_NE2K_IO)
CPPFLAGS += -DANTLER_NET_NE2K_IRQ=$(NET_NE2K_IRQ)
CPPFLAGS += -DANTLER_NET_NE2K_DEBUG_TX=$(NET_NE2K_DEBUG_TX)
CPPFLAGS += -DANTLER_NET_WD80X3_IO=$(NET_WD80X3_IO)
CPPFLAGS += -DANTLER_NET_WD80X3_IRQ=$(NET_WD80X3_IRQ)
CPPFLAGS += -DANTLER_NET_WD80X3_RAM=$(NET_WD80X3_RAM)
CPPFLAGS += -DANTLER_NET_WD80X3_FLAGS=$(NET_WD80X3_FLAGS)
CPPFLAGS += -DANTLER_NET_ULTRA_IO=$(NET_ULTRA_IO)
CPPFLAGS += -DANTLER_NET_ULTRA_IRQ=$(NET_ULTRA_IRQ)
CPPFLAGS += -DANTLER_NET_ULTRA_RAM=$(NET_ULTRA_RAM)
CPPFLAGS += -DANTLER_NET_ULTRA_FLAGS=$(NET_ULTRA_FLAGS)
ifeq ($(NET_DRIVER),ne2k)
CPPFLAGS += -DANTLER_NET_BUILD_NE2K=1
ifeq ($(NET_NE2K_BUS),8)
CPPFLAGS += -DANTLER_NET_NE2K_FORCE_8BIT=1
else ifeq ($(NET_NE2K_BUS),16)
CPPFLAGS += -DANTLER_NET_NE2K_FORCE_16BIT=1
else ifneq ($(NET_NE2K_BUS),auto)
$(error unsupported NET_NE2K_BUS=$(NET_NE2K_BUS); use auto, 8, or 16)
endif
else ifeq ($(NET_DRIVER),wd80x3)
CPPFLAGS += -DANTLER_NET_BUILD_WD80X3=1
else ifeq ($(NET_DRIVER),el3)
CPPFLAGS += -DANTLER_NET_BUILD_EL3=1
else ifeq ($(NET_DRIVER),83c790qf)
CPPFLAGS += -DANTLER_NET_BUILD_83C790QF=1
else ifeq ($(NET_DRIVER),ultra)
CPPFLAGS += -DANTLER_NET_BUILD_83C790QF=1
CPPFLAGS += -DANTLER_NET_DRIVER_ULTRA_ALIAS=1
else
$(error unsupported NET_DRIVER=$(NET_DRIVER); use ne2k, wd80x3, el3, 83c790qf, or ultra)
endif
ifeq ($(NET_PROBE_SELF_TEST),1)
CPPFLAGS += -DANTLER_NET_PROBE_SELF_TEST=1
endif
else
CPPFLAGS += -DENABLE_NETWORK=0
endif
ifeq ($(ENABLE_NFS),1)
CPPFLAGS += -DENABLE_NFS=1
else
CPPFLAGS += -DENABLE_NFS=0
endif
CPPFLAGS += -DENABLE_SOUND=$(ENABLE_SOUND)
CFLAGS := -std=gnu99 -Os -Wall -Wextra -ffreestanding -fno-builtin
CFLAGS += -ffunction-sections -fdata-sections -fno-jump-tables
CFLAGS += -fno-pic -fno-stack-protector -march=i8086 -mtune=i8086
CFLAGS += -mno-protected-mode -mcmodel=small -msegelf
CFLAGS += -mno-segment-relocation-stuff

# The fixed-capacity network profile places the cold system-message router in
# a far text bank.  Permit IA-16 to emit 8086 far-to-near call thunks for this
# object only; every other kernel object retains the stricter default above.
build/kern/nucleus/ipc/sysmsg.o: CFLAGS += -msegment-relocation-stuff
build/kern/nucleus/core/syscall_nucleus.o: CFLAGS += -msegment-relocation-stuff
# The staged-exec engine and the boot resident-server loader live in a far
# text bank (exec.c) and are far-called from the boot handoff, so both
# objects need the same 8086 far-call thunk relocations.
build/kern/system/proc/process/exec.o: CFLAGS += -msegment-relocation-stuff
build/kern/nucleus/boot/boot_handoff.o: CFLAGS += -msegment-relocation-stuff
# A KERNEL_VDI build hosts the raster boot logo (boot_logo.c) in a far text bank.
# anx_arch_init() in init.c far-calls those __far banner entry points, so this
# object also needs the 8086 far-call thunk relocations (same as sysmsg above).
build/kern/nucleus/core/init.o: CFLAGS += -msegment-relocation-stuff
# The IRQ1 keyboard path far-calls the __far VDI capture (anx_vdi_kbd_capture)
# in the far VDI bank, so irq.o needs the same far-call thunk relocs.
build/kern/system/service/drivers/core/irq.o: CFLAGS += -msegment-relocation-stuff
ASFLAGS := $(CPPFLAGS) $(CFLAGS) -x assembler-with-cpp
LDFLAGS := --gc-sections --no-warn-rwx-segments
KERNEL_LIBGCC := $(shell $(CC) -print-libgcc-file-name)
BUILD_CONFIG := build/.config.stamp

KERNEL_LD := arch/i8086/platform_kernel.ld
ANTLER_BOOT_DIR := kern/boot/pcxt
ANTLER_BOOT_BUILD_DIR := build/kern/boot/pcxt
ANTLER_BOOT_LD := $(ANTLER_BOOT_DIR)/raw.ld
ifeq ($(ROOT_BLOCK_BACKEND),mfm)
ANTLER_BOOT_MBR_SOURCE := $(ANTLER_BOOT_DIR)/xebec_mbr.S
ANTLER_BOOT_SECTOR_SOURCE := $(ANTLER_BOOT_DIR)/xebec_boot_sect.S
ANTLER_BOOT_ASM_DEPS := $(ANTLER_BOOT_DIR)/xebec_read.inc \
	$(ANTLER_BOOT_DIR)/xebec_boot_abi.h \
	kern/system/service/drivers/block/mfm_xebec.h \
	kern/system/service/drivers/core/dma_8237.h
else ifeq ($(ROOT_BLOCK_BACKEND),bios13)
# The portable firmware path: the MBR and ext2 VBR read sectors with INT 13h
# AH=42h (LBA extended read), so stock QEMU/SeaBIOS and any real BIOS can load
# the kernel.  Both reuse the shared boot_ext2.c C stage unchanged.
ANTLER_BOOT_MBR_SOURCE := $(ANTLER_BOOT_DIR)/bios13_mbr.S
ANTLER_BOOT_SECTOR_SOURCE := $(ANTLER_BOOT_DIR)/bios13_boot_sect.S
ANTLER_BOOT_ASM_DEPS :=
else
# Keep the established XTIDE sources and their generated bytes unchanged for
# every ATA-backed profile.  Only ROOT_BLOCK_BACKEND=mfm selects Xebec PIO.
ANTLER_BOOT_MBR_SOURCE := $(ANTLER_BOOT_DIR)/mbr.S
ANTLER_BOOT_SECTOR_SOURCE := $(ANTLER_BOOT_DIR)/boot_sect.S
ANTLER_BOOT_ASM_DEPS :=
endif
ANTLER_BOOT_CPPFLAGS := -I$(ANTLER_BOOT_DIR)
ANTLER_BOOT_CPPFLAGS += -DCONFIG_IMG_HD=1
ANTLER_BOOT_CPPFLAGS += -DANTLER_XTIDE_IO=$(XTIDE_IO_LITERAL)
ANTLER_BOOT_CPPFLAGS += -DANX_BOOT_RAM_KB=$(ANX_PLATFORM_RAM_KB)
ANTLER_BOOT_CFLAGS := -std=gnu99 -Os -Wall -Wno-strict-aliasing
ANTLER_BOOT_CFLAGS += -mregparmcall -fno-toplevel-reorder -fno-inline
ANTLER_BOOT_CFLAGS += -fno-jump-tables
ANTLER_BOOT_CFLAGS += -mcmodel=tiny -mno-segment-relocation-stuff
ANTLER_BOOT_CFLAGS += -ffreestanding -fno-builtin -march=i8086
ANTLER_BOOT_CFLAGS += -mtune=i8086 -mno-protected-mode
ANTLER_BOOT_ASFLAGS := --divide
MKE2FS ?= /sbin/mke2fs
DEBUGFS ?= /sbin/debugfs
E2FSCK ?= /sbin/e2fsck
E2CP ?= e2cp
.PHONY: FORCE check-toolchain
FORCE:

check-toolchain:
	@if ! command -v "$(CC)" >/dev/null 2>&1; then \
		echo "missing IA-16 compiler: $(CC)" >&2; \
		echo "install toolchain/gcc-ia16 to $(TOOLCHAIN_DIR)/cross, or set CROSS=/path/to/ia16-elf-" >&2; \
		exit 1; \
	fi
	@if ! command -v "$(AS)" >/dev/null 2>&1 \
		|| ! command -v "$(LD)" >/dev/null 2>&1 \
		|| ! command -v "$(OBJCOPY)" >/dev/null 2>&1 \
		|| ! command -v "$(OBJDUMP)" >/dev/null 2>&1 \
		|| ! command -v "$(SIZE)" >/dev/null 2>&1; then \
		echo "missing IA-16 binutils under CROSS=$(CROSS)" >&2; \
		exit 1; \
	fi

$(BUILD_CONFIG): FORCE check-toolchain
	@mkdir -p $(dir $@)
	@{ \
		printf '%s\n' 'CC=$(CC)'; \
		printf '%s\n' 'LD=$(LD)'; \
		printf '%s\n' 'CPPFLAGS=$(CPPFLAGS)'; \
		printf '%s\n' 'CFLAGS=$(CFLAGS)'; \
		printf '%s\n' 'ASFLAGS=$(ASFLAGS)'; \
		printf '%s\n' 'LDFLAGS=$(LDFLAGS)'; \
		printf '%s\n' 'KERNEL_SELFTEST=$(KERNEL_SELFTEST)'; \
		printf '%s\n' 'MACHINE_PROFILE=$(MACHINE_PROFILE)'; \
		printf '%s\n' 'ANX_PLATFORM_RAM_KB=$(ANX_PLATFORM_RAM_KB)'; \
		printf '%s\n' 'ANTLER_LPT_BASE=$(ANTLER_LPT_BASE)'; \
		printf '%s\n' 'GEM_VDI_DEFAULTS=$(GEM_VDI_DEFAULTS)'; \
		printf '%s\n' 'GEM_VDI_MOUSE_BACKEND=$(GEM_VDI_MOUSE_BACKEND)'; \
		printf '%s\n' 'GEM_VDI_MOUSE_PORT_NAME=$(GEM_VDI_MOUSE_PORT_NAME)'; \
		printf '%s\n' '86BOX_MOUSE_TYPE=$(86BOX_MOUSE_TYPE)'; \
		printf '%s\n' '86BOX_MOUSE_PORT=$(86BOX_MOUSE_PORT)'; \
		printf '%s\n' '86BOX_PCXT_CPU_PROFILE=$(86BOX_PCXT_CPU_PROFILE)'; \
		printf '%s\n' '86BOX_PCXT_CPU_SPEED=$(86BOX_PCXT_CPU_SPEED)'; \
		printf '%s\n' '86BOX_PCXT_ACCEPTANCE_SCOPE=$(86BOX_PCXT_ACCEPTANCE_SCOPE)'; \
			printf '%s\n' '86BOX_PC1640_CPU_SPEED=$(86BOX_PC1640_CPU_SPEED)'; \
			printf '%s\n' '86BOX_GEM_VIDEO=$(86BOX_GEM_VIDEO)'; \
		printf '%s\n' 'ROOT_BLOCK_BACKEND=$(ROOT_BLOCK_BACKEND)'; \
		printf '%s\n' 'BOOT_DEVICE=$(BOOT_DEVICE)'; \
			printf '%s\n' 'ROOT_DEVICE=$(ROOT_DEVICE)'; \
			printf '%s\n' 'ROOT_FS_TYPE=$(ROOT_FS_TYPE)'; \
			printf '%s\n' 'ROOT_FLAGS=$(ROOT_FLAGS)'; \
			printf '%s\n' 'GEM_RUNLEVEL=$(GEM_RUNLEVEL)'; \
			printf '%s\n' 'FS_ENABLE_EXT2_MOUNT_RUNTIME=$(FS_ENABLE_EXT2_MOUNT_RUNTIME)'; \
			printf '%s\n' 'FS_POSIX_IPC_ENABLE=$(FS_POSIX_IPC_ENABLE)'; \
			printf '%s\n' 'USERLAND_MEMMODEL=$(USERLAND_MEMMODEL)'; \
			printf '%s\n' 'USERLAND_SERVER_MEMMODEL=$(USERLAND_SERVER_MEMMODEL)'; \
			printf '%s\n' 'HD_GEOM_MODE=$(HD_GEOM_MODE)'; \
		printf '%s\n' 'HD_IMAGE_MB=$(HD_IMAGE_MB)'; \
		printf '%s\n' 'HD_IMAGE_BYTES=$(HD_IMAGE_BYTES)'; \
		printf '%s\n' 'BOOT_GEOM_SECT=$(BOOT_GEOM_SECT)'; \
		printf '%s\n' 'BOOT_GEOM_HEAD=$(BOOT_GEOM_HEAD)'; \
		printf '%s\n' 'BOOT_GEOM_CYL=$(BOOT_GEOM_CYL)'; \
		printf '%s\n' 'ANTLER_EXT2_BOOT_PART_START_SECTOR=$(ANTLER_EXT2_BOOT_PART_START_SECTOR)'; \
		printf '%s\n' 'ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR=$(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR)'; \
		printf '%s\n' 'ENTRY_TRACE=$(ENTRY_TRACE)'; \
			printf '%s\n' 'BOOT_USERLAND_DEBUG=$(BOOT_USERLAND_DEBUG)'; \
			printf '%s\n' 'KERNEL_DEBUG_TTY=$(KERNEL_DEBUG_TTY)'; \
				printf '%s\n' 'SERIAL_DEBUG=$(SERIAL_DEBUG)'; \
				printf '%s\n' 'LEGACY_8250_TRACE=$(LEGACY_8250_TRACE)'; \
				printf '%s\n' 'LEGACY_8250_TRACE_BASE=$(LEGACY_8250_TRACE_BASE)'; \
				printf '%s\n' 'SERVER_TRACE_COM2=$(SERVER_TRACE_COM2)'; \
				printf '%s\n' 'SERVER_TRACE_COM2_BASE=$(SERVER_TRACE_COM2_BASE)'; \
				printf '%s\n' 'SERVER_TRACE_COM2_DIVISOR=$(SERVER_TRACE_COM2_DIVISOR)'; \
				printf '%s\n' 'SERVER_TRACE_DEBUG_PORT=$(SERVER_TRACE_DEBUG_PORT)'; \
				printf '%s\n' 'SERVER_TRACE_DEBUG_PORT_IO=$(SERVER_TRACE_DEBUG_PORT_IO)'; \
				printf '%s\n' 'SERSYS_TTYS0=$(SERSYS_TTYS0)'; \
				printf '%s\n' 'SERSYS_TTYS1=$(SERSYS_TTYS1)'; \
				printf '%s\n' 'SERSYS_TTYS2=$(SERSYS_TTYS2)'; \
				printf '%s\n' 'SERSYS_TTYS3=$(SERSYS_TTYS3)'; \
				printf '%s\n' 'SERSYS_TTYS0_BASE=$(SERSYS_TTYS0_BASE)'; \
				printf '%s\n' 'SERSYS_TTYS1_BASE=$(SERSYS_TTYS1_BASE)'; \
				printf '%s\n' 'SERSYS_TTYS2_BASE=$(SERSYS_TTYS2_BASE)'; \
				printf '%s\n' 'SERSYS_TTYS3_BASE=$(SERSYS_TTYS3_BASE)'; \
					printf '%s\n' 'SERSYS_TTYS0_TYPE=$(SERSYS_TTYS0_TYPE)'; \
					printf '%s\n' 'SERSYS_TTYS1_TYPE=$(SERSYS_TTYS1_TYPE)'; \
					printf '%s\n' 'SERSYS_TTYS2_TYPE=$(SERSYS_TTYS2_TYPE)'; \
					printf '%s\n' 'SERSYS_TTYS3_TYPE=$(SERSYS_TTYS3_TYPE)'; \
					printf '%s\n' 'SERSYS_IRQ_AWARE=$(SERSYS_IRQ_AWARE)'; \
					printf '%s\n' 'SERSYS_TTYS0_IRQ=$(SERSYS_TTYS0_IRQ)'; \
					printf '%s\n' 'SERSYS_TTYS1_IRQ=$(SERSYS_TTYS1_IRQ)'; \
					printf '%s\n' 'SERSYS_TTYS2_IRQ=$(SERSYS_TTYS2_IRQ)'; \
					printf '%s\n' 'SERSYS_TTYS3_IRQ=$(SERSYS_TTYS3_IRQ)'; \
					printf '%s\n' 'SERSYS_TTYS0_FLOW=$(SERSYS_TTYS0_FLOW)'; \
					printf '%s\n' 'SERSYS_TTYS1_FLOW=$(SERSYS_TTYS1_FLOW)'; \
					printf '%s\n' 'SERSYS_TTYS2_FLOW=$(SERSYS_TTYS2_FLOW)'; \
					printf '%s\n' 'SERSYS_TTYS3_FLOW=$(SERSYS_TTYS3_FLOW)'; \
					printf '%s\n' '86BOX_COM1_DEVICE=$(86BOX_COM1_DEVICE)'; \
					printf '%s\n' '86BOX_COM1_PATH=$(86BOX_COM1_PATH)'; \
					printf '%s\n' '86BOX_COM1_MODE=$(86BOX_COM1_MODE)'; \
					printf '%s\n' '86BOX_COM2_DEVICE=$(86BOX_COM2_DEVICE)'; \
					printf '%s\n' '86BOX_COM2_PATH=$(86BOX_COM2_PATH)'; \
					printf '%s\n' '86BOX_COM2_MODE=$(86BOX_COM2_MODE)'; \
					printf '%s\n' '86BOX_MFM_SECOND_DRIVE=$(86BOX_MFM_SECOND_DRIVE)'; \
				printf '%s\n' '86BOX_MFM_IMAGE=$(86BOX_MFM_IMAGE)'; \
				printf '%s\n' '86BOX_MFM_IMAGE_CFG=$(86BOX_MFM_IMAGE_CFG)'; \
				printf '%s\n' '86BOX_MFM_IMAGE_BYTES=$(86BOX_MFM_IMAGE_BYTES)'; \
				printf '%s\n' '86BOX_MFM_CONTROLLER=$(86BOX_MFM_CONTROLLER)'; \
				printf '%s\n' '86BOX_MFM_CHANNEL=$(86BOX_MFM_CHANNEL)'; \
				printf '%s\n' '86BOX_MFM_SECT=$(86BOX_MFM_SECT)'; \
				printf '%s\n' '86BOX_MFM_HEAD=$(86BOX_MFM_HEAD)'; \
				printf '%s\n' '86BOX_MFM_CYL=$(86BOX_MFM_CYL)'; \
			printf '%s\n' 'PROCESS_DEBUG=$(PROCESS_DEBUG)'; \
			printf '%s\n' 'USER_PROC_DEBUG=$(USER_PROC_DEBUG)'; \
			printf '%s\n' 'NATIVE_EXEC_DEBUG=$(NATIVE_EXEC_DEBUG)'; \
			printf '%s\n' 'NATIVE_FORK_DEBUG=$(NATIVE_FORK_DEBUG)'; \
			printf '%s\n' 'INIT_DEBUG=$(INIT_DEBUG)'; \
				printf '%s\n' 'SERSYS_DEBUG=$(SERSYS_DEBUG)'; \
			printf '%s\n' 'DRIVER_LOG_VERBOSITY=$(DRIVER_LOG_VERBOSITY)'; \
			printf '%s\n' 'DEVICE_INIT_DEBUG=$(DEVICE_INIT_DEBUG)'; \
			printf '%s\n' 'MFM_DEBUG=$(MFM_DEBUG)'; \
			printf '%s\n' 'MFM_TRACE_SECTORS=$(MFM_TRACE_SECTORS)'; \
		printf '%s\n' 'EXT2_DEBUG=$(EXT2_DEBUG)'; \
			printf '%s\n' 'ENABLE_MFM_DRIVER=$(ENABLE_MFM_DRIVER)'; \
			printf '%s\n' 'ENABLE_ATA_DRIVER=$(ENABLE_ATA_DRIVER)'; \
			printf '%s\n' 'ENABLE_DIRECTFD_DRIVER=$(ENABLE_DIRECTFD_DRIVER)'; \
			printf '%s\n' 'ENABLE_GAMEPORT_DRIVER=$(ENABLE_GAMEPORT_DRIVER)'; \
			printf '%s\n' 'ENABLE_RAMDISK_DRIVER=$(ENABLE_RAMDISK_DRIVER)'; \
			printf '%s\n' 'ENABLE_LOADABLE_DRIVER_MODULES=$(ENABLE_LOADABLE_DRIVER_MODULES)'; \
			printf '%s\n' 'ENABLE_DRIVER_MODULE_COMPRESSION=$(ENABLE_DRIVER_MODULE_COMPRESSION)'; \
			printf '%s\n' 'ENABLE_KERNEL_COMPRESSION=$(ENABLE_KERNEL_COMPRESSION)'; \
			printf '%s\n' 'KERNEL_TIGHT_SIZE_LIMIT=$(KERNEL_TIGHT_SIZE_LIMIT)'; \
				printf '%s\n' 'ENABLE_NETWORK=$(ENABLE_NETWORK)'; \
				printf '%s\n' 'ENABLE_HTTPS=$(ENABLE_HTTPS)'; \
				printf '%s\n' 'ENABLE_KERNEL_ADMIN_CORE=$(ENABLE_KERNEL_ADMIN_CORE)'; \
			printf '%s\n' 'START_NETWORK_ON_BOOT=$(START_NETWORK_ON_BOOT)'; \
			printf '%s\n' 'START_EXT2FS_ON_BOOT=$(START_EXT2FS_ON_BOOT)'; \
			printf '%s\n' 'NFS_START_NETWORK_ON_BOOT=$(NFS_START_NETWORK_ON_BOOT)'; \
		printf '%s\n' 'NET_DRIVER=$(NET_DRIVER)'; \
		printf '%s\n' 'NET_PROBE_SELF_TEST=$(NET_PROBE_SELF_TEST)'; \
		printf '%s\n' 'NET_NE2K_IO=$(NET_NE2K_IO)'; \
		printf '%s\n' 'NET_NE2K_IRQ=$(NET_NE2K_IRQ)'; \
		printf '%s\n' 'NET_NE2K_BUS=$(NET_NE2K_BUS)'; \
		printf '%s\n' 'NET_NE2K_DEBUG_TX=$(NET_NE2K_DEBUG_TX)'; \
		printf '%s\n' 'NET_PACKET_PATH=$(NET_PACKET_PATH)'; \
		printf '%s\n' 'NET_COM2_TRACE=$(NET_COM2_TRACE)'; \
		printf '%s\n' 'NET_ULTRA_IO=$(NET_ULTRA_IO)'; \
		printf '%s\n' 'NET_ULTRA_IRQ=$(NET_ULTRA_IRQ)'; \
		printf '%s\n' 'NET_ULTRA_RAM=$(NET_ULTRA_RAM)'; \
		printf '%s\n' 'NET_ULTRA_FLAGS=$(NET_ULTRA_FLAGS)'; \
				printf '%s\n' 'NET_IPMODE=$(NET_IPMODE)'; \
				printf '%s\n' 'NET_DHCP_ENABLE=$(NET_DHCP_ENABLE)'; \
				printf '%s\n' 'NET_HOSTNAME=$(KERNEL_UTS_NODENAME)'; \
				printf '%s\n' 'NET_DHCP_WAIT=$(NET_DHCP_WAIT)'; \
				printf '%s\n' 'NET_LOCALIP=$(NET_LOCALIP)'; \
		printf '%s\n' 'NET_GATEWAY=$(NET_GATEWAY)'; \
			printf '%s\n' 'NET_NETMASK=$(NET_NETMASK)'; \
			printf '%s\n' 'NET_DNSSERVER=$(NET_DNSSERVER)'; \
			printf '%s\n' 'NTP_ENABLE=$(NTP_ENABLE)'; \
			printf '%s\n' 'NTP_SERVER=$(NTP_SERVER)'; \
			printf '%s\n' 'NTP_TIMEOUT=$(NTP_TIMEOUT)'; \
			printf '%s\n' 'NTP_TRIES=$(NTP_TRIES)'; \
			printf '%s\n' 'NTP_LOG=$(NTP_LOG)'; \
			printf '%s\n' 'IPSERIAL_ENABLE=$(IPSERIAL_ENABLE)'; \
			printf '%s\n' 'IPSERIAL_MODE=$(IPSERIAL_MODE)'; \
			printf '%s\n' 'IPSERIAL_PORT=$(IPSERIAL_PORT)'; \
			printf '%s\n' 'ENABLE_NFS=$(ENABLE_NFS)'; \
			printf '%s\n' 'NFS_SERVER=$(NFS_SERVER)'; \
			printf '%s\n' 'NFS_SERVER_IP=$(NFS_SERVER_IP)'; \
			printf '%s\n' 'NFS_EXPORT=$(NFS_EXPORT)'; \
			printf '%s\n' 'NFS_MOUNTPOINT=$(NFS_MOUNTPOINT)'; \
			printf '%s\n' 'NFS_TEST_ROOT=$(NFS_TEST_ROOT)'; \
			printf '%s\n' 'NFS_DEVICE=$(NFS_DEVICE)'; \
			printf '%s\n' 'NFS_PORT=$(NFS_PORT)'; \
			printf '%s\n' 'NFS_MOUNT_PORT=$(NFS_MOUNT_PORT)'; \
			printf '%s\n' 'NFS_TIMEOUT_TICKS=$(NFS_TIMEOUT_TICKS)'; \
			printf '%s\n' 'NFS_RETRIES=$(NFS_RETRIES)'; \
			printf '%s\n' 'ENABLE_SOUND=$(ENABLE_SOUND)'; \
			printf '%s\n' 'SOUND_SERVICE_ENABLE=$(SOUND_SERVICE_ENABLE)'; \
			printf '%s\n' 'START_SOUND_ON_BOOT=$(START_SOUND_ON_BOOT)'; \
			printf '%s\n' 'ESD_ENABLE=$(ESD_ENABLE)'; \
			printf '%s\n' 'DRIVERD_SOUND_ENABLE=$(DRIVERD_SOUND_ENABLE)'; \
			printf '%s\n' 'ESD_PORT=$(ESD_PORT)'; \
			printf '%s\n' 'ESD_RATE=$(ESD_RATE)'; \
			printf '%s\n' 'ESD_LOG=$(ESD_LOG)'; \
			printf '%s\n' 'SOUND_RATE=$(SOUND_RATE)'; \
			printf '%s\n' 'SB_PORT=$(SB_PORT)'; \
		printf '%s\n' 'SB_IRQ=$(SB_IRQ)'; \
		printf '%s\n' 'SB_DMA=$(SB_DMA)'; \
		printf '%s\n' 'SOUND_DRIVER=$(SOUND_DRIVER)'; \
		printf '%s\n' 'SOUND_IRQ_AWARE=$(SOUND_IRQ_AWARE)'; \
			printf '%s\n' 'MFM_ST506_IRQ=$(MFM_ST506_IRQ)'; \
			printf '%s\n' 'MFM_ST506_DMA=$(MFM_ST506_DMA)'; \
			printf '%s\n' 'MFM_IRQ_AWARE=$(MFM_IRQ_AWARE)'; \
			printf '%s\n' 'MFM_DMA_MODE=$(MFM_DMA_MODE)'; \
			printf '%s\n' 'SB_BOUNCE=$(SB_BOUNCE)'; \
		printf '%s\n' 'SOUND_OPTI_SB_MODE=$(SOUND_OPTI_SB_MODE)'; \
		printf '%s\n' 'SOUND_OPTI_SB_PORT=$(SOUND_OPTI_SB_PORT)'; \
		printf '%s\n' 'SOUND_OPTI_SB_IRQ=$(SOUND_OPTI_SB_IRQ)'; \
		printf '%s\n' 'SOUND_OPTI_SB_DMA=$(SOUND_OPTI_SB_DMA)'; \
	} > $@.tmp; \
	if test -f $@ && cmp -s $@.tmp $@; then \
		rm -f $@.tmp; \
	else \
		mv $@.tmp $@; \
	fi

HD_GEOM_MODES := xtelks mb manual
XTIDE_GEOMETRY_VALIDATOR := scripts/validate-xtide-geometry.py
XTIDE_IMAGE_SECTORS_MAX := 65535
ifeq ($(filter $(HD_GEOM_MODE),$(HD_GEOM_MODES)),)
$(error unsupported HD_GEOM_MODE=$(HD_GEOM_MODE); use xtelks, mb, or manual)
endif

ifeq ($(HD_GEOM_MODE),xtelks)
# Match the active XT-ELKS hd.img CHS profile:
# 612 cylinders, 4 heads, 17 sectors/track.
BOOT_GEOM_SECT := 17
BOOT_GEOM_HEAD := 4
BOOT_GEOM_CYL := 612
endif

ifeq ($(HD_GEOM_MODE),mb)
ifeq ($(strip $(HD_IMAGE_MB)),)
HD_IMAGE_MB := 20
endif
# Keep small XT/FileCard-style images on 17/4 CHS.  Above 20 MiB, use the
# stock ELKS hd32-style 63 sectors and 16 heads.  Cylinders are rounded up so
# the image is never smaller than the requested MiB size.  The host helper
# also rejects a rounded image whose exclusive sector count cannot fit in the
# one-word target ABI; this happens before any `...u` C literal is generated.
HD_IMAGE_MB_GEOMETRY := $(shell python3 $(XTIDE_GEOMETRY_VALIDATOR) mib '$(HD_IMAGE_MB)' 2>/dev/null || printf '%s\n' invalid)
ifeq ($(firstword $(HD_IMAGE_MB_GEOMETRY)),invalid)
$(error HD_GEOM_MODE=mb requires a positive integer HD_IMAGE_MB whose rounded geometry contains at most $(XTIDE_IMAGE_SECTORS_MAX) sectors)
endif
BOOT_GEOM_SECT := $(word 1,$(HD_IMAGE_MB_GEOMETRY))
BOOT_GEOM_HEAD := $(word 2,$(HD_IMAGE_MB_GEOMETRY))
BOOT_GEOM_CYL := $(word 3,$(HD_IMAGE_MB_GEOMETRY))
endif

ifeq ($(HD_GEOM_MODE),manual)
BOOT_GEOM_SECT ?= $(if $(filter mfm,$(ROOT_BLOCK_BACKEND)),17,63)
BOOT_GEOM_HEAD ?= $(if $(filter mfm,$(ROOT_BLOCK_BACKEND)),4,16)
BOOT_GEOM_CYL ?= $(if $(filter mfm,$(ROOT_BLOCK_BACKEND)),612,63)
endif

# This check is deliberately shared by fixed, MiB-derived, manual, and
# command-line-overridden geometry.  A 16-bit LBA can name sector 0xffff, but
# Antler range checks use an exclusive sector count.  The count must therefore
# remain at most 0xffff (valid LBA 0..0xfffe); accepting 65536 would create a
# wide target constant or wrap the count to zero on an 8086 compiler.
HD_GEOMETRY_VALIDATION := $(shell python3 $(XTIDE_GEOMETRY_VALIDATOR) chs '$(BOOT_GEOM_SECT)' '$(BOOT_GEOM_HEAD)' '$(BOOT_GEOM_CYL)' 2>/dev/null || printf '%s\n' invalid)
ifeq ($(firstword $(HD_GEOMETRY_VALIDATION)),invalid)
$(error BOOT_GEOM_SECT, BOOT_GEOM_HEAD, and BOOT_GEOM_CYL must be positive decimal integers whose product is at most $(XTIDE_IMAGE_SECTORS_MAX))
endif
HD_IMAGE_SECTORS := $(word 4,$(HD_GEOMETRY_VALIDATION))
CPPFLAGS += -DANTLER_BOOT_IMAGE_SECTORS=$(HD_IMAGE_SECTORS)u
CPPFLAGS += -DANTLER_BOOT_GEOM_SECT=$(BOOT_GEOM_SECT)u
CPPFLAGS += -DANTLER_BOOT_GEOM_HEAD=$(BOOT_GEOM_HEAD)u
CPPFLAGS += -DANTLER_BOOT_GEOM_CYL=$(BOOT_GEOM_CYL)u
HD_IMAGE_BYTES ?= $(shell expr $(HD_IMAGE_SECTORS) \* 512)
ANTLER_EXT2_BOOT_PART_START_SECTOR ?= $(BOOT_GEOM_SECT)
ANTLER_EXT2_BOOT_FS_BLOCKS ?= $(shell expr \( $(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR) - $(ANTLER_EXT2_BOOT_PART_START_SECTOR) \) / 2)
ANTLER_BOOT_CPPFLAGS += -DCONFIG_IMG_SECT=$(BOOT_GEOM_SECT)
ANTLER_BOOT_CPPFLAGS += -DCONFIG_IMG_HEAD=$(BOOT_GEOM_HEAD)
ANTLER_BOOT_CPPFLAGS += -DCONFIG_IMG_CYL=$(BOOT_GEOM_CYL)
ANTLER_BOOT_CPPFLAGS += -DCONFIG_IMG_BLOCKS=$(ANTLER_EXT2_BOOT_FS_BLOCKS)
# The IRQ, syscall, and main stacks are explicit linked BSS objects.  This
# additional end-of-segment guard catches accidental near-data growth; 384
# bytes retains a bounded emergency margin while permitting the two-word
# signal-mask and job-control dispatcher required by the XT UNIX profile.
# All three kernel stacks are explicit linked BSS objects.  No hidden tail
# stack shares the end of the 64 KiB near segment, so an additional image-end
# reserve would count stack memory twice.
KERNEL_STACK_RESERVE_BYTES ?= 0
# The kernel keeps one small-model near code/data segment.  Explicit cold
# far-text banks are permitted, and the linker separately asserts that the
# near text plus BSS remains within 64 KiB.  This value limits bytes loaded
# from disk, not NOLOAD BSS which is cleared by the entry code.
# A KERNEL_VDI build stores the VDI + video-driver far text AND far data (the
# dispatcher, built-in font, and its tables) in the flat image at the BSS file
# offset, so the loaded size grows past 64 KiB up to the raised native-init
# boundary (0x20000 physical - 0x1200 load = 0x1EE00 = 126464).
KERNEL_IMAGE_BYTES_MAX := 134656
SERDRIVE ?= $(shell command -v serdrive 2>/dev/null || printf '%s\n' /tmp/SerDrive-linux/build/serdrive)
SERDRIVE_PORT ?= /dev/ttyUSB1
SERDRIVE_BAUD ?= 115200
SERDRIVE_VERBOSE ?= 5
SERDRIVE_GEOMETRY ?= $(BOOT_GEOM_CYL):$(BOOT_GEOM_HEAD):$(BOOT_GEOM_SECT)
SERDRIVE_LOG_DIR ?= /home/arduino/serdrive-logs
SERDRIVE_STATE_DIR ?= /home/arduino/serdrive
SERDRIVE_SCREEN ?= antler-serdrive-$(notdir $(SERDRIVE_PORT))

#
# The kernel links as one 8086 real-mode image.  Keep interrupt/trap, IPC,
# boot, and bounded nucleus helpers in kern/nucleus/.  Process management is a
# kernel system, so its implementation lives under kern/system/proc even though
# the resident kernel image links it directly.  The ELKS user source API is the
# POSIX syscall surface, so process, timer, memory, and native-control syscalls
# use the same POSIX handler source in kernel builds.  Resource managers such
# as fsys, sersys, driverd, vdisys, aessys, and netsys remain separate IPC
# programs.  VDI exclusively owns display and pointing-device policy; linking
# those hardware paths here would bypass its named service boundary and consume
# the nucleus' already-tight 64 KiB real-mode image budget.
#

KERNEL_BACKEND_SRCS := \
		kern/lib/bit/bit_string.c \
		kern/system/memory/ledger/segment_ledger.c \
		kern/system/memory/access/far_segment.c \
		kern/system/service/drivers/core/irq.c \
		kern/system/service/drivers/core/firmware_vectors.c \
		kern/system/service/drivers/input/xt_keyboard.c \
		kern/system/service/drivers/timer/pit8253.c \
	kern/nucleus/core/native_state.c \
	kern/nucleus/core/server_hooks.c

KERNEL_MAIN_SRCS := \
	kern/nucleus/core/arith.c \
	kern/nucleus/boot/boot_banner.c \
	kern/nucleus/boot/boot_debug.c \
	kern/nucleus/boot/boot_handoff.c \
	kern/nucleus/boot/boot_xebec.c \
	kern/nucleus/boot/boot_xtide.c \
	$(if $(ROOT_BLOCK_BACKEND_USES_BIOS),kern/nucleus/boot/boot_bios13.c) \
	kern/nucleus/core/init.c \
	kern/nucleus/ipc/ipc.c \
	kern/nucleus/ipc/ipc_bulk.c \
	kern/nucleus/core/kconsole.c \
	kern/nucleus/core/panic.c \
	kern/nucleus/core/main.c \
	kern/nucleus/ipc/name.c \
	kern/nucleus/ipc/port.c \
	kern/system/proc/process.c \
	kern/system/proc/process/exec.c \
	kern/system/proc/process/image.c \
	kern/system/proc/process/wait.c \
	kern/system/proc/proc_lifecycle.c \
	kern/system/proc/proc_signal.c \
	kern/system/proc/scheduler.c \
	kern/nucleus/ipc/sysmsg.c \
	kern/nucleus/ipc/sysmsg/access.c \
	kern/nucleus/ipc/sysmsg/lifecycle.c \
	kern/nucleus/ipc/sysmsg/native.c \
	kern/nucleus/ipc/sysmsg/route.c \
	kern/nucleus/ipc/sysmsg/tty.c \
	kern/nucleus/core/syscall_nucleus.c \
	kern/system/proc/task/state.c \
	kern/system/proc/task.c \
	kern/system/proc/task/event.c \
	kern/system/proc/task/taskctl_runtime.c

KERNEL_POSIX_CORE_SRCS := \
	kern/system/service/posix/proc/exec.c \
	kern/system/service/posix/proc/info.c \
	kern/system/memory/syscall/native_memory_syscalls.c \
	kern/system/service/posix/proc/signal.c \
	kern/system/service/posix/time/time.c \
	kern/system/service/posix/sys/system.c \
	kern/system/service/posix/native/server_control.c

ifeq ($(KERNEL_SELFTEST),1)
KERNEL_MAIN_SRCS += \
		kern/nucleus/selftest/selftest.c \
		kern/system/memory/pool/fixed_block_pool.c \
		kern/system/memory/heap/kernel_heap.c
endif

ifeq ($(ENABLE_KERNEL_ADMIN_CORE),1)
KERNEL_CORE_SRCS := $(KERNEL_MAIN_SRCS)
CPPFLAGS += -DANTLER_KERNEL_HAS_IPC=1
else
$(error ENABLE_KERNEL_ADMIN_CORE=0 is unsupported: kern/nucleus is the required kernel nucleus)
endif

# Mirror legacy ANTLER_* aliases after all CPPFLAGS are assembled, but keep
# block backend selection on the single ANTLER_* macro family.
# KERNEL_HAS_IPC is added above when the admin core is enabled.
CPPFLAGS += $(patsubst -DANTLER_%,-DANX_%,\
	$(filter-out -DANTLER_ROOT_BLOCK_BACKEND%,\
	$(filter -DANTLER_%,$(CPPFLAGS))))

# VDI-in-nucleus (KERNEL_VDI=1): compile the VDI raster + selected video driver
# into the kernel image as medium-model far code (near-code footprint ~0; all
# functions land in .fartext banks).
KERNEL_VDI_C_SRCS :=
KERNEL_VDI_S_SRCS :=
CPPFLAGS += -DANX_KERNEL_VDI=1
# Match the vdisys adapter selection so the in-nucleus driver programs the same
# mode: 86BOX_GEM_VIDEO vga=0 ega=1 cga=2 hercules=3 pega=1(+PC1640 latch).
KERNEL_VDI_ADAPTER_ID := $(if $(filter vga,$(86BOX_GEM_VIDEO)),0,$(if $(filter cga,$(86BOX_GEM_VIDEO)),2,$(if $(filter hercules,$(86BOX_GEM_VIDEO)),3,1)))
KERNEL_VDI_PEGA := $(if $(filter pega,$(86BOX_GEM_VIDEO)),1,0)
CPPFLAGS += -DANTLER_GEM_VIDEO_ADAPTER=$(KERNEL_VDI_ADAPTER_ID)
CPPFLAGS += -DANTLER_GEM_PC1640_PEGA=$(KERNEL_VDI_PEGA)
KERNEL_VDI_C_SRCS := \
	kern/system/service/vdisys/public/gem_vdi.c \
	kern/system/service/vdisys/resident/gem_vdi_resident.c \
	kern/system/service/vdisys/resident/gem_vdi_nuc_call.c \
	kern/system/service/vdisys/public/gem_vdi_rotation.c \
	kern/system/service/vdisys/font/gem_vdi_font_nuc.c \
	kern/system/service/vdisys/input/service/gem_vdi_input_nuc.c \
	kern/system/service/keysys/input/set1_decoder.c \
	kern/system/service/aessys/memory/gem_resident_memory.c \
	kern/nucleus/boot/boot_font_bios.c \
	kern/nucleus/boot/boot_logo.c
ifeq ($(VDI_VIDEO_DRIVER),pcvideo)
KERNEL_VDI_C_SRCS += kern/system/service/drivers/video/gem_pcvideo.c
KERNEL_VDI_S_SRCS += kern/system/service/drivers/video/gem_pcvideo_asm.S
CPPFLAGS += -DANX_VDI_DRIVER_PCVIDEO=1
else
$(error unsupported VDI_VIDEO_DRIVER=$(VDI_VIDEO_DRIVER); supported: pcvideo)
endif
$(info VDI + $(VDI_VIDEO_DRIVER) driver hosted in the nucleus as far code)

C_SRCS := \
	$(KERNEL_BACKEND_SRCS) \
	$(KERNEL_CORE_SRCS) \
	$(KERNEL_POSIX_CORE_SRCS) \
	$(KERNEL_VDI_C_SRCS)


ifeq ($(ENABLE_RAMDISK_DRIVER),1)
$(info ENABLE_RAMDISK_DRIVER=1: runtime ramdisk is /system/servers/ramdisk, not a nucleus object)
$(info block-device backends are resident user servers; no block backend belongs in kern/nucleus)
endif

ifeq ($(ENABLE_MFM_DRIVER),1)
$(info MFM block driver enabled as a loadable block path)
endif

ifeq ($(ENABLE_SOUND),1)
$(info ENABLE_SOUND=1: sound policy/user tools enabled; sound service stays outside the kernel)
endif

ifeq ($(ENABLE_NETWORK), 1)
ifeq ($(ENABLE_DHCP_CLIENT),1)
CPPFLAGS += -DANX_NET_DHCP_CLIENT=1
endif
ifeq ($(ENABLE_DNS_RESOLVER),1)
CPPFLAGS += -DANX_NET_DNS_RESOLVER=1
endif
$(info ENABLE_NETWORK=1: network stack builds as separate small-model netsys/driverd server code)
else
$(info ENABLE_NETWORK=0: no network backend is linked into the kernel)
endif

S_SRCS := \
	arch/i8086/firmware_gate.S \
	arch/i8086/native_user_trap.S \
	arch/i8086/timer_irq0.S \
	arch/i8086/irq_stubs.S \
	arch/i8086/platform_pc.S \
	arch/i8086/platform_entry.S \
	arch/i8086/xebec_resident.S \
	$(KERNEL_VDI_S_SRCS)

OBJS := \
	$(patsubst %.S,build/%.o,$(S_SRCS)) \
	$(patsubst %.c,build/%.o,$(C_SRCS))

# The VDI + video driver are hosted as medium-model far code so they add no
# near-code to the 64K kernel segment.  -mcmodel=medium overrides the kernel's
# -mcmodel=small for these objects (last flag wins); segment relocation matches
# the mechanism the nucleus already uses for its far sysmsg functions.
#
# Every far call in this build resolves through one far-text segment, so all far
# code must land in a single bank; platform_kernel.ld routes every .fartext*
# input section into one output bank for exactly that reason.  (Keep the global
# -ffunction-sections so --gc-sections can still drop the unreferenced VDI far
# functions; forcing -fno-function-sections here retains that dead code, which
# both breaks the link with undefined input-side references and overflows the
# tight far region.)
KERNEL_VDI_OBJS := \
	$(patsubst %.c,build/%.o,$(KERNEL_VDI_C_SRCS)) \
	$(patsubst %.S,build/%.o,$(KERNEL_VDI_S_SRCS))
$(KERNEL_VDI_OBJS): CFLAGS += -mcmodel=large -msegment-relocation-stuff
# The in-nucleus VDI far code enlarges the image; raise the native-init text
# boundary the linker enforces past the ld default (0x13400) to physical 0x22000
# so the enlarged nucleus image still ends below native init.
LDFLAGS += --defsym __anx_native_text_phys=0x22000

KERNEL_PUBLIC_HEADERS := \
	$(wildcard arch/i8086/*.h) \
	$(wildcard kern/**/*.h) \
	$(wildcard kern/*/*.h) \
	$(wildcard kern/*/*/*.h) \
	$(wildcard kern/system/service/*.h) \
	$(wildcard kern/system/service/*/*.h) \
	$(wildcard kern/system/service/*/*/*.h)
KERNEL_INTERNAL_DEPS := \
	$(wildcard kern/nucleus/*.h) \
	$(wildcard kern/system/*.h) \
	$(wildcard kern/system/*/*.c) \
	$(wildcard kern/system/*/*.h) \
	$(wildcard kern/system/memory/*.h) \
	$(wildcard kern/system/memory/*/*.c) \
	$(wildcard kern/system/memory/*/*.h) \
	$(wildcard kern/nucleus/*/*.c) \
	$(wildcard kern/nucleus/*/*.h) \
	$(wildcard kern/system/service/posix/*/*.c) \
	$(wildcard kern/system/service/posix/*/*.h)

.PHONY: all clean disasm userland hd-userland userland-image hd-image hd-geometry \
	userland-debug-image 86box-userland-debug gdb-userland-debug \
	menuconfig \
	kernel-compressed exomizer-tool \
	serdrive-start serdrive-screen serdrive-stop \
	audit-no-bios test-no-bios-audit test-pcxt-ram \
	test-gem-ega-profile test-input-capture test-input-capture-model \
	test-input-capture-source test-pc-speaker-pcm \
	test-server-common-linkage test-xtide-geometry \
	test-mfm-boot-image test-mfm-boot-image-selected \
	test-mfm-runtime-dma

hd-geometry:
	@printf '%s\n' 'HD_GEOM_MODE=$(HD_GEOM_MODE)'
	@printf '%s\n' 'XTIDE_IO=$(XTIDE_IO_LITERAL)'
	@printf '%s\n' 'XTIDE_IO_LAST_PORT=$(XTIDE_IO_LAST_PORT)'
	@printf '%s\n' 'HD_IMAGE_MB=$(HD_IMAGE_MB)'
	@printf '%s\n' 'BOOT_GEOM_SECT=$(BOOT_GEOM_SECT)'
	@printf '%s\n' 'BOOT_GEOM_HEAD=$(BOOT_GEOM_HEAD)'
	@printf '%s\n' 'BOOT_GEOM_CYL=$(BOOT_GEOM_CYL)'
	@printf '%s\n' 'HD_IMAGE_SECTORS=$(HD_IMAGE_SECTORS)'
	@printf '%s\n' 'XTIDE_IMAGE_SECTORS_MAX=$(XTIDE_IMAGE_SECTORS_MAX)'
	@printf '%s\n' 'HD_IMAGE_BYTES=$(HD_IMAGE_BYTES)'
	@printf '%s\n' 'ANTLER_EXT2_BOOT_PART_START_SECTOR=$(ANTLER_EXT2_BOOT_PART_START_SECTOR)'
	@printf '%s\n' 'ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR=$(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR)'
	@printf '%s\n' 'SERDRIVE_GEOMETRY=$(SERDRIVE_GEOMETRY)'

test-xtide-geometry:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-xtide-geometry.py

test-pcxt-ram:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-pcxt-ram.py

test-gem-ega-profile:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-gem-ega-profile.py

test-input-capture: test-input-capture-source test-input-capture-model

test-input-capture-source:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-input-capture-source.py

test-input-capture-model:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-input-capture-model.py

test-pc-speaker-pcm:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-pc-speaker-pcm.py

test-server-common-linkage:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-server-common-linkage.py

test-mfm-runtime-dma: scripts/test-mfm-runtime-dma.py \
		kern/system/service/drivers/block/mfm.c
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-mfm-runtime-dma.py

# Build the real MFM profile, then prove that hd.img contains the exact audited
# stage-0 and stage-1 Xebec binaries.  The private target prevents recursion
# while still making a plain `make test-mfm-boot-image` select MFM for students.
test-mfm-boot-image:
	$(MAKE) ROOT_BLOCK_BACKEND=mfm test-mfm-boot-image-selected

test-mfm-boot-image-selected: test-mfm-runtime-dma build/hd.img \
		scripts/test-mfm-boot-image.py
	@test "$(ROOT_BLOCK_BACKEND)" = mfm || { \
		echo "test-mfm-boot-image-selected requires ROOT_BLOCK_BACKEND=mfm" >&2; \
		exit 1; \
	}
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-mfm-boot-image.py \
		--repo . --image build/hd.img \
		--mbr $(ANTLER_BOOT_BUILD_DIR)/mbr-part.bin \
		--vbr $(ANTLER_BOOT_BUILD_DIR)/ext2-vbr.bin

all: build/hd.img
.DEFAULT_GOAL := all

.PHONY: audit-image
audit-image: build/hd.img
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/audit-antler-image.py

.PHONY: test-gem-unified-trace
test-gem-unified-trace:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-gem-unified-trace.py

USERLAND_MAKE_ARGS = \
	CROSS=$(CROSS) \
	ROOT_BLOCK_BACKEND=$(ROOT_BLOCK_BACKEND) \
	KERNEL_BLOCK_XTIDE=$(KERNEL_BLOCK_XTIDE) \
	XTIDE_IO=$(XTIDE_IO_LITERAL) \
	ANTLER_LPT_BASE=$(ANTLER_LPT_BASE) \
	GEM_VIDEO_ADAPTER=$(86BOX_GEM_VIDEO) \
	MACHINE_PROFILE=$(MACHINE_PROFILE) \
	ROOT_DEVICE=$(ROOT_DEVICE) \
	ROOT_FS_TYPE=$(ROOT_FS_TYPE) \
	BOOT_GEOM_SECT=$(BOOT_GEOM_SECT) \
	BOOT_GEOM_HEAD=$(BOOT_GEOM_HEAD) \
	BOOT_GEOM_CYL=$(BOOT_GEOM_CYL) \
	BOOT_ARCHIVE_BASE_SECTOR=$(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR) \
			ROOT_FLAGS=$(ROOT_FLAGS) \
			GEM_RUNLEVEL=$(GEM_RUNLEVEL) \
			BOOT_OPTIONS="$(BOOT_OPTIONS)" \
			SYSINIT_EXTRA_LINE="$(SYSINIT_EXTRA_LINE)" \
			USERLAND_MEMMODEL=$(USERLAND_MEMMODEL) \
		USERLAND_SERVER_MEMMODEL=$(USERLAND_SERVER_MEMMODEL) \
		KERNEL_MAX_PORTS=$(KERNEL_MAX_PORTS) \
		KERNEL_MAX_TASKS=$(KERNEL_MAX_TASKS) \
			KERNEL_MAX_USER_PROCS=$(KERNEL_MAX_USER_PROCS) \
			KERNEL_MAX_SOCK_ADDR=$(KERNEL_MAX_SOCK_ADDR) \
			KERNEL_UNIX_PATH_MAX=$(KERNEL_UNIX_PATH_MAX) \
			KLOG_BUFFER_BYTES=$(KLOG_BUFFER_BYTES) \
			KERNEL_NATIVE_MOUNT_MAX=$(KERNEL_NATIVE_MOUNT_MAX) \
			KERNEL_NATIVE_MAX_NODES=$(KERNEL_NATIVE_MAX_NODES) \
			KERNEL_UTS_SYSNAME="$(KERNEL_UTS_SYSNAME)" \
			KERNEL_UTS_NODENAME="$(KERNEL_UTS_NODENAME)" \
			KERNEL_UTS_RELEASE="$(KERNEL_UTS_RELEASE)" \
			KERNEL_UTS_VERSION="$(KERNEL_UTS_VERSION)" \
			KERNEL_UTS_MACHINE="$(KERNEL_UTS_MACHINE)" \
				FSYS_MAX_FDS=$(FSYS_MAX_FDS) \
			FSYS_MAX_NODES=$(FSYS_MAX_NODES) \
			FSYS_MAX_MOUNTS=$(FSYS_MAX_MOUNTS) \
			FSYS_PTYS=$(FSYS_PTYS) \
			FSYS_PTY_BUF_BYTES=$(FSYS_PTY_BUF_BYTES) \
		KERNEL_DEBUG_TTY=$(KERNEL_DEBUG_TTY) \
			SERIAL_DEBUG=$(SERIAL_DEBUG) \
			LEGACY_8250_TRACE=$(LEGACY_8250_TRACE) \
			LEGACY_8250_TRACE_BASE=$(LEGACY_8250_TRACE_BASE) \
			SERVER_TRACE_COM2=$(SERVER_TRACE_COM2) \
			SERVER_TRACE_COM2_BASE=$(SERVER_TRACE_COM2_BASE) \
			SERVER_TRACE_COM2_DIVISOR=$(SERVER_TRACE_COM2_DIVISOR) \
			SERVER_TRACE_DEBUG_PORT=$(SERVER_TRACE_DEBUG_PORT) \
			SERVER_TRACE_DEBUG_PORT_IO=$(SERVER_TRACE_DEBUG_PORT_IO) \
			TTY_DEFAULTS=$(abspath $(TTY_DEFAULTS)) \
			UART_DEFAULTS=$(abspath $(UART_DEFAULTS)) \
			GEM_VDI_DEFAULTS=$(abspath $(GEM_VDI_DEFAULTS)) \
			SOUND_DEFAULTS=$(abspath $(SOUND_DEFAULTS)) \
			DRIVER_DEFAULTS=$(abspath $(DRIVER_DEFAULTS)) \
			SERSYS_TTYS0=$(SERSYS_TTYS0) \
			SERSYS_TTYS1=$(SERSYS_TTYS1) \
			SERSYS_TTYS2=$(SERSYS_TTYS2) \
			SERSYS_TTYS3=$(SERSYS_TTYS3) \
			SERSYS_TTYS0_BASE=$(SERSYS_TTYS0_BASE) \
			SERSYS_TTYS1_BASE=$(SERSYS_TTYS1_BASE) \
			SERSYS_TTYS2_BASE=$(SERSYS_TTYS2_BASE) \
			SERSYS_TTYS3_BASE=$(SERSYS_TTYS3_BASE) \
				SERSYS_TTYS0_TYPE=$(SERSYS_TTYS0_TYPE) \
				SERSYS_TTYS1_TYPE=$(SERSYS_TTYS1_TYPE) \
				SERSYS_TTYS2_TYPE=$(SERSYS_TTYS2_TYPE) \
				SERSYS_TTYS3_TYPE=$(SERSYS_TTYS3_TYPE) \
				SERSYS_IRQ_AWARE=$(SERSYS_IRQ_AWARE) \
				SERSYS_TTYS0_IRQ=$(SERSYS_TTYS0_IRQ) \
				SERSYS_TTYS1_IRQ=$(SERSYS_TTYS1_IRQ) \
				SERSYS_TTYS2_IRQ=$(SERSYS_TTYS2_IRQ) \
				SERSYS_TTYS3_IRQ=$(SERSYS_TTYS3_IRQ) \
				SERSYS_TTYS0_FLOW=$(SERSYS_TTYS0_FLOW) \
				SERSYS_TTYS1_FLOW=$(SERSYS_TTYS1_FLOW) \
				SERSYS_TTYS2_FLOW=$(SERSYS_TTYS2_FLOW) \
				SERSYS_TTYS3_FLOW=$(SERSYS_TTYS3_FLOW) \
				BOOT_USERLAND_DEBUG=$(BOOT_USERLAND_DEBUG) \
	PROCESS_DEBUG=$(PROCESS_DEBUG) \
	MOUNTED_EXEC_TRACE=$(MOUNTED_EXEC_TRACE) \
	EXT2_DEBUG=$(EXT2_DEBUG) \
	ENTRY_TRACE=$(ENTRY_TRACE) \
	INIT_DEBUG=$(INIT_DEBUG) \
	SERSYS_DEBUG=$(SERSYS_DEBUG) \
	FSYS_DEBUG=$(FSYS_DEBUG) \
	DRIVER_LOG_VERBOSITY=$(DRIVER_LOG_VERBOSITY) \
	MFM_DEBUG=$(MFM_DEBUG) \
	MFM_TRACE_SECTORS=$(MFM_TRACE_SECTORS) \
	ENABLE_NETWORK=$(ENABLE_NETWORK) \
	ENABLE_HTTPS=$(ENABLE_HTTPS) \
	START_NETWORK_ON_BOOT=$(START_NETWORK_ON_BOOT) \
	START_EXT2FS_ON_BOOT=$(START_EXT2FS_ON_BOOT) \
	ENABLE_MFM_DRIVER=$(ENABLE_MFM_DRIVER) \
	ENABLE_ATA_DRIVER=$(ENABLE_ATA_DRIVER) \
	ENABLE_DIRECTFD_DRIVER=$(ENABLE_DIRECTFD_DRIVER) \
	ENABLE_GAMEPORT_DRIVER=$(ENABLE_GAMEPORT_DRIVER) \
	ENABLE_RAMDISK_DRIVER=$(ENABLE_RAMDISK_DRIVER) \
	NET_DRIVER=$(NET_DRIVER) \
	NET_PROBE_SELF_TEST=$(NET_PROBE_SELF_TEST) \
	NET_NE2K_IO=$(NET_NE2K_IO) \
		NET_NE2K_IRQ=$(NET_NE2K_IRQ) \
		NET_NE2K_BUS=$(NET_NE2K_BUS) \
		NET_NE2K_DEBUG_TX=$(NET_NE2K_DEBUG_TX) \
		NET_PACKET_PATH=$(NET_PACKET_PATH) \
		NET_COM2_TRACE=$(NET_COM2_TRACE) \
		NET_WD80X3_IO=$(NET_WD80X3_IO) \
		NET_WD80X3_IRQ=$(NET_WD80X3_IRQ) \
		NET_WD80X3_RAM=$(NET_WD80X3_RAM) \
		NET_WD80X3_FLAGS=$(NET_WD80X3_FLAGS) \
		NET_ULTRA_IO=$(NET_ULTRA_IO) \
	NET_ULTRA_IRQ=$(NET_ULTRA_IRQ) \
	NET_ULTRA_RAM=$(NET_ULTRA_RAM) \
	NET_ULTRA_FLAGS=$(NET_ULTRA_FLAGS) \
	NET_HOSTNAME=$(KERNEL_UTS_NODENAME) \
	NET_IPMODE=$(NET_IPMODE) \
	NET_DHCP_ENABLE=$(NET_DHCP_ENABLE) \
	NET_DHCP_WAIT=$(NET_DHCP_WAIT) \
	NET_LOCALIP=$(NET_LOCALIP) \
	NET_GATEWAY=$(NET_GATEWAY) \
		NET_NETMASK=$(NET_NETMASK) \
		NET_DNSSERVER=$(NET_DNSSERVER) \
		NET_EXTRA_HOST_NAME=$(NET_EXTRA_HOST_NAME) \
		NET_EXTRA_HOST_IP=$(NET_EXTRA_HOST_IP) \
		ENABLE_DNS_RESOLVER=$(ENABLE_DNS_RESOLVER) \
	NTP_ENABLE=$(NTP_ENABLE) \
	NTP_SERVER=$(NTP_SERVER) \
	NTP_TIMEOUT=$(NTP_TIMEOUT) \
	NTP_TRIES=$(NTP_TRIES) \
	NTP_LOG=$(NTP_LOG) \
	IPSERIAL_ENABLE=$(IPSERIAL_ENABLE) \
	IPSERIAL_MODE=$(IPSERIAL_MODE) \
	IPSERIAL_PORT=$(IPSERIAL_PORT) \
	ENABLE_NFS=$(ENABLE_NFS) \
	NFS_START_NETWORK_ON_BOOT=$(NFS_START_NETWORK_ON_BOOT) \
	ENABLE_LOADABLE_DRIVER_MODULES=$(ENABLE_LOADABLE_DRIVER_MODULES) \
	ENABLE_DRIVER_MODULE_COMPRESSION=$(ENABLE_DRIVER_MODULE_COMPRESSION) \
	ENABLE_KERNEL_COMPRESSION=$(ENABLE_KERNEL_COMPRESSION) \
	FS_ENABLE_FAT_RUNTIME=$(FS_ENABLE_FAT_RUNTIME) \
	FS_FAT_ENABLE_WRITE=$(FS_FAT_ENABLE_WRITE) \
	FS_ENABLE_EXT2_MOUNT_RUNTIME=$(FS_ENABLE_EXT2_MOUNT_RUNTIME) \
	FS_POSIX_IPC_ENABLE=$(FS_POSIX_IPC_ENABLE) \
	ENABLE_SOUND=$(ENABLE_SOUND) \
	SOUND_SERVICE_ENABLE=$(SOUND_SERVICE_ENABLE) \
	START_SOUND_ON_BOOT=$(START_SOUND_ON_BOOT) \
	ESD_ENABLE=$(ESD_ENABLE) \
	DRIVERD_SOUND_ENABLE=$(DRIVERD_SOUND_ENABLE) \
	ESD_PORT=$(ESD_PORT) \
	ESD_RATE=$(ESD_RATE) \
	ESD_LOG=$(ESD_LOG) \
	SOUND_RATE=$(SOUND_RATE) \
	SB_PORT=$(SB_PORT) \
	SB_IRQ=$(SB_IRQ) \
	SB_DMA=$(SB_DMA) \
	SOUND_DRIVER=$(SOUND_DRIVER) \
	SOUND_IRQ_AWARE=$(SOUND_IRQ_AWARE) \
		MFM_ST506_IRQ=$(MFM_ST506_IRQ) \
		MFM_ST506_DMA=$(MFM_ST506_DMA) \
		MFM_IRQ_AWARE=$(MFM_IRQ_AWARE) \
		MFM_DMA_MODE=$(MFM_DMA_MODE) \
		SOUND_OPTI_SB_MODE=$(SOUND_OPTI_SB_MODE) \
	SOUND_OPTI_SB_PORT="$(SOUND_OPTI_SB_PORT)" \
	SOUND_OPTI_SB_IRQ="$(SOUND_OPTI_SB_IRQ)" \
	SOUND_OPTI_SB_DMA="$(SOUND_OPTI_SB_DMA)" \
	NFS_SERVER=$(NFS_SERVER) \
	NFS_SERVER_IP=$(NFS_SERVER_IP) \
	NFS_EXPORT=$(NFS_EXPORT) \
	NFS_MOUNTPOINT=$(NFS_MOUNTPOINT) \
	NFS_TEST_ROOT=$(NFS_TEST_ROOT) \
	NFS_DEVICE=$(NFS_DEVICE) \
	NFS_PORT=$(NFS_PORT) \
	NFS_MOUNT_PORT=$(NFS_MOUNT_PORT) \
	NFS_TIMEOUT_TICKS=$(NFS_TIMEOUT_TICKS) \
	NFS_RETRIES=$(NFS_RETRIES) \
	NETSTACK_RX_BYTES=$(NETSTACK_RX_BYTES) \
	NFS_FRAME_BYTES=$(NFS_FRAME_BYTES) \
	KERNEL_NFS_DATA_BYTES=$(KERNEL_NFS_DATA_BYTES) \
	KERNEL_NETSTACK_IP_MTU=$(KERNEL_NETSTACK_IP_MTU) \
	KERNEL_NETSTACK_TCP_MSS=$(KERNEL_NETSTACK_TCP_MSS) \
	KERNEL_NETSTACK_SOCKETS=$(KERNEL_NETSTACK_SOCKETS) \
	KERNEL_NETSTACK_ARP_ENTRIES=$(KERNEL_NETSTACK_ARP_ENTRIES) \
	$(if $(INIT_BIN),INIT_BIN=$(INIT_BIN),) \
	ROOT_ARCHIVE_EXCLUDE="$(ROOT_ARCHIVE_EXCLUDE)" \
	ARCHIVE_EXTRA_DEPS="$(ARCHIVE_EXTRA_DEPS)" \
	ARCHIVE_EXTRA_ARGS="$(ARCHIVE_EXTRA_ARGS)"

menuconfig:
	@bash config/Menuconfig

userland:
	$(MAKE) -C userland all $(USERLAND_MAKE_ARGS)

hd-userland:
	$(MAKE) -C userland full-archive $(USERLAND_MAKE_ARGS)

userland-image: hd-image

ifeq ($(86BOX_MFM_SECOND_DRIVE),1)
86BOX_MFM_IMAGE_DEPS := $(86BOX_MFM_IMAGE)
else
86BOX_MFM_IMAGE_DEPS :=
endif

$(86BOX_MFM_IMAGE):
	@mkdir -p $(dir $@)
	@truncate -s $(86BOX_MFM_IMAGE_BYTES) $@

build/xtide-boot.img: $(ANTLER_BOOT_BUILD_DIR)/mbr-part.bin
	@mkdir -p $(dir $@)
	@rm -f $@
	@truncate -s 737280 $@
	@dd if=$< of=$@ bs=512 count=1 conv=notrunc >/dev/null 2>&1

hd-image: build/hd.img build/xtide-boot.img build/86box-antler.cfg

# A local disk name keeps copied CGA and MDA 86Box VM directories
# self-contained; 86Box resolves it relative to the VM directory.
# PC1640 firmware places the exact Antler MBR from a 720 KiB bootstrap floppy
# at 0000:7c00.  From its first instruction onward, the loader reads the hard
# disk by direct XTIDE PIO.  This avoids depending on an option-ROM boot policy
# while retaining the XTIDE controller as the native hard-disk interface.
build/86box-antler.cfg: build/hd.img build/xtide-boot.img $(86BOX_MFM_IMAGE_DEPS) $(BUILD_CONFIG)
	@mkdir -p $(dir $@)
	@protocol="$(IPSERIAL_MODE)"; \
	case "$$protocol" in tcp|TCP) protocol=tcp ;; *) protocol=udp ;; esac; \
	mouse_com1=0; \
	mouse_com2=0; \
	if test "$(86BOX_MOUSE_TYPE)" = "msserial"; then \
		case "$(86BOX_MOUSE_PORT)" in \
			0) mouse_com1=1 ;; \
			1) mouse_com2=1 ;; \
		esac; \
	fi; \
		guest_port="$(IPSERIAL_PORT)"; \
		host_port="$(86BOX_IPSERIAL_HOST_PORT)"; \
	ne2k_base="$(NET_NE2K_IO)"; \
	ne2k_base=$${ne2k_base#0x}; \
	ne2k_base=$${ne2k_base#0X}; \
	wd_base="$(NET_WD80X3_IO)"; \
	wd_base=$${wd_base#0x}; \
	wd_base=$${wd_base#0X}; \
	wd_ram="$(NET_WD80X3_RAM)"; \
	wd_ram=$${wd_ram#0x}; \
	wd_ram=$${wd_ram#0X}; \
	sb_base="$(SB_PORT)"; \
	sb_base=$${sb_base#0x}; \
	sb_base=$${sb_base#0X}; \
	case "$$ne2k_base" in \
		?) ne2k_base="000$$ne2k_base" ;; \
		??) ne2k_base="00$$ne2k_base" ;; \
		???) ne2k_base="0$$ne2k_base" ;; \
	esac; \
	case "$$wd_base" in \
		?) wd_base="000$$wd_base" ;; \
		??) wd_base="00$$wd_base" ;; \
		???) wd_base="0$$wd_base" ;; \
	esac; \
	case "$$wd_ram" in \
		?) wd_ram="000$$wd_ram" ;; \
		??) wd_ram="00$$wd_ram" ;; \
		???) wd_ram="0$$wd_ram" ;; \
	esac; \
	wd_ram="$${wd_ram}0"; \
	wd_ram=$$(printf '%s' "$$wd_ram" | tr 'abcdef' 'ABCDEF'); \
	case "$$sb_base" in \
		?) sb_base="000$$sb_base" ;; \
		??) sb_base="00$$sb_base" ;; \
		???) sb_base="0$$sb_base" ;; \
	esac; \
	{ \
		printf '%s\n' '[General]'; \
		printf '%s\n' 'hide_status_bar = 1'; \
		printf '%s\n' 'hide_tool_bar = 1'; \
		if test "$(ENABLE_SOUND)" = "1" \
			-o "$(SOUND_SERVICE_ENABLE)" = "1" \
			-o "$(START_SOUND_ON_BOOT)" = "1"; then \
			printf '%s\n' 'sound_muted = 0'; \
		else \
			printf '%s\n' 'sound_muted = 1'; \
		fi; \
		printf 'force_10ms = %s\n' '$(86BOX_FORCE_10MS)'; \
		printf 'force_constant_mouse = %s\n' \
			'$(86BOX_FORCE_CONSTANT_MOUSE)'; \
		printf 'scale = %s\n' '$(86BOX_DISPLAY_SCALE)'; \
		printf 'uuid = %s\n' '$(86BOX_UUID)'; \
		printf 'vid_renderer = %s\n' '$(86BOX_VIDEO_RENDERER)'; \
		printf 'video_filter_method = %s\n' '$(86BOX_VIDEO_FILTER)'; \
		printf '\n%s\n' '[Machine]'; \
			if test "$(MACHINE_PROFILE)" = "pcxt"; then \
			printf '%s\n' 'cpu_family = 8088'; \
			printf '%s\n' 'cpu_multi = 1'; \
			printf '%s\n' 'cpu_speed = $(86BOX_PCXT_CPU_SPEED)'; \
			printf '%s\n' 'cpu_use_dynarec = 0'; \
			printf '%s\n' 'machine = genxt'; \
		else \
			printf '%s\n' 'cpu_family = 8086'; \
			printf '%s\n' 'cpu_multi = 1'; \
			printf '%s\n' 'cpu_speed = $(86BOX_PC1640_CPU_SPEED)'; \
			printf '%s\n' 'cpu_use_dynarec = 0'; \
			printf '%s\n' 'machine = pc1640'; \
		fi; \
		printf 'mem_size = %s\n' '$(ANX_PLATFORM_RAM_KB)'; \
		printf '%s\n' 'time_sync = disabled'; \
		printf '\n%s\n' '[Video]'; \
			if test "$(MACHINE_PROFILE)" = "pcxt"; then \
				printf 'gfxcard = %s\n' '$(86BOX_GEM_VIDEO)'; \
		else \
			printf '%s\n' 'gfxcard = internal'; \
		fi; \
			printf '\n%s\n' '[Input devices]'; \
			printf '%s\n' 'keyboard_type = keyboard_pc_xt'; \
			printf 'mouse_type = %s\n' '$(86BOX_MOUSE_TYPE)'; \
			if test "$(86BOX_MOUSE_TYPE)" = "msserial"; then \
				printf '\n%s\n' '[Microsoft Serial Mouse]'; \
				printf 'port = %s\n' '$(86BOX_MOUSE_PORT)'; \
			fi; \
			if test "$(ENABLE_NETWORK)" = "1"; then \
			printf '\n%s\n' '[Network]'; \
			case "$(NET_DRIVER)" in \
			ne2k) printf '%s\n' 'net_01_card = ne2k8' ;; \
			wd80x3) printf '%s\n' 'net_01_card = wd8003eb' ;; \
			*) printf '# no 86Box ISA NIC generator for NET_DRIVER=%s\n' \
				'$(NET_DRIVER)' ;; \
			esac; \
			printf '%s\n' 'net_01_net_type = slirp'; \
			printf '%s\n' 'net_01_switch_group = 0'; \
			printf '%s\n' 'net_02_link = 0'; \
			printf '%s\n' 'net_03_link = 0'; \
			printf '%s\n' 'net_04_link = 0'; \
			case "$(NET_DRIVER)" in \
			ne2k) \
				printf '\n%s\n' '[NE2000 Compatible 8-bit #1]'; \
				printf 'base = %s\n' "$$ne2k_base"; \
				printf 'irq = %s\n' '$(NET_NE2K_IRQ)'; \
				printf '%s\n' 'bios_addr = 00000'; \
				printf '%s\n' 'mac = 1a:f9:d4'; \
				printf '%s\n' 'mac_oui = 00:86:b0' ;; \
			wd80x3) \
				printf '\n%s\n' '[Western Digital WD8003EB #1]'; \
				printf 'base = %s\n' "$$wd_base"; \
				printf 'irq = %s\n' '$(NET_WD80X3_IRQ)'; \
				printf 'ram_addr = %s\n' "$$wd_ram"; \
				printf '%s\n' 'ram_size = 8192'; \
				printf '%s\n' 'mac = 49:4c:9f' ;; \
			esac; \
			if test "$(IPSERIAL_ENABLE)" = "1" \
				-o "$(IPSERIAL_ENABLE)" = "yes"; then \
				printf '\n%s\n' '[SLiRP Port Forwarding #1]'; \
				printf '0_protocol = %s\n' "$$protocol"; \
				printf '0_external = %s\n' "$$host_port"; \
				printf '0_internal = %s\n' "$$guest_port"; \
				if test "$(ESD_ENABLE)" = "1" \
					-o "$(ESD_ENABLE)" = "yes"; then \
					printf '%s\n' '1_protocol = udp'; \
					printf '1_external = %s\n' '$(ESD_PORT)'; \
					printf '1_internal = %s\n' '$(ESD_PORT)'; \
				fi; \
			elif test "$(ESD_ENABLE)" = "1" \
				-o "$(ESD_ENABLE)" = "yes"; then \
				printf '\n%s\n' '[SLiRP Port Forwarding #1]'; \
				printf '%s\n' '0_protocol = udp'; \
				printf '0_external = %s\n' '$(ESD_PORT)'; \
				printf '0_internal = %s\n' '$(ESD_PORT)'; \
			fi; \
		fi; \
		if test "$(SOUND_DRIVER_SB)" = "1"; then \
			if test "$(ENABLE_SOUND)" = "1" \
				-o "$(SOUND_SERVICE_ENABLE)" = "1" \
				-o "$(START_SOUND_ON_BOOT)" = "1"; then \
				printf '\n%s\n' '[Sound]'; \
				printf '%s\n' 'sndcard = sb2.0'; \
				printf '\n%s\n' '[Sound Blaster v2.0 #1]'; \
				printf 'base = %s\n' "$$sb_base"; \
				printf 'irq = %s\n' '$(SB_IRQ)'; \
				printf 'dma = %s\n' '$(SB_DMA)'; \
				printf '%s\n' 'gameport = 0'; \
				printf '%s\n' 'opl = 1'; \
				printf '%s\n' 'cms = 0'; \
				printf '%s\n' 'receive_input = 1'; \
			fi; \
		fi; \
		printf '\n%s\n' '[PC/XT XTIDE #1]'; \
		printf '%s\n' 'bios = xt'; \
		printf 'base = %04x\n' $(XTIDE_IO_DECIMAL); \
		printf '%s\n' 'rom_writes_enabled = 0'; \
		printf '%s\n' 'bios_addr = D0000'; \
		printf '\n%s\n' '[Storage controllers]'; \
		printf '%s\n' 'hdc_1 = xtide'; \
		if test "$(86BOX_MFM_SECOND_DRIVE)" = "1"; then \
			printf 'hdc_2 = %s\n' '$(86BOX_MFM_CONTROLLER)'; \
		fi; \
			printf '\n%s\n' '[Floppy and CD-ROM drives]'; \
			printf '%s\n' 'fdd_01_type = 35_2dd'; \
			if test "$(MACHINE_PROFILE)" = "pc1640-dd"; then \
				printf '%s\n' 'fdd_02_type = 35_2dd'; \
			fi; \
		printf '\n%s\n' '[Ports (COM & LPT)]'; \
		printf '%s\n' 'lpt1_enabled = 1'; \
		printf '%s\n' 'lpt1_device = file'; \
		if test "$(SERSYS_TTYS0)$(SERSYS_TTYS1)" != "00"; then \
			if test "$(SERSYS_TTYS0)" = "1" \
				-a "$$mouse_com1" != "1"; then \
				if test "$(86BOX_COM1_DEVICE)" = "stdio"; then \
					printf '%s\n' 'serial1_device = stdio'; \
				elif test "$(86BOX_COM1_DEVICE)" = "serial_passthrough"; then \
					printf '%s\n' 'serial1_passthrough_enabled = 1'; \
				else \
					printf '%s\n' 'serial1_device = pipe'; \
				fi; \
			fi; \
			if test "$(SERSYS_TTYS1)" = "1" \
				-a "$$mouse_com2" != "1"; then \
				if test "$(86BOX_COM2_DEVICE)" = "stdio"; then \
					printf '%s\n' 'serial2_device = stdio'; \
				elif test "$(86BOX_COM2_DEVICE)" = "serial_passthrough"; then \
					printf '%s\n' 'serial2_passthrough_enabled = 1'; \
				else \
					printf '%s\n' 'serial2_device = pipe'; \
				fi; \
			fi; \
			if test "$(SERSYS_TTYS0)" = "1" \
				-a "$$mouse_com1" != "1"; then \
				if test "$(86BOX_COM1_DEVICE)" = "stdio"; then \
					printf '\n%s\n' '[Virtual Console (COM) #1]'; \
					printf 'mode = %s\n' '$(86BOX_COM1_MODE)'; \
				elif test "$(86BOX_COM1_DEVICE)" = "serial_passthrough"; then \
					printf '\n%s\n' '[Serial Passthrough Device #1]'; \
					printf 'mode = %s\n' '$(86BOX_COM1_MODE)'; \
					printf '%s\n' 'baudrate = 9600'; \
					printf '%s\n' 'data_bits = 8'; \
					printf '%s\n' 'stop_bits = 1'; \
					if test "$(86BOX_COM1_MODE)" != "0"; then \
						printf 'host_serial_path = %s\n' '$(86BOX_COM1_PATH)'; \
					fi; \
				else \
					printf '\n%s\n' '[Named Pipe (COM) #1]'; \
					printf 'path = %s\n' '$(86BOX_COM1_PATH)'; \
					printf 'mode = %s\n' '$(86BOX_COM1_MODE)'; \
				fi; \
			fi; \
			if test "$(SERSYS_TTYS1)" = "1" \
				-a "$$mouse_com2" != "1"; then \
				if test "$(86BOX_COM2_DEVICE)" = "stdio"; then \
					printf '\n%s\n' '[Virtual Console (COM) #2]'; \
					printf 'mode = %s\n' '$(86BOX_COM2_MODE)'; \
				elif test "$(86BOX_COM2_DEVICE)" = "serial_passthrough"; then \
					printf '\n%s\n' '[Serial Passthrough Device #2]'; \
					printf 'mode = %s\n' '$(86BOX_COM2_MODE)'; \
					printf '%s\n' 'baudrate = 9600'; \
					printf '%s\n' 'data_bits = 8'; \
					printf '%s\n' 'stop_bits = 1'; \
					if test "$(86BOX_COM2_MODE)" != "0"; then \
						printf 'host_serial_path = %s\n' '$(86BOX_COM2_PATH)'; \
					fi; \
				else \
					printf '\n%s\n' '[Named Pipe (COM) #2]'; \
					printf 'path = %s\n' '$(86BOX_COM2_PATH)'; \
					printf 'mode = %s\n' '$(86BOX_COM2_MODE)'; \
				fi; \
			fi; \
		fi; \
		printf '\n%s\n' '[File (LPT) #1]'; \
		printf '%s\n' 'strobe = 1'; \
		printf 'path = %s\n' '$(86BOX_LPT1_PATH)'; \
		printf '%s\n' 'append = 0'; \
		printf '\n%s\n' '[Hard disks]'; \
		printf '%s\n' 'hdd_01_fn = hd.img'; \
		printf '%s\n' 'hdd_01_ide_channel = 0:0'; \
		printf 'hdd_01_parameters = %s, %s, %s, 0, ide\n' \
			'$(BOOT_GEOM_SECT)' '$(BOOT_GEOM_HEAD)' '$(BOOT_GEOM_CYL)'; \
		printf '%s\n' 'hdd_01_speed = ramdisk'; \
		if test "$(86BOX_MFM_SECOND_DRIVE)" = "1"; then \
			printf 'hdd_02_fn = %s\n' '$(86BOX_MFM_IMAGE_CFG)'; \
			printf 'hdd_02_mfm_channel = %s\n' '$(86BOX_MFM_CHANNEL)'; \
			printf 'hdd_02_parameters = %s, %s, %s, 0, mfm\n' \
				'$(86BOX_MFM_SECT)' '$(86BOX_MFM_HEAD)' '$(86BOX_MFM_CYL)'; \
			printf '%s\n' 'hdd_02_speed = ramdisk'; \
		fi; \
	} > $@.tmp; \
	if test -f $@ && cmp -s $@.tmp $@; then \
		rm -f $@.tmp; \
	else \
		mv $@.tmp $@; \
	fi

userland-debug-image:
	$(MAKE) hd-image BOOT_USERLAND_DEBUG=1 ENABLE_NETWORK=0 \
		START_NETWORK_ON_BOOT=0 ENABLE_SOUND=0 SOUND_SERVICE_ENABLE=0

86box-userland-debug: userland-debug-image
	scripts/86box-userland-debug.sh

86box-com-link: hd-image
	scripts/86box-com-link.sh

gdb-userland-debug:
	$${GDB:-gdb} -x scripts/antler-gdbstub.gdb build/antler.elf

serdrive-start:
	@test -f build/hd.img || \
		{ echo "build/hd.img missing; run make hd-image first"; exit 1; }
	$(SERDRIVE) -g $(SERDRIVE_GEOMETRY) -c $(SERDRIVE_PORT) \
		-b $(SERDRIVE_BAUD) -v $(SERDRIVE_VERBOSE) build/hd.img

serdrive-screen:
	@test -f build/hd.img || \
		{ echo "build/hd.img missing; run make hd-image first"; exit 1; }
	@mkdir -p $(SERDRIVE_LOG_DIR) $(SERDRIVE_STATE_DIR)
	@screen -S "$(SERDRIVE_SCREEN)" -X quit >/dev/null 2>&1 || true
	@log="$(SERDRIVE_LOG_DIR)/antler-serdrive-$(notdir $(SERDRIVE_PORT))-$(SERDRIVE_BAUD)-$$(date +%Y%m%d-%H%M%S).log"; \
	printf '%s\n' "$$log" > "$(SERDRIVE_STATE_DIR)/serdrive-$(notdir $(SERDRIVE_PORT)).log.path"; \
	screen -dmS "$(SERDRIVE_SCREEN)" sh -lc \
		"exec '$(SERDRIVE)' -g '$(SERDRIVE_GEOMETRY)' -c '$(SERDRIVE_PORT)' -b '$(SERDRIVE_BAUD)' -v '$(SERDRIVE_VERBOSE)' '$(abspath build/hd.img)' > '$$log' 2>&1"; \
	printf 'SerDrive screen %s logging to %s\n' "$(SERDRIVE_SCREEN)" "$$log"

serdrive-stop:
	-screen -S "$(SERDRIVE_SCREEN)" -X quit >/dev/null 2>&1 || true

.PHONY: martypc-ui-modes-smoke martypc-cli-smoke
martypc-ui-modes-smoke:
	@test -n "$(MARTYPC_MFM_IMAGE)" || \
		{ echo "set MARTYPC_MFM_IMAGE to a separate raw MFM image"; exit 1; }
	python3 scripts/martypc-antler-smoke.py \
		--mfm-image "$(MARTYPC_MFM_IMAGE)"

martypc-cli-smoke:
	python3 scripts/martypc-antler-smoke.py --self-test

build/antler.elf: $(OBJS) $(KERNEL_LD) $(BUILD_CONFIG) $(KERNEL_LIBGCC)
	@mkdir -p $(dir $@)
	$(LD) $(LDFLAGS) -T $(KERNEL_LD) -Map build/antler.map -o $@ $(OBJS) \
		$(KERNEL_LIBGCC)
	@total=$$($(SIZE) $@ | awk 'NR==2 { print $$4 }'); \
	if [ "$(RAM_PROFILE)" = "tight" ] && [ "$(ENABLE_NETWORK)" = "0" ] && [ "$(KERNEL_TIGHT_SIZE_LIMIT)" != "0" ] && [ $$total -gt "$(KERNEL_TIGHT_SIZE_LIMIT)" ]; then \
		echo "Antler/86 tight non-network kernel is $$total bytes; target limit is $(KERNEL_TIGHT_SIZE_LIMIT) bytes"; \
		exit 1; \
	fi

$(NO_BIOS_KERNEL_STAMP): build/antler.elf $(NO_BIOS_AUDIT) \
		$(NO_BIOS_SOURCE_AUDIT)
	python3 $(NO_BIOS_SOURCE_AUDIT) --repo .
	python3 $(NO_BIOS_AUDIT) --repo . --tool-prefix $(CROSS) build/antler.elf
	@touch $@

build/antler.bin: build/antler.elf $(NO_BIOS_KERNEL_DEP)
	$(OBJCOPY) -O binary $< $@
	@size=$$(wc -c < $@); \
	if [ $$size -gt $(KERNEL_IMAGE_BYTES_MAX) ]; then \
		echo "Antler/86 kernel is $$size bytes; image limit is $(KERNEL_IMAGE_BYTES_MAX) bytes"; \
		exit 1; \
	fi

exomizer-tool: $(EXOMIZER)

$(EXOMIZER):
	@echo "install an Antler-local exomizer or set EXOMIZER=/path/to/exomizer" >&2
	@exit 1

kernel-compressed: build/antler.bin.exo

build/antler.bin.exo: build/antler.bin $(EXOMIZER)
	$(EXOMIZER) raw -q -C -o $@ $<
	@orig=$$(wc -c < build/antler.bin); \
	comp=$$(wc -c < $@); \
	printf 'antler exomizer raw: %s -> %s bytes\n' "$$orig" "$$comp"

build/antler-boot.bin: build/antler.bin
	@mkdir -p $(dir $@)
	dd if=/dev/zero of=$@ bs=512 count=1 >/dev/null 2>&1
	cat build/antler.bin >> $@
	@handoff=$$(( $(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR) - 1 )); \
	if [ $$handoff -lt 1 ] || [ $$handoff -gt 65535 ]; then \
			echo "Antler/86 boot archive handoff sector $$handoff does not fit in the setup word"; \
		exit 1; \
	fi; \
	perl -e 'use strict; use warnings; my ($$path, $$handoff) = @ARGV; open(my $$fh, "+<", $$path) or die "$$path: $$!"; binmode($$fh); seek($$fh, 486, 0) or die "seek antler magic: $$!"; print $$fh "ANTK"; seek($$fh, 500, 0) or die "seek archive handoff: $$!"; print $$fh pack("v", $$handoff); seek($$fh, 510, 0) or die "seek boot flag: $$!"; print $$fh pack("v", 0xaa55); close($$fh) or die "$$path: $$!";' $@ $$handoff


$(ANTLER_BOOT_BUILD_DIR)/boot_sect.o: $(ANTLER_BOOT_SECTOR_SOURCE) \
		$(ANTLER_BOOT_ASM_DEPS) \
		$(ANTLER_BOOT_DIR)/boot_err.h \
		$(ANTLER_BOOT_DIR)/anx_boot.h \
		$(ANTLER_BOOT_DIR)/anx_config.h FORCE check-toolchain
	@mkdir -p $(dir $@)
	$(CC) $(ANTLER_BOOT_CPPFLAGS) -E -o $(ANTLER_BOOT_BUILD_DIR)/boot_sect.tmp $<
	$(AS) $(ANTLER_BOOT_ASFLAGS) -o $@ $(ANTLER_BOOT_BUILD_DIR)/boot_sect.tmp
	rm -f $(ANTLER_BOOT_BUILD_DIR)/boot_sect.tmp

$(ANTLER_BOOT_BUILD_DIR)/boot_ext2.o: $(ANTLER_BOOT_DIR)/boot_ext2.c \
		$(ANTLER_BOOT_DIR)/anx_boot.h \
		$(ANTLER_BOOT_DIR)/anx_config.h FORCE check-toolchain
	@mkdir -p $(dir $@)
	$(CC) $(ANTLER_BOOT_CPPFLAGS) $(ANTLER_BOOT_CFLAGS) \
		-fno-strict-aliasing -c -o $@ $<

$(NO_BIOS_EXT2_AUDIT_ELF): \
		$(ANTLER_BOOT_BUILD_DIR)/boot_sect.o \
		$(ANTLER_BOOT_BUILD_DIR)/boot_ext2.o \
		$(ANTLER_BOOT_LD) check-toolchain
	$(LD) -m elf_i386 --oformat elf32-i386 -T $(ANTLER_BOOT_LD) \
		-Map $(ANTLER_BOOT_BUILD_DIR)/ext2.audit.map -o $@ \
		$(ANTLER_BOOT_BUILD_DIR)/boot_sect.o \
		$(ANTLER_BOOT_BUILD_DIR)/boot_ext2.o

$(NO_BIOS_EXT2_STAMP): $(NO_BIOS_EXT2_AUDIT_ELF) $(NO_BIOS_AUDIT) \
		$(NO_BIOS_SOURCE_AUDIT)
	python3 $(NO_BIOS_SOURCE_AUDIT) --repo . $(ANTLER_BOOT_DIR)
	python3 $(NO_BIOS_AUDIT) --repo . --tool-prefix $(CROSS) --boot \
		$(NO_BIOS_EXT2_AUDIT_ELF)
	@touch $@

$(ANTLER_BOOT_BUILD_DIR)/ext2.bin: \
		$(ANTLER_BOOT_BUILD_DIR)/boot_sect.o \
		$(ANTLER_BOOT_BUILD_DIR)/boot_ext2.o \
		$(NO_BIOS_EXT2_DEP) \
		$(ANTLER_BOOT_LD) check-toolchain
	$(LD) -T $(ANTLER_BOOT_LD) -Map $(ANTLER_BOOT_BUILD_DIR)/ext2.map \
		-o $@ $(ANTLER_BOOT_BUILD_DIR)/boot_sect.o \
		$(ANTLER_BOOT_BUILD_DIR)/boot_ext2.o
	$(OBJCOPY) -O binary $(NO_BIOS_EXT2_AUDIT_ELF) $@.audit
	@cmp -s $@ $@.audit || { \
		rm -f $@.audit; \
		echo "Antler ext2 raw loader differs from its audited ELF" >&2; \
		exit 1; \
	}
	rm -f $@.audit
	@bytes=$$(wc -c < $@); \
	if [ $$bytes -gt 1024 ]; then \
			echo "Antler/86 ext2 boot block is $$bytes bytes; expected at most 1024"; \
		exit 1; \
	fi
	dd if=$@ of=$@.tmp ibs=1024 count=1 conv=sync >/dev/null 2>&1
	mv $@.tmp $@

$(ANTLER_BOOT_BUILD_DIR)/ext2-vbr.bin: $(ANTLER_BOOT_BUILD_DIR)/ext2.bin FORCE
	cp $< $@
	perl -e 'use strict; use warnings; my ($$path, $$start) = @ARGV; open(my $$fh, "+<", $$path) or die "$$path: $$!"; binmode($$fh); seek($$fh, 499, 0) or die "seek sect_offset: $$!"; print $$fh pack("V", $$start); close($$fh) or die "$$path: $$!";' \
		$@ $(ANTLER_EXT2_BOOT_PART_START_SECTOR)

$(ANTLER_BOOT_BUILD_DIR)/mbr.o: $(ANTLER_BOOT_MBR_SOURCE) \
		$(ANTLER_BOOT_ASM_DEPS) FORCE \
		check-toolchain
	@mkdir -p $(dir $@)
	$(CC) $(ANTLER_BOOT_CPPFLAGS) -E -o $(ANTLER_BOOT_BUILD_DIR)/mbr.tmp $<
	$(AS) $(ANTLER_BOOT_ASFLAGS) -o $@ $(ANTLER_BOOT_BUILD_DIR)/mbr.tmp
	rm -f $(ANTLER_BOOT_BUILD_DIR)/mbr.tmp

$(NO_BIOS_MBR_AUDIT_ELF): $(ANTLER_BOOT_BUILD_DIR)/mbr.o \
		$(ANTLER_BOOT_LD) check-toolchain
	$(LD) -m elf_i386 --oformat elf32-i386 -T $(ANTLER_BOOT_LD) \
		-Map $(ANTLER_BOOT_BUILD_DIR)/mbr.audit.map -o $@ \
		$(ANTLER_BOOT_BUILD_DIR)/mbr.o

$(NO_BIOS_MBR_STAMP): $(NO_BIOS_MBR_AUDIT_ELF) $(NO_BIOS_AUDIT) \
		$(NO_BIOS_SOURCE_AUDIT)
	python3 $(NO_BIOS_SOURCE_AUDIT) --repo . $(ANTLER_BOOT_DIR)
	python3 $(NO_BIOS_AUDIT) --repo . --tool-prefix $(CROSS) --boot \
		$(NO_BIOS_MBR_AUDIT_ELF)
	@touch $@

$(ANTLER_BOOT_BUILD_DIR)/mbr.bin: $(ANTLER_BOOT_BUILD_DIR)/mbr.o \
		$(NO_BIOS_MBR_DEP) \
		$(ANTLER_BOOT_LD) check-toolchain
	$(LD) -T $(ANTLER_BOOT_LD) -Map $(ANTLER_BOOT_BUILD_DIR)/mbr.map \
		-o $@ $(ANTLER_BOOT_BUILD_DIR)/mbr.o
	$(OBJCOPY) -O binary $(NO_BIOS_MBR_AUDIT_ELF) $@.audit
	@cmp -s $@ $@.audit || { \
		rm -f $@.audit; \
		echo "Antler MBR raw loader differs from its audited ELF" >&2; \
		exit 1; \
	}
	rm -f $@.audit
	@bytes=$$(wc -c < $@); \
	if [ $$bytes -gt 512 ]; then \
			echo "Antler/86 MBR is $$bytes bytes; expected at most 512"; \
		exit 1; \
	fi
	dd if=$@ of=$@.tmp ibs=512 count=1 conv=sync >/dev/null 2>&1
	mv $@.tmp $@

$(ANTLER_BOOT_BUILD_DIR)/mbr-part.bin: $(ANTLER_BOOT_BUILD_DIR)/mbr.bin FORCE
	cp $< $@
	@part_sectors=$$(( $(ANTLER_EXT2_BOOT_FS_BLOCKS) * 2 )); \
	perl -e 'use strict; use warnings; my ($$path, $$spt, $$heads, $$start, $$count) = @ARGV; sub chs { my ($$lba, $$spt, $$heads) = @_; my $$spc = $$spt * $$heads; my $$c = int($$lba / $$spc); my $$r = $$lba % $$spc; my $$h = int($$r / $$spt); my $$s = ($$r % $$spt) + 1; if ($$c > 1023) { $$c = 1023; $$h = 254; $$s = 63; } return pack("CCC", $$h & 255, ($$s & 63) | (($$c >> 2) & 192), $$c & 255); } open(my $$fh, "+<", $$path) or die "$$path: $$!"; binmode($$fh); my $$end = $$start + $$count - 1; my $$entry = pack("C", 128) . chs($$start, $$spt, $$heads) . pack("C", 0x83) . chs($$end, $$spt, $$heads) . pack("V", $$start) . pack("V", $$count); seek($$fh, 446, 0) or die "seek partition: $$!"; print $$fh $$entry; seek($$fh, 510, 0) or die "seek boot signature: $$!"; print $$fh pack("v", 0xaa55); close($$fh) or die "$$path: $$!";' \
		$@ $(BOOT_GEOM_SECT) $(BOOT_GEOM_HEAD) \
		$(ANTLER_EXT2_BOOT_PART_START_SECTOR) $$part_sectors

build/rootfs.debugfs: FORCE
	$(MAKE) -C userland rootfs-debugfs-script $(USERLAND_MAKE_ARGS) \
		ROOTFS_DEBUGFS_SCRIPT=$(abspath $@)

build/hd.img: $(ANTLER_BOOT_BUILD_DIR)/mbr-part.bin \
		$(ANTLER_BOOT_BUILD_DIR)/ext2-vbr.bin \
		build/antler-boot.bin build/rootfs.debugfs
	@mkdir -p $(dir $@)
	rm -f $@ build/bootfs.img build/bootfs.debugfs
	@fs_end=$$(( $(ANTLER_EXT2_BOOT_FS_BLOCKS) * 1024 )); \
	part_start=$$(( $(ANTLER_EXT2_BOOT_PART_START_SECTOR) * 512 )); \
	fs_disk_end=$$(( $$part_start + $$fs_end )); \
	archive_start=$$(( $(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR) * 512 )); \
	archive_end=$$(( $$archive_start + $$(wc -c < build/userland/antler-full.uar) )); \
	if [ $$fs_disk_end -gt $$archive_start ]; then \
		echo "Antler/86 boot ext2 filesystem ends at $$fs_disk_end but archive starts at $$archive_start"; \
		exit 1; \
	fi; \
	if [ $$archive_end -gt $(HD_IMAGE_BYTES) ]; then \
		echo "Antler/86 HD image needs $$archive_end bytes; exceeds $(HD_IMAGE_BYTES) bytes"; \
		exit 1; \
	fi
	@if command -v $(MKE2FS) >/dev/null 2>&1 \
		&& command -v $(E2CP) >/dev/null 2>&1 \
		&& command -v $(DEBUGFS) >/dev/null 2>&1; then \
		truncate -s $$(( $(ANTLER_EXT2_BOOT_FS_BLOCKS) * 1024 )) build/bootfs.img; \
		$(MKE2FS) -q -F -t ext2 -b 1024 -I 128 -O none -L ANTLERBOOT \
			build/bootfs.img $(ANTLER_EXT2_BOOT_FS_BLOCKS) >/dev/null 2>&1; \
		$(E2CP) -P 0555 -O 0 -G 0 build/antler-boot.bin build/bootfs.img:/kernel; \
		: > build/bootfs.debugfs; \
		cat build/rootfs.debugfs >> build/bootfs.debugfs; \
		$(DEBUGFS) -w -f build/bootfs.debugfs build/bootfs.img >/dev/null 2>&1; \
		if command -v $(E2FSCK) >/dev/null 2>&1; then \
			$(E2FSCK) -fn build/bootfs.img >/dev/null 2>&1; \
		fi; \
	else \
		echo "missing ext2 image tools: need $(MKE2FS), $(E2CP), and $(DEBUGFS)" >&2; \
		exit 1; \
	fi
	truncate -s $(HD_IMAGE_BYTES) $@
	dd if=build/bootfs.img of=$@ bs=512 \
		seek=$(ANTLER_EXT2_BOOT_PART_START_SECTOR) conv=notrunc \
		>/dev/null 2>&1
	dd if=$(ANTLER_BOOT_BUILD_DIR)/ext2-vbr.bin of=$@ bs=512 \
		seek=$(ANTLER_EXT2_BOOT_PART_START_SECTOR) count=2 \
		conv=notrunc >/dev/null 2>&1
	dd if=$(ANTLER_BOOT_BUILD_DIR)/mbr-part.bin of=$@ bs=512 count=1 \
		conv=notrunc >/dev/null 2>&1
	dd if=build/userland/antler-full.uar of=$@ bs=512 \
		seek=$(ANTLER_EXT2_BOOT_ARCHIVE_BASE_SECTOR) conv=notrunc \
		>/dev/null 2>&1

build/%.o: %.c $(BUILD_CONFIG) $(KERNEL_PUBLIC_HEADERS) $(KERNEL_INTERNAL_DEPS)
	@mkdir -p $(dir $@)
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# The archive/image handoff runs only while bootstrapping the first process.
# GCC 6's general inliner duplicates several checked disk and image helpers in
# this one object, costing near text without improving steady-state IPC.  Keep
# the small helpers callable; scheduler and syscall objects retain normal -Os
# inlining.
build/kern/nucleus/boot/boot_handoff.o: CFLAGS += -fno-inline-functions

# The VFS boundary may pass 16-bit segment offsets through pointer-shaped ABI
# placeholders when it is built into a large-memory diagnostic image.
build/kern/system/service/filesystem/core/vfs.o: CFLAGS += -Wno-int-to-pointer-cast -Wno-pointer-to-int-cast


# eth.c is the tested ELKS-derived Ethernet adapter table.  Tight RAM builds
# expose fewer network interfaces than the ELKS table contains, so keep the
# driver body unchanged and silence only this object.
build/kern/system/service/network_stack/link/eth.o: CFLAGS += -w



build/%.o: %.S $(BUILD_CONFIG) $(KERNEL_PUBLIC_HEADERS)
	@mkdir -p $(dir $@)
	$(CC) $(ASFLAGS) -c $< -o $@

disasm: build/antler.elf
	$(OBJDUMP) -dr build/antler.elf > build/antler.dis

# The motherboard firmware necessarily runs before it loads Antler' first
# sector.  From the first Antler MBR instruction onward, however, bootloaders,
# the nucleus, services, and applications may not call firmware or ROM.  Keep
# parser fixtures independent so regressions can be diagnosed without a build.
test-no-bios-audit:
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-no-bios-runtime-audit.py
	PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-no-bios-source-audit.py

audit-no-bios: $(NO_BIOS_MBR_STAMP) $(NO_BIOS_EXT2_STAMP) \
		$(NO_BIOS_KERNEL_STAMP) userland
	python3 scripts/audit-no-bios-source.py --repo .
	python3 scripts/audit-no-bios-runtime.py --repo .

clean:
	rm -rf build
