Files
LithosAnanake/Makefile.starkernel
T
Robert Allan James bd266942ce v2.0.1: generic GPT/FAT32 UEFI bootable thumbdrive image (thumbdrive goal)
New `make -f Makefile.starkernel thumbdrive` goal builds a generic
UEFI-bootable GPT disk image (disk image -> GPT + one FAT32 "STARKERNEL"
EFI System partition with EFI/BOOT/BOOT<ARCH>.EFI + startup.nsh) that can be
written directly to a USB thumbdrive with dd and boots on any real amd64 UEFI
firmware (Beelink SER5 path) as well as under QEMU. The monolithic loader
embeds the whole kernel, so the ESP needs only the UEFI fallback boot path.

Verified under QEMU by attaching the image as a USB mass-storage device (not
cdrom): OVMF BDS auto-selected "UEFI QEMU QEMU USB HARDDRIVE" (Boot0002),
then kernel booted normally -- LithosAnanke v2.0.0, POST 1012/0/0 + ok>,
rng: backend = virtio-rng + entropy: ready, Artemis ready, and the USB BOT/
xHCI storage path enumerated (READ CAPACITY10 -> MSC device ready -> READ10
CSW PASS). This mirrors the real-hardware SER5 flow: firmware boots the USB
thumbdrive's ESP, and the OS's own storage rides the same USB BOT/xHCI path.

Works for all three arches via the arch-mapped EFI boot name.
2026-08-29 10:12:43 -04:00

1190 lines
50 KiB
Makefile

