#!/usr/bin/env python3
from glob import glob
import os

file = open('Makefile', 'w', encoding='utf-8')
def raw(s):
	print(s, file=file)

def srcobj(path):
	srcs = None
	if os.path.isfile(path):
		srcs = [path]
	else:
		srcs = glob(path + '/**/*.[csS]', recursive=True)
	objs = ['out/obj/' + src.removeprefix('src/') + '.o' for src in srcs]
	return ' ' + ' '.join(objs) + ' '

def t(out, deps='', cmds=[], phony=False, verbose=False):
	if phony:
		raw(f".PHONY: {out}")
	else:
		cmds = ["mkdir -p $(@D)"] + cmds
	raw(f"{out}: {deps}")
	v = '' if verbose else '$(V)'
	for cmd in cmds:
		raw(f"\t{v}{cmd}")


raw("""
# automatically generated by ./configure

PATH := $(shell pwd)/toolchain/prefix/bin/:$(PATH)

AR = x86_64-camellia-ar
AS = x86_64-camellia-as
CC = x86_64-camellia-gcc

V=@

CFLAGS = -g -std=gnu99 -Os -fPIC -ftrack-macro-expansion=0 \
 -Wall -Wextra -Wold-style-definition -Wno-address-of-packed-member \
 -Werror=incompatible-pointer-types -Werror=implicit-function-declaration

CFLAGS_KERNEL  = $(CFLAGS) \
	-ffreestanding -mno-sse -mgeneral-regs-only \
	--sysroot=out/sysrootk/ -Isrc/
CFLAGS_LIBC    = $(CFLAGS) -ffreestanding -Isrc/

SPARSEFLAGS = $(CFLAGS_KERNEL) -Wno-non-pointer-null -Iout/sysrootk/usr/include/ -D__SPARSE

LIB = out/sysrootu/lib/libc.a out/sysrootu/lib/libm.a out/sysrootu/lib/crt0.o
""")

t('all', 'out/boot.iso out/fs.e2 check', phony=True)
t('portdeps', 'out/sysrootu/usr/include/ $(LIB)', phony=True)

# TODO check userland sources too
t('check', '', [
	'find src/kernel/ -type f -name *.c | xargs -n 1 sparse $(SPARSEFLAGS)'
], phony=True)

t('clean', '', [
	'rm -rf out/'
], phony=True, verbose=True)

raw("""
PORTS=
.PHONY: ports
# portdeps is phony, so ports/% is automatically "phony" too
ports: $(patsubst %,ports/%,$(PORTS))
ports/%: portdeps
	+$@/port install
""")

t('out/boot.iso', 'out/fs/boot/kernel out/fs/boot/grub/grub.cfg out/fs/boot/init.gz', [
	'grub-mkrescue -o $@ out/fs/ >/dev/null 2>&1'
])

t('out/fs/boot/kernel', 'src/kernel/arch/amd64/linker.ld' + srcobj('src/kernel/') + srcobj('src/libk/'), [
	'$(CC) -nostdlib -Wl,-zmax-page-size=4096 -Wl,--no-warn-mismatch -Wl,-no-pie -T $^ -o $@ -lgcc',
	'grub-file --is-x86-multiboot2 $@ || echo "$@ has an invalid multiboot2 header"',
	'grub-file --is-x86-multiboot2 $@ || rm $@; test -e $@'
])

t('out/sysrootu/lib/libc.a', srcobj('src/libc/') + srcobj('src/libk/'), [
	'$(AR) rcs $@ $^'
])
t('out/sysrootu/lib/libm.a', '', [
	'$(AR) rcs $@ $^'
])

t('out/bootstrap', 'out/bootstrap.elf', ['objcopy -O binary $^ $@'])
t('out/bootstrap.elf', 'src/bootstrap/linker.ld' + srcobj('src/bootstrap/') + 'out/sysrootu/lib/libc.a', [
	'$(CC) -nostdlib -Wl,-no-pie -T $^ -o $@'
])

t('out/fs/boot/init.gz', 'out/bootstrap out/initrd.tar', ['cat $^ | gzip > $@'])

