ifneq (,$(wildcard ./.env))
  include .env
endif

ROOT_DIR := $(CURDIR)
OUT_DIR := $(ROOT_DIR)/out
SCRIPT_DIR := $(ROOT_DIR)/scripts
CONFIG_DIR := $(ROOT_DIR)/config
CLANG_DIR ?= $(ROOT_DIR)/toolchain/clang

CLANG_URL := https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.6/clang+llvm-15.0.6-x86_64-linux-gnu-ubuntu-18.04.tar.xz

SHARED_DIR := $(ROOT_DIR)/shared

VERBOSE ?= 0

ARCH ?= x86_64

GREEN := $(shell tput setaf 2)
YELLOW := $(shell tput setaf 3)
NC := $(shell tput sgr0)

ifeq ($(ARCH),aarch64)
  $(warning $(YELLOW)Incorrect architecture 'aarch64', using 'arm64' instead $(NC))
  ARCH := arm64
else ifeq ($(ARCH),x86)
  $(warning $(YELLOW)Incorrect architecture 'x86', using 'x86_64' instead $(NC))
  ARCH := x86_64
else ifeq ($(filter x86_64 arm64 i386,$(ARCH)),)
  $(error Invalid architecture $(ARCH))
endif

.PHONY: default
default: linux tools-vm

.PHONY: clean
clean: linux_clean tools-vm_clean