# ==============================================================================
# Makefile.starkernel — StarKernel UEFI Build System
# Multi-architecture: amd64 (x86_64), aarch64 (ARM64), riscv64 (RISC-V 64)
#
# Quick start:
# make -f Makefile.starkernel — build for host arch
# make -f Makefile.starkernel ARCH=amd64 qemu — build + boot (x86_64)
# make -f Makefile.starkernel ARCH=aarch64 qemu — build + boot (aarch64)
# make -f Makefile.starkernel ARCH=riscv64 qemu — build + boot (riscv64)
# make -f Makefile.starkernel 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))
# 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 include/version.h)
NPROC := $(shell nproc 2>/dev/null || echo 4)
ifeq ($(filter clean,$(MAKECMDGOALS)),)
MAKEFLAGS += -j$(NPROC)
endif
# Version
# Roadmap (per docs/lithosananke/ROADMAP.md "Release Versioning Policy" and
# FABRIC-3.md §G — X.0.0 = QEMU release, X.5.0 = hardware bare-metal release):
# v1.0.x — serial-only production (released)
# v1.5.x — framebuffer VT100 terminal/console milestone (released)
# v2.0.0 — QEMU release (even major = LTS): three-arch QEMU story complete
# v2.5.0 — hardware bare-metal release: real per-arch RNG + real-board boot
VERSION ?= 3.1.0
LITHOS_VERSION ?= 2.0.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 := src/starkernel
KERNEL_INC := include/starkernel
STARFORTH_CONFIG_HEADER := 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
# 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 (src/starkernel/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 Makefile.starkernel qemu ZUSEDISK=`.
ZUSEDISK ?= disk/zuse.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,drive=zusedrv
else
ZUSEDISK_QEMU_ARGS :=
endif
MKCAPSULE_SRC = tools/mkcapsule.c tools/pkcs8_ed25519.c \
src/starkernel/crypto/ed25519.c \
src/starkernel/crypto/fe25519.c \
src/starkernel/crypto/scalar25519.c \
src/starkernel/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-3.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
# ==============================================================================
# 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 := linker/starkernel-loader-amd64.ld
KERNEL_LINKER_SCRIPT := 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)
LOADER_CC := clang-18
# Debian/Ubuntu package only installs the versioned lld-link-18 on PATH by
# default (unversioned lld-link lives under /usr/lib/llvm-18/bin, which
# isn't on PATH unless explicitly prepended) — detect whichever resolves.
LOADER_LD := $(shell which lld-link 2>/dev/null || which lld-link-18 2>/dev/null || echo lld-link)
ARCH_CFLAGS := -march=armv8-a -mcpu=cortex-a72 -DARCH_AARCH64 -mno-outline-atomics
LOADER_LINKER_SCRIPT := linker/starkernel-loader-aarch64.ld
KERNEL_LINKER_SCRIPT := 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 := linker/starkernel-loader-riscv64.ld
KERNEL_LINKER_SCRIPT := 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.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) -Iinclude -Isrc \
-include $(STARFORTH_CONFIG_HEADER) \
-DPARITY_MODE=$(PARITY_MODE)
VMCORE_CFLAGS_COMMON := \
$(filter-out -I$(KERNEL_INC),$(COMMON_CFLAGS)) \
-Iinclude -I. -Isrc/word_source -Isrc/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) -Iinclude -Isrc \
-Iinclude/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.
$(eval $(call kconfig_bool,EMERGENCY_CONSOLE_ENABLED,1))
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 := 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 Makefile.starkernel 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 Makefile.starkernel 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
LOADER_ASM := \
$(KERNEL_SRC)/arch/$(ARCH)/boot.S \
$(KERNEL_SRC)/arch/$(ARCH)/isr.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 \
$(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_ASM := $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.S)
ifeq ($(STARFORTH_ENABLE_VM),1)
VM_ALL_SRCS := \
$(wildcard src/*.c) \
$(wildcard src/word_source/*.c) \
$(wildcard src/test_runner/*.c) \
$(wildcard src/test_runner/modules/*.c)
VM_EXCLUDE := \
src/main.c src/cli.c src/repl.c \
src/platform/% \
src/blkio_factory.c src/blkio_file.c src/blkio_ram.c \
src/log.c src/doe_metrics.c \
src/vm.c src/vm_core.c src/vm_bootstrap.c src/vm_runtime.c src/vm_time.c \
src/word_source/q48_16_words.c \
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/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_EXTRA_SRCS := $(LOADER_EXTRA_SRCS)
LOADER_VM_OBJS := $(patsubst src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
KERNEL_VM_OBJS := $(patsubst src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
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) \
$(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) \
$(CAPSULE_GENERATED_KOBJ)
# ==============================================================================
# PHONY TARGETS
# ==============================================================================
.PHONY: all clean clean-kernel kernel kernel-all
.PHONY: qemu qemu-esp qemu-gdb
.PHONY: thumbdrive
.PHONY: info help
# ==============================================================================
# MAIN TARGETS
# ==============================================================================
all: 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 Makefile.starkernel ARCH=amd64 TARGET=$(TARGET)
@$(MAKE) -f Makefile.starkernel ARCH=aarch64 TARGET=$(TARGET)
@$(MAKE) -f Makefile.starkernel ARCH=riscv64 TARGET=$(TARGET)
clean:
rm -rf build/$(ARCH)/$(TARGET)
rm -f include/version.h
clean-kernel:
@echo "Cleaning all StarKernel build artifacts..."
@rm -rf build/*/kernel
@echo "Done."
# ==============================================================================
# BUILD RULES
# ==============================================================================
include/version.h:
@mkdir -p 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 -Iinclude -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: src/%.c include/version.h | $(LOADER_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (loader) $<"
ifeq ($(ARCH),aarch64)
@$(LOADER_CC) $(LOADER_CFLAGS) \
-Iinclude -I. -Isrc/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
# 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: src/%.c include/version.h | $(KERNEL_OBJ_DIR)
@mkdir -p $(dir $@)
@echo "CC (kernel) $<"
@$(CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(KERNEL_CFLAGS)) -Isrc/starkernel/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 Makefile.starkernel 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,drive=usbtest"
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 \
$(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 \
$(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 \
$(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
@mkdir -p $(BUILD_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
@case "$(ARCH)" in \
amd64) BOOTNAME="BOOTX64.EFI" ;; \
aarch64) BOOTNAME="BOOTAA64.EFI" ;; \
riscv64) BOOTNAME="BOOTRISCV64.EFI" ;; \
*) echo "Error: no thumbdrive EFI boot name for ARCH=$(ARCH)"; exit 1 ;; \
esac; \
DISK=$(BUILD_DIR)/starkernel-thumbdrive.img; \
rm -f $$DISK $(BUILD_DIR)/thumbdrive-esp.img; \
dd if=/dev/zero of=$(BUILD_DIR)/thumbdrive-esp.img bs=512 count=131072 2>/dev/null; \
mkfs.fat -F 32 -n "STARKERNEL" $(BUILD_DIR)/thumbdrive-esp.img >/dev/null 2>&1; \
mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI; \
mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI/BOOT; \
mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \
printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $(BUILD_DIR)/thumbdrive-startup.nsh; \
mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh ::/startup.nsh; \
dd if=/dev/zero of=$$DISK bs=1M count=64 2>/dev/null; \
sgdisk -n 1:2048:0 -t 1:ef00 -c 1:"EFI System" $$DISK >/dev/null 2>&1; \
dd if=$(BUILD_DIR)/thumbdrive-esp.img of=$$DISK bs=512 seek=2048 conv=notrunc 2>/dev/null; \
rm -f $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh; \
echo "Thumbdrive image: $$DISK"; \
echo "Write to a USB stick with: dd if=$$DISK 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: 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: 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 "Monolithic: $(MONOLITHIC)"
help:
@echo "StarKernel Build System"
@echo "======================="
@echo ""
@echo "Usage: make -f Makefile.starkernel [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 ""
@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 Makefile.starkernel ARCH=amd64 qemu"
@echo " make -f Makefile.starkernel ARCH=aarch64 clean qemu"
@echo " make -f Makefile.starkernel ARCH=riscv64 clean qemu"
@echo " make -f Makefile.starkernel ARCH=x86_64 qemu # alias works"
@echo " make -f Makefile.starkernel kernel-all"