# KHY OS Kernel Makefile

# Toolchain. Defaults force the known-good GNU/Linux binaries (do NOT use `?=`:
# make pre-defines CC=cc as a built-in, so `?=` would silently leave CC=cc and
# pick up clang on macOS/BSD). Override per-build from the command line, which has
# the highest precedence and always wins over these `=` assignments — e.g.
# `make CC=x86_64-elf-gcc LD=ld.lld GRUB_MKRESCUE=grub2-mkrescue iso`. `khy os
# build` forwards KHY_CC / KHY_NASM / KHY_LD / KHY_GRUB_MKRESCUE as exactly these
# command-line variables, so the Makefile itself never needs editing per host.
ASM           = nasm
CC            = gcc
LD            = ld
GRUB_MKRESCUE = grub-mkrescue

# Directories
BOOT_DIR = boot
SRC_DIR  = src
BUILD    = build
MOONBIT_DIR = moonbit

# Detect GCC version (kept for diagnostics)
GCC_VER := $(shell $(CC) -dumpversion)

# GCC's own freestanding header dir (stddef.h, stdint.h, stdarg.h, …). Resolve it
# portably from the compiler itself instead of hardcoding a Debian path: on
# Debian/Ubuntu `-print-file-name=include` yields the very same
# /usr/lib/gcc/x86_64-linux-gnu/<ver>/include the path was hardcoded to, while on
# Arch/Fedora/macOS/MSYS2/cross toolchains it resolves to their layout. Override
# GCC_INCLUDE to force a specific path.
GCC_INCLUDE ?= $(shell $(CC) -print-file-name=include)

# Compiler flags — freestanding x86_64 kernel
CFLAGS   = -ffreestanding -nostdlib -nostdinc -fno-builtin \
           -fno-stack-protector -mno-red-zone -mcmodel=kernel \
           -fno-pic -fno-pie \
           -Wall -Wextra -O2 -g \
           -isystem $(GCC_INCLUDE)

# MoonBit generated C flags — same as kernel but with MOONBIT_NATIVE_NO_SYS_HEADER
MOONBIT_CFLAGS = $(CFLAGS) -DMOONBIT_NATIVE_NO_SYS_HEADER -Wno-unused-function -Wno-unused-variable

ASMFLAGS = -f elf64
LDFLAGS  = -n -T linker.ld -nostdlib