.PHONY: help
help:
	@echo '$(GREEN)General Environment Variables:$(NC)'
	@echo '   ARCH                             - Specify one of the supported architectures: x86_64, i386, arm64 (default: x86_64)'
	@echo '   ACK                              - Set to 1 to build ACK instead of the Linux kernel. Does not need to be set for `ack` and `run-ack` targets (default: 0)'
	@echo '   VERBOSE                          - Set to 1 to enable verbose output (default: 0)'
	@echo ''
	@echo '$(GREEN)Build/Config:$(NC)'
	@echo '   Targets:'
	@echo '      linux (default)               - Build the Linux kernel'
	@echo '      linux_defconfig               - Run `make defconfig`'
	@echo '      linux_menuconfig              - Run `make menuconfig`'
	@echo '      linux_modules                 - Build the Linux kernel modules'
	@echo '      linux_debpkg                  - Creates a Debian package for the kernel'
	@echo '      ack                           - Build the Android Common Kernel'
	@echo '      tools-vm                      - Build linux/tools/vm'
	@echo ''
	@echo '   Environment Variables:'
	@echo '      LINUX_DEFCONFIG               - The defconfig to use when building the kernel (default: defconfig, ACK default: gki_defconfig)'
	@echo '      LINUX_SRC                     - The path to the kernel source directory (default: linux, ACK default: ack/common)'
	@echo '      LINUX_OUT                     - The path where the kernel build output should be stored (default: out/linux/$$ARCH, ACK default: out/ack/common/$$ARCH)'
	@echo '      LINUX_CONFIG_FRAGMENT         - A kernel config fragment to merge with the defconfig (default: config/config.fragment)'
	@echo ''
	@echo '$(GREEN)Clean:$(NC)'
	@echo '   Targets:'
	@echo '      clean                         - Clean output from default build targets'
	@echo '      <target>_clean                - Clean output for <target>, where <target> is one of: ack, linux, tools-vm, rootfs'
	@echo ''
	@echo '$(GREEN)Run/Debug:$(NC)'
	@echo '   Targets:'
	@echo '      run                           - Run QEMU with the built kernel and rootfs image'
	@echo '      run-ack                       - Same as `run` but runs ACK instead'
	@echo ''
	@echo '   Environment Variables:'
	@echo '      GDB                           - Set to 1 to start a gdbserver and wait for GDB when running QEMU (default: 0)'
	@echo '      CPU                           - The number of CPUs to use when running QEMU (default: 4)'
	@echo '      MEM                           - The memory size in MB to use when running QEMU (default: 1024)'
	@echo '      QEMU_EXTRA_ARGS               - Additional arguments to pass to QEMU (default: "")'
	@echo '      QEMU_EXTRA_KERNEL_CMDLINE     - Additional arguments to pass to the kernel (default: "")'
	@echo '      QEMU_KERNEL_IMAGE             - The path to the kernel image to run (x86_64/i386 default: $$LINUX_OUT/arch/$$ARCH/boot/bzImage, arm64 default: $$LINUX_OUT/arch/$$ARCH/boot/Image)'
	@echo '      ROOTFS                        - The path to the rootfs image file (default: rootfs/alpine-$$ARCH.img)'
	@echo '      ROOTFS_FORMAT                 - The format of the rootfs image file: raw, qcow2 (default: qcow2)'
	@echo '      INITRD                        - Set to 1 to use the $$CPIO_FILE initramfs instead of the $$ROOTFS image as the rootfs, or specify the path to an alternative CPIO file (default: "")'
	@echo '      RDINIT                        - The value of the `rdinit` kernel command line parameter (default: "", default if INITRD is set: /sbin/init)'
	@echo '      ECHR                          - The value of the QEMU `-echr` flag (default: 1)'
	@echo '      ROOT                          - The value of the `root` kernel command line parameter (default: /dev/vda)'
	@echo '      RW                            - Whether to mount the rootfs as read-write or read-only: ro, rw (default: rw)'
	@echo '      KASLR                         - Set to 1 to enable KASLR (default: 0)'
	@echo ''
	@echo '$(GREEN)rootfs:$(NC)'
	@echo '   Targets:'
	@echo '      rootfs-init                   - Extract the Alpine Linux rootfs to $$ROOTFS_DIR and then run the `rootfs-overlay` target'
	@echo '      rootfs-overlay                - Apply arch-specific changes to $$ROOTFS_DIR and run the `rootfs` target'
	@echo '      rootfs                        - Run the `ext4` and `cpio` targets'
	@echo '      ext4                          - Build a $$ROOTFS $$ROOTFS_FORMAT (default qcow2) image with an ext4 filesystem from $$ROOTFS_DIR'
	@echo '      cpio                          - Build a $$CPIO_FILE gzipped initramfs CPIO file from $$ROOTFS_DIR'
	@echo '      initramfs                     - An alias for the `cpio` target'
	@echo '      uncpio                        - Extract $$CPIO_FILE to $$ROOTFS_DIR'
	@echo '      rootfs-mount                  - Mount $$ROOTFS image at /tmp/rootfs'
	@echo '      rootfs-unmount                - Unmount rootfs image from /tmp/rootfs'
	@echo '      chroot                        - chroot into $$ROOTFS_DIR'
	@echo ''
	@echo '   Environment Variables:'
	@echo '      EXT4_SIZE                     - The disk size of the rootfs image to build'
	@echo '      ROOTFS_DIR                    - The directory to create the ext4 rootfs image and initramfs CPIO from (default: rootfs/alpine-$$ARCH)'
	@echo '      ROOTFS                        - The path to the rootfs image file (default: rootfs/alpine-$$ARCH.img)'
	@echo '      ROOTFS_FORMAT                 - The format of the rootfs image file: raw, qcow2 (default: qcow2)'
	@echo '      CPIO_FILE                     - The path to the CPIO file to create (default: rootfs/alpine-$$ARCH.cpio.gz)'
	@echo ''
	@echo '$(GREEN)Miscellaneous:$(NC)'
	@echo '   Targets:'
	@echo '      linux_download                - Downloads an archive of the Linux kernel source for the version specified in $$VERSION'
	@echo '      linux_checkout                - Checks out the version specified by $$VERSION of the linux kernel in $$LINUX_SRC'
	@echo ''
	@echo '   Environment Variables:'
	@echo "      VERSION                       - The version to download or checkout. For checkout only, if the third number in the version string is a 'y', the latest version of the kernel with that major and minor version is used. Examples: 5.10, 5.10.107, v5.10, 5.10.y, linux-5.10.y"

$(SHARED_DIR):
	mkdir -p $(SHARED_DIR)

##
## Linux Kernel and Android Common Kernel
##

ACK_TARGETS := ack ack_clean run-ack
ACK ?= 0

ACK_SRC := $(ROOT_DIR)/ack/common
ACK_OUT := $(OUT_DIR)/ack/common/$(ARCH)

ifneq ($(filter $(ACK_TARGETS),$(MAKECMDGOALS)),)
  ACK := 1
endif

# If we're building ACK instead of Linux, set the appropriate variables
ifeq ($(ACK),1)
  LINUX_SRC ?= $(ACK_SRC)
  LINUX_OUT ?= $(ACK_OUT)
  LINUX_DEFCONFIG ?= gki_defconfig
else
  LINUX_SRC ?= $(ROOT_DIR)/linux
  LINUX_OUT ?= $(OUT_DIR)/linux/$(ARCH)
  LINUX_DEFCONFIG ?= defconfig
