summaryrefslogtreecommitdiff
path: root/Makefile
blob: c21c4ac5cae829d4cd0a02e992feb9655ff1ad89 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Richard Qin
#
# LumeOS Kernel Makefile (Non-Recursive)
#
# Usage:
#   make          - Build kernel ELF
#   make run      - Run in QEMU
#   make debug    - Run in QEMU with GDB server
#   make clean    - Clean build artifacts
#   make fmt      - Format all source files
#   make disasm   - Generate disassembly listing
#
# Reference: docs/specs/00_conventions.md §7

# ============================================================
# Build Configuration
# ============================================================
O ?= build
BUILD := $(patsubst %/,%,$(O))

# Make sure `make` with no arguments builds the kernel image, not the
# first rule encountered during makefile parsing.
.DEFAULT_GOAL := all

# Load command helpers before Kconfig and target evaluation.
include scripts/Kbuild.include

# 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:
#   VMLUME -> build/vmlume.elf : ELF with .symtab/PT_DYNAMIC/.rela.dyn
#   IMAGE  -> build/vmlume.bin : raw binary (objcopy -O binary)
VMLUME := $(BUILD)/vmlume.elf
IMAGE  := $(BUILD)/vmlume.bin

# Initramfs paths
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.
LDS_SRC := arch/$(ARCH)/kernel/lumeos.lds.S
LINKER_SCRIPT := $(BUILD)/lumeos.lds
PYTHON ?= python3

# Objcopy output formats for binary-to-ELF relocation blob
OBJCOPY_ELF_riscv := elf64-littleriscv
OBJCOPY_ELF_aarch64 := elf64-littleaarch64
OBJCOPY_ELF_x86_64 := elf64-x86-64
OBJCOPY_ELF := $(OBJCOPY_ELF_$(ARCH))

OBJCOPY_BINARCH_riscv := riscv
OBJCOPY_BINARCH_aarch64 := aarch64
OBJCOPY_BINARCH_x86_64 := i386:x86-64
OBJCOPY_BINARCH := $(OBJCOPY_BINARCH_$(ARCH))

# Common include roots. Environment-specific flags are defined after target
# usage requirements have been collected.
INCLUDES := -Iinclude -I.

# ============================================================
# Declarative Target Graph
# ============================================================
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
# ============================================================
# 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 \
        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

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 $(KCONFIG_CONFIG)) \
               scripts/build/generate-autoconf.sh FORCE
	$(Q)mkdir -p $(dir $@)
	$(call if_changed,autoconf)

# ============================================================
# Generated header: asm-offsets.h
#
# Linux-style trick (mirrors include/linux/kbuild.h + arch/*/kernel/
# asm-offsets.c + scripts/Makefile.build): compile asm-offsets.c to
# assembly, scrape the magic `->SYM val` markers produced by the
# DEFINE/OFFSET inline-asm macros, rewrite them into `#define`
# directives via sed.  head.S and friends then `#include
# <generated/asm-offsets.h>` to access struct field offsets without
# having to hand-maintain them — re-order a struct field, the
# generated constant follows.
# ============================================================
ASM_OFFSETS_C   := arch/$(ARCH)/kernel/asm-offsets.c
ASM_OFFSETS_S   := $(BUILD)/arch/$(ARCH)/kernel/asm-offsets.s
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): 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) \
                  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) FORCE

# ============================================================
# Generic %.pi.o post-processing rule
#
# Mirrors arch/riscv/kernel/pi/Makefile §26-30 in Linux:
#
#   $(obj)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_
#                                  --remove-section=.note.gnu.property
#                                  --prefix-alloc-sections=.init.pi
#   $(obj)/%.pi.o: $(obj)/%.o FORCE
#       $(call if_changed,objcopy)
#
# Purpose: take a regular .o produced by the normal compile rule and
# rewrite it so the early position-independent code can coexist with
# the rest of the kernel without symbol clashes:
#
#   --prefix-symbols=__pi_       Every global symbol becomes __pi_<name>.
#                                head.S calls them as __pi_fdt_path_offset
#                                etc., keeping the pre-MMU namespace
#                                cleanly isolated.
#
#   --prefix-alloc-sections=.init.pi
#                                All SHF_ALLOC sections get re-prefixed
#                                so they land in .init.pi in vmlinux.lds.S,
#                                which positions them where the boot
#                                loader has already mapped memory.
#
#   --remove-section=.note.gnu.property
#                                Discarded because post-rename it can
#                                confuse the linker about ELF features.
#
# Generic pattern (`$(BUILD)/%.pi.o`) so any subdir's source can opt in
# by listing a `.pi.o` target in its SRCS.  The companion compile-side
# CFLAGS overrides (replacing -fPIE with -fpie, adding -Os, etc.) are
# expected to come from the subdir's own .mk fragment via target-
# specific CFLAGS_LIBC / CXXFLAGS overrides.
# ============================================================
$(BUILD)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_ \
                                 --remove-section=.note.gnu.property \
                                 --prefix-alloc-sections=.init.pi

