Files
LithosAnanake/kernel/Makefile
T
rajamesandClaude Opus 5.5 d5b7235464 feat(v4.0.0): every node asks the kernel for its blocks; a born node is not POSTed
A block is a kernel request, as ENGINE.md 3.3 has it: the node puts the
block's number and the address of 256 cells on its stack and writes the
request to port 0, and the kernel leaves the status there.  The requests
are -1, read, and -2, write, the same for every node.  v4/system/blocks.c
serves them from the kernel's block subsystem, which is v3's.  The four
storage registers are gone from the engine.

The device that spoke block messages (4a505a15) is withdrawn with its
test and its message types: Captain Bob ruled on 2026-10-07 that it, a
node's own drive, and nodes with no storage had left the OS as designed
(docs/v4.0.0/MESH.md 8.5).

Hera no longer sends POST to the nodes she births: POST is the kernel's,
once.  Every node has its kernel on port 0; it serves a node's blocks and,
for Hera alone, her requests for nodes and capsules.

Bare metal: the node boots and is POSTed against POST's own block RAM,
and the kernel's chain -- fast RAM, the ramdrive, the virtio disk -- is
set up after POST and before the prompt, as on the v3 path.  The disk is
read and not written: nothing in v4 yet gives the owner's word that it
may be formatted.  A hosted program has the chain's fast RAM, as hosted
v3 has with no disk.  Error 17 is Storage refused.

make -C v4 test and sanitize pass at both widths; hosted-check passes on
three ISAs; amd64, aarch64 and riscv64 boot, POST 538 of 538, with the
typed session: logs/20261007-081603, -081839, -082226.  The hashes are
the same on all six.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:24:47 -04:00

1449 lines
63 KiB
Makefile