endif

LINUX_CONFIG_FRAGMENT ?= $(CONFIG_DIR)/config.fragment
LINUX_OUT_MODULES_DEP := $(LINUX_OUT)/modules_install.stamp
LINUX_MODULES_INSTALL_PATH := $(LINUX_OUT)/modules_install
LINUX_CONFIG := $(LINUX_OUT)/.config

ifeq ($(ARCH),x86_64)
  TARGET := x86_64-pc-linux-gnu
  KERNEL_IMAGE := $(LINUX_OUT)/arch/$(ARCH)/boot/bzImage
else ifeq ($(ARCH),i386)
  TARGET := i386-pc-linux-gnu
  KERNEL_IMAGE := $(LINUX_OUT)/arch/$(ARCH)/boot/bzImage
else ifeq ($(ARCH),arm64)
  TARGET := aarch64-linux-gnu
  KERNEL_IMAGE := $(LINUX_OUT)/arch/$(ARCH)/boot/Image
endif

LINUX_MAKE := \
  PATH=$(CLANG_DIR)/bin:$(PATH) \
  $(MAKE) \
  -C $(LINUX_SRC) \
  ARCH=$(ARCH) \
  CROSS_COMPILE=$(TARGET)- \
  LLVM=1 LLVM_IAS=1 \
  V=$(VERBOSE) \
  O=$(LINUX_OUT) \
  -j `nproc`

$(CLANG_DIR):
	$(warning $(YELLOW)Clang directory $(CLANG_DIR) does not exist, downloading prebuilt binaries $(NC))
	wget --no-verbose --show-progress $(CLANG_URL) -O clang.tar.xz
	mkdir -p $(CLANG_DIR)
	tar -xf clang.tar.xz -C $(CLANG_DIR) --strip-components=1

.PHONY: linux_defconfig
linux_defconfig $(LINUX_CONFIG): $(LINUX_CONFIG_FRAGMENT) | $(CLANG_DIR)
	+ $(LINUX_MAKE) $(LINUX_DEFCONFIG)
	KCONFIG_CONFIG=$(LINUX_CONFIG) \
		$(LINUX_SRC)/scripts/kconfig/merge_config.sh \
		-m \
		$(LINUX_CONFIG) \
		$(LINUX_CONFIG_FRAGMENT)
	+ $(LINUX_MAKE) olddefconfig
	$(SCRIPT_DIR)/check_merged_config.sh $(LINUX_CONFIG) $(LINUX_CONFIG_FRAGMENT)

.PHONY: linux_menuconfig
linux_menuconfig:
	+ $(LINUX_MAKE) menuconfig
	$(SCRIPT_DIR)/check_merged_config.sh $(LINUX_CONFIG) $(LINUX_CONFIG_FRAGMENT)

.PHONY: linux
linux $(KERNEL_IMAGE): $(LINUX_CONFIG) | $(CLANG_DIR)
	+ $(LINUX_MAKE)
# Older versions of Linux don't have this script
ifneq (,$(wildcard ./.env))
	$(LINUX_SRC)/scripts/clang-tools/gen_compile_commands.py -d $(LINUX_OUT)
endif


.PHONY: linux_modules
linux_modules $(LINUX_OUT_MODULES_DEP): $(KERNEL_IMAGE)
	+ $(LINUX_MAKE) modules
	+ $(LINUX_MAKE) INSTALL_MOD_PATH=$(LINUX_MODULES_INSTALL_PATH) modules_install
	@find $(LINUX_MODULES_INSTALL_PATH) -type f | sort | \
		xargs sha1sum > $(LINUX_OUT_MODULES_DEP).tmp
	@cmp $(LINUX_OUT_MODULES_DEP).tmp $(LINUX_OUT_MODULES_DEP) || \
		mv $(LINUX_OUT_MODULES_DEP).tmp $(LINUX_OUT_MODULES_DEP)

.PHONY: linux_clean
linux_clean:
	+ $(LINUX_MAKE) mrproper

.PHONY: linux_download
linux_download:
ifndef VERSION
	$(error VERSION environment variable is not defined)
endif
	$(SCRIPT_DIR)/download_linux.sh $(VERSION) $(ACK)

.PHONY: linux_checkout
linux_checkout:
ifndef VERSION
	$(error VERSION environment variable is not defined)