t('out/fs/boot/grub/grub.cfg', 'src/kernel/arch/amd64/grub.cfg', ['cp $< $@'])

t('out/fs.e2', '', ['mkfs.ext2 $@ 1024 >/dev/null'])

userbins = []
for src in glob('*', root_dir='src/cmd/'):
	cmd = src.removesuffix('.c')
	t(f'out/initrd/bin/amd64/{cmd}', '$(LIB) ' + srcobj(f'src/cmd/{src}'), [
		'$(CC) $^ -o $@'
	])
	userbins.append(cmd)

raw("USERBINS = " + " ".join(userbins))

# don't build the example implementation from libext2
t('out/obj/cmd/ext2fs/ext2/example.c.o', '', ['touch $@'])

t('out/initrd/%', 'sysroot/%', ['cp $< $@'])

t('out/initrd.tar',
	'$(patsubst sysroot/%,out/initrd/%,$(shell find sysroot/ -type f)) '
	'$(patsubst %,out/initrd/bin/amd64/%,$(USERBINS)) '
	'$(shell find out/initrd/) '
	'ports ',
	['cd out/initrd; tar chf ../initrd.tar *'])

t('src/libc/include/__errno.h',
	'src/libc/include/__errno.h.awk src/libk/include/camellia/errno.h',
	['awk -f $^ > $@'])

t('src/libc/syscall.c',
	'src/libc/syscall.c.awk src/libk/include/camellia/syscalls.h',
	['awk -f $^ > $@'])

def includes(target, modules, deps='', cmds=[]):
	dirs = " ".join([f"src/{mod}/include/" for mod in modules])
	t(target, deps, [
		"set -eu;"
		f"for file in $$(find {dirs} -type f,l -name '*.h'); do "
		"    out=$@$${file#src/*/include/};"
		"    mkdir -p $$(dirname $$out);"
		"    ln -rfs $$file $$out;"
		"done"
	] + cmds, phony=True)

includes("out/sysrootk/usr/include/", ["libk"])
# this must work without a toolchain available, as it's required to build
# the toolchain
includes("out/sysrootu/usr/include/", ["libc", "libk"], 'src/libc/include/__errno.h', [
	'ln -rfs src/libc/include/__errno.h $@',
])
t('out/sysrootu/lib/crt0.o', 'out/obj/libc/_start.s.o', ['cp $^ $@'])

def cc(rule, args, deps):
	# The | marks an order-only dependency. This means that sysroot will be
	# built every time the rule is ran, but without forcing a rebuild if the
	# rule is fresh.
	# https://www.gnu.org/software/make/manual/make.html#index-order_002donly-prerequisites
	t(f'out/obj/{rule}.o', f'src/{rule} | {deps}', [
		f'$(CC) {args} -c $< -o $@'
	])

cc("%.c",           "$(CFLAGS)",          "out/sysrootu/usr/include/")
cc("%.s",           "$(CFLAGS)",          "out/sysrootu/usr/include/")
cc("%.S",           "$(CFLAGS)",          "out/sysrootu/usr/include/")
cc("bootstrap/%.c", "$(CFLAGS) -fno-pie", "out/sysrootu/usr/include/")
cc("libc/%.c",      "$(CFLAGS_LIBC)",     "out/sysrootu/usr/include/")

cc("libc/vendor/dlmalloc/%.c", "$(CFLAGS_LIBC) -DMAP_ANONYMOUS -DHAVE_MORECORE=0 -DNO_MALLOC_H -Wno-expansion-to-defined -Wno-old-style-definition", "out/sysrootu/usr/include/")

cc("kernel/%.c",    "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")
cc("kernel/%.s",    "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")
cc("kernel/%.S",    "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")
cc("libk/%.c",      "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")
cc("libk/%.s",      "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")
cc("libk/%.S",      "$(CFLAGS_KERNEL)", "out/sysrootk/usr/include/")

cc("kernel/arch/amd64/32/%.c", "$(CFLAGS_KERNEL) -fno-pic -m32", "out/sysrootk/usr/include/")
cc("kernel/arch/amd64/32/%.s", "$(CFLAGS_KERNEL) -fno-pic -m32", "out/sysrootk/usr/include/")