summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 23:20:03 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 23:20:03 +0800
commit48ef95c24e9ac3d11200b61af617a01b10b507d1 (patch)
tree57f0e26ab98e795b19839b6eece98ca58a4b702d
parentb9ff206e6bd3f4d67851263a6f2acb5f575cf3b0 (diff)
downloadlume-48ef95c24e9ac3d11200b61af617a01b10b507d1.tar.gz
lume-48ef95c24e9ac3d11200b61af617a01b10b507d1.tar.xz
build: introduce declarative non-recursive build graph
-rw-r--r--.gitignore3
-rw-r--r--Build.mk57
-rw-r--r--Makefile568
-rw-r--r--arch/riscv/Build.mk19
-rw-r--r--arch/riscv/include/asm/sections.h2
-rw-r--r--arch/riscv/mm/init.cc2
-rw-r--r--arch/riscv/module.mk18
-rw-r--r--drivers/Build.mk17
-rw-r--r--drivers/base/module.mk9
-rw-r--r--drivers/module.mk11
-rw-r--r--drivers/tty/module.mk5
-rw-r--r--fdt/Build.mk4
-rw-r--r--fdt/module.mk3
-rw-r--r--fs/Build.mk8
-rw-r--r--fs/module.mk17
-rw-r--r--kernel/Build.mk11
-rw-r--r--kernel/main.cc6
-rw-r--r--kernel/module.mk26
-rw-r--r--lib/Build.mk35
-rw-r--r--lib/module.mk66
-rw-r--r--mm/Build.mk6
-rw-r--r--mm/module.mk7
-rw-r--r--scripts/Kbuild.include136
-rw-r--r--scripts/Kconfig.mk89
-rw-r--r--scripts/build/Build.mk10
-rwxr-xr-xscripts/build/archive.sh10
-rwxr-xr-xscripts/build/check-kernel-elf.sh47
-rw-r--r--scripts/build/environments/host.mk8
-rw-r--r--scripts/build/environments/kernel.mk28
-rw-r--r--scripts/build/environments/phys.mk12
-rwxr-xr-xscripts/build/generate-asm-offsets.sh25
-rwxr-xr-xscripts/build/generate-autoconf.sh29
-rwxr-xr-xscripts/build/generate-initramfs.sh12
-rwxr-xr-xscripts/build/link-kernel.sh10
-rwxr-xr-xscripts/build/probe-toolchain.sh48
-rwxr-xr-xscripts/build/run-if-changed.sh45
-rw-r--r--scripts/build/targets.mk81
-rw-r--r--scripts/build/tests/host_library.cc8
-rw-r--r--scripts/build/tests/host_test.cc7
-rw-r--r--scripts/build/tests/phys_library.cc5
-rw-r--r--scripts/build/toolchain.mk103
-rwxr-xr-xscripts/build/write-compdb.py26
-rwxr-xr-xscripts/gen_initramfs.sh2
-rw-r--r--test/Build.mk12
-rw-r--r--test/module.mk30
-rw-r--r--usr/Build.mk4
-rw-r--r--usr/initramfs.S17
47 files changed, 1205 insertions, 499 deletions
diff --git a/.gitignore b/.gitignore
index d0a2443..bef5906 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-build/
+/build/
+/out/
lume-legacy/build/
*.o
*.d
diff --git a/Build.mk b/Build.mk
new file mode 100644
index 0000000..f2b374a
--- /dev/null
+++ b/Build.mk
@@ -0,0 +1,57 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Explicit directory traversal, following the Kbuild model. New directories do
+# not enter a build merely because a file appeared in the source tree.
+include arch/$(ARCH)/Build.mk
+include kernel/Build.mk
+include mm/Build.mk
+include fs/Build.mk
+include drivers/Build.mk
+include fdt/Build.mk
+include lib/Build.mk
+include test/Build.mk
+include usr/Build.mk
+include scripts/build/Build.mk
+
+# The image root is an interface target: it carries the complete dependency
+# set but contributes no sources of its own.
+targets += generated/autoconf generated/asm-offsets generated/linker-script \
+ generated/initramfs-cpio blob/initramfs kernel/image
+target.generated/autoconf.kind := generated
+target.generated/autoconf.outputs := $(BUILD)/generated/autoconf.h
+target.generated/autoconf.inputs := scripts/Kconfig.mk $(KCONFIG_CONFIG)
+target.generated/autoconf.action := autoconf
+
+target.generated/asm-offsets.kind := generated
+target.generated/asm-offsets.outputs := $(BUILD)/generated/asm-offsets.h
+target.generated/asm-offsets.inputs := arch/$(ARCH)/kernel/asm-offsets.c \
+ scripts/extract-asm-offsets.sed
+target.generated/asm-offsets.generated-deps := generated/autoconf
+target.generated/asm-offsets.action := asm-offsets
+
+target.generated/linker-script.kind := generated
+target.generated/linker-script.outputs := $(BUILD)/lumeos.lds
+target.generated/linker-script.inputs := arch/$(ARCH)/kernel/lumeos.lds.S \
+ include/lume/lumeos.lds.h
+target.generated/linker-script.action := linker-script
+
+target.generated/initramfs-cpio.kind := generated
+target.generated/initramfs-cpio.outputs := $(BUILD)/initramfs.cpio
+target.generated/initramfs-cpio.inputs := $(CONFIG_INITRAMFS_SOURCE) \
+ scripts/gen_initramfs.sh
+target.generated/initramfs-cpio.tool-deps := host/gen_init_cpio
+target.generated/initramfs-cpio.action := initramfs
+
+target.blob/initramfs.kind := binary-blob
+target.blob/initramfs.outputs := $(BUILD)/initramfs.o
+target.blob/initramfs.inputs := usr/initramfs.S
+target.blob/initramfs.generated-deps := generated/initramfs-cpio
+
+target.kernel/image.kind := product
+target.kernel/image.deps := \
+ arch/$(ARCH) kernel/core mm/core fs/core drivers/core fdt/core lib/core \
+ $(if $(filter y,$(CONFIG_SELFTEST)),test/kernel)
+target.kernel/image.generated-deps := \
+ generated/autoconf generated/asm-offsets generated/linker-script \
+ blob/initramfs
+target.kernel/image.outputs := $(VMLUME) $(IMAGE)
diff --git a/Makefile b/Makefile
index 88689f5..2d832ba 100644
--- a/Makefile
+++ b/Makefile
@@ -16,24 +16,30 @@
# ============================================================
# Build Configuration
# ============================================================
-BUILD := build
+O ?= build
+BUILD := $(patsubst %/,%,$(O))
# Make sure `make` with no arguments builds the kernel image, not the
-# first rule encountered during makefile parsing. scripts/Kconfig.mk
-# (included next) defines `defconfig:` at the top of its rule list,
-# which would otherwise become the default goal and silently overwrite
-# .config from arch/$(ARCH)/defconfig.
+# first rule encountered during makefile parsing.
.DEFAULT_GOAL := all
-# Load Kbuild helpers (Q, $(call cmd,...), quiet_cmd_* / cmd_* templates,
-# kbuild-objs translator). Must come before Kconfig.mk so subsequent
-# rules can use $(call cmd,...) immediately.
+# Load command helpers before Kconfig and target evaluation.
include scripts/Kbuild.include
-# Load Kconfig infrastructure (reads .config, sets KCPPFLAGS)
+# Load the O=-scoped configuration and derive compiler definitions.
include scripts/Kconfig.mk
ARCH ?= $(CONFIG_ARCH)
+PLATFORM ?= qemu-virt
+BUILD_VARIANT ?= debug
+
+ifneq ($(filter $(BUILD_VARIANT),debug release),$(BUILD_VARIANT))
+$(error unsupported BUILD_VARIANT '$(BUILD_VARIANT)' (expected debug or release))
+endif
+
+# Toolchain selection needs the configured architecture but must precede flag
+# and module evaluation.
+include scripts/build/toolchain.mk
# Two boot products — same pair as Linux RISC-V's arch/riscv/boot/Makefile
# (vmlinux + Image), but named LumeOS-style:
@@ -46,6 +52,7 @@ IMAGE := $(BUILD)/vmlume.bin
INITRAMFS_DIR := $(BUILD)/initramfs
INITRAMFS_CPIO := $(BUILD)/initramfs.cpio
INITRAMFS_OBJ := $(BUILD)/initramfs.o
+INITRAMFS_ASM := usr/initramfs.S
USR_INIT_ELF := $(BUILD)/usr/init
# Relocations: linker emits .rela.dyn under -shared -Bsymbolic; boot
# code in arch/riscv/mm/init.cc::relocate_kernel walks it at startup.
@@ -53,36 +60,6 @@ LDS_SRC := arch/$(ARCH)/kernel/lumeos.lds.S
LINKER_SCRIPT := $(BUILD)/lumeos.lds
PYTHON ?= python3
-# ============================================================
-# Toolchain Detection
-# ============================================================
-ifndef TOOLCHAIN_PREFIX
-ifeq ($(ARCH),riscv)
- # Prefer the linux-gnu toolchain — it supports -shared / -pie which
- # are required to emit .rela.dyn (Linux RISC-V kernel uses the same).
- # The bare-metal `riscv64-unknown-elf-` is kept only as a last-resort
- # fallback; with that one, `-shared` fails ("not supported") and the
- # kernel can't relocate at runtime.
- TOOLCHAIN_PREFIX := $(shell \
- if command -v riscv64-unknown-linux-gnu-g++ >/dev/null 2>&1; then \
- echo riscv64-unknown-linux-gnu-; \
- elif command -v riscv64-linux-gnu-g++ >/dev/null 2>&1; then \
- echo riscv64-linux-gnu-; \
- else \
- echo riscv64-unknown-linux-gnu-; \
- fi)
-else ifeq ($(ARCH),aarch64)
- TOOLCHAIN_PREFIX := aarch64-linux-gnu-
-else ifeq ($(ARCH),x86_64)
- TOOLCHAIN_PREFIX := x86_64-linux-gnu-
-endif
-endif
-
-CC := $(TOOLCHAIN_PREFIX)gcc
-CXX := $(TOOLCHAIN_PREFIX)g++
-LD := $(TOOLCHAIN_PREFIX)ld
-OBJCOPY := $(TOOLCHAIN_PREFIX)objcopy
-OBJDUMP := $(TOOLCHAIN_PREFIX)objdump
# Objcopy output formats for binary-to-ELF relocation blob
OBJCOPY_ELF_riscv := elf64-littleriscv
OBJCOPY_ELF_aarch64 := elf64-littleaarch64
@@ -94,92 +71,117 @@ OBJCOPY_BINARCH_aarch64 := aarch64
OBJCOPY_BINARCH_x86_64 := i386:x86-64
OBJCOPY_BINARCH := $(OBJCOPY_BINARCH_$(ARCH))
-# Common flags (base, before module includes)
-WARNINGS := -Wall -Wextra -Wno-unused-parameter
+# Common include roots. Environment-specific flags are defined after target
+# usage requirements have been collected.
INCLUDES := -Iinclude -I.
# ============================================================
-# Source Collection (Non-Recursive Module Fragments)
-# Module .mk files may append to SRCS_CC, SRCS_S, INCLUDES
+# Declarative Target Graph
# ============================================================
-SRCS_CC :=
-SRCS_C :=
-SRCS_S :=
-SRCS_s :=
-
--include arch/$(ARCH)/module.mk
-include kernel/module.mk
-include mm/module.mk
-include fs/module.mk
-include drivers/base/module.mk
-include drivers/module.mk
-include drivers/tty/module.mk
-include fdt/module.mk
-include lib/module.mk
-include test/module.mk
-
-# Compiler flags (defined AFTER includes so INCLUDES is complete)
-CFLAGS := $(WARNINGS) -O2 -g \
- -ffreestanding -nostdlib \
- -mcmodel=medany \
- -fPIE \
- -MMD -MP \
- $(KCPPFLAGS) \
- $(INCLUDES) \
- -I$(BUILD)/generated
-
-# -fPIE makes the compiler emit position-independent code (PC-relative
-# addressing + GOT for globals), which is what `-shared` linking expects.
-# Mirrors Linux's arch/riscv/Makefile under CONFIG_RELOCATABLE=y:
-# KBUILD_CFLAGS += -fPIE
-
-# C flags for musl libc sources: add libc-shim headers first
-CFLAGS_LIBC := $(CFLAGS) -Iinclude/libc-shim \
- -Wno-parentheses -Wno-sign-compare
-
-CXXFLAGS := $(CFLAGS) \
- -std=c++17 \
- -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
- -fno-threadsafe-statics
-
-# Assembler flags MUST NOT include -fPIE.
-# Linux's arch/riscv/Makefile adds -fPIE only to KBUILD_CFLAGS, NOT to
-# KBUILD_AFLAGS: with -fPIE the gcc driver puts the assembler in PIC
-# mode, where the `la` macro silently expands to a GOT-indirect load
-# (`auipc + ld` reading a slot that holds the link-time VA). In our
-# pre-MMU boot path that VA is unmapped, so any `la sym, sym` faults
-# and reboots into a `_start` loop. Stripping -fPIE makes `la` emit
-# direct PC-relative addressing (`auipc + addi`) which works at PA.
-ASFLAGS := $(filter-out -fPIE,$(CFLAGS))
-
-# Link as a position-independent shared object so the linker emits
-# `.rela.dyn` with R_RISCV_RELATIVE entries that the boot stub applies
-# at load time. Mirrors Linux's arch/riscv/Makefile §10:
-# LDFLAGS_vmlinux += -shared -Bsymbolic -z notext
-# `-Bsymbolic` resolves all global refs at link time (no dynamic
-# resolution); `-z notext` lets us keep relocations in code pages.
-LDFLAGS := -nostdlib -shared -Bsymbolic -z notext -z norelro --no-dynamic-linker
+include scripts/build/targets.mk
+include Build.mk
+$(eval $(call validate-targets))
+
+KERNEL_ROOT := kernel/image
+KERNEL_TARGETS := $(sort $(call target-closure,$(KERNEL_ROOT)))
+KERNEL_LINK_TARGET_ORDER := $(call unique-words,$(call target-link-order,$(KERNEL_ROOT)))
+KERNEL_SOURCE_KINDS := object-library source-set static-library whole-archive kernel-test
+KERNEL_SOURCE_TARGETS := $(foreach target,$(KERNEL_TARGETS),\
+ $(if $(filter $(KERNEL_SOURCE_KINDS),$(target.$(target).kind)),$(target)))
+KERNEL_SOURCES := $(sort $(foreach target,$(KERNEL_SOURCE_TARGETS),\
+ $(target.$(target).sources)))
+
+SRCS_CC := $(filter %.cc,$(KERNEL_SOURCES))
+SRCS_C := $(filter %.c,$(KERNEL_SOURCES))
+SRCS_S := $(filter %.S,$(KERNEL_SOURCES))
+SRCS_s := $(filter %.s,$(KERNEL_SOURCES))
+
+# Only public usage requirements propagate. Private include paths and flags are
+# attached to the objects belonging to the declaring target below.
+INCLUDES += $(addprefix -I,$(call target-public-includes,$(KERNEL_ROOT)))
+INCLUDES += $(foreach dir,$(call target-public-after-includes,$(KERNEL_ROOT)),-idirafter $(dir))
+
+include scripts/build/environments/kernel.mk
+include scripts/build/environments/phys.mk
# ============================================================
# Object File Computation
# ============================================================
-OBJS := $(patsubst %.cc,$(BUILD)/%.o,$(SRCS_CC)) \
- $(patsubst %.c,$(BUILD)/%.o,$(SRCS_C)) \
- $(patsubst %.S,$(BUILD)/%.o,$(SRCS_S)) \
- $(patsubst %.s,$(BUILD)/%.o,$(SRCS_s))
-
+# A source may be compiled by multiple targets/environments. Object identity
+# therefore includes both the logical target and source path.
+source-object = $(BUILD)/obj/$(call target-key,$(1))/$(basename $(2)).o
+define apply-target-usage
+target.$(1).objects := $$(foreach src,$$(target.$(1).sources),$$(call source-object,$(1),$$(src)))
+$$(target.$(1).objects): PRIVATE_INCLUDES := \
+ $$(addprefix -I,$$(call target-compile-includes,$(1))) \
+ $$(foreach dir,$$(call target-compile-after-includes,$(1)),-idirafter $$(dir))
+$$(target.$(1).objects): PRIVATE_DEFINES := $$(addprefix -D,$$(call target-compile-defines,$(1)))
+$$(target.$(1).objects): PRIVATE_CFLAGS := $$(target.$(1).cflags)
+$$(target.$(1).objects): PRIVATE_CXXFLAGS := $$(target.$(1).cxxflags)
+$$(target.$(1).objects): PRIVATE_ASFLAGS := $$(target.$(1).asflags)
+endef
+$(foreach target,$(KERNEL_SOURCE_TARGETS),$(eval $(call apply-target-usage,$(target))))
+
+define apply-source-identity
+$(call source-object,$(1),$(2)): PRIVATE_SOURCE := $(2)
+endef
+$(foreach target,$(KERNEL_SOURCE_TARGETS),$(foreach src,$(target.$(target).sources),\
+ $(eval $(call apply-source-identity,$(target),$(src)))))
+
+OBJS := $(foreach target,$(KERNEL_SOURCE_TARGETS),$(target.$(target).objects))
DEPS := $(OBJS:.o=.d)
+# libfdt is freestanding but intentionally does not use the libc-shim include
+# overlay used by the imported string implementation.
+$(target.lib/libfdt.objects): CFLAGS_LIBC = $(CFLAGS) -std=gnu23
+
+KERNEL_STATIC_TARGETS := $(foreach target,$(KERNEL_SOURCE_TARGETS),\
+ $(if $(filter static-library,$(target.$(target).kind)),$(target)))
+KERNEL_WHOLE_TARGETS := $(foreach target,$(KERNEL_SOURCE_TARGETS),\
+ $(if $(filter whole-archive,$(target.$(target).kind)),$(target)))
+KERNEL_OBJECT_TARGETS := $(filter-out $(KERNEL_STATIC_TARGETS) $(KERNEL_WHOLE_TARGETS),\
+ $(KERNEL_SOURCE_TARGETS))
+
+define define-archive
+target.$(1).archive := $(BUILD)/lib/$(call target-key,$(1)).a
+$$(target.$(1).archive): PRIVATE_ARCHIVE_OBJECTS := $$(target.$(1).objects)
+$$(target.$(1).archive): $$(target.$(1).objects) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,archive)
+endef
+$(foreach target,$(KERNEL_STATIC_TARGETS) $(KERNEL_WHOLE_TARGETS),\
+ $(eval $(call define-archive,$(target))))
+
+KERNEL_LINK_OBJECTS := $(foreach target,$(KERNEL_LINK_TARGET_ORDER),\
+ $(if $(filter $(target),$(KERNEL_OBJECT_TARGETS)),$(target.$(target).objects)))
+KERNEL_LINK_ARCHIVES := $(foreach target,$(KERNEL_LINK_TARGET_ORDER),\
+ $(if $(filter $(target),$(KERNEL_STATIC_TARGETS)),$(target.$(target).archive)))
+KERNEL_LINK_WHOLE := $(foreach target,$(KERNEL_LINK_TARGET_ORDER),\
+ $(if $(filter $(target),$(KERNEL_WHOLE_TARGETS)),$(target.$(target).archive)))
+KERNEL_LINK_INPUTS := $(KERNEL_LINK_OBJECTS) $(KERNEL_LINK_ARCHIVES) \
+ $(if $(KERNEL_LINK_WHOLE),--whole-archive $(KERNEL_LINK_WHOLE) --no-whole-archive)
+
# ============================================================
# Build Rules
# ============================================================
-.PHONY: all clean run debug fmt disasm defconfig savedefconfig showconfig
+.PHONY: all clean run debug fmt disasm defconfig savedefconfig showconfig \
+ help info check-toolchain check-image targets target-info \
+ host-libraries host-tests kernel-tests compile_commands \
+ phys-libraries toolchain-probe graph FORCE
+
+FORCE:
+
+all: $(VMLUME) $(IMAGE) check-image
-all: $(VMLUME) $(IMAGE)
+check-image: $(VMLUME)
+ @RE=$(READELF); NM=$(NM); \
+ scripts/build/check-kernel-elf.sh $(VMLUME) "$$RE" "$$NM" $(ARCH)
# Generate autoconf.h before any compilation
-$(AUTOCONF_H): scripts/Kconfig.mk $(wildcard .config)
- $(generate-autoconf)
+$(AUTOCONF_H): scripts/Kconfig.mk $(wildcard $(KCONFIG_CONFIG)) \
+ scripts/build/generate-autoconf.sh FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,autoconf)
# ============================================================
# Generated header: asm-offsets.h
@@ -199,27 +201,20 @@ ASM_OFFSETS_H := $(BUILD)/generated/asm-offsets.h
ASM_OFFSETS_SED := scripts/extract-asm-offsets.sed
# Compile asm-offsets.c to *.s only — we never want the .o linked in.
-$(ASM_OFFSETS_S): $(ASM_OFFSETS_C) $(AUTOCONF_H)
- @echo " CC -S $<"
- @mkdir -p $(dir $@)
- @$(CC) $(CFLAGS_LIBC) -S $< -o $@
+$(ASM_OFFSETS_S): PRIVATE_SOURCE := $(ASM_OFFSETS_C)
+$(ASM_OFFSETS_S): $(ASM_OFFSETS_C) $(AUTOCONF_H) FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,cc_s_c)
# Rewrite ->NAME markers into #define lines. Header guard hand-rolled
# at the top, sed body in the middle, closing #endif at the bottom.
-$(ASM_OFFSETS_H): $(ASM_OFFSETS_S) $(ASM_OFFSETS_SED)
- @echo " GEN $@"
- @mkdir -p $(dir $@)
- @(echo "/* AUTO-GENERATED — DO NOT EDIT */"; \
- echo "/* Generated from $(ASM_OFFSETS_C) by scripts/extract-asm-offsets.sed */"; \
- echo "#ifndef _LUME_GENERATED_ASM_OFFSETS_H"; \
- echo "#define _LUME_GENERATED_ASM_OFFSETS_H"; \
- echo ""; \
- sed -n -f $(ASM_OFFSETS_SED) $<; \
- echo ""; \
- echo "#endif /* _LUME_GENERATED_ASM_OFFSETS_H */") > $@
+$(ASM_OFFSETS_H): $(ASM_OFFSETS_S) $(ASM_OFFSETS_SED) \
+ scripts/build/generate-asm-offsets.sh FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,gen_asm_offsets)
# All objects depend on autoconf.h and asm-offsets.h
-$(OBJS): $(AUTOCONF_H) $(ASM_OFFSETS_H)
+$(OBJS): $(AUTOCONF_H) $(ASM_OFFSETS_H) FORCE
# ============================================================
# Generic %.pi.o post-processing rule
@@ -265,10 +260,10 @@ $(BUILD)/%.pi.o: $(BUILD)/%.o
$(call cmd,objcopy)
# Preprocess linker script template (.lds.S → .lds)
-$(LINKER_SCRIPT): $(LDS_SRC) include/lume/lumeos.lds.h
- @echo " LDS $@"
- @mkdir -p $(dir $@)
- @$(CC) -E -P -x c $(INCLUDES) $< -o $@
+$(LINKER_SCRIPT): PRIVATE_SOURCE := $(LDS_SRC)
+$(LINKER_SCRIPT): $(LDS_SRC) include/lume/lumeos.lds.h FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,linker_script)
# ============================================================
# Initramfs: pack a root filesystem into a newc cpio.
@@ -282,84 +277,143 @@ $(LINKER_SCRIPT): $(LDS_SRC) include/lume/lumeos.lds.h
# Sourced from $(CONFIG_INITRAMFS_SOURCE) — see scripts/Kconfig.mk.
# ============================================================
-# Host compiler — distinct from $(CC), which may be the cross compiler.
-HOSTCC ?= cc
-HOSTCFLAGS ?= -O2 -Wall
-
-# Output path matches Linux convention (usr/gen_init_cpio) so the
-# verbatim copy of scripts/gen_initramfs.sh from Linux finds it via its
-# hardcoded relative path when invoked from the kernel source root.
-GEN_INIT_CPIO := usr/gen_init_cpio
-
-$(GEN_INIT_CPIO): usr/gen_init_cpio.c
- @echo " HOSTCC $@"
- @$(HOSTCC) $(HOSTCFLAGS) -o $@ $<
+# Host compiler environment is distinct from the cross kernel environment.
+include scripts/build/environments/host.mk
+
+# Host tools are registered targets and use the host compiler environment.
+HOST_TOOL_TARGETS := $(foreach target,$(targets),\
+ $(if $(filter host-tool,$(target.$(target).kind)),$(target)))
+HOST_LIBRARY_TARGETS := $(foreach target,$(targets),\
+ $(if $(filter host-library,$(target.$(target).kind)),$(target)))
+HOST_TEST_TARGETS := $(foreach target,$(targets),\
+ $(if $(filter host-test,$(target.$(target).kind)),$(target)))
+PHYS_LIBRARY_TARGETS := $(foreach target,$(targets),\
+ $(if $(filter phys-library,$(target.$(target).kind)),$(target)))
+define define-host-tool
+target.$(1).output := $(BUILD)/host/$(call target-key,$(1))
+$$(target.$(1).output): PRIVATE_HOST_SOURCE := $$(firstword $$(target.$(1).sources))
+$$(target.$(1).output): PRIVATE_HOST_CFLAGS := $$(target.$(1).host-cflags)
+$$(target.$(1).output): $$(target.$(1).sources) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,host_tool)
+endef
+$(foreach target,$(HOST_TOOL_TARGETS),$(eval $(call define-host-tool,$(target))))
+
+host-source-object = $(BUILD)/host/obj/$(call target-key,$(1))/$(basename $(2)).o
+define define-host-source
+$(call host-source-object,$(1),$(2)): PRIVATE_SOURCE := $(2)
+$(call host-source-object,$(1),$(2)): $(2) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,$(if $(filter %.cc,$(2)),host_cxx_o_cc,host_cc_o_c))
+endef
+define define-host-library
+target.$(1).objects := $$(foreach src,$$(target.$(1).sources),$$(call host-source-object,$(1),$$(src)))
+target.$(1).archive := $(BUILD)/host/lib/$(call target-key,$(1)).a
+$$(foreach src,$$(target.$(1).sources),$$(eval $$(call define-host-source,$(1),$$(src))))
+$$(target.$(1).archive): PRIVATE_ARCHIVE_OBJECTS := $$(target.$(1).objects)
+$$(target.$(1).archive): $$(target.$(1).objects) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,host_archive)
+endef
+$(foreach target,$(HOST_LIBRARY_TARGETS),$(eval $(call define-host-library,$(target))))
+HOST_DEPS := $(foreach target,$(HOST_LIBRARY_TARGETS),$(target.$(target).objects:.o=.d))
+
+define define-host-test
+target.$(1).objects := $$(foreach src,$$(target.$(1).sources),$$(call host-source-object,$(1),$$(src)))
+target.$(1).output := $(BUILD)/host/test/$(call target-key,$(1))
+$$(foreach src,$$(target.$(1).sources),$$(eval $$(call define-host-source,$(1),$$(src))))
+$$(target.$(1).output): PRIVATE_HOST_OBJECTS := $$(target.$(1).objects)
+$$(target.$(1).output): PRIVATE_HOST_LIBS := $$(foreach dep,$$(call target-deps,$(1)),$$(target.$$(dep).archive))
+$$(target.$(1).output): $$(target.$(1).objects) \
+ $$(foreach dep,$$(call target-deps,$(1)),$$(target.$$(dep).archive)) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,host_link)
+endef
+$(foreach target,$(HOST_TEST_TARGETS),$(eval $(call define-host-test,$(target))))
+HOST_TEST_DEPS := $(foreach target,$(HOST_TEST_TARGETS),$(target.$(target).objects:.o=.d))
+DEPS += $(HOST_DEPS) $(HOST_TEST_DEPS)
+
+host-libraries: $(foreach target,$(HOST_LIBRARY_TARGETS),$(target.$(target).archive))
+
+host-tests: $(foreach target,$(HOST_TEST_TARGETS),$(target.$(target).output))
+ @set -e; $(foreach target,$(HOST_TEST_TARGETS),\
+ echo " TEST $(target)"; $(target.$(target).output);)
+
+phys-source-object = $(BUILD)/phys/obj/$(call target-key,$(1))/$(basename $(2)).o
+define define-phys-source
+$(call phys-source-object,$(1),$(2)): PRIVATE_SOURCE := $(2)
+$(call phys-source-object,$(1),$(2)): $(2) $(AUTOCONF_H) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,$(if $(filter %.cc,$(2)),phys_cxx_o_cc,$(if $(filter %.c,$(2)),phys_cc_o_c,phys_as_o_S)))
+endef
+define define-phys-library
+target.$(1).objects := $$(foreach src,$$(target.$(1).sources),$$(call phys-source-object,$(1),$$(src)))
+target.$(1).archive := $(BUILD)/phys/lib/$(call target-key,$(1)).a
+$$(foreach src,$$(target.$(1).sources),$$(eval $$(call define-phys-source,$(1),$$(src))))
+$$(target.$(1).archive): PRIVATE_ARCHIVE_OBJECTS := $$(target.$(1).objects)
+$$(target.$(1).archive): $$(target.$(1).objects) FORCE
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,archive)
+endef
+$(foreach target,$(PHYS_LIBRARY_TARGETS),$(eval $(call define-phys-library,$(target))))
+PHYS_DEPS := $(foreach target,$(PHYS_LIBRARY_TARGETS),$(target.$(target).objects:.o=.d))
+DEPS += $(PHYS_DEPS)
+
+phys-libraries: $(foreach target,$(PHYS_LIBRARY_TARGETS),$(target.$(target).archive))
+
+kernel-tests: all
+ @test "$(CONFIG_SELFTEST)" = y || { \
+ echo "kernel-tests requires CONFIG_SELFTEST=y in $(KCONFIG_CONFIG)"; exit 2; }
+
+GEN_INIT_CPIO := $(target.host/gen_init_cpio.output)
INITRAMFS_SOURCE := $(CONFIG_INITRAMFS_SOURCE)
-# Record the chosen source so changing CONFIG_INITRAMFS_SOURCE on the
-# command line forces a rebuild — without this, `make` only compares
-# file mtimes and would happily reuse a cpio packed from a different
-# source as long as that other source isn't newer. Evaluated at
-# Makefile parse time so the dep edge is correct from the first rule
-# check. Mirrors the idea behind Linux's $(call if_changed,...).
-INITRAMFS_SRC_STAMP := $(BUILD)/.initramfs_src.stamp
-$(shell mkdir -p $(BUILD) 2>/dev/null; \
- prev=$$([ -f "$(INITRAMFS_SRC_STAMP)" ] && cat "$(INITRAMFS_SRC_STAMP)" || echo); \
- if [ "$$prev" != "$(INITRAMFS_SOURCE)" ]; then \
- printf '%s\n' "$(INITRAMFS_SOURCE)" > "$(INITRAMFS_SRC_STAMP)"; \
- fi)
+# Encode the selected source in the stamp filename. Changing the source changes
+# the dependency without writing anything during Makefile parsing.
+INITRAMFS_SOURCE_KEY := $(shell printf '%s' '$(INITRAMFS_SOURCE)' | cksum | cut -d' ' -f1)
+INITRAMFS_SRC_STAMP := $(BUILD)/.initramfs-source-$(INITRAMFS_SOURCE_KEY)
-$(INITRAMFS_CPIO): $(INITRAMFS_SOURCE) $(INITRAMFS_SRC_STAMP) \
- $(GEN_INIT_CPIO) scripts/gen_initramfs.sh
- @echo " INITRAMFS $(INITRAMFS_SOURCE)"
+$(INITRAMFS_SRC_STAMP):
@mkdir -p $(dir $@)
- @./scripts/gen_initramfs.sh -o $@ \
- -u $(CONFIG_INITRAMFS_ROOT_UID) \
- -g $(CONFIG_INITRAMFS_ROOT_GID) \
- $(INITRAMFS_SOURCE)
+ @touch $@
-$(INITRAMFS_OBJ): $(INITRAMFS_CPIO)
- @echo " OBJCOPY $@ (initramfs)"
- @$(OBJCOPY) -I binary -O $(OBJCOPY_ELF) -B $(OBJCOPY_BINARCH) \
- --rename-section .data=.init.ramfs,alloc,load,readonly,data,contents \
- $< $@
+$(INITRAMFS_CPIO): $(INITRAMFS_SOURCE) $(INITRAMFS_SRC_STAMP) \
+ $(GEN_INIT_CPIO) scripts/gen_initramfs.sh \
+ scripts/build/generate-initramfs.sh FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,initramfs)
+
+$(INITRAMFS_OBJ): PRIVATE_SOURCE := $(INITRAMFS_ASM)
+$(INITRAMFS_OBJ): $(INITRAMFS_CPIO) $(INITRAMFS_ASM) FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,initramfs_obj)
# Single-pass kernel link. With `-shared -Bsymbolic`, the linker
# emits `.rela.dyn` containing R_RISCV_RELATIVE entries directly into
# the output ELF — no separate gen_reloc pass.
-$(VMLUME): $(OBJS) $(INITRAMFS_OBJ) $(LINKER_SCRIPT)
- @echo " LD $@"
- @mkdir -p $(dir $@)
- @$(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $(OBJS) $(INITRAMFS_OBJ)
- @$(TOOLCHAIN_PREFIX)size $@
+$(VMLUME): $(OBJS) $(foreach target,$(KERNEL_STATIC_TARGETS) $(KERNEL_WHOLE_TARGETS),\
+ $(target.$(target).archive)) \
+ $(INITRAMFS_OBJ) $(LINKER_SCRIPT) FORCE
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,link_vmlume)
# Image = raw binary, mirrors Linux's `arch/riscv/boot/Image` (objcopy
# vmlinux to raw bytes, no ELF header — what U-Boot or QEMU's `-bios`
# bare-loading would consume). Linux always builds both vmlinux and
# Image; LumeOS follows the same convention.
-$(IMAGE): $(VMLUME)
- @echo " OBJCOPY $@"
- @$(OBJCOPY) -O binary $< $@
-
-# Compile C++ sources. Recipe goes through $(call cmd,...) so that
-# `make V=1` echoes the full command line. $(Q) prefix on mkdir keeps
-# the directory-creation step silent in quiet mode but visible at V=1.
-$(BUILD)/%.o: %.cc
- $(Q)mkdir -p $(dir $@)
- $(call cmd,cxx_o_cc)
-
-$(BUILD)/%.o: %.c
- $(Q)mkdir -p $(dir $@)
- $(call cmd,cc_o_c)
-
-$(BUILD)/%.o: %.S
- $(Q)mkdir -p $(dir $@)
- $(call cmd,as_o_S)
-
-$(BUILD)/%.o: %.s
- $(Q)mkdir -p $(dir $@)
- $(call cmd,as_o_S)
+$(IMAGE): $(VMLUME) FORCE
+ $(call if_changed,objcopy_bin)
+
+# Materialize one explicit action per target/source pair. This avoids accidental
+# object reuse when the same source is built in more than one environment.
+define define-kernel-source
+$(call source-object,$(1),$(2)): $(2)
+ $$(Q)mkdir -p $$(dir $$@)
+ $$(call if_changed,$(if $(filter %.cc,$(2)),cxx_o_cc,$(if $(filter %.c,$(2)),cc_o_c,as_o_S)))
+endef
+$(foreach target,$(KERNEL_SOURCE_TARGETS),$(foreach src,$(target.$(target).sources),\
+ $(eval $(call define-kernel-source,$(target),$(src)))))
# Auto-dependency tracking
-include $(DEPS)
@@ -412,8 +466,128 @@ lint:
# Print build info (for debugging Makefile issues)
info:
- @echo "TOOLCHAIN_PREFIX = $(TOOLCHAIN_PREFIX)"
- @echo "SRCS_CC = $(SRCS_CC)"
- @echo "SRCS_S = $(SRCS_S)"
- @echo "OBJS = $(OBJS)"
- @echo "KCPPFLAGS = $(KCPPFLAGS)"
+ @echo "LumeOS build configuration"
+ @echo " ARCH $(ARCH)"
+ @echo " PLATFORM $(PLATFORM)"
+ @echo " VARIANT $(BUILD_VARIANT)"
+ @echo " OUTPUT $(BUILD)"
+ @echo " COMPILER $(COMPILER_FAMILY): $(CXX)"
+ @echo " TARGET $(TARGET_TRIPLE)"
+ @echo " GCC_TOOLCHAIN $(GCC_TOOLCHAIN)"
+ @echo " SYSROOT $(GCC_SYSROOT)"
+ @echo " LINKER $(LINKER_FAMILY): $(LD)"
+ @echo " OBJCOPY $(OBJCOPY)"
+ @echo " INITRAMFS $(INITRAMFS_SOURCE)"
+ @echo " C++ STANDARD C++23"
+ @echo " C++ LIBRARY $(CXX_STDLIB)"
+ @echo " C++ ABI $(CXX_ABI)"
+ @echo " COMPILER RUNTIME $(COMPILER_RUNTIME)"
+ @echo " CONFIG $(KCONFIG_CONFIG)"
+ @echo " BUILD ROOT $(KERNEL_ROOT)"
+ @echo " TARGETS $(words $(KERNEL_TARGETS)) selected / $(words $(targets)) registered"
+
+targets:
+ @$(foreach target,$(targets),printf '%-24s %-16s %s\n' \
+ '$(target)' '$(target.$(target).kind)' '$(target.$(target).deps)';)
+
+target-info:
+ @test -n "$(TARGET)" || { echo "usage: make target-info TARGET=<name>"; exit 2; }
+ @$(if $(filter $(TARGET),$(targets)),,echo "unknown target: $(TARGET)"; exit 2;)
+ @echo "Target: $(TARGET)"
+ @echo "Kind: $(target.$(TARGET).kind)"
+ @echo "Dependencies: $(target.$(TARGET).deps)"
+ @echo "Public deps: $(target.$(TARGET).public-deps)"
+ @echo "Private deps: $(target.$(TARGET).private-deps)"
+ @echo "Link deps: $(target.$(TARGET).link-deps)"
+ @echo "Tool deps: $(target.$(TARGET).tool-deps)"
+ @echo "Generated deps: $(target.$(TARGET).generated-deps)"
+ @echo "Closure: $(sort $(call target-closure,$(TARGET)))"
+ @echo "Public includes: $(target.$(TARGET).public-includes)"
+ @echo "Private includes: $(target.$(TARGET).private-includes)"
+ @echo "Sources: $(target.$(TARGET).sources)"
+
+TARGET_GRAPH := $(BUILD)/targets.dot
+graph:
+ @mkdir -p $(BUILD)
+ @$(file >$(TARGET_GRAPH),digraph lume_build {)
+ @$(foreach target,$(targets),$(file >>$(TARGET_GRAPH), "$(target)" [label="$(target)\n$(target.$(target).kind)"];))
+ @$(foreach target,$(targets),$(foreach dep,$(call target-deps,$(target)),\
+ $(file >>$(TARGET_GRAPH), "$(target)" -> "$(dep)";)))
+ @$(file >>$(TARGET_GRAPH),})
+ @echo " GRAPH $(TARGET_GRAPH)"
+
+COMPDB := $(BUILD)/compile_commands.json
+COMPDB_INPUT := $(BUILD)/.compile_commands.tsv
+compile-command = $(if $(filter %.cc,$(2)),$(CXX) $(CXXFLAGS) \
+ $(addprefix -I,$(call target-compile-includes,$(1))) \
+ $(foreach dir,$(call target-compile-after-includes,$(1)),-idirafter $(dir)) \
+ $(addprefix -D,$(call target-compile-defines,$(1))) $(target.$(1).cxxflags),\
+ $(CC) $(if $(filter %.c,$(2)),$(CFLAGS_LIBC) \
+ $(addprefix -I,$(call target-compile-includes,$(1))) \
+ $(foreach dir,$(call target-compile-after-includes,$(1)),-idirafter $(dir)) \
+ $(addprefix -D,$(call target-compile-defines,$(1))) $(target.$(1).cflags),\
+ $(ASFLAGS) $(addprefix -I,$(call target-compile-includes,$(1))) \
+ $(foreach dir,$(call target-compile-after-includes,$(1)),-idirafter $(dir)) \
+ $(addprefix -D,$(call target-compile-defines,$(1))) \
+ $(target.$(1).asflags))) -c $(2) -o $(call source-object,$(1),$(2))
+
+$(BUILD):
+ @mkdir -p $@
+
+$(COMPDB_INPUT): FORCE | $(BUILD)
+ @$(file >$(COMPDB_INPUT))
+ @$(foreach target,$(KERNEL_SOURCE_TARGETS),$(foreach src,$(target.$(target).sources),\
+ $(file >>$(COMPDB_INPUT),$(src) $(call source-object,$(target),$(src)) $(call compile-command,$(target),$(src)))))
+
+compile_commands: $(COMPDB_INPUT)
+ @$(PYTHON) scripts/build/write-compdb.py $(COMPDB_INPUT) $(COMPDB)
+ @echo " COMPDB $(COMPDB)"
+
+check-toolchain:
+ @command -v $(firstword $(CC)) >/dev/null || { echo "missing C compiler: $(CC)"; exit 1; }
+ @command -v $(firstword $(CXX)) >/dev/null || { echo "missing C++ compiler: $(CXX)"; exit 1; }
+ @command -v $(firstword $(LD)) >/dev/null || { echo "missing linker: $(LD)"; exit 1; }
+ @command -v $(firstword $(OBJCOPY)) >/dev/null || { echo "missing objcopy: $(OBJCOPY)"; exit 1; }
+ @$(CXX) $(TOOLCHAIN_CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
+ -ffreestanding -fno-exceptions -fno-rtti -fsyntax-only -x c++ /dev/null
+ @echo "Toolchain OK: $$($(CXX) --version | head -n 1)"
+
+TOOLCHAIN_REPORT := $(BUILD)/toolchain.report
+$(TOOLCHAIN_REPORT): FORCE scripts/build/probe-toolchain.sh
+ $(Q)mkdir -p $(dir $@)
+ $(call if_changed,toolchain_probe)
+
+toolchain-probe: $(TOOLCHAIN_REPORT)
+ @cat $(TOOLCHAIN_REPORT)
+
+check-toolchain: $(TOOLCHAIN_REPORT)
+
+help:
+ @echo "LumeOS build targets:"
+ @echo " all Build vmlume.elf and vmlume.bin (default)"
+ @echo " run Run the selected image in QEMU"
+ @echo " debug Run QEMU with a GDB server on :1234"
+ @echo " disasm Generate an interleaved source disassembly"
+ @echo " info Show the effective build configuration"
+ @echo " check-toolchain Validate compiler, linker, and C++23 support"
+ @echo " check-image Validate ELF architecture, symbols, segments, and relocations"
+ @echo " targets List registered build targets and dependencies"
+ @echo " target-info Explain TARGET=<name>"
+ @echo " compile_commands Generate an O= scoped compilation database"
+ @echo " graph Write the target dependency graph as DOT"
+ @echo " host-libraries Build registered native static libraries"
+ @echo " host-tests Build and run registered native tests"
+ @echo " phys-libraries Build early physical-environment libraries"
+ @echo " kernel-tests Build the test-enabled kernel (CONFIG_SELFTEST=y)"
+ @echo " toolchain-probe Probe and cache required compiler capabilities"
+ @echo " clean Remove only the selected O= output tree"
+ @echo ""
+ @echo "Common options:"
+ @echo " O=<dir> Output directory (default: build)"
+ @echo " LLVM=1 Use Clang with GNU ld"
+ @echo " LLVM=1 LD_TYPE=lld Use Clang with LLD"
+ @echo " LLVM_VERSION=<version> Versioned LLVM tools (default: 20)"
+ @echo " CROSS_COMPILE=<prefix> Override GNU cross-tool prefix"
+ @echo " GCC_TOOLCHAIN=<dir> GCC installation used by Clang for target headers/builtins"
+ @echo " V=1 Print full command lines"
+ @echo " CONFIG_INITRAMFS_SOURCE=<path> Select initramfs input"
diff --git a/arch/riscv/Build.mk b/arch/riscv/Build.mk
new file mode 100644
index 0000000..0a18344
--- /dev/null
+++ b/arch/riscv/Build.mk
@@ -0,0 +1,19 @@
+targets += arch/riscv
+target.arch/riscv.kind := source-set
+target.arch/riscv.public-includes := arch/riscv/include
+target.arch/riscv.sources := \
+ arch/riscv/mm/pmap.cc \
+ arch/riscv/mm/init.cc \
+ arch/riscv/config.cc \
+ arch/riscv/trap/trap_md.cc \
+ arch/riscv/syscall/syscall_md.cc \
+ arch/riscv/lib/random.cc \
+ arch/riscv/mm/tlb_md.cc \
+ arch/riscv/kernel/head.S \
+ arch/riscv/trap/swtch.S \
+ arch/riscv/trap/fpu.S \
+ arch/riscv/trap/trap_vector.S \
+ arch/riscv/uaccess/uaccess_memcpy.S \
+ arch/riscv/trap/extable.cc \
+ arch/riscv/kernel/signal.cc
+
diff --git a/arch/riscv/include/asm/sections.h b/arch/riscv/include/asm/sections.h
index 454d9de..7067444 100644
--- a/arch/riscv/include/asm/sections.h
+++ b/arch/riscv/include/asm/sections.h
@@ -23,7 +23,7 @@ extern "C" {
#endif
extern char _start[];
-extern char _end[];
+extern char _kernel_end[];
extern char _stext[];
extern char _etext[];
diff --git a/arch/riscv/mm/init.cc b/arch/riscv/mm/init.cc
index 3cebcd9..30ea45e 100644
--- a/arch/riscv/mm/init.cc
+++ b/arch/riscv/mm/init.cc
@@ -88,7 +88,7 @@ extern "C" void __init setup_vm(uintptr_t load_pa, uintptr_t dtb_pa)
kernel_map.virt_offset = 0;
kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;
kernel_map.phys_addr = load_pa;
- kernel_map.size = (uintptr_t)_end - (uintptr_t)_start;
+ kernel_map.size = (uintptr_t)_kernel_end - (uintptr_t)_start;
kernel_map.page_offset = PAGE_OFFSET;
/* Mirrors Linux: va_pa_offset is the constant added to a physical
diff --git a/arch/riscv/module.mk b/arch/riscv/module.mk
deleted file mode 100644
index 97d22de..0000000
--- a/arch/riscv/module.mk
+++ /dev/null
@@ -1,18 +0,0 @@
-# arch/riscv/module.mk
-SRCS_CC += arch/riscv/mm/pmap.cc
-SRCS_CC += arch/riscv/mm/init.cc
-SRCS_CC += arch/riscv/config.cc
-SRCS_CC += arch/riscv/trap/trap_md.cc
-SRCS_CC += arch/riscv/syscall/syscall_md.cc
-SRCS_CC += arch/riscv/lib/random.cc
-SRCS_CC += arch/riscv/mm/tlb_md.cc
-SRCS_S += arch/riscv/kernel/head.S
-SRCS_S += arch/riscv/trap/swtch.S
-SRCS_S += arch/riscv/trap/fpu.S
-SRCS_S += arch/riscv/trap/trap_vector.S
-SRCS_S += arch/riscv/uaccess/uaccess_memcpy.S
-SRCS_CC += arch/riscv/trap/extable.cc
-SRCS_CC += arch/riscv/kernel/signal.cc
-
-# Arch-specific headers (added to include path)
-INCLUDES += -Iarch/riscv/include
diff --git a/drivers/Build.mk b/drivers/Build.mk
new file mode 100644
index 0000000..342f02b
--- /dev/null
+++ b/drivers/Build.mk
@@ -0,0 +1,17 @@
+targets += drivers/base drivers/tty drivers/core
+target.drivers/base.kind := source-set
+target.drivers/base.sources := \
+ drivers/base/bus.cc drivers/base/core.cc drivers/base/driver.cc \
+ drivers/base/platform.cc
+
+target.drivers/tty.kind := source-set
+target.drivers/tty.sources := \
+ drivers/tty/tty_core.cc drivers/tty/n_tty.cc drivers/tty/tty_uart.cc
+
+target.drivers/core.kind := source-set
+target.drivers/core.deps := drivers/base drivers/tty
+target.drivers/core.sources := drivers/uart.cc drivers/console.cc
+ifeq ($(ARCH),riscv)
+target.drivers/core.sources += drivers/plic.cc
+endif
+
diff --git a/drivers/base/module.mk b/drivers/base/module.mk
deleted file mode 100644
index 1d8fb05..0000000
--- a/drivers/base/module.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# Copyright (C) 2026 Richard Qin
-#
-# Driver model core (mirrors Linux drivers/base/)
-
-SRCS_CC += drivers/base/bus.cc
-SRCS_CC += drivers/base/core.cc
-SRCS_CC += drivers/base/driver.cc
-SRCS_CC += drivers/base/platform.cc
diff --git a/drivers/module.mk b/drivers/module.mk
deleted file mode 100644
index 9326f30..0000000
--- a/drivers/module.mk
+++ /dev/null
@@ -1,11 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# Copyright (C) 2026 Richard Qin
-#
-# Drivers module
-
-SRCS_CC += drivers/uart.cc
-SRCS_CC += drivers/console.cc
-
-ifeq ($(ARCH),riscv)
-SRCS_CC += drivers/plic.cc
-endif
diff --git a/drivers/tty/module.mk b/drivers/tty/module.mk
deleted file mode 100644
index 49190d4..0000000
--- a/drivers/tty/module.mk
+++ /dev/null
@@ -1,5 +0,0 @@
-# drivers/tty/module.mk — TTY subsystem build rules
-
-SRCS_CC += drivers/tty/tty_core.cc
-SRCS_CC += drivers/tty/n_tty.cc
-SRCS_CC += drivers/tty/tty_uart.cc
diff --git a/fdt/Build.mk b/fdt/Build.mk
new file mode 100644
index 0000000..ee8be88
--- /dev/null
+++ b/fdt/Build.mk
@@ -0,0 +1,4 @@
+targets += fdt/core
+target.fdt/core.kind := source-set
+target.fdt/core.sources := fdt/fdt.cc fdt/device_node.cc
+
diff --git a/fdt/module.mk b/fdt/module.mk
deleted file mode 100644
index 3e490ca..0000000
--- a/fdt/module.mk
+++ /dev/null
@@ -1,3 +0,0 @@
-# fdt/module.mk
-SRCS_CC += fdt/fdt.cc
-SRCS_CC += fdt/device_node.cc
diff --git a/fs/Build.mk b/fs/Build.mk
new file mode 100644
index 0000000..057d1df
--- /dev/null
+++ b/fs/Build.mk
@@ -0,0 +1,8 @@
+targets += fs/core
+target.fs/core.kind := source-set
+target.fs/core.sources := \
+ fs/super.cc fs/inode.cc fs/dcache.cc fs/file_table.cc fs/fd.cc \
+ fs/fs_struct.cc fs/namei.cc fs/ramfs/inode.cc fs/ramfs/file.cc \
+ fs/ramfs/super.cc fs/vfs_init.cc fs/chardev.cc fs/devfs.cc fs/pipe.cc \
+ fs/initramfs.cc
+
diff --git a/fs/module.mk b/fs/module.mk
deleted file mode 100644
index f1be157..0000000
--- a/fs/module.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-# fs/module.mk — VFS subsystem build rules
-
-SRCS_CC += fs/super.cc
-SRCS_CC += fs/inode.cc
-SRCS_CC += fs/dcache.cc
-SRCS_CC += fs/file_table.cc
-SRCS_CC += fs/fd.cc
-SRCS_CC += fs/fs_struct.cc
-SRCS_CC += fs/namei.cc
-SRCS_CC += fs/ramfs/inode.cc
-SRCS_CC += fs/ramfs/file.cc
-SRCS_CC += fs/ramfs/super.cc
-SRCS_CC += fs/vfs_init.cc
-SRCS_CC += fs/chardev.cc
-SRCS_CC += fs/devfs.cc
-SRCS_CC += fs/pipe.cc
-SRCS_CC += fs/initramfs.cc
diff --git a/kernel/Build.mk b/kernel/Build.mk
new file mode 100644
index 0000000..8f8ffa0
--- /dev/null
+++ b/kernel/Build.mk
@@ -0,0 +1,11 @@
+targets += kernel/core
+target.kernel/core.kind := source-set
+target.kernel/core.sources := \
+ kernel/main.cc kernel/sync/spinlock.cc kernel/sync/mutex.cc \
+ kernel/trap.cc kernel/irq.cc kernel/sched_core.cc \
+ kernel/sched_class_o1.cc kernel/timer.cc kernel/pid.cc kernel/pgrp.cc \
+ kernel/task.cc kernel/syscall.cc kernel/sys_process.cc kernel/sys_mm.cc \
+ kernel/sys_info.cc kernel/sys_fs.cc kernel/uaccess.cc kernel/waitqueue.cc \
+ kernel/signal.cc kernel/sys_signal.cc kernel/sys_misc.cc kernel/exec.cc \
+ kernel/poll.cc kernel/ptrace.cc kernel/smp.cc
+
diff --git a/kernel/main.cc b/kernel/main.cc
index db5843c..c84f3ad 100644
--- a/kernel/main.cc
+++ b/kernel/main.cc
@@ -82,7 +82,9 @@ void tty_init(); // 15.5. TTY subsystem (console TTY + UART RX IRQ)
void populate_rootfs(); // 15.6. Unpack embedded initramfs CPIO into ramfs
// Boot self-test runner
+#ifdef CONFIG_SELFTEST
void selftest_run_all();
+#endif
// AP per-CPU init
void vmm_init_ap(); // Switch to BSP-built kernel page table
@@ -100,6 +102,7 @@ static void init_thread_func(void *) {
printk(KERN_INFO "[init] PID 1 started\n");
/* Run post-SMP tests — APs are online by now */
+#ifdef CONFIG_SELFTEST
extern void selftest_smp();
kprintf("\n========================================\n");
kprintf(" Post-SMP Test Suite\n");
@@ -108,6 +111,7 @@ static void init_thread_func(void *) {
kprintf("========================================\n");
kprintf(" Post-SMP tests PASSED\n");
kprintf("========================================\n\n");
+#endif
/*
* Set up init's own VFS context (like Linux kernel_init).
@@ -326,7 +330,9 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// 17. Boot self-tests — verify subsystem correctness.
// Tests that create kthreads must wait() for them (freeing PIDs).
+#ifdef CONFIG_SELFTEST
selftest_run_all();
+#endif
// 18. Spawn PID 1 (init) — the first persistent task.
// Like Linux's rest_init() → user_mode_thread(kernel_init).
diff --git a/kernel/module.mk b/kernel/module.mk
deleted file mode 100644
index 754a14c..0000000
--- a/kernel/module.mk
+++ /dev/null
@@ -1,26 +0,0 @@
-# kernel/module.mk
-SRCS_CC += kernel/main.cc
-SRCS_CC += kernel/sync/spinlock.cc
-SRCS_CC += kernel/sync/mutex.cc
-SRCS_CC += kernel/trap.cc
-SRCS_CC += kernel/irq.cc
-SRCS_CC += kernel/sched_core.cc
-SRCS_CC += kernel/sched_class_o1.cc
-SRCS_CC += kernel/timer.cc
-SRCS_CC += kernel/pid.cc
-SRCS_CC += kernel/pgrp.cc
-SRCS_CC += kernel/task.cc
-SRCS_CC += kernel/syscall.cc
-SRCS_CC += kernel/sys_process.cc
-SRCS_CC += kernel/sys_mm.cc
-SRCS_CC += kernel/sys_info.cc
-SRCS_CC += kernel/sys_fs.cc
-SRCS_CC += kernel/uaccess.cc
-SRCS_CC += kernel/waitqueue.cc
-SRCS_CC += kernel/signal.cc
-SRCS_CC += kernel/sys_signal.cc
-SRCS_CC += kernel/sys_misc.cc
-SRCS_CC += kernel/exec.cc
-SRCS_CC += kernel/poll.cc
-SRCS_CC += kernel/ptrace.cc
-SRCS_CC += kernel/smp.cc
diff --git a/lib/Build.mk b/lib/Build.mk
new file mode 100644
index 0000000..77d380c
--- /dev/null
+++ b/lib/Build.mk
@@ -0,0 +1,35 @@
+targets += lib/runtime lib/string lib/libfdt lib/core
+
+target.lib/runtime.kind := source-set
+target.lib/runtime.sources := \
+ lib/cxx_abi.cc lib/rbtree.cc lib/kobject.cc lib/kprintf.cc lib/klog.cc \
+ lib/random.cc lib/printf.c
+
+target.lib/string.kind := static-library
+target.lib/string.sources := $(sort $(wildcard lib/string/*.c))
+
+# Architecture assembly replaces the matching generic C implementation.
+string-arch.riscv :=
+string-arch.x86_64 := x86_64
+string-arch.aarch64 := aarch64
+string-arch.i386 := i386
+string-arch.arm := arm
+string-arch := $(string-arch.$(ARCH))
+ifneq ($(string-arch),)
+string-asm := $(sort $(wildcard lib/string/$(string-arch)/*.S) \
+ $(wildcard lib/string/$(string-arch)/*.s))
+string-asm-names := $(sort $(basename $(notdir $(string-asm))))
+target.lib/string.sources := $(filter-out \
+ $(foreach name,$(string-asm-names),lib/string/$(name).c), \
+ $(target.lib/string.sources)) $(string-asm)
+endif
+
+target.lib/libfdt.kind := static-library
+target.lib/libfdt.sources := \
+ lib/fdt.c lib/fdt_ro.c lib/fdt_wip.c lib/fdt_rw.c lib/fdt_sw.c \
+ lib/fdt_strerror.c lib/fdt_empty_tree.c lib/fdt_addresses.c
+target.lib/libfdt.private-includes := scripts/dtc/libfdt
+target.lib/libfdt.cflags := -Wno-parentheses -Wno-sign-compare
+
+target.lib/core.kind := header-library
+target.lib/core.deps := lib/runtime lib/string lib/libfdt
diff --git a/lib/module.mk b/lib/module.mk
deleted file mode 100644
index 0b178f8..0000000
--- a/lib/module.mk
+++ /dev/null
@@ -1,66 +0,0 @@
-# lib/module.mk
-SRCS_CC += lib/cxx_abi.cc
-SRCS_CC += lib/rbtree.cc
-SRCS_CC += lib/kobject.cc
-SRCS_CC += lib/kprintf.cc
-SRCS_CC += lib/klog.cc
-SRCS_CC += lib/random.cc
-SRCS_C += lib/printf.c
-
-# ============================================================
-# musl string library (cross-platform with arch asm overrides)
-# ============================================================
-
-# Map LumeOS ARCH to musl string arch directory
-# riscv has no musl asm optimizations → pure C fallback
-MUSL_STRING_ARCH_riscv :=
-MUSL_STRING_ARCH_x86_64 := x86_64
-MUSL_STRING_ARCH_i386 := i386
-MUSL_STRING_ARCH_aarch64:= aarch64
-MUSL_STRING_ARCH_arm := arm
-
-MUSL_STRING_ARCH := $(MUSL_STRING_ARCH_$(ARCH))
-
-# Collect arch-specific asm sources (if any for this arch)
-MUSL_ASM_SRCS :=
-ifneq ($(MUSL_STRING_ARCH),)
- MUSL_ASM_SRCS += $(wildcard lib/string/$(MUSL_STRING_ARCH)/*.S) \
- $(wildcard lib/string/$(MUSL_STRING_ARCH)/*.s)
-endif
-
-# Derive which C files are overridden by asm (strip path+ext to get base name)
-MUSL_ASM_BASENAMES := $(sort $(basename $(notdir $(MUSL_ASM_SRCS))))
-
-# All C sources, then filter out those with asm replacements
-MUSL_ALL_C := $(wildcard lib/string/*.c)
-MUSL_EXCLUDED_C := $(foreach name,$(MUSL_ASM_BASENAMES),lib/string/$(name).c)
-MUSL_C_SRCS := $(filter-out $(MUSL_EXCLUDED_C),$(MUSL_ALL_C))
-
-SRCS_C += $(MUSL_C_SRCS)
-SRCS_S += $(filter %.S,$(MUSL_ASM_SRCS))
-SRCS_s += $(filter %.s,$(MUSL_ASM_SRCS))
-
-# ============================================================
-# libfdt — kernel adapter shims around scripts/dtc/libfdt/.
-#
-# Each shim is two lines:
-# #include <linux/libfdt_env.h>
-# #include "../scripts/dtc/libfdt/fdt*.c"
-# matching Linux lib/fdt*.c verbatim. The compile recipe needs:
-# - <linux/libfdt_env.h> reachable via the default -Iinclude path
-# - -I scripts/dtc/libfdt so the upstream source's #include <libfdt.h>
-# and friends resolve to scripts/dtc/libfdt/libfdt.h
-# Overriding CFLAGS_LIBC (which the %.o: %.c rule uses) avoids dragging
-# in libc-shim headers — libfdt is freestanding-compatible and uses
-# only the string functions forward-declared in <linux/libfdt_env.h>.
-# ============================================================
-LIBFDT_FILES := fdt.c fdt_ro.c fdt_wip.c fdt_rw.c fdt_sw.c \
- fdt_strerror.c fdt_empty_tree.c fdt_addresses.c
-SRCS_C += $(addprefix lib/,$(LIBFDT_FILES))
-
-# Per-target CFLAGS override: plain CFLAGS plus the upstream include dir.
-# Use `=` (recursive) not `:=` because $(CFLAGS) is defined in the top
-# Makefile AFTER this file is included — `:=` would expand it now (empty)
-# and lose -Iinclude, breaking <linux/libfdt_env.h>.
-$(addprefix $(BUILD)/lib/,$(LIBFDT_FILES:.c=.o)): CFLAGS_LIBC = \
- $(CFLAGS) -I scripts/dtc/libfdt
diff --git a/mm/Build.mk b/mm/Build.mk
new file mode 100644
index 0000000..b6d816d
--- /dev/null
+++ b/mm/Build.mk
@@ -0,0 +1,6 @@
+targets += mm/core
+target.mm/core.kind := source-set
+target.mm/core.sources := \
+ mm/memblock.cc mm/pmm.cc mm/slab.cc mm/vmm.cc mm/vma.cc \
+ mm/page_cache.cc mm/tlb.cc
+
diff --git a/mm/module.mk b/mm/module.mk
deleted file mode 100644
index 93cef80..0000000
--- a/mm/module.mk
+++ /dev/null
@@ -1,7 +0,0 @@
-SRCS_CC += mm/memblock.cc
-SRCS_CC += mm/pmm.cc
-SRCS_CC += mm/slab.cc
-SRCS_CC += mm/vmm.cc
-SRCS_CC += mm/vma.cc
-SRCS_CC += mm/page_cache.cc
-SRCS_CC += mm/tlb.cc
diff --git a/scripts/Kbuild.include b/scripts/Kbuild.include
index 76466bd..7ccc03d 100644
--- a/scripts/Kbuild.include
+++ b/scripts/Kbuild.include
@@ -9,14 +9,7 @@
# - Q / KBUILD_VERBOSE : `make V=1` shows full commands,
# default V=0 prints a one-line summary
# - $(call cmd,<name>) : echo a quiet summary, then run cmd_<name>
-# - $(call if_changed,<name>) : currently a thin alias for $(cmd) —
-# no .cmd-file hashing yet (Linux's full
-# if_changed uses fixdep to detect both
-# recipe-text and header changes; LumeOS
-# relies on `-MMD -MP` for headers and
-# on `make clean` for flag changes).
-# - $(call kbuild-objs,...) : translate Kbuild-style `obj-y := foo.o`
-# entries into SRCS_C / SRCS_CC.
+# - $(call if_changed,<name>) : rebuild when inputs or the command change.
#
# Naming convention adopted from Linux: each command has both a
# `quiet_cmd_<name>` (single-line summary) and a `cmd_<name>` (full
@@ -49,11 +42,11 @@ echo-cmd = $(if $($(quiet)cmd_$(1)),echo ' $($(quiet)cmd_$(1))';)
cmd = @$(echo-cmd) $(cmd_$(1))
-# if_changed currently degenerates to `cmd` — full Linux semantics
-# require tracking the command-line via per-target .<file>.cmd files
-# and comparing on every rebuild, which needs fixdep. Until LumeOS
-# grows that infrastructure, `make clean` after editing CFLAGS suffices.
-if_changed = $(cmd)
+# Record a checksum of the exact argv next to each output. FORCE makes Make
+# reconsider the action; run-if-changed executes it only for a changed input or
+# command signature.
+if_changed = @scripts/build/run-if-changed.sh \
+ $(dir $@).$(notdir $@).cmd $@ '$($(quiet)cmd_$(1))' $^ -- $(cmd_$(1))
# --- Standard quiet_cmd_* / cmd_* templates ---------------------------
#
@@ -61,21 +54,99 @@ if_changed = $(cmd)
# name via $(call cmd,<name>). Add new pairs here as new recipe shapes
# show up (e.g., cmd_lz / cmd_gz / cmd_zstd for compressed Image targets).
- quiet_cmd_cc_o_c = CC $<
- cmd_cc_o_c = $(CC) $(CFLAGS_LIBC) -c $< -o $@
+ quiet_cmd_cc_o_c = CC $(PRIVATE_SOURCE)
+ cmd_cc_o_c = $(CC) $(CFLAGS_LIBC) $(PRIVATE_INCLUDES) $(PRIVATE_DEFINES) $(PRIVATE_CFLAGS) -c $(PRIVATE_SOURCE) -o $@
- quiet_cmd_cxx_o_cc = CXX $<
- cmd_cxx_o_cc = $(CXX) $(CXXFLAGS) -c $< -o $@
+ quiet_cmd_cxx_o_cc = CXX $(PRIVATE_SOURCE)
+ cmd_cxx_o_cc = $(CXX) $(CXXFLAGS) $(PRIVATE_INCLUDES) $(PRIVATE_DEFINES) $(PRIVATE_CXXFLAGS) -c $(PRIVATE_SOURCE) -o $@
- quiet_cmd_as_o_S = AS $<
- cmd_as_o_S = $(CC) $(ASFLAGS) -c $< -o $@
+ quiet_cmd_as_o_S = AS $(PRIVATE_SOURCE)
+ cmd_as_o_S = $(CC) $(ASFLAGS) $(PRIVATE_INCLUDES) $(PRIVATE_DEFINES) $(PRIVATE_ASFLAGS) -c $(PRIVATE_SOURCE) -o $@
quiet_cmd_cc_s_c = CC -S $<
- cmd_cc_s_c = $(CC) $(CFLAGS_LIBC) -S $< -o $@
+ cmd_cc_s_c = $(CC) $(CFLAGS_LIBC) -S $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_gen_asm_offsets = GEN $@
+ cmd_gen_asm_offsets = scripts/build/generate-asm-offsets.sh $@ \
+ $(ASM_OFFSETS_S) $(ASM_OFFSETS_SED) $(ASM_OFFSETS_C)
+
+ quiet_cmd_autoconf = GEN $@
+ cmd_autoconf = scripts/build/generate-autoconf.sh $@ \
+ CONFIG_NR_CPUS $(CONFIG_NR_CPUS) value \
+ CONFIG_HZ $(CONFIG_HZ) value \
+ CONFIG_NR_PRIORITIES $(CONFIG_NR_PRIORITIES) value \
+ CONFIG_DEFAULT_PRIORITY $(CONFIG_DEFAULT_PRIORITY) value \
+ CONFIG_IDLE_PRIORITY $(CONFIG_IDLE_PRIORITY) value \
+ CONFIG_DEFAULT_TIMESLICE $(CONFIG_DEFAULT_TIMESLICE) value \
+ CONFIG_PAGE_SIZE $(CONFIG_PAGE_SIZE) value \
+ CONFIG_MAX_ORDER $(CONFIG_MAX_ORDER) value \
+ CONFIG_KERNEL_STACK_SIZE $(CONFIG_KERNEL_STACK_SIZE) value \
+ CONFIG_KASLR $(CONFIG_KASLR) bool \
+ CONFIG_KASLR_POLICY_ID $(CONFIG_KASLR_POLICY_ID) value \
+ CONFIG_KASLR_LOG_LEVEL $(CONFIG_KASLR_LOG_LEVEL) value \
+ CONFIG_STACK_CANARY $(CONFIG_STACK_CANARY) bool \
+ CONFIG_DEBUG $(CONFIG_DEBUG) bool \
+ CONFIG_DEBUG_LOCKS $(CONFIG_DEBUG_LOCKS) bool \
+ CONFIG_SELFTEST $(CONFIG_SELFTEST) bool \
+ CONFIG_KLOG $(CONFIG_KLOG) bool \
+ CONFIG_KERNEL_COMPRESSED $(CONFIG_KERNEL_COMPRESSED) bool
+
+ quiet_cmd_linker_script = LDS $@
+ cmd_linker_script = $(CC) -E -P -x c $(INCLUDES) $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_initramfs = INITRAMFS $(INITRAMFS_SOURCE)
+ cmd_initramfs = scripts/build/generate-initramfs.sh $(GEN_INIT_CPIO) \
+ ./scripts/gen_initramfs.sh $@ \
+ $(CONFIG_INITRAMFS_ROOT_UID) $(CONFIG_INITRAMFS_ROOT_GID) \
+ $(INITRAMFS_SOURCE)
+
+ quiet_cmd_initramfs_obj = AS $@ (initramfs)
+ cmd_initramfs_obj = $(CC) $(ASFLAGS) \
+ -DINITRAMFS_IMAGE='"$(INITRAMFS_CPIO)"' \
+ -c $(PRIVATE_SOURCE) -o $@
quiet_cmd_objcopy_bin = OBJCOPY $@
cmd_objcopy_bin = $(OBJCOPY) -O binary $< $@
+ quiet_cmd_archive = AR $@
+ cmd_archive = scripts/build/archive.sh $@ $(AR) $(PRIVATE_ARCHIVE_OBJECTS)
+
+ quiet_cmd_host_tool = HOSTCC $@
+ cmd_host_tool = $(HOSTCC) $(HOSTCFLAGS) $(PRIVATE_HOST_CFLAGS) \
+ -o $@ $(PRIVATE_HOST_SOURCE)
+
+ quiet_cmd_host_cc_o_c = HOSTCC $(PRIVATE_SOURCE)
+ cmd_host_cc_o_c = $(HOSTCC) $(HOSTCFLAGS) -MMD -MP -c $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_host_cxx_o_cc = HOSTCXX $(PRIVATE_SOURCE)
+ cmd_host_cxx_o_cc = $(HOSTCXX) $(HOSTCXXFLAGS) -MMD -MP -c $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_host_archive = HOSTAR $@
+ cmd_host_archive = scripts/build/archive.sh $@ $(HOSTAR) $(PRIVATE_ARCHIVE_OBJECTS)
+
+ quiet_cmd_host_link = HOSTLD $@
+ cmd_host_link = $(HOSTCXX) $(HOSTCXXFLAGS) -o $@ \
+ $(PRIVATE_HOST_OBJECTS) $(PRIVATE_HOST_LIBS)
+
+ quiet_cmd_toolchain_probe = PROBE toolchain
+ cmd_toolchain_probe = scripts/build/probe-toolchain.sh $@ \
+ $(CC) $(CXX) $(LD) $(COMPILER_FAMILY) $(TARGET_TRIPLE) \
+ '$(GCC_TOOLCHAIN)' '$(GCC_SYSROOT)'
+
+ quiet_cmd_phys_cc_o_c = PHYSCC $(PRIVATE_SOURCE)
+ cmd_phys_cc_o_c = $(CC) $(PHYS_CFLAGS) -MMD -MP -c $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_phys_cxx_o_cc = PHYSCXX $(PRIVATE_SOURCE)
+ cmd_phys_cxx_o_cc = $(CXX) $(PHYS_CXXFLAGS) -MMD -MP -c $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_phys_as_o_S = PHYSAS $(PRIVATE_SOURCE)
+ cmd_phys_as_o_S = $(CC) $(PHYS_ASFLAGS) -MMD -MP -c $(PRIVATE_SOURCE) -o $@
+
+ quiet_cmd_link_vmlume = LD $@
+ cmd_link_vmlume = scripts/build/link-kernel.sh $@ $(SIZE) \
+ $(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ \
+ $(KERNEL_LINK_INPUTS) $(INITRAMFS_OBJ)
+
# Objcopy with custom flags via per-target OBJCOPYFLAGS variable —
# used by the .pi.o post-processing rule (--prefix-symbols=__pi_ etc.).
quiet_cmd_objcopy = OBJCOPY $@
@@ -83,28 +154,3 @@ if_changed = $(cmd)
quiet_cmd_ld_kernel = LD $@
cmd_ld_kernel = $(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $^
-
-# --- kbuild-objs: obj-y -> SRCS_C / SRCS_CC ---------------------------
-#
-# Usage from a subdir Makefile (e.g. arch/riscv/kernel/pi/Makefile):
-#
-# obj-dir := arch/riscv/kernel/pi
-# obj-y := fdt_early.pi.o cmdline_early.pi.o archrandom_early.pi.o
-# $(eval $(call kbuild-objs,$(obj-dir),$(obj-y:.pi.o=.o)))
-#
-# For each <name>.o entry, looks up <dir>/<name>.cc first, then
-# <dir>/<name>.c, and appends to SRCS_CC / SRCS_C respectively.
-# Pure .pi.o entries should be passed through `:.pi.o=.o` first by the
-# caller so the .pi suffix doesn't confuse the source-file lookup.
-
-define _kbuild-find-src
-$(if $(wildcard $(1)/$(basename $(2)).cc),\
- $(eval SRCS_CC += $(1)/$(basename $(2)).cc),\
- $(if $(wildcard $(1)/$(basename $(2)).c),\
- $(eval SRCS_C += $(1)/$(basename $(2)).c),\
- $(error kbuild-objs: no $(1)/$(basename $(2)).{c,cc} for obj $(2))))
-endef
-
-define kbuild-objs
-$(foreach _obj,$(2),$(call _kbuild-find-src,$(1),$(_obj)))
-endef
diff --git a/scripts/Kconfig.mk b/scripts/Kconfig.mk
index 2bdc934..3db7a2f 100644
--- a/scripts/Kconfig.mk
+++ b/scripts/Kconfig.mk
@@ -6,15 +6,15 @@
# This file is the heart of the LumeOS build-time configuration system.
# It mirrors the role of Linux's Kconfig + auto.conf pipeline:
#
-# 1. Developer edits `.config` (or copies from arch/$(ARCH)/defconfig)
-# 2. This file reads `.config`, applies defaults for anything missing,
+# 1. Developer edits `$(O)/.config` (or runs `make O=... defconfig`)
+# 2. This file reads that configuration and applies missing defaults,
# validates constraints, and exports two things:
# a) KCPPFLAGS — a set of -DCONFIG_xxx=value flags for the compiler
# b) $(BUILD)/generated/autoconf.h — the same values as C #defines
#
# Design Principles (matching Linux conventions):
-# - Every CONFIG_ symbol has a default defined HERE, so a bare `make`
-# with no .config still builds a sane kernel.
+# - Every CONFIG_ symbol has a default defined HERE, so an empty output
+# directory still builds a sane kernel.
# - Boolean options use 'y' / 'n'. Numeric options are plain integers.
# - The generated autoconf.h uses the exact same names so that
# `#ifdef CONFIG_DEBUG` works identically in Makefile and C.
@@ -23,7 +23,8 @@
# ------------------------------------------------------------------
# §1 Read user configuration (if it exists)
# ------------------------------------------------------------------
--include .config
+KCONFIG_CONFIG ?= $(BUILD)/.config
+-include $(KCONFIG_CONFIG)
# ------------------------------------------------------------------
# §2 Provide defaults for every CONFIG_ symbol
@@ -143,42 +144,6 @@ endif
# ------------------------------------------------------------------
AUTOCONF_H := $(BUILD)/generated/autoconf.h
-define generate-autoconf
- @mkdir -p $(dir $(AUTOCONF_H))
- @echo " GEN $(AUTOCONF_H)"
- @echo "/* AUTO-GENERATED — DO NOT EDIT */" > $(AUTOCONF_H)
- @echo "/* Generated by scripts/Kconfig.mk */" >> $(AUTOCONF_H)
- @echo "#pragma once" >> $(AUTOCONF_H)
- @echo "" >> $(AUTOCONF_H)
- @echo "/* Platform */" >> $(AUTOCONF_H)
- @echo "#define CONFIG_NR_CPUS $(CONFIG_NR_CPUS)" >> $(AUTOCONF_H)
- @echo "" >> $(AUTOCONF_H)
- @echo "/* Scheduler */" >> $(AUTOCONF_H)
- @echo "#define CONFIG_HZ $(CONFIG_HZ)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_NR_PRIORITIES $(CONFIG_NR_PRIORITIES)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_DEFAULT_PRIORITY $(CONFIG_DEFAULT_PRIORITY)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_IDLE_PRIORITY $(CONFIG_IDLE_PRIORITY)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_DEFAULT_TIMESLICE $(CONFIG_DEFAULT_TIMESLICE)" >> $(AUTOCONF_H)
- @echo "" >> $(AUTOCONF_H)
- @echo "/* Memory Management */" >> $(AUTOCONF_H)
- @echo "#define CONFIG_PAGE_SIZE $(CONFIG_PAGE_SIZE)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_MAX_ORDER $(CONFIG_MAX_ORDER)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_KERNEL_STACK_SIZE $(CONFIG_KERNEL_STACK_SIZE)" >> $(AUTOCONF_H)
- @echo "" >> $(AUTOCONF_H)
- @echo "/* Security */" >> $(AUTOCONF_H)
- $(if $(filter y,$(CONFIG_KASLR)), @echo "#define CONFIG_KASLR 1" >> $(AUTOCONF_H))
- @echo "#define CONFIG_KASLR_POLICY_ID $(CONFIG_KASLR_POLICY_ID)" >> $(AUTOCONF_H)
- @echo "#define CONFIG_KASLR_LOG_LEVEL $(CONFIG_KASLR_LOG_LEVEL)" >> $(AUTOCONF_H)
- $(if $(filter y,$(CONFIG_STACK_CANARY)), @echo "#define CONFIG_STACK_CANARY 1" >> $(AUTOCONF_H))
- @echo "" >> $(AUTOCONF_H)
- @echo "/* Debug */" >> $(AUTOCONF_H)
- $(if $(filter y,$(CONFIG_DEBUG)), @echo "#define CONFIG_DEBUG 1" >> $(AUTOCONF_H))
- $(if $(filter y,$(CONFIG_DEBUG_LOCKS)), @echo "#define CONFIG_DEBUG_LOCKS 1" >> $(AUTOCONF_H))
- $(if $(filter y,$(CONFIG_SELFTEST)), @echo "#define CONFIG_SELFTEST 1" >> $(AUTOCONF_H))
- $(if $(filter y,$(CONFIG_KLOG)), @echo "#define CONFIG_KLOG 1" >> $(AUTOCONF_H))
- $(if $(filter y,$(CONFIG_KERNEL_COMPRESSED)), @echo "#define CONFIG_KERNEL_COMPRESSED 1" >> $(AUTOCONF_H))
-endef
-
# ------------------------------------------------------------------
# §6 Utility targets
# ------------------------------------------------------------------
@@ -187,29 +152,31 @@ endef
.PHONY: defconfig savedefconfig showconfig
defconfig:
- @echo " DEFCFG arch/$(CONFIG_ARCH)/defconfig → .config"
- @cp arch/$(CONFIG_ARCH)/defconfig .config
+ @echo " DEFCFG arch/$(CONFIG_ARCH)/defconfig → $(KCONFIG_CONFIG)"
+ @mkdir -p $(dir $(KCONFIG_CONFIG))
+ @cp arch/$(CONFIG_ARCH)/defconfig $(KCONFIG_CONFIG)
# 'make savedefconfig' — save current effective config
savedefconfig:
- @echo " SAVE .config"
- @echo "# LumeOS Kernel Configuration (saved $$(date +%Y-%m-%d))" > .config
- @echo "CONFIG_ARCH = $(CONFIG_ARCH)" >> .config
- @echo "CONFIG_NR_CPUS = $(CONFIG_NR_CPUS)" >> .config
- @echo "CONFIG_HZ = $(CONFIG_HZ)" >> .config
- @echo "CONFIG_NR_PRIORITIES = $(CONFIG_NR_PRIORITIES)" >> .config
- @echo "CONFIG_PAGE_SIZE = $(CONFIG_PAGE_SIZE)" >> .config
- @echo "CONFIG_MAX_ORDER = $(CONFIG_MAX_ORDER)" >> .config
- @echo "CONFIG_KERNEL_STACK_SIZE = $(CONFIG_KERNEL_STACK_SIZE)" >> .config
- @echo "CONFIG_KASLR = $(CONFIG_KASLR)" >> .config
- @echo "CONFIG_KASLR_POLICY_ID = $(CONFIG_KASLR_POLICY_ID)" >> .config
- @echo "CONFIG_KASLR_LOG_LEVEL = $(CONFIG_KASLR_LOG_LEVEL)" >> .config
- @echo "CONFIG_STACK_CANARY = $(CONFIG_STACK_CANARY)" >> .config
- @echo "CONFIG_DEBUG = $(CONFIG_DEBUG)" >> .config
- @echo "CONFIG_DEBUG_LOCKS = $(CONFIG_DEBUG_LOCKS)" >> .config
- @echo "CONFIG_SELFTEST = $(CONFIG_SELFTEST)" >> .config
- @echo "CONFIG_KLOG = $(CONFIG_KLOG)" >> .config
- @echo "CONFIG_KERNEL_COMPRESSED = $(CONFIG_KERNEL_COMPRESSED)" >> .config
+ @echo " SAVE $(KCONFIG_CONFIG)"
+ @mkdir -p $(dir $(KCONFIG_CONFIG))
+ @echo "# LumeOS Kernel Configuration (saved $$(date +%Y-%m-%d))" > $(KCONFIG_CONFIG)
+ @echo "CONFIG_ARCH = $(CONFIG_ARCH)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_NR_CPUS = $(CONFIG_NR_CPUS)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_HZ = $(CONFIG_HZ)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_NR_PRIORITIES = $(CONFIG_NR_PRIORITIES)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_PAGE_SIZE = $(CONFIG_PAGE_SIZE)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_MAX_ORDER = $(CONFIG_MAX_ORDER)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KERNEL_STACK_SIZE = $(CONFIG_KERNEL_STACK_SIZE)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KASLR = $(CONFIG_KASLR)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KASLR_POLICY_ID = $(CONFIG_KASLR_POLICY_ID)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KASLR_LOG_LEVEL = $(CONFIG_KASLR_LOG_LEVEL)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_STACK_CANARY = $(CONFIG_STACK_CANARY)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_DEBUG = $(CONFIG_DEBUG)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_DEBUG_LOCKS = $(CONFIG_DEBUG_LOCKS)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_SELFTEST = $(CONFIG_SELFTEST)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KLOG = $(CONFIG_KLOG)" >> $(KCONFIG_CONFIG)
+ @echo "CONFIG_KERNEL_COMPRESSED = $(CONFIG_KERNEL_COMPRESSED)" >> $(KCONFIG_CONFIG)
# 'make showconfig' — print effective configuration
showconfig:
diff --git a/scripts/build/Build.mk b/scripts/build/Build.mk
new file mode 100644
index 0000000..0a15b38
--- /dev/null
+++ b/scripts/build/Build.mk
@@ -0,0 +1,10 @@
+targets += build/host-library-smoke build/host-test-smoke build/phys-library-smoke
+target.build/host-library-smoke.kind := host-library
+target.build/host-library-smoke.sources := scripts/build/tests/host_library.cc
+
+target.build/host-test-smoke.kind := host-test
+target.build/host-test-smoke.sources := scripts/build/tests/host_test.cc
+target.build/host-test-smoke.private-deps := build/host-library-smoke
+
+target.build/phys-library-smoke.kind := phys-library
+target.build/phys-library-smoke.sources := scripts/build/tests/phys_library.cc
diff --git a/scripts/build/archive.sh b/scripts/build/archive.sh
new file mode 100755
index 0000000..79f0bb9
--- /dev/null
+++ b/scripts/build/archive.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+archive=$1
+ar=$2
+shift 2
+rm -f "$archive"
+"$ar" rcs "$archive" "$@"
+
diff --git a/scripts/build/check-kernel-elf.sh b/scripts/build/check-kernel-elf.sh
new file mode 100755
index 0000000..eb92b33
--- /dev/null
+++ b/scripts/build/check-kernel-elf.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+elf=$1
+readelf=$2
+nm=$3
+arch=$4
+
+case "$arch" in
+ riscv) expected_machine='RISC-V' ;;
+ aarch64) expected_machine='AArch64' ;;
+ x86_64) expected_machine='Advanced Micro Devices X86-64' ;;
+ *) echo "ELF check: unsupported architecture: $arch" >&2; exit 2 ;;
+esac
+
+machine=$($readelf -h "$elf" | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')
+[ "$machine" = "$expected_machine" ] || {
+ echo "ELF check: expected machine '$expected_machine', got '$machine'" >&2
+ exit 1
+}
+
+entry=$($readelf -h "$elf" | sed -n 's/^[[:space:]]*Entry point address:[[:space:]]*//p')
+[ -n "$entry" ] && [ "$entry" != 0x0 ] || {
+ echo "ELF check: kernel has no entry point" >&2
+ exit 1
+}
+
+undefined=$($nm -u "$elf" || true)
+[ -z "$undefined" ] || {
+ echo "ELF check: undefined symbols:" >&2
+ printf '%s\n' "$undefined" >&2
+ exit 1
+}
+
+if $readelf -W -l "$elf" | awk '$1 == "LOAD" && $0 ~ /W/ && $0 ~ /E/ { found=1 } END { exit !found }'; then
+ echo "ELF check: writable and executable LOAD segment" >&2
+ exit 1
+fi
+
+$readelf -S "$elf" | grep -q '[.]rela[.]dyn' || {
+ echo "ELF check: missing relocation section .rela.dyn" >&2
+ exit 1
+}
+
+echo "Kernel ELF OK: $machine, entry $entry"
+
diff --git a/scripts/build/environments/host.mk b/scripts/build/environments/host.mk
new file mode 100644
index 0000000..0911ad3
--- /dev/null
+++ b/scripts/build/environments/host.mk
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+HOSTCC ?= cc
+HOSTCXX ?= c++
+HOSTAR ?= ar
+HOSTCFLAGS ?= -O2 -g -Wall -Wextra
+HOSTCXXFLAGS ?= $(HOSTCFLAGS) -std=c++23
+
diff --git a/scripts/build/environments/kernel.mk b/scripts/build/environments/kernel.mk
new file mode 100644
index 0000000..088cdf0
--- /dev/null
+++ b/scripts/build/environments/kernel.mk
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+WARNINGS ?= -Wall -Wextra -Wno-unused-parameter
+
+ifeq ($(BUILD_VARIANT),release)
+KERNEL_OPT_FLAGS := -O2 -g0 -DNDEBUG
+else
+KERNEL_OPT_FLAGS := -O2 -g
+endif
+
+CFLAGS := $(WARNINGS) $(KERNEL_OPT_FLAGS) \
+ -ffreestanding -nostdlib \
+ -mcmodel=medany -fPIE -MMD -MP \
+ $(TOOLCHAIN_CFLAGS) $(KCPPFLAGS) $(INCLUDES) \
+ -I$(BUILD)/generated
+
+CFLAGS_LIBC := $(CFLAGS) -std=gnu23 -Iinclude/libc-shim \
+ -Wno-parentheses -Wno-sign-compare
+
+CXXFLAGS := $(CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
+ -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
+ -fno-threadsafe-statics
+
+# Pre-MMU assembly must never use GOT-indirect addressing.
+ASFLAGS := $(filter-out -fPIE,$(CFLAGS)) -fno-pie
+
+LDFLAGS := -nostdlib -shared -Bsymbolic -z notext -z norelro \
+ --no-dynamic-linker
diff --git a/scripts/build/environments/phys.mk b/scripts/build/environments/phys.mk
new file mode 100644
index 0000000..6a11c12
--- /dev/null
+++ b/scripts/build/environments/phys.mk
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Early physical-address execution environment. It deliberately has no heap,
+# exceptions, RTTI, thread-safe statics, or PIE/GOT assumptions.
+PHYS_CFLAGS := $(WARNINGS) -Os -g -ffreestanding -nostdlib \
+ -fno-pie -fno-stack-protector $(TOOLCHAIN_CFLAGS) \
+ $(KCPPFLAGS) $(INCLUDES) -I$(BUILD)/generated
+PHYS_CXXFLAGS := $(PHYS_CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
+ -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
+ -fno-threadsafe-statics
+PHYS_ASFLAGS := $(PHYS_CFLAGS)
+
diff --git a/scripts/build/generate-asm-offsets.sh b/scripts/build/generate-asm-offsets.sh
new file mode 100755
index 0000000..4e90f94
--- /dev/null
+++ b/scripts/build/generate-asm-offsets.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+assembly=$2
+sed_script=$3
+source=$4
+tmp="$output.tmp.$$"
+{
+ echo '/* AUTO-GENERATED — DO NOT EDIT */'
+ echo "/* Generated from $source */"
+ echo '#ifndef _LUME_GENERATED_ASM_OFFSETS_H'
+ echo '#define _LUME_GENERATED_ASM_OFFSETS_H'
+ echo
+ sed -n -f "$sed_script" "$assembly"
+ echo
+ echo '#endif /* _LUME_GENERATED_ASM_OFFSETS_H */'
+} > "$tmp"
+if [ -e "$output" ] && cmp -s "$tmp" "$output"; then
+ rm -f "$tmp"
+else
+ mv -f "$tmp" "$output"
+fi
+
diff --git a/scripts/build/generate-autoconf.sh b/scripts/build/generate-autoconf.sh
new file mode 100755
index 0000000..493d630
--- /dev/null
+++ b/scripts/build/generate-autoconf.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+shift
+tmp="$output.tmp.$$"
+mkdir -p "$(dirname "$output")"
+{
+ echo '/* AUTO-GENERATED — DO NOT EDIT */'
+ echo '#pragma once'
+ while [ "$#" -gt 0 ]; do
+ name=$1
+ value=$2
+ kind=$3
+ shift 3
+ if [ "$kind" = bool ]; then
+ [ "$value" = y ] && echo "#define $name 1"
+ else
+ echo "#define $name $value"
+ fi
+ done
+} > "$tmp"
+if [ -e "$output" ] && cmp -s "$tmp" "$output"; then
+ rm -f "$tmp"
+else
+ mv -f "$tmp" "$output"
+fi
+
diff --git a/scripts/build/generate-initramfs.sh b/scripts/build/generate-initramfs.sh
new file mode 100755
index 0000000..194e206
--- /dev/null
+++ b/scripts/build/generate-initramfs.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+generator=$1
+wrapper=$2
+output=$3
+uid=$4
+gid=$5
+source=$6
+GEN_INIT_CPIO=$generator "$wrapper" -o "$output" -u "$uid" -g "$gid" "$source"
+
diff --git a/scripts/build/link-kernel.sh b/scripts/build/link-kernel.sh
new file mode 100755
index 0000000..31164e7
--- /dev/null
+++ b/scripts/build/link-kernel.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+size_tool=$2
+shift 2
+"$@"
+"$size_tool" "$output"
+
diff --git a/scripts/build/probe-toolchain.sh b/scripts/build/probe-toolchain.sh
new file mode 100755
index 0000000..2f1f600
--- /dev/null
+++ b/scripts/build/probe-toolchain.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+output=$1
+cc=$2
+cxx=$3
+ld=$4
+family=$5
+target=$6
+gcc_toolchain=$7
+sysroot=$8
+
+set --
+if [ "$family" = clang ]; then
+ set -- "$@" "--target=$target"
+ [ -z "$gcc_toolchain" ] || set -- "$@" "--gcc-toolchain=$gcc_toolchain"
+ [ -z "$sysroot" ] || set -- "$@" "--sysroot=$sysroot"
+fi
+
+probe() {
+ language=$1
+ standard=$2
+ shift 2
+ compiler=$cc
+ [ "$language" = c++ ] && compiler=$cxx
+ printf '' | "$compiler" "$@" "-std=$standard" -x "$language" \
+ -fsyntax-only - >/dev/null 2>&1
+}
+
+tmp="$output.tmp.$$"
+mkdir -p "$(dirname "$output")"
+{
+ echo "cc=$($cc --version | head -n 1)"
+ echo "cxx=$($cxx --version | head -n 1)"
+ echo "ld=$($ld --version | head -n 1)"
+ echo "target=$target"
+ printf 'c23='; if probe c gnu23 "$@"; then echo yes; else echo no; fi
+ printf 'cxx23='; if probe c++ c++23 "$@"; then echo yes; else echo no; fi
+ printf 'fno-pie='; if probe c++ c++23 "$@" -fno-pie; then echo yes; else echo no; fi
+ printf 'mcmodel-medany='; if probe c++ c++23 "$@" -mcmodel=medany; then echo yes; else echo no; fi
+} > "$tmp"
+mv -f "$tmp" "$output"
+
+grep -q '^c23=yes$' "$output"
+grep -q '^cxx23=yes$' "$output"
+grep -q '^fno-pie=yes$' "$output"
+grep -q '^mcmodel-medany=yes$' "$output"
diff --git a/scripts/build/run-if-changed.sh b/scripts/build/run-if-changed.sh
new file mode 100755
index 0000000..6cdc7c4
--- /dev/null
+++ b/scripts/build/run-if-changed.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set -eu
+
+cmdfile=$1
+output=$2
+description=$3
+shift 3
+
+rebuild=false
+if [ ! -e "$output" ] || [ ! -e "$cmdfile" ]; then
+ rebuild=true
+fi
+
+while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
+ prerequisite=$1
+ shift
+ if [ "$prerequisite" != FORCE ] && [ -e "$prerequisite" ] && \
+ [ "$prerequisite" -nt "$output" ]; then
+ rebuild=true
+ fi
+done
+
+[ "$#" -gt 0 ] || { echo "run-if-changed: missing --" >&2; exit 2; }
+shift
+
+signature=$(printf '%s\0' "$@" | cksum)
+old_signature=
+if [ -e "$cmdfile" ]; then
+ old_signature=$(cat "$cmdfile")
+fi
+if [ "$signature" != "$old_signature" ]; then
+ rebuild=true
+fi
+
+if [ "$rebuild" = true ]; then
+ printf ' %s\n' "$description"
+ mkdir -p "$(dirname "$output")" "$(dirname "$cmdfile")"
+ "$@"
+ tmp="$cmdfile.tmp.$$"
+ printf '%s\n' "$signature" > "$tmp"
+ mv -f "$tmp" "$cmdfile"
+fi
+
diff --git a/scripts/build/targets.mk b/scripts/build/targets.mk
new file mode 100644
index 0000000..1a66379
--- /dev/null
+++ b/scripts/build/targets.mk
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+
+# Declarative target registry for the LumeOS build.
+#
+# Build.mk files populate target.<name>.* and append the name to targets. A
+# target name is metadata, not a Make rule, so names may contain '/' and ':'.
+
+target-kinds := \
+ interface-library header-library object-library source-set \
+ static-library whole-archive host-library phys-library host-tool generated \
+ binary-blob kernel-test host-test product
+targets :=
+generated-actions := autoconf asm-offsets linker-script initramfs
+
+empty :=
+space := $(empty) $(empty)
+comma := ,
+
+# Convert a logical target/source name into a path-safe component.
+target-key = $(subst :,_,$(subst /,_,$(1)))
+
+# Recursively collect a target and all of its dependencies. Dependency cycles
+# are rejected separately before this is evaluated for build roots.
+target-deps = $(sort $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps) \
+ $(target.$(1).link-deps) $(target.$(1).tool-deps) \
+ $(target.$(1).generated-deps) $(target.$(1).runtime-deps) \
+ $(target.$(1).order-only-deps))
+target-closure = $(strip \
+ $(if $(filter $(1),$(2)),$(error target dependency cycle: $(2) -> $(1))) \
+ $(1) $(foreach dep,$(call target-deps,$(1)),$(call target-closure,$(dep),$(2) $(1))))
+target-link-deps = $(sort $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps) \
+ $(target.$(1).link-deps))
+target-link-order = $(strip $(1) $(foreach dep,$(call target-link-deps,$(1)),\
+ $(call target-link-order,$(dep))))
+unique-words = $(if $(strip $(1)),$(firstword $(1)) \
+ $(call unique-words,$(filter-out $(firstword $(1)),$(1))))
+
+duplicate-words = $(sort $(foreach word,$(1),\
+ $(if $(word 2,$(filter $(word),$(1))),$(word))))
+all-target-sources = $(foreach target,$(targets),$(target.$(target).sources))
+duplicate-target-sources = $(call duplicate-words,$(all-target-sources))
+
+define validate-target
+$(if $(filter $(target.$(1).kind),$(target-kinds)),,$(error target '$(1)' has invalid kind '$(target.$(1).kind)'))
+$(if $(filter header-library interface-library product generated binary-blob,$(target.$(1).kind)),$(if $(strip $(target.$(1).sources)),$(error target '$(1)' of kind $(target.$(1).kind) declares sources),),)
+$(if $(filter host-tool,$(target.$(1).kind)),$(if $(word 2,$(target.$(1).sources)),$(error host-tool '$(1)' currently requires exactly one source),),)
+$(foreach src,$(target.$(1).sources),$(if $(wildcard $(src)),,$(error target '$(1)' references missing source '$(src)')))
+$(foreach dep,$(call target-deps,$(1)),$(if $(filter $(dep),$(targets)),,$(error target '$(1)' references unknown dependency '$(dep)')))
+$(foreach dep,$(call target-deps,$(1)),$(if $(strip $(target.$(dep).visibility)),\
+ $(if $(filter all $(1),$(target.$(dep).visibility)),,\
+ $(error target '$(1)' cannot depend on '$(dep)': visibility is '$(target.$(dep).visibility)')),))
+$(if $(filter generated,$(target.$(1).kind)),$(if $(strip $(target.$(1).outputs)),,$(error generated target '$(1)' has no outputs)),)
+$(if $(filter generated,$(target.$(1).kind)),$(if $(filter $(target.$(1).action),$(generated-actions)),,$(error generated target '$(1)' has invalid action '$(target.$(1).action)')),)
+endef
+
+define validate-targets
+$(if $(strip $(targets)),,$(error no build targets were registered))
+$(if $(filter-out $(words $(sort $(targets))),$(words $(targets))),$(error duplicate build target name),)
+$(foreach target,$(targets),$(call validate-target,$(target)))
+endef
+
+# Usage requirements are deliberately separate from link inputs. Public
+# include paths and defines propagate through deps; private values apply only
+# while compiling the target itself.
+target-public-closure = $(sort $(1) $(foreach dep,$(target.$(1).public-deps) \
+ $(target.$(1).deps),$(call target-public-closure,$(dep))))
+target-public-includes = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-includes)))
+target-public-after-includes = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-after-includes)))
+target-public-defines = $(sort $(foreach target,$(call target-public-closure,$(1)),$(target.$(target).public-defines)))
+
+target-usage-roots = $(sort $(1) $(target.$(1).deps) $(target.$(1).public-deps) \
+ $(target.$(1).private-deps) $(target.$(1).interface-deps))
+target-compile-includes = $(sort $(target.$(1).private-includes) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-includes,$(root))))
+target-compile-after-includes = $(sort $(target.$(1).private-after-includes) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-after-includes,$(root))))
+target-compile-defines = $(sort $(target.$(1).private-defines) \
+ $(foreach root,$(call target-usage-roots,$(1)),$(call target-public-defines,$(root))))
diff --git a/scripts/build/tests/host_library.cc b/scripts/build/tests/host_library.cc
new file mode 100644
index 0000000..c545815
--- /dev/null
+++ b/scripts/build/tests/host_library.cc
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace lume::build::test {
+
+int HostLibrarySmokeTestSymbol() { return 0; }
+
+} // namespace lume::build::test
+
diff --git a/scripts/build/tests/host_test.cc b/scripts/build/tests/host_test.cc
new file mode 100644
index 0000000..6b13417
--- /dev/null
+++ b/scripts/build/tests/host_test.cc
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace lume::build::test {
+int HostLibrarySmokeTestSymbol();
+}
+
+int main() { return lume::build::test::HostLibrarySmokeTestSymbol(); }
diff --git a/scripts/build/tests/phys_library.cc b/scripts/build/tests/phys_library.cc
new file mode 100644
index 0000000..2cf1596
--- /dev/null
+++ b/scripts/build/tests/phys_library.cc
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+extern "C" unsigned long LumePhysBuildSmoke(unsigned long value) {
+ return value ^ 0x4c554d45UL;
+}
diff --git a/scripts/build/toolchain.mk b/scripts/build/toolchain.mk
new file mode 100644
index 0000000..49d4f0a
--- /dev/null
+++ b/scripts/build/toolchain.mk
@@ -0,0 +1,103 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+
+# Toolchain selection:
+# make GCC + GNU ld
+# make LLVM=1 Clang + GNU ld
+# make LLVM=1 LD_TYPE=lld Clang + LLD
+
+LLVM ?= 0
+LLVM_VERSION ?= 20
+LD_TYPE ?= bfd
+LLVM_BINUTILS ?= 0
+CXX_STDLIB ?= libstdc++
+CXX_ABI ?= lume
+COMPILER_RUNTIME ?= compiler-default
+
+GCC_TOOLCHAIN ?=
+CROSS_COMPILE ?=
+
+ifeq ($(ARCH),riscv)
+TARGET_TRIPLE := riscv64-unknown-linux-gnu
+ifeq ($(strip $(GCC_TOOLCHAIN)),)
+ifneq ($(wildcard /opt/riscv64-glibc/bin/riscv64-unknown-linux-gnu-g++),)
+GCC_TOOLCHAIN := /opt/riscv64-glibc
+endif
+endif
+ifeq ($(strip $(CROSS_COMPILE)),)
+ifneq ($(strip $(GCC_TOOLCHAIN)),)
+CROSS_COMPILE := $(GCC_TOOLCHAIN)/bin/$(TARGET_TRIPLE)-
+else
+CROSS_COMPILE := $(TARGET_TRIPLE)-
+endif
+endif
+else ifeq ($(ARCH),aarch64)
+TARGET_TRIPLE := aarch64-linux-gnu
+else ifeq ($(ARCH),x86_64)
+TARGET_TRIPLE := x86_64-linux-gnu
+endif
+
+ifeq ($(strip $(CROSS_COMPILE)),)
+CROSS_COMPILE := $(TARGET_TRIPLE)-
+endif
+ifeq ($(strip $(GCC_TOOLCHAIN)),)
+GCC_TOOLCHAIN := $(patsubst %/bin/,%, $(dir $(CROSS_COMPILE)))
+endif
+
+GCC_SYSROOT := $(strip $(shell $(CROSS_COMPILE)g++ -print-sysroot 2>/dev/null))
+
+CLANG_SUFFIX := $(if $(LLVM_VERSION),-$(LLVM_VERSION),)
+CLANG ?= clang$(CLANG_SUFFIX)
+CLANGXX ?= clang++$(CLANG_SUFFIX)
+LLD ?= ld.lld$(CLANG_SUFFIX)
+
+CLANG_TARGET_FLAGS := --target=$(TARGET_TRIPLE)
+ifneq ($(strip $(GCC_TOOLCHAIN)),)
+CLANG_TARGET_FLAGS += --gcc-toolchain=$(GCC_TOOLCHAIN)
+endif
+ifneq ($(strip $(GCC_SYSROOT)),)
+CLANG_TARGET_FLAGS += --sysroot=$(GCC_SYSROOT)
+endif
+
+ifeq ($(LLVM),1)
+CC := $(CLANG)
+CXX := $(CLANGXX)
+COMPILER_FAMILY := clang
+TOOLCHAIN_CFLAGS := $(CLANG_TARGET_FLAGS)
+ifeq ($(CXX_STDLIB),libstdc++)
+TOOLCHAIN_CXXFLAGS := -stdlib=libstdc++
+else
+$(error unsupported CXX_STDLIB '$(CXX_STDLIB)' for the current imported runtime)
+endif
+ifeq ($(LD_TYPE),lld)
+LD := $(LLD)
+LINKER_FAMILY := lld
+else
+ifneq ($(CXX_STDLIB),libstdc++)
+$(error GCC configuration currently requires CXX_STDLIB=libstdc++)
+endif
+LD := $(CROSS_COMPILE)ld
+LINKER_FAMILY := bfd
+endif
+else
+CC := $(CROSS_COMPILE)gcc
+CXX := $(CROSS_COMPILE)g++
+LD := $(CROSS_COMPILE)ld
+COMPILER_FAMILY := gcc
+LINKER_FAMILY := bfd
+endif
+
+ifeq ($(LLVM_BINUTILS),1)
+AR := llvm-ar$(CLANG_SUFFIX)
+NM := llvm-nm$(CLANG_SUFFIX)
+OBJCOPY := llvm-objcopy$(CLANG_SUFFIX)
+OBJDUMP := llvm-objdump$(CLANG_SUFFIX)
+READELF := llvm-readelf$(CLANG_SUFFIX)
+else
+AR := $(CROSS_COMPILE)ar
+NM := $(CROSS_COMPILE)nm
+OBJCOPY := $(CROSS_COMPILE)objcopy
+OBJDUMP := $(CROSS_COMPILE)objdump
+READELF := $(CROSS_COMPILE)readelf
+endif
+SIZE := $(CROSS_COMPILE)size
diff --git a/scripts/build/write-compdb.py b/scripts/build/write-compdb.py
new file mode 100755
index 0000000..9ec5a49
--- /dev/null
+++ b/scripts/build/write-compdb.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import json
+import pathlib
+import sys
+
+source = pathlib.Path(sys.argv[1])
+output = pathlib.Path(sys.argv[2])
+directory = pathlib.Path.cwd()
+entries = []
+for line in source.read_text().splitlines():
+ file_name, object_name, command = line.split("\t", 2)
+ entries.append(
+ {
+ "directory": str(directory),
+ "file": file_name,
+ "output": object_name,
+ "command": command,
+ }
+ )
+output.parent.mkdir(parents=True, exist_ok=True)
+temporary = output.with_suffix(output.suffix + ".tmp")
+temporary.write_text(json.dumps(entries, indent=2) + "\n")
+temporary.replace(output)
+
diff --git a/scripts/gen_initramfs.sh b/scripts/gen_initramfs.sh
index 7eba2fd..8ad0cd7 100755
--- a/scripts/gen_initramfs.sh
+++ b/scripts/gen_initramfs.sh
@@ -247,4 +247,4 @@ done
# If output_file is set we will generate cpio archive
# we are careful to delete tmp files
-usr/gen_init_cpio $output $timestamp $cpio_list
+"${GEN_INIT_CPIO:-usr/gen_init_cpio}" $output $timestamp $cpio_list
diff --git a/test/Build.mk b/test/Build.mk
new file mode 100644
index 0000000..cb8f200
--- /dev/null
+++ b/test/Build.mk
@@ -0,0 +1,12 @@
+targets += test/kernel
+target.test/kernel.kind := kernel-test
+target.test/kernel.sources := \
+ test/selftest.cc test/test_pmm.cc test/test_slab.cc test/test_spinlock.cc \
+ test/test_mutex.cc test/test_driver_model.cc test/test_rbtree.cc \
+ test/test_vmm.cc test/test_vma.cc test/test_atomic.cc test/test_fdt.cc \
+ test/test_trap.cc test/test_sched.cc test/test_klog.cc test/test_random.cc \
+ test/test_waitqueue.cc test/test_uaccess.cc test/test_sys_mm.cc \
+ test/test_smp.cc test/test_dcache.cc test/test_namei.cc \
+ test/test_vfs_io.cc test/test_exec.cc test/test_pipe_devfs.cc \
+ test/test_sys_process.cc test/test_signal.cc test/test_pid.cc \
+ test/test_sys_fs_extra.cc test/test_page_cache.cc
diff --git a/test/module.mk b/test/module.mk
deleted file mode 100644
index 47a693c..0000000
--- a/test/module.mk
+++ /dev/null
@@ -1,30 +0,0 @@
-# test/module.mk
-SRCS_CC += test/selftest.cc
-SRCS_CC += test/test_pmm.cc
-SRCS_CC += test/test_slab.cc
-SRCS_CC += test/test_spinlock.cc
-SRCS_CC += test/test_mutex.cc
-SRCS_CC += test/test_driver_model.cc
-SRCS_CC += test/test_rbtree.cc
-SRCS_CC += test/test_vmm.cc
-SRCS_CC += test/test_vma.cc
-SRCS_CC += test/test_atomic.cc
-SRCS_CC += test/test_fdt.cc
-SRCS_CC += test/test_trap.cc
-SRCS_CC += test/test_sched.cc
-SRCS_CC += test/test_klog.cc
-SRCS_CC += test/test_random.cc
-SRCS_CC += test/test_waitqueue.cc
-SRCS_CC += test/test_uaccess.cc
-SRCS_CC += test/test_sys_mm.cc
-SRCS_CC += test/test_smp.cc
-SRCS_CC += test/test_dcache.cc
-SRCS_CC += test/test_namei.cc
-SRCS_CC += test/test_vfs_io.cc
-SRCS_CC += test/test_exec.cc
-SRCS_CC += test/test_pipe_devfs.cc
-SRCS_CC += test/test_sys_process.cc
-SRCS_CC += test/test_signal.cc
-SRCS_CC += test/test_pid.cc
-SRCS_CC += test/test_sys_fs_extra.cc
-SRCS_CC += test/test_page_cache.cc
diff --git a/usr/Build.mk b/usr/Build.mk
new file mode 100644
index 0000000..f734640
--- /dev/null
+++ b/usr/Build.mk
@@ -0,0 +1,4 @@
+targets += host/gen_init_cpio
+target.host/gen_init_cpio.kind := host-tool
+target.host/gen_init_cpio.sources := usr/gen_init_cpio.c
+target.host/gen_init_cpio.host-cflags := -Wno-sign-compare
diff --git a/usr/initramfs.S b/usr/initramfs.S
new file mode 100644
index 0000000..fa21553
--- /dev/null
+++ b/usr/initramfs.S
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/*
+ * Embed the generated CPIO through the target assembler instead of converting
+ * it with `objcopy -I binary`. The resulting object inherits the target ABI
+ * attributes (notably RISC-V float ABI flags), which keeps both GNU ld and LLD
+ * strict-linking compatible.
+ */
+
+#ifndef INITRAMFS_IMAGE
+#error "INITRAMFS_IMAGE must name the generated CPIO archive"
+#endif
+
+ .section .init.ramfs, "a", @progbits
+ .balign 8
+ .incbin INITRAMFS_IMAGE
+