endif
	$(SCRIPT_DIR)/checkout_linux.sh $(VERSION) $(LINUX_SRC)

# Use xz compression instead of zstd, as the latter is not supported by some
# versions of dpkg.
.PHONY: linux_debpkg
linux_debpkg: $(KERNEL_IMAGE)
	+ $(LINUX_MAKE) KDEB_COMPRESS=xz bindeb-pkg


# These targets do the same thing as the `linux` targets, but because they're in
# `ACK_TARGETS`, the `ACK` variable is set to 1 and the ACK source and output
# directories are used instead of the Linux directories. The same thing can be
# achieved by setting the `ACK=1` environment variable, so these are just
# provided for convenience.
.PHONY: ack
ack: linux

.PHONY: ack_clean
ack_clean: linux_clean

# Because of how `tools/vm/Makefile` is designed, we can't use `$(LINUX_MAKE)`
# here, as specifying the `O` variable causes compilation errors, and `clang`
# won't properly cross-compile the binary so we need to use GCC
TOOLS_MAKE := $(MAKE) \
  -C $(LINUX_SRC)/tools \
  DESTDIR=$(SHARED_DIR) \
  sbindir=/tools/$(ARCH)

# Assuming we're building on an x86 system, we should only set `CROSS_COMPILE`
# if we're building arm64.
ifeq ($(ARCH),arm64)
  TOOLS_MAKE += CROSS_COMPILE=$(TARGET)-
endif

.PHONY: tools-vm
tools-vm: | $(SHARED_DIR)
	+ $(TOOLS_MAKE) vm_install

.PHONY: tools-vm_clean
tools-vm_clean:
	+ $(TOOLS_MAKE) vm_clean
	rm -f $(SHARED_DIR)/tools/$(ARCH)/{page_owner_sort,page-types,slabinfo}

##
## Generate rootfs images
##

ROOTFS_PARENT_DIR := $(ROOT_DIR)/rootfs
ROOTFS ?= $(ROOTFS_PARENT_DIR)/$$(basename $(ROOTFS_DIR)).img
ROOTFS_FORMAT ?= qcow2
ROOTFS_DIR ?= $(ROOTFS_PARENT_DIR)/alpine-$(ARCH)
CPIO_FILE ?= $(ROOTFS_PARENT_DIR)/$$(basename $(ROOTFS_DIR)).cpio.gz
EXT4_SIZE ?= 1G
ifeq ($(SUDO),1)
	SUDO := sudo
endif

# The user might set a relative path for `CPIO_FILE`, so we need to get the
# absolute path, as we need to reference it after we've changed directories
CPIO_FILE := $(shell realpath $(CPIO_FILE))

$(ROOTFS_DIR):
	mkdir -p $(ROOTFS_DIR)

.PHONY: rootfs-init
rootfs-init: | $(ROOTFS_DIR)
	tar -xf $(ROOTFS_PARENT_DIR)/alpine-minirootfs-*-$(ARCH).tar.gz -C $(ROOTFS_DIR)
	$(MAKE) rootfs-overlay

.PHONY: rootfs-overlay
rootfs-overlay: | $(ROOTFS_DIR)
	$(SUDO) cp -f -r $(CONFIG_DIR)/rootfs-overlay/. $(ROOTFS_DIR)
	$(SUDO) $(SCRIPT_DIR)/arch_specific.sh $(ARCH) $(ROOTFS_DIR)
	$(MAKE) rootfs

.PHONY: ext4
ext4: | $(ROOTFS_DIR)
	dd if=/dev/zero of=$(ROOTFS) bs=1 count=0 seek=$(EXT4_SIZE)
	$(SUDO) mkfs.ext4 -b 4096 -d $(ROOTFS_DIR) -F $(ROOTFS)
	qemu-img convert -O $(ROOTFS_FORMAT) $(ROOTFS) $(ROOTFS).tmp
	mv $(ROOTFS).tmp $(ROOTFS)

.PHONY: cpio initramfs
initramfs cpio: | $(ROOTFS_DIR)
	(cd $(ROOTFS_DIR) && $(SUDO) find . -print0 \
		| $(SUDO) cpio --null --create --verbose --format=newc) \
		| gzip --best > $(CPIO_FILE)

.PHONY: uncpio
uncpio: | $(ROOTFS_DIR)
	cd $(ROOTFS_DIR) && zcat $(CPIO_FILE) | $(SUDO) cpio --extract --make-directories --format=newc --no-absolute-filenames