# ==============================================================================
# kernel/Makefile — StarKernel UEFI Build System
# Multi-architecture: amd64 (x86_64), aarch64 (ARM64), riscv64 (RISC-V 64)
#
# Quick start:
# make -f kernel/Makefile — build for host arch
# make -f kernel/Makefile ARCH=amd64 qemu — build + boot (x86_64)
# make -f kernel/Makefile ARCH=aarch64 qemu — build + boot (aarch64)
# make -f kernel/Makefile ARCH=riscv64 qemu — build + boot (riscv64)
# make -f kernel/Makefile help — show all targets
# ==============================================================================
# ==============================================================================
# CONFIGURATION
# ==============================================================================
# Architecture normalization — accept common aliases
# x86_64 → amd64
# arm64 → aarch64
# riscv → riscv64
canon_arch = $(strip \
$(if $(filter x86_64,$1),amd64, \
$(if $(filter amd64,$1),amd64, \
$(if $(filter arm64,$1),aarch64, \
$(if $(filter aarch64,$1),aarch64, \
$(if $(filter riscv riscv64,$1),riscv64,$1))))))
UNAME_M := $(shell uname -m)
ARCH ?= $(call canon_arch,$(UNAME_M))
override ARCH := $(call canon_arch,$(ARCH))
ifeq ($(filter amd64 aarch64 riscv64,$(ARCH)),)
$(error Unsupported ARCH='$(ARCH)'. Use: amd64, aarch64, riscv64)
endif
# Kconfig bridge (menuconfig/xconfig/config/oldconfig/*_defconfig). See
# mk/Kconfig.mk for why KCONFIG_ARCH_DIR must be set here, before include.
# .DEFAULT_GOAL is pinned explicitly because mk/Kconfig.mk's first rule
# would otherwise silently become Make's default goal, since it's included
# before this Makefile's own `all:` target is defined below.
.DEFAULT_GOAL := all
KCONFIG_ARCH_DIR := $(ARCH)
include mk/Kconfig.mk
# Target type (default: kernel)
TARGET ?= kernel
# Kernel-only flags (Phase 6): repointed through the Kconfig bridge, same
# drop-in-replacement-for-`?=` behavior as every other family in this
# migration. Kconfig.kernel's symbols only exist for a kernel-variant
# .config (`if STARFORTH_VARIANT_KERNEL`), so a hosted-variant .config
# leaves CONFIG_PARITY_MODE/etc undefined and kconfig_bool falls back to
# its $2 default exactly as if Kconfig were inactive -- this file is never
# built from a hosted-variant config in practice, but the fallback is
# correct either way.
# Parity mode (0 = normal, 1 = parity harness)
$(eval $(call kconfig_bool,PARITY_MODE,0))
# StarForth VM integration (default: enabled)
$(eval $(call kconfig_bool,STARFORTH_ENABLE_VM,1))
# StarForth v4 at a single prompt in place of the v3 VM (default: off)
$(eval $(call kconfig_bool,STARFORTH_V4,0))
# Monolithic build — loader + kernel compiled together (default: enabled)
MONOLITHIC ?= 1
# Parallel build — use all available cores when not cleaning
# (adding -j when 'clean' is also a goal causes a race on v3/include/version.h)
NPROC := $(shell nproc 2>/dev/null || echo 4)
ifeq ($(filter clean,$(MAKECMDGOALS)),)
MAKEFLAGS += -j$(NPROC)
endif
# Version
# Versioning policy (FABRIC-3.5.md §XXX, ratified 2026-09-19, replaces the old
# X.0.0/X.5.0 QEMU/hardware encoding this comment used to carry):
# Major: ODD = LTS line (non-breaking fixes only). EVEN = working line
# (breaking changes land here). Minor: release within the line.
# Patch: working builds within a release.
# v1.0.x — serial-only production (released, historical encoding)
# v1.5.x — framebuffer VT100 console milestone (released, historical encoding)
# v2.0.0 — QEMU release: three-arch QEMU story complete (released)
# v2.0.1 — SER5 hardware-track line (historical; superseded by this policy)
# v2.1.0 — Tripod/kernel reshuffle: Hermes into the kernel, Tripod = Hera/Artemis/Hestia
# v2.2.0 — amd64 bare-metal bring-up (Beelink SER5)
# v2.x — further working releases; riscv64 / aarch64 board bring-up
# v3.0.0 — FIRST LTS, when and only when FABRIC-3.5.md §XXX.5's 5 criteria are met
# LITHOS_VERSION (kernel) and VERSION (embedded StarForth engine) are independent
# and do not auto-sync (§XXX.4) — do not infer a relationship if they ever coincide.
# Engine VERSION bumps at tag time by what actually changed (§XXX.6): major = any
# FORTH-79-visible word semantics change; minor = words added/removed/relocated;
# patch = build-only. This reshuffle is a minor bump: BIRTH's registration widened,
# the messaging layer was replaced by kernel-Hermes, PLOT/FB-* relocated to Hestia —
# all dictionary-visible, no FORTH-79 word semantics changed.
VERSION ?= 3.2.0
LITHOS_VERSION ?= 2.1.0
# ==============================================================================
# BUILD PATHS
# ==============================================================================
BUILD_DIR := build/$(ARCH)/$(TARGET)
OBJ_DIR := $(BUILD_DIR)/obj
LOADER_OBJ_DIR := $(OBJ_DIR)/loader
KERNEL_OBJ_DIR := $(OBJ_DIR)/kernel
LOADER_EFI := $(BUILD_DIR)/starkernel_loader.efi
KERNEL_ELF := $(BUILD_DIR)/starkernel_kernel.elf
KERNEL_SRC := kernel/src
KERNEL_INC := kernel/include/starkernel
STARFORTH_CONFIG_HEADER := v3/include/starforth_config.h
QEMU_LOG_DIR := logs
QEMU_ISO := $(BUILD_DIR)/starkernel.iso
DOE_RUNS_DIR := experiments/bare_metal/runs
DOE_LATEST_DIR := experiments/bare_metal/latest
CAPSULES_DIR ?= capsules
ARTDISK ?= disk/artemis.img
# XHCI_PORTS -- real top-level port count the qemu-xhci controller exposes on the
# xhci0.0 bus (p2 == p3 == this value; these are the SAME physical dual-role ports
# counted from the USB2 and USB3 register sets respectively, NOT additive to p2+p3 --
# confirmed live 2026-09-06, the stock default p2=4,p3=4 exposes exactly 4 real ports,
# not 8). Every usb-storage device attached below or via QEMU_EXTRA must get its own
# explicit port=N less than this value: exceed it and QEMU silently drops the overflow
# onto an auto-inserted internal USB2 hub, which the xHCI/BOT driver correctly refuses
# to treat as Mass Storage (root cause of the FABRIC-3.md §IX.5 "4th-device enumeration
# failure" -- misread as a driver bug at the time, it never was one). Sized with real
# headroom above the current 9-device identity roster (Zuse + 8 minted identities)
# rather than matching it exactly, per Bob's standing ruling against hardcoding a bound
# to today's scale (FABRIC-3.md §VII.4). Override if a future test needs more, e.g.
# `make -f kernel/Makefile qemu XHCI_PORTS=32`.
XHCI_PORTS ?= 16
# ZUSEDISK -- Zuse's own minted USB thumbdrive, attached by default so a
# plain `make qemu` lands already authenticated into the Zuse identity
# (Captain Bob, 2026-08-28). Attached at QEMU launch time via -drive/
# -device on the qemu-xhci controller each arch's qemu target already
# creates; xhci_bringup()'s initial port scan (kernel/src/usb/xhci.c)
# is what makes an already-connected-at-launch device visible at all --
# xhci_poll_events() alone is purely hotplug-event-driven and would never
# see a device present before controller reset. Override to "" (empty) to
# boot without Zuse attached, e.g. `make -f kernel/Makefile qemu ZUSEDISK=`.
ZUSEDISK ?= disk/thumbdrives/zuse-thumb-ident.img
# Precomputed (not inlined as $(if ...,...)) because the drive/device specs
# below are comma-heavy and GNU make's $(if) function splits its own
# arguments on every top-level comma -- inlining it silently truncated the
# flags to just "-drive if=none" with everything past the next comma
# dropped, confirmed live via `ps aux` showing the truncated qemu command
# line (2026-08-28).
ifneq ($(strip $(ZUSEDISK)),)
ZUSEDISK_QEMU_ARGS := -drive if=none,id=zusedrv,file=$(ZUSEDISK),format=raw -device usb-storage,bus=xhci0.0,port=1,drive=zusedrv
else
ZUSEDISK_QEMU_ARGS :=
endif
MKCAPSULE_SRC = tools/mkcapsule.c tools/pkcs8_ed25519.c \
kernel/src/crypto/ed25519.c \
kernel/src/crypto/fe25519.c \
kernel/src/crypto/scalar25519.c \
kernel/src/crypto/sha512.c
MKCAPSULE_BIN = $(BUILD_DIR)/tools/mkcapsule
# Milestone 6 (Phase 8): the snakeoil intermediate's private key, generated
# offline outside this repo entirely (see FABRIC-2.md's Phase 8 §Milestone 6
# writeup) -- not present in CI or a fresh checkout, so signing is skipped
# gracefully ($(wildcard ...) below) rather than failing the build. Override
# with `make SIGN_KEY=/path/to/key.pem ...` on a machine that holds it.
SIGN_KEY ?= /home/rajames/CLionProjects/lithosananke-ca/intermediate/snakeoil-intermediate.key
SIGN_KEY_ARGS = $(if $(wildcard $(SIGN_KEY)),--sign-key $(SIGN_KEY),)
CAPSULE_GENERATED = $(BUILD_DIR)/capsule_generated.c
CAPSULE_GENERATED_OBJ = $(BUILD_DIR)/capsule_generated.o
CAPSULE_GENERATED_KOBJ = $(KERNEL_OBJ_DIR)/capsule_generated.o
# ==============================================================================
# BOARD (boot_image)
# ==============================================================================
# BOARD names a directory under boards/ (ser5, raspi, milkv, ...). Its
# board.mk sets BOARD_ARCH, BOARD_BOOT (how the firmware finds us) and
# BOARD_IMAGE (the single file written to the boot medium). The root
# Makefile's `make boot_image TARGET=<NAME>` maps NAME to BOARD and passes
# ARCH=$(BOARD_ARCH); the kernel objects themselves are the ordinary
# build/$(ARCH)/kernel ones, shared with `make qemu`.
BOARD ?=
ifneq ($(strip $(BOARD)),)
ifeq ($(wildcard boards/$(BOARD)/board.mk),)
$(error Unknown BOARD='$(BOARD)': no boards/$(BOARD)/board.mk)
endif
include boards/$(BOARD)/board.mk
ifneq ($(BOARD_ARCH),$(ARCH))
$(error BOARD=$(BOARD) is ARCH=$(BOARD_ARCH), but ARCH=$(ARCH) was requested)
endif
BOARD_OUT := build/boards/$(BOARD)
endif
BOOT_IMAGE_SIZE_MIB ?= 128
# ==============================================================================
# TOOLCHAIN
# ==============================================================================
ifeq ($(ARCH),amd64)
ifneq ($(shell which x86_64-elf-gcc 2>/dev/null),)
CC := x86_64-elf-gcc
LD := x86_64-elf-ld
OBJCOPY := x86_64-elf-objcopy
else
CC := gcc
LD := ld
OBJCOPY := objcopy
endif
# -fno-pic -fno-pie: this is a fixed-base, statically-linked freestanding
# image with no dynamic linker to populate a GOT at load time. -fPIC (set
# in COMMON_CFLAGS, needed by riscv64's -shared link pipeline) makes GCC
# emit R_X86_64_REX_GOTPCRELX (GOT-indirect) addressing for some cross-TU
# extern globals depending on per-call-site codegen heuristics; the "GOT
# slot" is just an unpopulated .bss cell here, so those reads silently
# return NULL instead of the real address. Root-caused 2026-08-06 via
# dict_find_word_heat_aware() reading sf_fc_count as NULL while
# vm_find_word() (same globals, same TU as the definition) read it
# correctly. These flags come after COMMON_CFLAGS on the command line so
# they win for amd64 only; riscv64/aarch64 keep -fPIC.
ARCH_CFLAGS := -m64 -march=x86-64 -mno-red-zone -DARCH_AMD64 -fno-pic -fno-pie
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-amd64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-amd64.ld
else ifeq ($(ARCH),aarch64)
ifneq ($(shell which aarch64-linux-gnu-gcc 2>/dev/null),)
CC := aarch64-linux-gnu-gcc
LD := aarch64-linux-gnu-ld
OBJCOPY := aarch64-linux-gnu-objcopy
else ifneq ($(shell which aarch64-none-elf-gcc 2>/dev/null),)
CC := aarch64-none-elf-gcc
LD := aarch64-none-elf-ld
OBJCOPY := aarch64-none-elf-objcopy
else
$(error No ARM64 toolchain found. Install aarch64-linux-gnu-gcc or aarch64-none-elf-gcc)
endif
# aarch64 loader uses clang + lld-link (COFF target — GCC cannot produce aarch64 PE)
# Any clang with the aarch64 COFF backend works; the version is not
# pinned (Ubuntu 26.04 ships clang-21, older hosts clang-18).
LOADER_CC := $(shell which clang 2>/dev/null || ls /usr/bin/clang-[0-9]* 2>/dev/null | sort -V | tail -1 || echo clang)
# Debian/Ubuntu package only installs the versioned lld-link-NN on PATH by
# default (unversioned lld-link lives under /usr/lib/llvm-NN/bin, which
# isn't on PATH unless explicitly prepended) — detect whichever resolves.
LOADER_LD := $(shell which lld-link 2>/dev/null || ls /usr/bin/lld-link-[0-9]* /usr/lib/llvm-*/bin/lld-link 2>/dev/null | sort -V | tail -1 || echo lld-link)
ARCH_CFLAGS := -march=armv8-a -mcpu=cortex-a72 -DARCH_AARCH64 -mno-outline-atomics
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-aarch64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-aarch64.ld
else ifeq ($(ARCH),riscv64)
ifneq ($(shell which riscv64-unknown-elf-gcc 2>/dev/null),)
CC := riscv64-unknown-elf-gcc
LD := riscv64-unknown-elf-ld
OBJCOPY := riscv64-unknown-elf-objcopy
else ifneq ($(shell which riscv64-linux-gnu-gcc 2>/dev/null),)
CC := riscv64-linux-gnu-gcc
LD := riscv64-linux-gnu-ld
OBJCOPY := riscv64-linux-gnu-objcopy
else
$(error No RISC-V toolchain found. Install riscv64-unknown-elf-gcc or riscv64-linux-gnu-gcc)
endif
ARCH_CFLAGS := -march=rv64gc -mabi=lp64d -DARCH_RISCV64
LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-riscv64.ld
KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-riscv64.ld
endif
# Default loader toolchain = kernel toolchain (overridden for aarch64 above)
LOADER_CC ?= $(CC)
LOADER_LD ?= $(LD)
# ==============================================================================
# COMPILER FLAGS
# ==============================================================================
# Common flags for all arches / both loader and kernel.
# -fPIC + -fvisibility=hidden: needed by riscv64's -shared loader link
# pipeline (amd64 overrides back to -fno-pic/-fno-pie in its ARCH_CFLAGS
# above -- see that comment for why: -fPIC made GCC emit GOT-indirect
# addressing for some cross-TU extern globals, and this freestanding image
# has no dynamic linker to populate a GOT, so those reads silently returned
# NULL. PLATFORM_TIME_NO_INLINE (removed 2026-08-06) was a symbol-specific
# workaround for the same underlying bug, made unnecessary once amd64 got
# the real fix; see FABRIC-0.md for the write-up).
COMMON_CFLAGS := \
-std=c99 -Wall -Werror -Wextra \
-O2 -U_FORTIFY_SOURCE \
-ffreestanding -nostdlib -fno-builtin \
-fPIC -fvisibility=hidden -fno-stack-protector -fshort-wchar \
$(ARCH_CFLAGS) \
-I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
-include $(STARFORTH_CONFIG_HEADER) \
-DPARITY_MODE=$(PARITY_MODE)
VMCORE_CFLAGS_COMMON := \
$(filter-out -I$(KERNEL_INC),$(COMMON_CFLAGS)) \
-Iv3/include -Ikernel/include -I. -Iv3/src/word_source -Iv3/src/test_runner/include \
-Wno-error=unused-parameter -Wno-error=shift-negative-value \
-Wno-error=sign-compare -Wno-error=missing-field-initializers
# aarch64 loader flags: clang COFF target — drop ELF-only flags (-fPIC,
# -fvisibility=hidden), add COFF-specific flags instead.
ifeq ($(ARCH),aarch64)
LOADER_BASE_CFLAGS := \
-std=c99 -Wall -Werror -Wextra \
--target=aarch64-pc-windows-msvc \
-ffreestanding -fno-builtin \
-fno-stack-protector -fshort-wchar \
-mno-stack-arg-probe \
-march=armv8-a \
-DARCH_AARCH64 \
-I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
-Ikernel/include/starkernel/freestanding \
-include $(STARFORTH_CONFIG_HEADER) \
-DPARITY_MODE=$(PARITY_MODE) \
-DPLATFORM_TIME_NO_INLINE
else
LOADER_BASE_CFLAGS := $(COMMON_CFLAGS)
endif
DOE_INJECT ?= 0
DOE_REPS ?= 3
DOE_SEED ?= 12345
# EMERGENCY_CONSOLE_ENABLED is blanket-forwarded (unlike the opt-in-only
# knobs below), so it's repointed through the Kconfig bridge the same way
# as the hosted Makefile's STRICT_PTR (Phase 3) -- Phase 4 heartbeat family.
# Default flipped 0 (was 1) 2026-09-05: headless-until-login is now the
# production default (Kconfig.heartbeat's own default n, same decision).
$(eval $(call kconfig_bool,EMERGENCY_CONSOLE_ENABLED,0))
LOADER_CFLAGS := $(LOADER_BASE_CFLAGS) -I$(KERNEL_SRC)/vm
KERNEL_CFLAGS := $(COMMON_CFLAGS) -D__STARKERNEL__ -I$(KERNEL_SRC)/vm
# Linker flags
ifeq ($(ARCH),aarch64)
LOADER_LDFLAGS_PE := # lld-link called directly in link rule
else ifeq ($(ARCH),riscv64)
LOADER_LDFLAGS_PE := # ELF→objcopy pipeline in link rule
else
LOADER_LINKER_SCRIPT_PE := kernel/linker/starkernel-loader-amd64-pe.ld
LOADER_LDFLAGS_PE := \
-T $(LOADER_LINKER_SCRIPT_PE) -m i386pep -nostdlib \
--enable-reloc-section --image-base 0 --subsystem 10 -e efi_main
endif
LOADER_LDFLAGS := \
-T $(LOADER_LINKER_SCRIPT) -nostdlib --build-id=none \
--emit-relocs -shared -Bsymbolic -znocombreloc -e efi_main
KERNEL_LDFLAGS := \
-T $(KERNEL_LINKER_SCRIPT) -nostdlib --build-id=none \
--emit-relocs -e kernel_main
# ==============================================================================
# VM INTEGRATION FLAGS
# ==============================================================================
ifeq ($(STARFORTH_ENABLE_VM),1)
KERNEL_CFLAGS += -DSTARFORTH_ENABLE_VM=1 -DSTARFORTH_MINIMAL=1
LOADER_CFLAGS += -DSTARFORTH_ENABLE_VM=1 -DSTARFORTH_MINIMAL=1
# Physics/SSM knob family: pre-assign from the Kconfig-fed CONFIG_* value
# when a build/$(ARCH)/.config exists (Kconfig migration Phase 3). This is
# additive to, not a replacement for, this Makefile's existing opt-in
# philosophy below: a plain `:=`/derived assignment here makes
# `origin($1)` "file" (not "undefined"), so `add_vm_flag` picks it up and
# forwards it -- exactly as if a developer had passed it on the command
# line. When no .config exists (KCONFIG_ACTIVE unset), none of this fires,
# and a knob is only forwarded if a developer explicitly sets it, same as
# before this migration touched the kernel build at all. Command-line
# values always win regardless, per ordinary Make semantics.
ifeq ($(KCONFIG_ACTIVE),1)
$(eval $(call kconfig_bool,STRICT_PTR,1))
$(eval $(call kconfig_bool,ENABLE_HOTWORDS_CACHE,0))
$(eval $(call kconfig_bool,ENABLE_PIPELINING,0))
$(eval $(call kconfig_int,ROLLING_WINDOW_SIZE,4096))
$(eval $(call kconfig_int,TRANSITION_WINDOW_SIZE,8))
$(eval $(call kconfig_int,ADAPTIVE_SHRINK_RATE,50))
$(eval $(call kconfig_int,ADAPTIVE_MIN_WINDOW_SIZE,256))
$(eval $(call kconfig_int,ADAPTIVE_CHECK_FREQUENCY,512))
$(eval $(call kconfig_int,ADAPTIVE_GROWTH_THRESHOLD,5))
$(eval $(call kconfig_int,INITIAL_DECAY_SLOPE_Q48,21845))
$(eval $(call kconfig_int,DECAY_MIN_INTERVAL,500))
$(eval $(call kconfig_int,DECAY_RATE_PER_US_Q16,1))
$(eval $(call kconfig_int,HEARTBEAT_INFERENCE_FREQUENCY,1000))
$(eval $(call kconfig_int,STADIUM_MAX_VM_COUNT,4))
$(eval $(call kconfig_int,STADIUM_CONTAINS_DEPTH_MAX,5))
$(eval $(call kconfig_int,STADIUM_CAPACITY_TICK,1000))
$(eval $(call kconfig_int,STADIUM_MEMORY_PERCENT,1))
$(eval $(call kconfig_int,STADIUM_WORD_HEAT_QUANTUM,2048))
$(eval $(call kconfig_int,STADIUM_WORD_COOL_RATE_Q48,21845))
$(eval $(call kconfig_int,STADIUM_BLOCK_HEAT_QUANTUM,2048))
$(eval $(call kconfig_int,STADIUM_BLOCK_COOL_RATE_Q48,21845))
$(eval $(call kconfig_int,STADIUM_BLOCK_TRACK_CAP_MULT,2))
$(eval $(call kconfig_int,SSM_ENTROPY_HIGH_THRESHOLD,0.75))
$(eval $(call kconfig_int,SSM_CV_HIGH_THRESHOLD,0.15))
$(eval $(call kconfig_int,SSM_TEMPORAL_DECAY_THRESHOLD,0.5))
$(eval $(call kconfig_int,SSM_TEMPORAL_DECAY_LOW_THRESHOLD,0.3))
$(eval $(call kconfig_int,SSM_HYSTERESIS_TICKS,5))
# Heartbeat family (Phase 4). HEARTBEAT_THREAD_ENABLED/HEARTBEAT_TICK_NS
# `depends on STARFORTH_VARIANT_HOSTED` in Kconfig.heartbeat, so for a
# kernel-variant .config CONFIG_HEARTBEAT_THREAD_ENABLED is simply absent
# and kconfig_bool resolves it to 0 -- correctly, since LithosAnanke has no
# pthreads -- with no special case needed here. The unconditional
# `VM_FEATURE_OVERRIDES += -DHEARTBEAT_THREAD_ENABLED=0` below is kept as a
# deliberate second, independent enforcement of the same fact; see
# Kconfig.heartbeat's help text for why both layers exist.
$(eval $(call kconfig_bool,HEARTBEAT_THREAD_ENABLED,0))
$(eval $(call kconfig_int,HEARTBEAT_TICK_NS,10000))
$(eval $(call kconfig_int,HEARTBEAT_CHECK_FREQUENCY,256))
$(eval $(call kconfig_int,HEARTBEAT_WINDOW_TUNING_FREQUENCY,1000))
$(eval $(call kconfig_int,HEARTBEAT_SLOPE_VALIDATION_FREQUENCY,5000))
# Remaining physics_pipelining_metrics.h constants (Phase 5). Previously
# hardcoded, non-overridable #defines; see Kconfig.physics for the exact
# hex/decimal defaults. MINIMUM_PREFETCH_ROI's default was corrected from
# the long-shipped 0x11999A to 0x1199A (rev y) -- the former was ~17.6 in
# Q48.16, not the documented 1.10.
$(eval $(call kconfig_int,SPECULATION_THRESHOLD_Q48,0x8000))
$(eval $(call kconfig_int,SPECULATION_DEPTH,1))
$(eval $(call kconfig_int,MIN_SAMPLES_FOR_SPECULATION,10))
$(eval $(call kconfig_int,MISPREDICTION_COST_Q48,0x190000))
$(eval $(call kconfig_int,MINIMUM_PREFETCH_ROI,0x1199A))
# SK_PARITY_DEBUG (Phase 6): previously opt-in-only via VM_FEATURE_FLAG_VARS
# below with no Kconfig presence at all.
$(eval $(call kconfig_bool,SK_PARITY_DEBUG,0))
endif
VM_FEATURE_FLAG_VARS := \
STRICT_PTR ENABLE_HOTWORDS_CACHE ENABLE_PIPELINING \
ROLLING_WINDOW_SIZE TRANSITION_WINDOW_SIZE \
ADAPTIVE_SHRINK_RATE ADAPTIVE_MIN_WINDOW_SIZE \
ADAPTIVE_CHECK_FREQUENCY ADAPTIVE_GROWTH_THRESHOLD \
INITIAL_DECAY_SLOPE_Q48 DECAY_MIN_INTERVAL \
DECAY_RATE_PER_US_Q16 HEARTBEAT_THREAD_ENABLED \
HEARTBEAT_TICK_NS HEARTBEAT_INFERENCE_FREQUENCY \
HEARTBEAT_CHECK_FREQUENCY HEARTBEAT_WINDOW_TUNING_FREQUENCY \
HEARTBEAT_SLOPE_VALIDATION_FREQUENCY SK_PARITY_DEBUG \
EMERGENCY_CONSOLE_ENABLED STADIUM_MAX_VM_COUNT \
STADIUM_CONTAINS_DEPTH_MAX STADIUM_CAPACITY_TICK \
STADIUM_MEMORY_PERCENT \
SSM_ENTROPY_HIGH_THRESHOLD SSM_CV_HIGH_THRESHOLD \
SSM_TEMPORAL_DECAY_THRESHOLD SSM_TEMPORAL_DECAY_LOW_THRESHOLD \
SSM_HYSTERESIS_TICKS \
SPECULATION_THRESHOLD_Q48 SPECULATION_DEPTH \
MIN_SAMPLES_FOR_SPECULATION MISPREDICTION_COST_Q48 \
MINIMUM_PREFETCH_ROI
VM_FEATURE_OVERRIDES :=
define add_vm_flag
ifneq ($$(origin $1),undefined)
VM_FEATURE_OVERRIDES += -D$1=$$($1)
endif
endef
$(foreach f,$(VM_FEATURE_FLAG_VARS),$(eval $(call add_vm_flag,$f)))
VM_FEATURE_OVERRIDES += -DHEARTBEAT_THREAD_ENABLED=0
KERNEL_CFLAGS += $(VM_FEATURE_OVERRIDES)
LOADER_CFLAGS += $(VM_FEATURE_OVERRIDES)
endif
# SK_CMD — optional startup FORTH script injected before the interactive REPL.
# Usage: make -f kernel/Makefile qemu SK_CMD="TIME-TICKS . BYE"
ifdef SK_CMD
KERNEL_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
LOADER_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
endif
# KERNEL_ARGS — runtime boot flags written into starforth.cfg on the ESP image.
# The loader reads this file from the EFI partition and parses it at boot.
# Supported flags: --doe --log-level=<debug|info|warn|error>
# --stack=<N>[MG] --heap=<N>[MG]
# Usage: make -f kernel/Makefile qemu KERNEL_ARGS="--doe --log-level=info"
KERNEL_ARGS ?=
# ==============================================================================
# SOURCE FILES
# ==============================================================================
ifeq ($(MONOLITHIC),1)
LOADER_CFLAGS += -DMONOLITHIC_BUILD -D__STARKERNEL__
LOADER_SRCS_BASE := \
$(KERNEL_SRC)/boot/uefi_loader.c \
$(KERNEL_SRC)/boot/elf_loader.c \
$(KERNEL_SRC)/boot/reloc_stub.c \
$(KERNEL_SRC)/boot/cmdline.c \
$(KERNEL_SRC)/kernel_main.c \
$(wildcard $(KERNEL_SRC)/hal/*.c) \
$(wildcard $(KERNEL_SRC)/memory/*.c) \
$(wildcard $(KERNEL_SRC)/math/*.c) \
$(wildcard $(KERNEL_SRC)/hash/*.c) \
$(wildcard $(KERNEL_SRC)/crypto/*.c) \
$(wildcard $(KERNEL_SRC)/capsule/*.c) \
$(wildcard $(KERNEL_SRC)/pci/*.c) \
$(wildcard $(KERNEL_SRC)/virtio/*.c) \
$(wildcard $(KERNEL_SRC)/rng/*.c) \
$(wildcard $(KERNEL_SRC)/usb/*.c) \
$(KERNEL_SRC)/repl.c \
$(KERNEL_SRC)/doe_log.c \
$(KERNEL_SRC)/heartbeat.c \
$(KERNEL_SRC)/homeblocks_sig.c \
$(KERNEL_SRC)/artemis_sig.c \
$(KERNEL_SRC)/log_region.c
LOADER_ASM := \
$(KERNEL_SRC)/arch/$(ARCH)/boot.S \
$(KERNEL_SRC)/arch/$(ARCH)/isr.S \
$(KERNEL_SRC)/arch/$(ARCH)/switch.S
ifeq ($(ARCH),amd64)
LOADER_ASM += $(KERNEL_SRC)/arch/$(ARCH)/kernel_entry.S
endif
ifeq ($(ARCH),riscv64)
LOADER_ASM += $(KERNEL_SRC)/arch/$(ARCH)/kernel_entry.S
endif
LOADER_ARCH_SRCS := \
$(KERNEL_SRC)/arch/$(ARCH)/arch.c \
$(KERNEL_SRC)/arch/$(ARCH)/interrupts.c \
$(KERNEL_SRC)/arch/$(ARCH)/apic.c \
$(KERNEL_SRC)/arch/$(ARCH)/timer.c
ifeq ($(ARCH),amd64)
LOADER_ARCH_SRCS += \
$(KERNEL_SRC)/arch/$(ARCH)/ioapic.c \
$(KERNEL_SRC)/arch/$(ARCH)/i8042.c
endif
ifeq ($(ARCH),riscv64)
LOADER_ARCH_SRCS += \
$(KERNEL_SRC)/arch/$(ARCH)/plic.c
endif
else
LOADER_SRCS_BASE := \
$(KERNEL_SRC)/boot/uefi_loader.c \
$(KERNEL_SRC)/boot/elf_loader.c \
$(KERNEL_SRC)/boot/reloc_stub.c
LOADER_ASM :=
LOADER_ARCH_SRCS := $(KERNEL_SRC)/arch/$(ARCH)/arch.c
endif
KERNEL_SRCS_BASE := \
$(KERNEL_SRC)/kernel_main.c \
$(KERNEL_SRC)/boot/cmdline.c \
$(wildcard $(KERNEL_SRC)/hal/*.c) \
$(wildcard $(KERNEL_SRC)/memory/*.c) \
$(wildcard $(KERNEL_SRC)/math/*.c) \
$(wildcard $(KERNEL_SRC)/hash/*.c) \
$(wildcard $(KERNEL_SRC)/crypto/*.c) \
$(wildcard $(KERNEL_SRC)/vm_arbiter/*.c) \
$(wildcard $(KERNEL_SRC)/capsule/*.c) \
$(wildcard $(KERNEL_SRC)/pci/*.c) \
$(wildcard $(KERNEL_SRC)/virtio/*.c) \
$(wildcard $(KERNEL_SRC)/rng/*.c) \
$(wildcard $(KERNEL_SRC)/usb/*.c) \
$(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.c) \
$(KERNEL_SRC)/repl.c \
$(KERNEL_SRC)/doe_log.c \
$(KERNEL_SRC)/heartbeat.c \
$(KERNEL_SRC)/homeblocks_sig.c \
$(KERNEL_SRC)/artemis_sig.c \
$(KERNEL_SRC)/log_region.c
KERNEL_ASM := $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.S)
ifeq ($(STARFORTH_ENABLE_VM),1)
VM_ALL_SRCS := \
$(wildcard v3/src/*.c) \
$(wildcard v3/src/word_source/*.c) \
$(wildcard v3/src/test_runner/*.c) \
$(wildcard v3/src/test_runner/modules/*.c)
VM_EXCLUDE := \
v3/src/main.c v3/src/cli.c v3/src/repl.c \
v3/src/platform/% \
v3/src/blkio_factory.c v3/src/blkio_file.c \
v3/src/log.c v3/src/doe_metrics.c \
v3/src/vm.c v3/src/vm_core.c v3/src/vm_bootstrap.c v3/src/vm_runtime.c v3/src/vm_time.c \
v3/src/word_source/q48_16_words.c \
v3/src/test_runner/modules/break_me_tests.c
VM_CORE_SRCS := $(filter-out $(VM_EXCLUDE),$(VM_ALL_SRCS))
LOADER_EXTRA_SRCS := \
$(KERNEL_SRC)/vm/arena.c \
$(KERNEL_SRC)/vm/switch.c \
$(KERNEL_SRC)/vm/parity.c \
$(KERNEL_SRC)/vm/bootstrap/sk_vm_bootstrap.c \
$(KERNEL_SRC)/vm/host/shim.c \
$(KERNEL_SRC)/vm/vm_core.c \
$(KERNEL_SRC)/vm/vm_bootstrap.c \
$(KERNEL_SRC)/vm/vm_runtime.c \
$(KERNEL_SRC)/vm/alloc_kernel.c \
$(KERNEL_SRC)/vm/q48_stubs.c \
$(KERNEL_SRC)/vm/stadium.c \
$(KERNEL_SRC)/vm/stadium_words.c \
$(KERNEL_SRC)/vm/stadium_blocks.c \
$(KERNEL_SRC)/vm/session.c \
$(KERNEL_SRC)/vm/kernel_hermes.c
KERNEL_EXTRA_SRCS := $(LOADER_EXTRA_SRCS)
LOADER_VM_OBJS := $(patsubst v3/src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
KERNEL_VM_OBJS := $(patsubst v3/src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
endif
# ------------------------------------------------------------------------------
# StarForth v4 (STARFORTH_V4=1): the golden model of the F18-derived engine,
# v4/src, and the nucleus image v4's own Makefile builds on this machine from
# v4/capsule (docs/v4.0.0/NUCLEUS.md). The node is the host node's size, with 64-bit cells, as every
# ISA this kernel boots on has (DECOMPOSITION.md D-5), so the image is the
# same file for all three.
# ------------------------------------------------------------------------------
ifeq ($(STARFORTH_V4),1)
V4_DEFS := -DSTARFORTH_V4=1 -Iv4/include \
-DV4_CELL_BITS=64 -DV4_NODE_WORDS=16384 -DV4_DATA_RING=30 -DV4_RET_RING=31
KERNEL_CFLAGS += $(V4_DEFS)
LOADER_CFLAGS += $(V4_DEFS)
V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c)
V4_IMAGE_C := $(BUILD_DIR)/v4_image_64.c
LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
# v4/system/boot.c is the boot the hosted v4 system runs too: nucleus, then
# the capsules from this kernel's own capsule directory, then the prompt.
LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_image.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o
KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_image.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o
endif
LOADER_SRCS := $(LOADER_SRCS_BASE) $(LOADER_EXTRA_SRCS)
KERNEL_SRCS := $(KERNEL_SRCS_BASE) $(KERNEL_EXTRA_SRCS)
LOADER_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ARCH_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ASM)) \
$(LOADER_VM_OBJS) \
$(LOADER_V4_OBJS) \
$(CAPSULE_GENERATED_OBJ)
KERNEL_OBJS := \
$(patsubst $(KERNEL_SRC)/%.c,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_SRCS)) \
$(patsubst $(KERNEL_SRC)/%.S,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_ASM)) \
$(KERNEL_VM_OBJS) \
$(KERNEL_V4_OBJS) \
$(CAPSULE_GENERATED_KOBJ)
# ==============================================================================
# PHONY TARGETS
# ==============================================================================
.PHONY: all clean clean-kernel kernel kernel-all
.PHONY: qemu qemu-esp qemu-gdb
.PHONY: thumbdrive iso-usb
.PHONY: info help
.PHONY: FORCE
# ==============================================================================
# MAIN TARGETS
# ==============================================================================
all: v3/include/version.h $(CAPSULE_GENERATED_OBJ) $(CAPSULE_GENERATED_KOBJ) $(LOADER_EFI) $(KERNEL_ELF)
@echo "StarKernel built successfully for $(ARCH)"
@echo " Loader: $(LOADER_EFI)"
@echo " Kernel: $(KERNEL_ELF)"
kernel: all
kernel-all:
@$(MAKE) -f kernel/Makefile ARCH=amd64 TARGET=$(TARGET)
@$(MAKE) -f kernel/Makefile ARCH=aarch64 TARGET=$(TARGET)
@$(MAKE) -f kernel/Makefile ARCH=riscv64 TARGET=$(TARGET)
clean:
rm -rf build/$(ARCH)/$(TARGET)
rm -f v3/include/version.h
clean-kernel:
@echo "Cleaning all StarKernel build artifacts..."
@rm -rf build/*/kernel
@echo "Done."
# ==============================================================================
# BUILD RULES
# ==============================================================================
FORCE:
# FORCE prerequisite (matches the hosted Makefile's own pattern, Makefile:613/626):
# this file is also written by the hosted Makefile with different, incompatible
# content (no LITHOS_VERSION/LITHOS_VERSION_STR) -- without FORCE, a bare build
# here after a hosted `make` run silently reuses that stale/wrong-flavored file
# and fails with "LITHOS_VERSION_STR undeclared" deep in kernel_main.c, instead
# of regenerating its own correct version.
v3/include/version.h: FORCE
@mkdir -p v3/include
@BUILD_TS=$$(date -Iseconds 2>/dev/null || echo "unknown"); \
printf '#ifndef STARFORTH_VERSION_H\n#define STARFORTH_VERSION_H\n\n' > $@; \
printf '#define STARFORTH_VERSION "%s"\n' "$(VERSION)" >> $@; \
printf '#define STARFORTH_ARCH "%s"\n' "$(ARCH)" >> $@; \
printf '#define STARFORTH_TARGET "%s"\n' "kernel" >> $@; \
printf '#define STARFORTH_TIMESTAMP "%s"\n' "$$BUILD_TS" >> $@; \
printf '#define STARFORTH_VERSION_FULL "StarForth v%s %s kernel %s"\n\n' "$(VERSION)" "$(ARCH)" "$$BUILD_TS" >> $@; \
printf '#define LITHOS_VERSION "%s"\n' "$(LITHOS_VERSION)" >> $@; \
printf '#define LITHOS_VERSION_STR "LithosAnanke v%s"\n\n' "$(LITHOS_VERSION)" >> $@; \
printf '#endif /* STARFORTH_VERSION_H */\n' >> $@
# Host tool: capsule packer
$(MKCAPSULE_BIN): $(MKCAPSULE_SRC)
@mkdir -p $(dir $@)
@echo "HOSTCC $(MKCAPSULE_SRC)"
@cc -std=c99 -Wall -Wextra -O2 -Iv3/include -Ikernel/include -Itools -o $@ $(MKCAPSULE_SRC)
# Generate capsule_generated.c from capsules/
CAPSULE_SRCS := $(shell find $(CAPSULES_DIR) -type f ! -name '.*' 2>/dev/null)
$(CAPSULE_GENERATED): $(MKCAPSULE_BIN) $(CAPSULE_SRCS)
@mkdir -p $(dir $@)
@echo " MKCAP $(CAPSULES_DIR) -> $@"
@$(MKCAPSULE_BIN) $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $@
@echo " MKCAP $(CAPSULES_DIR) -> $(CAPSULES_DIR)/BLOCK_MAP.md"
@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
# manifest — regenerate capsules/BLOCK_MAP.md without a full kernel build
.PHONY: manifest
manifest: $(MKCAPSULE_BIN)
@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
@echo " Wrote $(CAPSULES_DIR)/BLOCK_MAP.md"
$(CAPSULE_GENERATED_OBJ): $(CAPSULE_GENERATED)
@echo " CC $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(CAPSULE_GENERATED_KOBJ): $(CAPSULE_GENERATED) | $(KERNEL_OBJ_DIR)
@echo " CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
# Create build directories
$(BUILD_DIR) $(LOADER_OBJ_DIR) $(KERNEL_OBJ_DIR):
@mkdir -p \
$(LOADER_OBJ_DIR)/boot $(LOADER_OBJ_DIR)/arch/$(ARCH) \
$(LOADER_OBJ_DIR)/vm $(LOADER_OBJ_DIR)/capsule $(LOADER_OBJ_DIR)/hash
@mkdir -p \
$(KERNEL_OBJ_DIR)/boot $(KERNEL_OBJ_DIR)/hal $(KERNEL_OBJ_DIR)/memory \
$(KERNEL_OBJ_DIR)/vm_arbiter $(KERNEL_OBJ_DIR)/capsule \
$(KERNEL_OBJ_DIR)/hash $(KERNEL_OBJ_DIR)/arch/$(ARCH) $(KERNEL_OBJ_DIR)/vm
# Compile loader C sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
# Compile loader VM core sources
$(LOADER_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
ifeq ($(ARCH),aarch64)
@$(LOADER_CC) $(LOADER_CFLAGS) \
-Iv3/include -Ikernel/include -I. -Iv3/src/test_runner/include \
-Wno-error=unused-parameter -Wno-error=shift-negative-value \
-Wno-error=sign-compare -Wno-error=missing-field-initializers \
-Wno-unused-function \
-c $< -o $@
else
@$(LOADER_CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(LOADER_CFLAGS)) -c $< -o $@
endif
# StarForth v4: the capsule image, built on this machine by v4's Makefile, and
# the engine, compiled like any other kernel source.
ifeq ($(STARFORTH_V4),1)
# The nucleus is a capsule (capsules/v4/nucleus-64.f18), written by the same
# step that writes its description, so the capsule directory waits for it.
$(CAPSULE_GENERATED): $(V4_IMAGE_C)
$(V4_IMAGE_C): FORCE
@mkdir -p $(dir $@)
@echo " V4IMG v4/capsule -> $@"
@$(MAKE) --no-print-directory -C v4 CC=cc build/v4_image_64.c
@cmp -s v4/build/v4_image_64.c $@ || cp v4/build/v4_image_64.c $@
$(LOADER_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/v4_image.o: $(V4_IMAGE_C) | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
endif
# Compile loader assembly sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "AS (loader) $<"
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
# Compile kernel C sources
$(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
# Compile kernel VM core sources
$(KERNEL_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(KERNEL_CFLAGS)) -Ikernel/src/vm -c $< -o $@
# Compile kernel assembly sources
$(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "AS (kernel) $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
# ==============================================================================
# LINK RULES
# ==============================================================================
# Link loader to PE32+
# amd64: ld -m i386pep → PE directly
# aarch64: lld-link links COFF objects → PE (auto-generates .reloc)
# riscv64: ELF → gen_pe_reloc.py patches addends → objcopy → pei-riscv64-little
$(LOADER_EFI): $(LOADER_OBJS) | $(BUILD_DIR)
ifeq ($(ARCH),aarch64)
@echo "LLD-LINK (loader PE) $@"
@$(LOADER_LD) /subsystem:EFI_APPLICATION /entry:efi_main /nodefaultlib \
/align:4096 /base:0 /out:$@ $(LOADER_OBJS)
@echo "Loader built: $@"
else ifeq ($(ARCH),riscv64)
@echo "LD+OBJCOPY (loader ELF→PE) $@"
@$(LD) -T $(LOADER_LINKER_SCRIPT) -nostdlib --build-id=none \
-shared -Bsymbolic -znocombreloc -e efi_main -m elf64lriscv \
$(LOADER_OBJS) -o $(BUILD_DIR)/starkernel_loader_tmp.elf
@python3 scripts/gen_pe_reloc.py \
$(BUILD_DIR)/starkernel_loader_tmp.elf \
$(BUILD_DIR)/starkernel_loader_patched.elf \
$(BUILD_DIR)/reloc_rv64.bin
@$(OBJCOPY) \
--remove-section=.rela.dyn \
--remove-section=.dynstr \
--remove-section=.hash \
--remove-section=.gnu.hash \
--remove-section=.dynsym \
--remove-section=.dynamic \
--remove-section=.riscv.attributes \
--update-section=.reloc=$(BUILD_DIR)/reloc_rv64.bin \
$(BUILD_DIR)/starkernel_loader_patched.elf \
$(BUILD_DIR)/starkernel_loader_stripped.elf
@$(OBJCOPY) -O pei-riscv64-little --subsystem 10 \
$(BUILD_DIR)/starkernel_loader_stripped.elf $@
@echo "Loader built: $@"
else
@echo "LD (loader PE) $@"
@$(LD) $(LOADER_LDFLAGS_PE) $(LOADER_OBJS) -o $@
@echo "Loader built: $@"
endif
# Keep ELF for debugging
$(BUILD_DIR)/starkernel_loader.elf: $(LOADER_OBJS) $(LOADER_LINKER_SCRIPT) | $(BUILD_DIR)
@echo "LD (loader ELF) $@"
@$(LD) $(LOADER_LDFLAGS) $(LOADER_OBJS) -o $@
# Link kernel ELF
$(KERNEL_ELF): $(KERNEL_OBJS) $(KERNEL_LINKER_SCRIPT) | $(BUILD_DIR)
@echo "LD (kernel) $@"
@$(LD) $(KERNEL_LDFLAGS) $(KERNEL_OBJS) -o $@
@echo "Kernel built: $@"
# ==============================================================================
# QEMU TARGETS
# ==============================================================================
# qemu — full build, pack boot media, boot under QEMU, serial → timestamped log
#
# amd64: El Torito ISO + OVMF pflash
# aarch64: El Torito ISO + AAVMF -bios
# riscv64: GPT/FAT32 disk image + EDK2 pflash (cdrom/bios don't work on virt)
#
# Requires: qemu-system-* ovmf/qemu-efi-* xorriso mtools gdisk dosfstools
OVMF_CODE := /usr/share/OVMF/OVMF_CODE_4M.fd
OVMF_VARS_RO := /usr/share/OVMF/OVMF_VARS_4M.fd
# QEMU_DISPLAY — window backend for the framebuffer console (gtk/sdl/none/...).
# Usage: make -f kernel/Makefile qemu QEMU_DISPLAY=none # headless/CI
QEMU_DISPLAY ?= gtk
# QEMU_EXTRA — extra raw qemu-system-* args appended verbatim to every arch's
# qemu invocation below. Empty by default (no behavior change for a normal
# boot). Existing use: attaching a disposable USB test image for WRITE(10)
# validation, e.g.
# QEMU_EXTRA="-drive if=none,id=usbtest,file=disk/usbwrite-test.img,format=raw \
# -device usb-storage,bus=xhci0.0,port=2,drive=usbtest"
# Give every extra usb-storage device its own explicit port= (ZUSEDISK_QEMU_ARGS
# above already claims port 1), distinct from every other device attached on
# bus=xhci0.0 -- without it, QEMU's own port auto-assignment fills the
# controller's real top-level ports in device order and then silently drops
# any further device behind an auto-inserted internal USB2 hub, which the
# xHCI/BOT driver correctly refuses to treat as Mass Storage (confirmed live,
# 2026-09-06, root cause of the FABRIC-3.md §IX.5 "4th-device enumeration
# failure" -- it was never a driver bug, the emulated controller only exposes
# 4 real ports for p2=4,p3=4 despite the port count parameters implying 8).
# The qemu-xhci controller below is sized via XHCI_PORTS (see its own doc comment
# above) to give real ports to Zuse plus every identity drive attached this way.
QEMU_EXTRA ?=
qemu: all
ifeq ($(ARCH),amd64)
@echo "=== Building ISO (amd64) ==="
@mkdir -p $(BUILD_DIR)/iso
@dd if=/dev/zero of=$(BUILD_DIR)/iso/efi.img bs=512 count=8192 2>/dev/null
@mformat -i $(BUILD_DIR)/iso/efi.img ::
@mmd -i $(BUILD_DIR)/iso/efi.img ::/EFI
@mmd -i $(BUILD_DIR)/iso/efi.img ::/EFI/BOOT
@mcopy -i $(BUILD_DIR)/iso/efi.img $(LOADER_EFI) ::/EFI/BOOT/BOOTX64.EFI
$(if $(KERNEL_ARGS),\
@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/iso/starforth.cfg && \
mcopy -i $(BUILD_DIR)/iso/efi.img $(BUILD_DIR)/iso/starforth.cfg ::/starforth.cfg,)
@xorriso -as mkisofs -r -J \
-e efi.img -no-emul-boot \
-o $(QEMU_ISO) $(BUILD_DIR)/iso 2>&1
@if [ ! -f $(OVMF_CODE) ]; then echo "Error: $(OVMF_CODE) not found"; exit 1; fi
@if [ ! -f $(OVMF_VARS_RO) ]; then echo "Error: $(OVMF_VARS_RO) not found"; exit 1; fi
@if [ ! -f $(BUILD_DIR)/OVMF_VARS.fd ]; then \
cp $(OVMF_VARS_RO) $(BUILD_DIR)/OVMF_VARS.fd; \
fi
@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
@TS=$$(date +%Y%m%d-%H%M%S); \
SESSION=$(QEMU_LOG_DIR)/$$TS; \
mkdir -p $$SESSION/amd64; \
LOG=$$SESSION/amd64/qemu-amd64-$$TS.log; \
CSV=$(DOE_RUNS_DIR)/doe-amd64-$$TS.csv; \
SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-amd64-$$TS.sock; \
QMP_SOCK=$(BUILD_DIR)/qemu-qmp-amd64-$$TS.sock; \
echo "=== Launching QEMU (amd64) ==="; \
echo " Serial log: $$LOG"; \
echo " QMP socket: $$QMP_SOCK"; \
echo " (interactive — close the window or QEMU monitor 'quit' to exit)"; \
echo ""; \
touch $$LOG; \
tail -n +1 -f $$LOG & TAILPID=$$!; \
qemu-system-x86_64 \
-machine q35,accel=tcg \
-cpu qemu64 \
-m 1024 \
-vga std \
-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
-cdrom $(QEMU_ISO) -boot d \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-device qemu-xhci,id=xhci0,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS) \
$(ZUSEDISK_QEMU_ARGS) \
-object rng-random,id=rng0,filename=/dev/urandom \
-device virtio-rng-pci,rng=rng0 \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
-qmp unix:$$QMP_SOCK,server=on,wait=off \
$(QEMU_EXTRA) \
-display $(QEMU_DISPLAY) \
-no-reboot; \
kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
rm -f $$SERIAL_SOCK $$QMP_SOCK; \
echo ""; \
echo "=== Serial log: $$LOG ==="; \
echo "=== Extracting DOE CSV ==="; \
bash scripts/extract_doe.sh $$LOG $$CSV || true; \
cp $$CSV $(DOE_LATEST_DIR)/amd64.csv 2>/dev/null || true
else ifeq ($(ARCH),aarch64)
@echo "=== Building ISO (aarch64) ==="
@mkdir -p $(BUILD_DIR)/iso/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/iso/EFI/BOOT/BOOTAA64.EFI
@printf "FS0:\\EFI\\BOOT\\BOOTAA64.EFI\r\n" > $(BUILD_DIR)/iso/startup.nsh
@dd if=/dev/zero of=$(BUILD_DIR)/iso/efi.img bs=512 count=8192 2>/dev/null
@mformat -i $(BUILD_DIR)/iso/efi.img ::
@mmd -i $(BUILD_DIR)/iso/efi.img ::/EFI
@mmd -i $(BUILD_DIR)/iso/efi.img ::/EFI/BOOT
@mcopy -i $(BUILD_DIR)/iso/efi.img $(LOADER_EFI) ::/EFI/BOOT/BOOTAA64.EFI
$(if $(KERNEL_ARGS),\
@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/iso/starforth.cfg && \
mcopy -i $(BUILD_DIR)/iso/efi.img $(BUILD_DIR)/iso/starforth.cfg ::/starforth.cfg,)
@xorriso -as mkisofs -r -J \
-e efi.img -no-emul-boot \
-o $(QEMU_ISO) $(BUILD_DIR)/iso 2>&1
@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
@if [ -f /usr/share/AAVMF/AAVMF_CODE.fd ]; then \
AAVMF_CODE=/usr/share/AAVMF/AAVMF_CODE.fd; \
AAVMF_VARS_RO=/usr/share/AAVMF/AAVMF_VARS.fd; \
elif [ -f /usr/share/qemu-efi-aarch64/QEMU_EFI.fd ]; then \
AAVMF_CODE=/usr/share/qemu-efi-aarch64/QEMU_EFI.fd; \
AAVMF_VARS_RO=/usr/share/qemu-efi-aarch64/vars-template-pflash.raw; \
elif [ -f /usr/share/qemu/edk2-aarch64-code.fd ]; then \
AAVMF_CODE=/usr/share/qemu/edk2-aarch64-code.fd; \
AAVMF_VARS_RO=/usr/share/qemu/edk2-aarch64-vars.fd; \
else \
echo "Error: aarch64 UEFI firmware not found. Install qemu-efi-aarch64."; \
exit 1; \
fi; \
if [ -f "$$AAVMF_VARS_RO" ]; then \
if [ ! -f $(BUILD_DIR)/AAVMF_VARS.fd ]; then \
cp $$AAVMF_VARS_RO $(BUILD_DIR)/AAVMF_VARS.fd; \
fi; \
AAVMF_VARS=$(BUILD_DIR)/AAVMF_VARS.fd; \
else \
echo " (no split VARS template found for this firmware -- NVRAM will not persist across runs)"; \
AAVMF_VARS=""; \
fi; \
TS=$$(date +%Y%m%d-%H%M%S); \
SESSION=$(QEMU_LOG_DIR)/$$TS; \
mkdir -p $$SESSION/aarch64; \
LOG=$$SESSION/aarch64/qemu-aarch64-$$TS.log; \
CSV=$(DOE_RUNS_DIR)/doe-aarch64-$$TS.csv; \
SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-aarch64-$$TS.sock; \
QMP_SOCK=$(BUILD_DIR)/qemu-qmp-aarch64-$$TS.sock; \
echo "=== Launching QEMU (aarch64) ==="; \
echo " ISO: $(QEMU_ISO)"; \
echo " Serial log: $$LOG"; \
echo " QMP socket: $$QMP_SOCK"; \
echo " (interactive — close the window or QEMU monitor 'quit' to exit)"; \
echo ""; \
touch $$LOG; \
tail -n +1 -f $$LOG & TAILPID=$$!; \
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
$$(if [ -n "$$AAVMF_VARS" ]; then \
echo "-drive if=pflash,format=raw,readonly=on,file=$$AAVMF_CODE -drive if=pflash,format=raw,file=$$AAVMF_VARS"; \
else \
echo "-bios $$AAVMF_CODE"; \
fi) \
-drive if=virtio,format=raw,file=$(QEMU_ISO),media=cdrom \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-device virtio-keyboard-pci,addr=0x3 \
-device qemu-xhci,id=xhci0,addr=0x4,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS) \
$(ZUSEDISK_QEMU_ARGS) \
-object rng-random,id=rng0,filename=/dev/urandom \
-device virtio-rng-pci,rng=rng0,addr=0x5 \
-device ramfb \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
-qmp unix:$$QMP_SOCK,server=on,wait=off \
$(QEMU_EXTRA) \
-display $(QEMU_DISPLAY) \
-m 4096 \
-no-reboot \
-d guest_errors; \
kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
rm -f $$SERIAL_SOCK $$QMP_SOCK; \
echo ""; \
echo "=== Serial log: $$LOG ==="; \
echo "=== Extracting DOE CSV ==="; \
bash scripts/extract_doe.sh $$LOG $$CSV || true; \
cp $$CSV $(DOE_LATEST_DIR)/aarch64.csv 2>/dev/null || true
else ifeq ($(ARCH),riscv64)
@echo "=== Building GPT disk image (riscv64) ==="
@# riscv64 QEMU virt: pflash firmware + virtio-blk-pci GPT disk.
@# El Torito ISO and -bios mode do NOT work on riscv64 virt.
@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
@if ! which sgdisk >/dev/null 2>&1; then echo "Error: sgdisk not found. Install gdisk (apt-get install gdisk)."; exit 1; fi
@if ! which mkfs.fat >/dev/null 2>&1; then echo "Error: mkfs.fat not found. Install dosfstools (apt-get install dosfstools)."; exit 1; fi
@if [ -f /usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd ]; then \
EDK2_RISCV_CODE=/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd; \
elif [ -f /usr/share/qemu/edk2-riscv64-code.fd ]; then \
EDK2_RISCV_CODE=/usr/share/qemu/edk2-riscv64-code.fd; \
else \
echo "Error: riscv64 UEFI firmware not found. Install qemu-efi-riscv64."; \
exit 1; \
fi; \
EDK2_RISCV_VARS=""; \
if [ -f /usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd ]; then \
if [ ! -f $(BUILD_DIR)/RISCV_VARS.fd ]; then \
cp /usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd $(BUILD_DIR)/RISCV_VARS.fd; \
fi; \
EDK2_RISCV_VARS=$(BUILD_DIR)/RISCV_VARS.fd; \
fi; \
DISK=$(BUILD_DIR)/starkernel_rv64.img; \
dd if=/dev/zero of=$$DISK bs=1M count=64 2>/dev/null; \
sgdisk -n 1:2048:131038 -t 1:ef00 -c 1:"EFI System" $$DISK >/dev/null 2>&1; \
dd if=/dev/zero of=$(BUILD_DIR)/esp_rv64.img bs=512 count=128991 2>/dev/null; \
mkfs.fat -F 32 -n "ESP" $(BUILD_DIR)/esp_rv64.img >/dev/null 2>&1; \
mmd -i $(BUILD_DIR)/esp_rv64.img ::/EFI; \
mmd -i $(BUILD_DIR)/esp_rv64.img ::/EFI/BOOT; \
mcopy -i $(BUILD_DIR)/esp_rv64.img $(LOADER_EFI) ::/EFI/BOOT/BOOTRISCV64.EFI; \
mcopy -i $(BUILD_DIR)/esp_rv64.img $(KERNEL_ELF) ::/kernel.elf; \
printf 'FS0:\\EFI\\BOOT\\BOOTRISCV64.EFI\r\n' > $(BUILD_DIR)/startup.nsh; \
mcopy -i $(BUILD_DIR)/esp_rv64.img $(BUILD_DIR)/startup.nsh ::/startup.nsh; \
if [ -n "$(KERNEL_ARGS)" ]; then \
printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/starforth.cfg; \
mcopy -i $(BUILD_DIR)/esp_rv64.img $(BUILD_DIR)/starforth.cfg ::/starforth.cfg; \
fi; \
dd if=$(BUILD_DIR)/esp_rv64.img of=$$DISK bs=512 seek=2048 conv=notrunc 2>/dev/null; \
echo " Disk: $$DISK"; \
echo ""; \
PFLASH_VARS_ARG=""; \
if [ -n "$$EDK2_RISCV_VARS" ]; then \
PFLASH_VARS_ARG="-drive if=pflash,format=raw,unit=1,file=$$EDK2_RISCV_VARS"; \
fi; \
TS=$$(date +%Y%m%d-%H%M%S); \
SESSION=$(QEMU_LOG_DIR)/$$TS; \
mkdir -p $$SESSION/riscv64; \
LOG=$$SESSION/riscv64/qemu-riscv64-$$TS.log; \
CSV=$(DOE_RUNS_DIR)/doe-riscv64-$$TS.csv; \
SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-riscv64-$$TS.sock; \
QMP_SOCK=$(BUILD_DIR)/qemu-qmp-riscv64-$$TS.sock; \
echo "=== Launching QEMU (riscv64) ==="; \
echo " Serial log: $$LOG"; \
echo " QMP socket: $$QMP_SOCK"; \
echo " (interactive — close the window or QEMU monitor 'quit' to exit)"; \
echo ""; \
touch $$LOG; \
tail -n +1 -f $$LOG & TAILPID=$$!; \
qemu-system-riscv64 \
-machine virt \
-cpu rv64 \
-m 1024 \
-drive if=pflash,format=raw,unit=0,file=$$EDK2_RISCV_CODE,readonly=on \
$$PFLASH_VARS_ARG \
-drive id=hd0,file=$$DISK,format=raw,if=none \
-device virtio-blk-pci,drive=hd0,addr=0x2 \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk,addr=0x1 \
-device virtio-keyboard-pci,addr=0x3 \
-device qemu-xhci,id=xhci0,addr=0x4,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS) \
$(ZUSEDISK_QEMU_ARGS) \
-object rng-random,id=rng0,filename=/dev/urandom \
-device virtio-rng-pci,rng=rng0,addr=0x5 \
-device ramfb \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
-qmp unix:$$QMP_SOCK,server=on,wait=off \
$(QEMU_EXTRA) \
-display $(QEMU_DISPLAY) \
-no-reboot \
-d guest_errors; \
kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
rm -f $$SERIAL_SOCK $$QMP_SOCK; \
echo ""; \
echo "=== Serial log: $$LOG ==="; \
echo "=== Extracting DOE CSV ==="; \
bash scripts/extract_doe.sh $$LOG $$CSV || true; \
cp $$CSV $(DOE_LATEST_DIR)/riscv64.csv 2>/dev/null || true
endif
# thumbdrive — build a generic UEFI-bootable GPT/FAT32 disk image that can be
# written directly to a USB thumbdrive (dd) and boots on real amd64 UEFI
# hardware (e.g. Beelink SER5) as well as under QEMU. This is the "generic
# thumbdrive bootable OS" image for the v2.5.0/SER5 bare-metal path: the
# monolithic loader embeds the whole kernel, so the ESP needs only the UEFI
# fallback boot path EFI/BOOT/BOOT<ARCH>.EFI. Storage on real hardware is the
# USB BOT/xHCI path (already live), not virtio.
thumbdrive: all
@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BUILD_DIR)/thumbdrive-startup.nsh
@bash scripts/mkdiskimage.sh --out $(BUILD_DIR)/starkernel-thumbdrive.img --table gpt \
--size-mib 128 --label STARKERNEL \
$(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
$(BUILD_DIR)/thumbdrive-startup.nsh:/startup.nsh
@rm -f $(BUILD_DIR)/thumbdrive-startup.nsh
@echo "Write to a USB stick with: dd if=$(BUILD_DIR)/starkernel-thumbdrive.img of=/dev/sdX bs=4M status=progress"
# ==============================================================================
# BOOT IMAGES (one per board)
# ==============================================================================
# boot_image -- the single file written to a board's boot medium. Driven by
# BOARD (boards/<board>/board.mk); normally invoked from the repo root as
# make boot_image TARGET=SER5|RASPI|MILKV|ZYNQ7020
# Output: build/boards/<board>/$(BOARD_IMAGE).
#
# BOARD_BOOT selects the recipe:
# uefi-esp -- GPT disk, one FAT32 EFI System Partition holding the
# monolithic loader as EFI/BOOT/BOOT<ARCH>.EFI (+ startup.nsh
# for the UEFI shell, + starforth.cfg when KERNEL_ARGS is set).
# Real UEFI firmware (SER5) and U-Boot's bootefi (Milk-V) both
# boot it through the removable-media fallback path.
# rpi-native -- MBR disk, one FAT32 (LBA) partition holding config.txt,
# kernel_2712.img (kernel relinked at 0x80000 and flattened)
# and the stock BCM2712 DTB (+ cmdline.txt, which firmware
# copies into /chosen/bootargs, when KERNEL_ARGS is set).
ifeq ($(ARCH),amd64)
EFI_BOOTNAME := BOOTX64.EFI
else ifeq ($(ARCH),aarch64)
EFI_BOOTNAME := BOOTAA64.EFI
else ifeq ($(ARCH),riscv64)
EFI_BOOTNAME := BOOTRISCV64.EFI
endif
RPI5_NATIVE_LINKER_SCRIPT := kernel/linker/starkernel-native-rpi5.ld
RPI5_NATIVE_ELF := $(BUILD_DIR)/starkernel_rpi5.elf
RPI5_NATIVE_IMG := $(BUILD_DIR)/kernel_2712.img
$(RPI5_NATIVE_ELF): $(KERNEL_OBJS) $(RPI5_NATIVE_LINKER_SCRIPT) | $(BUILD_DIR)
@echo "LD (rpi5 native) $@"
@$(LD) -T $(RPI5_NATIVE_LINKER_SCRIPT) -nostdlib --build-id=none \
-e rpi5_native_start $(KERNEL_OBJS) -o $@
$(RPI5_NATIVE_IMG): $(RPI5_NATIVE_ELF)
@echo "OBJCOPY (raw) $@"
@$(OBJCOPY) -O binary $< $@
.PHONY: boot_image
ifeq ($(strip $(BOARD)),)
boot_image:
@echo "boot_image needs a board. From the repo root:"
@echo " make boot_image TARGET=SER5|RASPI|MILKV|ZYNQ7020"
@exit 1
else ifeq ($(BOARD_BOOT),uefi-esp)
boot_image: all
@mkdir -p $(BOARD_OUT)
@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BOARD_OUT)/startup.nsh
@rm -f $(BOARD_OUT)/starforth.cfg
$(if $(KERNEL_ARGS),@printf '%s\n' '$(KERNEL_ARGS)' > $(BOARD_OUT)/starforth.cfg)
@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table gpt \
--size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
$(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
$(BOARD_OUT)/startup.nsh:/startup.nsh \
$(if $(KERNEL_ARGS),$(BOARD_OUT)/starforth.cfg:/starforth.cfg)
@echo "$(BOARD_DESC)"
@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/sdX bs=4M conv=fsync status=progress"
else ifeq ($(BOARD_BOOT),rpi-native)
$(RPI5_DTB):
@mkdir -p $(dir $@)
@echo " FETCH $(RPI5_DTB_URL)"
@curl -fsSL -o $@.tmp $(RPI5_DTB_URL) || { rm -f $@.tmp; \
echo "Error: could not fetch the Pi 5 DTB. Copy bcm2712-rpi-5-b.dtb from a Raspberry Pi"; \
echo "firmware release and pass RPI5_DTB=/path/to/bcm2712-rpi-5-b.dtb"; exit 1; }
@mv $@.tmp $@
boot_image: v3/include/version.h $(RPI5_NATIVE_IMG) $(RPI5_DTB)
@mkdir -p $(BOARD_OUT)
@rm -f $(BOARD_OUT)/cmdline.txt
$(if $(KERNEL_ARGS),@printf '%s\n' '$(KERNEL_ARGS)' > $(BOARD_OUT)/cmdline.txt)
@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table mbr \
--size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
boards/$(BOARD)/config.txt:/config.txt \
$(RPI5_NATIVE_IMG):/kernel_2712.img \
$(RPI5_DTB):/bcm2712-rpi-5-b.dtb \
$(if $(KERNEL_ARGS),$(BOARD_OUT)/cmdline.txt:/cmdline.txt)
@echo "$(BOARD_DESC)"
@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/mmcblkX bs=4M conv=fsync status=progress"
else
boot_image:
@echo "Error: BOARD=$(BOARD) has BOARD_BOOT='$(BOARD_BOOT)', which has no boot_image recipe"
@exit 1
endif
# iso-usb — build a PURE UEFI isohybrid ISO for the Iso Image Writer workflow
# (GNOME Disks "Restore Disk Image...", or dd). Writes a single .iso to a USB
# stick as a raw image; the ISO carries a clean GPT with an EFI System
# Partition holding EFI/BOOT/BOOT<ARCH>.EFI, so real UEFI firmware (e.g.
# Beelink SER5) scans the ESP and boots it. No GRUB, no isolinux, no MBR boot
# code — the protective MBR template is zeroed (partition metadata only).
# Also still boots under QEMU via -cdrom. This is the novice path: one ISO,
# picked with a GUI, written to the stick.
iso-usb: all
@mkdir -p $(BUILD_DIR)
@if ! which xorriso >/dev/null 2>&1; then echo "Error: xorriso not found. Install xorriso (apt-get install xorriso)."; exit 1; fi
@if ! which mformat >/dev/null 2>&1 || ! which mmd >/dev/null 2>&1 || ! which mcopy >/dev/null 2>&1; then echo "Error: mtools not found. Install mtools (apt-get install mtools)."; exit 1; fi
@case "$(ARCH)" in \
amd64) BOOTNAME="BOOTX64.EFI" ;; \
aarch64) BOOTNAME="BOOTAA64.EFI" ;; \
*) echo "Error: iso-usb EFI boot name only defined for amd64/aarch64 yet"; exit 1 ;; \
esac; \
ISODIR=$(BUILD_DIR)/iso-usb; \
rm -rf $$ISODIR; mkdir -p $$ISODIR; \
dd if=/dev/zero of=$$ISODIR/efi.img bs=512 count=8192 2>/dev/null; \
mformat -i $$ISODIR/efi.img :: >/dev/null 2>&1; \
mmd -i $$ISODIR/efi.img ::/EFI; mmd -i $$ISODIR/efi.img ::/EFI/BOOT; \
mcopy -i $$ISODIR/efi.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \
printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $$ISODIR/startup.nsh; \
mcopy -i $$ISODIR/efi.img $$ISODIR/startup.nsh ::/startup.nsh; \
head -c 432 /dev/zero > $$ISODIR/protmbr.bin; \
ISO=$(BUILD_DIR)/starkernel-iso-usb.iso; \
rm -f $$ISO; \
xorriso -as mkisofs \
-V STARKERNEL -r -J \
-isohybrid-mbr $$ISODIR/protmbr.bin \
-eltorito-alt-boot -e efi.img -no-emul-boot \
-isohybrid-gpt-basdat \
-o $$ISO $$ISODIR >/dev/null 2>&1; \
rm -rf $$ISODIR; \
echo "Iso Image Writer ISO (pure UEFI): $$ISO"; \
echo " In GNOME Disks, pick the ISO with 'Restore Disk Image...' and select your USB stick."; \
echo " Or: dd if=$$ISO of=/dev/sdX bs=4M status=progress"
# qemu-esp — interactive dev boot from FAT directory (no disk image rebuild,
# no auto-kill/timeout/DOE-injection logic — stays up until you quit it
# yourself with Ctrl-A X or by closing the window).
# amd64/aarch64: serial → stdio (bidirectional, you can type into the REPL);
# riscv64: uses proper GPT (esp dir not supported)
qemu-esp: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
@rm -rf $(BUILD_DIR)/esp
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTX64.EFI
@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
@printf "FS0:\\EFI\\BOOT\\BOOTX64.EFI\r\n" > $(BUILD_DIR)/esp/startup.nsh
$(if $(KERNEL_ARGS),@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/esp/starforth.cfg,)
@OVMF_CODE=$(OVMF_CODE); \
if [ ! -f $$OVMF_CODE ]; then echo "Error: $$OVMF_CODE not found"; exit 1; fi; \
cp $(OVMF_VARS_RO) $(BUILD_DIR)/OVMF_VARS.fd; \
qemu-system-x86_64 \
-nodefaults -display $(QEMU_DISPLAY) -vga std \
-machine q35,accel=tcg \
-cpu qemu64 \
-m 1024 \
-chardev stdio,id=ser0,signal=off \
-device isa-serial,chardev=ser0,iobase=0x3f8 \
-drive if=pflash,format=raw,readonly=on,file=$$OVMF_CODE \
-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-no-reboot
else ifeq ($(ARCH),aarch64)
@rm -rf $(BUILD_DIR)/esp
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTAA64.EFI
@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
@printf "FS0:\\EFI\\BOOT\\BOOTAA64.EFI\r\n" > $(BUILD_DIR)/esp/startup.nsh
@if [ -f /usr/share/AAVMF/AAVMF_CODE.fd ]; then \
AAVMF_CODE=/usr/share/AAVMF/AAVMF_CODE.fd; \
elif [ -f /usr/share/qemu-efi-aarch64/QEMU_EFI.fd ]; then \
AAVMF_CODE=/usr/share/qemu-efi-aarch64/QEMU_EFI.fd; \
elif [ -f /usr/share/qemu/edk2-aarch64-code.fd ]; then \
AAVMF_CODE=/usr/share/qemu/edk2-aarch64-code.fd; \
else \
echo "Error: aarch64 UEFI firmware not found. Install qemu-efi-aarch64."; exit 1; \
fi; \
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-bios $$AAVMF_CODE \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
-serial mon:stdio \
-nographic \
-m 2048 \
-d guest_errors
else ifeq ($(ARCH),riscv64)
@echo "riscv64: qemu-esp not supported (FAT dir mode broken on virt); use 'make qemu'"
@exit 1
endif
# qemu-gdb — launch QEMU with GDB stub on port 1234
qemu-gdb: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTX64.EFI
@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
@echo "Launching QEMU with GDB server on :1234 (amd64)"
@echo " gdb $(KERNEL_ELF) -ex 'target remote :1234'"
@qemu-system-x86_64 \
-bios $(OVMF_CODE) \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
-serial stdio \
-m 1024 \
-s -S
else ifeq ($(ARCH),aarch64)
@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTAA64.EFI
@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
@echo "Launching QEMU with GDB server on :1234 (aarch64)"
@echo " gdb $(KERNEL_ELF) -ex 'target remote :1234'"
@qemu-system-aarch64 \
-machine virt -cpu cortex-a57 \
-bios /usr/share/AAVMF/AAVMF_CODE.fd \
-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
-serial stdio -m 2048 -s -S
else
@echo "Error: qemu-gdb not yet implemented for $(ARCH)"; exit 1
endif
# ==============================================================================
# INFO / HELP
# ==============================================================================
info:
@echo "StarKernel Build Configuration"
@echo "==============================="
@echo "Architecture: $(ARCH)"
@echo "Target: $(TARGET)"
@echo "Compiler: $(CC)"
@echo "Loader CC: $(LOADER_CC)"
@echo "Linker: $(LD)"
@echo "Build dir: $(BUILD_DIR)"
@echo "Loader script: $(LOADER_LINKER_SCRIPT)"
@echo "Kernel script: $(KERNEL_LINKER_SCRIPT)"
@echo "Loader output: $(LOADER_EFI)"
@echo "Kernel output: $(KERNEL_ELF)"
@echo "VM integration: $(STARFORTH_ENABLE_VM)"
@echo "StarForth v4: $(STARFORTH_V4)"
@echo "Monolithic: $(MONOLITHIC)"
help:
@echo "StarKernel Build System"
@echo "======================="
@echo ""
@echo "Usage: make -f kernel/Makefile [ARCH=<arch>] [TARGET=<target>] <goal>"
@echo ""
@echo "Architectures (aliases accepted):"
@echo " ARCH=amd64 / x86_64 — x86-64 (default on x86 host)"
@echo " ARCH=aarch64 / arm64 — ARM64"
@echo " ARCH=riscv64 / riscv — RISC-V 64-bit"
@echo ""
@echo "Build goals:"
@echo " all — build loader EFI + kernel ELF (default)"
@echo " kernel — alias for all"
@echo " kernel-all — build all three architectures"
@echo " clean — remove build/\$(ARCH)/\$(TARGET)"
@echo " clean-kernel — remove build/*/kernel for all arches"
@echo ""
@echo "QEMU goals:"
@echo " qemu — clean boot, serial tee'd live + logs/<session>/<arch>/ + DOE CSV → doe/ (all arches)"
@echo " qemu-esp — quick boot from FAT directory (amd64, aarch64)"
@echo " qemu-gdb — boot with GDB stub on :1234"
@echo " thumbdrive — build generic GPT/FAT32 UEFI disk image (write to a USB stick with dd; real-hardware/SER5 path)"
@echo " iso-usb — build UEFI isohybrid ISO for Iso Image Writer / GNOME Disks 'Restore Disk Image...' (novice path; real-hardware/SER5)"
@echo ""
@echo "QEMU display:"
@echo " QEMU_DISPLAY=gtk — framebuffer window backend for 'qemu' goal (default: gtk)"
@echo " QEMU_DISPLAY=none — headless (no window), e.g. for CI"
@echo ""
@echo "Info goals:"
@echo " info — show build configuration"
@echo " help — show this message"
@echo ""
@echo "Examples:"
@echo " make -f kernel/Makefile ARCH=amd64 qemu"
@echo " make -f kernel/Makefile ARCH=aarch64 clean qemu"
@echo " make -f kernel/Makefile ARCH=riscv64 clean qemu"
@echo " make -f kernel/Makefile ARCH=x86_64 qemu # alias works"
@echo " make -f kernel/Makefile kernel-all"