# Source files
ASM_SRC  = $(BOOT_DIR)/boot.asm $(BOOT_DIR)/long_mode.asm $(BOOT_DIR)/isr.asm $(BOOT_DIR)/context_switch.asm $(BOOT_DIR)/usermode.asm
C_SRC    = $(wildcard $(SRC_DIR)/*.c)

# MoonBit paths
MOONBIT_INCLUDE = $(HOME)/.moon/include
MOONBIT_RUNTIME = $(HOME)/.moon/lib/runtime.c
MOONBIT_GEN_DIR = $(MOONBIT_DIR)/_build/native/debug/build/lib/khy_kernel
MOONBIT_GEN_C   = $(BUILD)/moonbit_gen.c

# Object files
ASM_OBJ  = $(patsubst $(BOOT_DIR)/%.asm,$(BUILD)/%.o,$(ASM_SRC))
C_OBJ    = $(patsubst $(SRC_DIR)/%.c,$(BUILD)/%.o,$(C_SRC))
MOONBIT_OBJ = $(BUILD)/moonbit_gen.o $(BUILD)/moonbit_runtime.o
ALL_OBJ  = $(ASM_OBJ) $(C_OBJ) $(MOONBIT_OBJ)

# Output (kernel ISO is named -kernel to distinguish from the Alpine-based dist ISO)
KERNEL   = $(BUILD)/khy-os.bin
ISO      = $(BUILD)/khy-os-kernel.iso

# Default target
.PHONY: all clean run run-serial iso moonbit-build userland run-agent

all: $(ISO)

# Step 1: Build MoonBit project to generate .c file
moonbit-build:
	@echo "[MOONBIT] Building MoonBit module..."
	cd $(MOONBIT_DIR) && moon build --target native 2>&1 | grep -v "Cannot find TCC"
	@echo "[MOONBIT] Patching generated C: rename main() -> moonbit_entry()"
	@sed 's/^int main(int argc, char\*\* argv)/int moonbit_entry(int argc, char** argv)/' \
		$(MOONBIT_GEN_DIR)/khy_kernel.c > $(MOONBIT_GEN_C)
	@echo "[MOONBIT] Generated C ready: $(MOONBIT_GEN_C)"

# Step 2: Compile MoonBit generated C
$(BUILD)/moonbit_gen.o: moonbit-build | $(BUILD)
	$(CC) $(MOONBIT_CFLAGS) -I$(MOONBIT_INCLUDE) -I$(SRC_DIR) -c $(MOONBIT_GEN_C) -o $@

# Step 3: Compile MoonBit runtime
$(BUILD)/moonbit_runtime.o: $(MOONBIT_RUNTIME) | $(BUILD)
	$(CC) $(MOONBIT_CFLAGS) -I$(MOONBIT_INCLUDE) -I$(SRC_DIR) -c $< -o $@

# Assemble .asm files
$(BUILD)/%.o: $(BOOT_DIR)/%.asm | $(BUILD)
	$(ASM) $(ASMFLAGS) $< -o $@

# Compile .c files
$(BUILD)/%.o: $(SRC_DIR)/%.c | $(BUILD)
	$(CC) $(CFLAGS) -c $< -o $@

# ramfs.c embeds the generated Ring 3 program blobs; without this dependency a
# `make userland` regen would not trigger a ramfs.o rebuild (the pattern rule
# tracks only the .c), silently linking a stale program. Cost a debug cycle once.
$(BUILD)/ramfs.o: $(SRC_DIR)/user_init_blob.h $(SRC_DIR)/user_filetest_blob.h $(SRC_DIR)/user_argv_blob.h $(SRC_DIR)/user_badptr_blob.h $(SRC_DIR)/user_forktest_blob.h $(SRC_DIR)/user_exectest_blob.h $(SRC_DIR)/user_forkwait_blob.h $(SRC_DIR)/user_fault_blob.h $(SRC_DIR)/user_stackgrow_blob.h $(SRC_DIR)/user_cowtest_blob.h $(SRC_DIR)/user_proctest_blob.h $(SRC_DIR)/user_pipetest_blob.h $(SRC_DIR)/user_stdiotest_blob.h $(SRC_DIR)/user_sigtest_blob.h $(SRC_DIR)/user_pipesrc_blob.h $(SRC_DIR)/user_pipedst_blob.h $(SRC_DIR)/user_readtest_blob.h $(SRC_DIR)/user_siginttest_blob.h $(SRC_DIR)/user_spintest_blob.h $(SRC_DIR)/user_spinbare_blob.h $(SRC_DIR)/user_usertest_blob.h $(SRC_DIR)/user_cwdtest_blob.h $(SRC_DIR)/user_rmtest_blob.h $(SRC_DIR)/user_linktest_blob.h $(SRC_DIR)/user_mmaptest_blob.h $(SRC_DIR)/user_vmtest_blob.h $(SRC_DIR)/user_stattest_blob.h $(SRC_DIR)/user_dirtest_blob.h $(SRC_DIR)/user_lseektest_blob.h $(SRC_DIR)/user_trunctest_blob.h $(SRC_DIR)/user_renametest_blob.h $(SRC_DIR)/user_dirfdtest_blob.h $(SRC_DIR)/user_dup2test_blob.h $(SRC_DIR)/user_timetest_blob.h $(SRC_DIR)/user_mtimetest_blob.h $(SRC_DIR)/user_atimetest_blob.h $(SRC_DIR)/user_ptimetest_blob.h

# Link kernel binary
$(KERNEL): $(ALL_OBJ)
	$(LD) $(LDFLAGS) -o $@ $^

# Build ISO with GRUB
$(ISO): $(KERNEL)
	@mkdir -p $(BUILD)/isofiles/boot/grub
	cp $(KERNEL) $(BUILD)/isofiles/boot/khy-os.bin
	cp iso/boot/grub/grub.cfg $(BUILD)/isofiles/boot/grub/grub.cfg
	$(GRUB_MKRESCUE) -o $(ISO) $(BUILD)/isofiles 2>/dev/null

# Create build directory
$(BUILD):
	mkdir -p $(BUILD)

# Run in QEMU with serial output to terminal
run: $(ISO)
	qemu-system-x86_64 -cdrom $(ISO) -serial stdio -display none -no-reboot

# Run in QEMU with VGA display
run-vga: $(ISO)
	qemu-system-x86_64 -cdrom $(ISO) -serial stdio

# Run with both serial and monitor
run-debug: $(ISO)
	qemu-system-x86_64 -cdrom $(ISO) -serial stdio -monitor telnet:127.0.0.1:1234,server,nowait -d int -no-reboot

# Run with a persistent 16MB ATA disk on the primary master (creates it once).
# Try: diskwrite 100 hello / diskread 100, then re-run to see it survive reboot.
DISK ?= $(BUILD)/khy-disk.img
run-disk: $(ISO)
	@test -f $(DISK) || qemu-img create -f raw $(DISK) 16M
	qemu-system-x86_64 -cdrom $(ISO) -hda $(DISK) -serial stdio -display none -no-reboot

# Run with the Agent ⇄ OS bridge channel exposed. COM1 (first -serial) stays the
# human TTY on stdio; COM2 (second -serial) is the agent channel on a unix
# socket. A host agent — or the A1 echo test — connects to $(AGENT_SOCK).
# QEMU assigns serial ports in declaration order, so order matters here.
AGENT_SOCK ?= /tmp/khy-agent.sock
run-agent: $(ISO)
	@rm -f $(AGENT_SOCK)
	qemu-system-x86_64 -cdrom $(ISO) \
	  -serial stdio \
	  -serial unix:$(AGENT_SOCK),server,nowait \
	  -display none -no-reboot

# Regenerate the embedded Ring 3 programs (src/user_<name>_blob.h) from their
# sources. NOT part of the default build — the generated headers are checked in
# so a plain `make` never depends on the userland toolchain. Run this only after
# editing a userland/*.asm program.
USERLAND_PROGS = init filetest argv badptr forktest exectest forkwait fault stackgrow cowtest proctest pipetest stdiotest sigtest pipesrc pipedst readtest siginttest spintest spinbare usertest cwdtest rmtest linktest mmaptest vmtest stattest dirtest lseektest trunctest renametest dirfdtest dup2test timetest mtimetest atimetest ptimetest
userland:
	@for p in $(USERLAND_PROGS); do \
	  $(ASM) -f elf64 userland/$$p.asm -o userland/$$p.o; \
	  $(LD) -static -nostdlib -n -Ttext=0x400000 -e _start userland/$$p.o -o userland/$$p.elf; \
	  guard=USER_`echo $$p | tr a-z A-Z`_BLOB_H; \
	  { \
	    echo "/* user_$${p}_blob.h — GENERATED, do not edit by hand."; \
	    echo " *"; \
	    echo " * Embedded Ring 3 program written to /bin/$$p.elf by ramfs_init()."; \
	    echo " * Source: userland/$$p.asm. Regenerate with: make userland"; \
	    echo " */"; \
	    echo "#ifndef $$guard"; \
	    echo "#define $$guard"; \
	    echo ""; \
	    (cd userland && xxd -i $$p.elf) \
	      | sed -e "s/unsigned char $${p}_elf\[\]/static const unsigned char user_$${p}_elf[]/" \
	            -e "s/unsigned int $${p}_elf_len/static const unsigned int user_$${p}_elf_len/"; \
	    echo ""; \
	    echo "#endif /* $$guard */"; \
	  } > $(SRC_DIR)/user_$${p}_blob.h; \
	  echo "[userland] regenerated $(SRC_DIR)/user_$${p}_blob.h"; \
	done

# Build ISO only
iso: $(ISO)

# Clean build artifacts (including MoonBit build)
clean:
	rm -rf $(BUILD)
	cd $(MOONBIT_DIR) && moon clean 2>/dev/null || true