.PHONY: rootfs
rootfs: ext4 cpio

.PHONY: rootfs-mount
rootfs-mount:
	sudo modprobe nbd
	sudo qemu-nbd -c /dev/nbd0 -f $(ROOTFS_FORMAT) $(ROOTFS)
	mkdir -p /tmp/rootfs
	sudo mount /dev/nbd0 /tmp/rootfs

.PHONY: rootfs-unmount
rootfs-unmount:
	sudo umount /dev/nbd0
	sudo qemu-nbd -d /dev/nbd0

.PHONY: chroot
chroot:
	sudo chroot $(ROOTFS_DIR) /bin/sh

.PHONY: rootfs_clean
rootfs_clean:
	$(SUDO) rm -rf $(ROOTFS_DIR)
	$(SUDO) rm -f $(CPIO_FILE)
	$(SUDO) rm -f $(ROOTFS)

##
## Run QEMU
##

CPU ?= 4
MEM ?= 1024
QEMU_EXTRA_ARGS ?=
QEMU_EXTRA_KERNEL_CMDLINE ?=
GDB ?= 0
INITRD ?=
RDINIT ?= /sbin/init
QEMU_KERNEL_IMAGE ?= $(KERNEL_IMAGE)
ECHR ?= 1
ROOT ?= /dev/vda
RW ?= rw
KASLR ?= 0

QEMU_KERNEL_CMDLINE := selinux=0

QEMU_ARGS := \
  -m $(MEM) \
  -smp $(CPU) \
  -nographic \
  -no-reboot \
  -kernel $(QEMU_KERNEL_IMAGE) \
  -netdev user,id=eth0,hostfwd=tcp::7777-:7777,hostfwd=tcp::2222-:22,hostfwd=tcp::2223-:23 -device virtio-net-pci,netdev=eth0 \
  -virtfs local,security_model=mapped-xattr,path=$(SHARED_DIR),mount_tag=shared \
  -echr $(ECHR) \
  $(QEMU_EXTRA_ARGS)

ifneq ($(INITRD),)
  ifeq ($(INITRD),1)
    INITRD := $(CPIO_FILE)
  endif
  QEMU_KERNEL_CMDLINE += rdinit=$(RDINIT)
  QEMU_ARGS += -initrd $(INITRD)
else
  QEMU_ARGS += -drive file=$(ROOTFS),if=virtio,format=$(ROOTFS_FORMAT)
  QEMU_KERNEL_CMDLINE += root=$(ROOT) $(RW)
endif

ifeq ($(GDB),1)
  QEMU_ARGS += -s -S
endif

ifeq ($(ARCH),x86_64)
  QEMU_BIN := qemu-system-x86_64
  QEMU_KERNEL_CMDLINE += console=ttyS0 kpti no5lvl

  QEMU_ARGS += -cpu kvm64,+smep,+smap

  # We can't use KVM with GitHub Actions CI
  ifneq ($(CI),true)
    QEMU_ARGS += -accel kvm
  endif
else ifeq ($(ARCH),i386)
  QEMU_BIN := qemu-system-i386
  QEMU_KERNEL_CMDLINE += console=ttyS0
else
  QEMU_BIN := qemu-system-aarch64
  QEMU_KERNEL_CMDLINE += console=ttyAMA0

  QEMU_ARGS += \
    -M virt \
    -cpu cortex-a53 \
    -semihosting-config enable=on,target=native
endif

ifeq ($(KASLR),0)
  QEMU_KERNEL_CMDLINE += nokaslr
endif

QEMU_ARGS += -append "$(QEMU_KERNEL_CMDLINE) $(QEMU_EXTRA_KERNEL_CMDLINE)"

RUN_DEPS := $(QEMU_KERNEL_IMAGE)

.PHONY: run
run: $(RUN_DEPS) | $(SHARED_DIR)
	@echo "$(GREEN)Running QEMU, press 'ctrl-a x' to quit $(NC)"
ifeq ($(GDB),1)
	@echo "$(ARCH) $(ACK)" > $(OUT_DIR)/.gdb
	@echo "$(GREEN)Waiting for GDB, attach with \`scripts/gdb.sh\` $(NC)"

ifdef TERMINAL_CMD
	$(TERMINAL_CMD) $(SCRIPT_DIR)/gdb.sh
endif

endif
	@echo ''
	$(QEMU_BIN) $(QEMU_ARGS)

.PHONY: run-ack
run-ack: run