$(BUILD)/%.pi.o: $(BUILD)/%.o
	$(call cmd,objcopy)

# Preprocess linker script template (.lds.S → .lds)
$(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.
#
# Pipeline mirrors Linux's Kbuild:
#   usr/gen_init_cpio          — host C tool, produces cpio bytes
#   scripts/gen_initramfs.sh   — wrapper that prepares the listing,
#                                handles directories vs. listing files,
#                                applies uid/gid remap, then invokes the
#                                host tool.
# Sourced from $(CONFIG_INITRAMFS_SOURCE) — see scripts/Kconfig.mk.
# ============================================================

# 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)

# 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_SRC_STAMP):
	@mkdir -p $(dir $@)
	@touch $@

$(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) $(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) 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)

# ============================================================
# Run & Debug
# ============================================================
ifeq ($(ARCH),riscv)
QEMU := qemu-system-riscv64
QEMU_MACHINE := -machine virt -smp 4 -m 128M -nographic
QEMU_BIOS := -bios default
else ifeq ($(ARCH),aarch64)
QEMU := qemu-system-aarch64
QEMU_MACHINE := -machine virt -cpu cortex-a53 -m 128M -nographic
QEMU_BIOS := 
else ifeq ($(ARCH),x86_64)
QEMU := qemu-system-x86_64
QEMU_MACHINE := -m 128M -nographic
QEMU_BIOS := 
endif

run: $(VMLUME) $(IMAGE)
	@echo "  QEMU    Running..."
	$(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(VMLUME)

debug: $(VMLUME) $(IMAGE)
	@echo "  QEMU    Waiting for GDB on :1234..."
	$(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(VMLUME) -s -S

# ============================================================
# Utilities
# ============================================================
clean:
	rm -rf $(BUILD)

disasm: $(VMLUME)
	@echo "  OBJDUMP $<"
	@$(OBJDUMP) -d -S $(VMLUME) > $(VMLUME).asm
	@echo "  Written to $(VMLUME).asm"

fmt:
	@echo "  FMT     Formatting source files..."
	@find . -name '*.cc' -o -name '*.h' | xargs clang-format -i 2>/dev/null || true

lint:
	@echo "  LINT    Generating compile_commands.json via bear..."
	@bear -- $(MAKE) all
	@echo "  LINT    Running clang-tidy on C++ sources..."
	@clang-tidy -p . $(SRCS_CC) || true

# Print build info (for debugging Makefile issues)
info:
	@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 "  RESOURCE HEADERS  $(COMPILER_RESOURCE_INCLUDE)"
	@echo "  LINKER            $(LINKER_FAMILY): $(LD)"
	@echo "  OBJCOPY           $(OBJCOPY)"
	@echo "  INITRAMFS         $(INITRAMFS_SOURCE)"
	@echo "  C++ STANDARD      C++23"
	@echo "  C++ LIBRARY       $(CXX_STDLIB)"
	@echo "  libc++ HEADERS    $(LIBCXX_INCLUDE)"
	@echo "  KTL CONFIG        $(KTL_INCLUDE)"
	@echo "  KERNEL LIBC       $(KERNEL_LIBC_INCLUDE)"
	@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 \
		-Iinclude -I$(KTL_INCLUDE) -I$(KERNEL_LIBC_INCLUDE) \
		-idirafter $(KTL_AFTER_INCLUDE) -idirafter $(KERNEL_LIBC_AFTER_INCLUDE) \
		-fsyntax-only scripts/build/probes/libcxx.cc
	@$(CXX) $(TOOLCHAIN_CFLAGS) $(TOOLCHAIN_CXXFLAGS) -std=c++23 \
		-ffreestanding -fno-exceptions -fno-rtti \
		-Iinclude -I$(KTL_INCLUDE) -I$(KERNEL_LIBC_INCLUDE) \
		-idirafter $(KTL_AFTER_INCLUDE) -idirafter $(KERNEL_LIBC_AFTER_INCLUDE) \
		-fsyntax-only scripts/build/probes/libc_headers.cc
	@$(CC) $(TOOLCHAIN_CFLAGS) -std=gnu23 -ffreestanding \
		-Iinclude -I$(KERNEL_LIBC_INCLUDE) \
		-idirafter $(KERNEL_LIBC_AFTER_INCLUDE) \
		-fsyntax-only scripts/build/probes/libc_headers.c
	@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"