#!/bin/sh

# Declare used deviceinfo variables to pass shellcheck (order alphabetically)
deviceinfo_append_dtb=""
deviceinfo_arch=""
deviceinfo_bootimg_append_seandroidenforce=""
deviceinfo_bootimg_blobpack=""
deviceinfo_bootimg_dtb_second=""
deviceinfo_bootimg_mtk_mkimage=""
deviceinfo_bootimg_qcdt=""
deviceinfo_dtb=""
deviceinfo_flash_offset_base=""
deviceinfo_flash_offset_kernel=""
deviceinfo_flash_offset_ramdisk=""
deviceinfo_flash_offset_second=""
deviceinfo_flash_offset_tags=""
deviceinfo_flash_pagesize=""
deviceinfo_generate_bootimg=""
deviceinfo_generate_legacy_uboot_initfs=""
deviceinfo_mesa_driver=""
deviceinfo_initfs_compression=""
deviceinfo_kernel_cmdline=""
deviceinfo_legacy_uboot_load_address=""
deviceinfo_modules_initfs=""

# Overwritten by mkinitfs.sh
tmpdir=""

source_deviceinfo()
{
	if [ ! -e "/etc/deviceinfo" ]; then
		echo "NOTE: deviceinfo (from device package) not installed yet," \
			"not building the initramfs now (it should get built later" \
			"automatically.)"
		exit 0
	fi
	# shellcheck disable=SC1091
	. /etc/deviceinfo
}

parse_commandline()
{
	if [ "$1" != "-o" ]; then
		echo "postmarketos-mkinitfs"
		echo "usage: $(basename "$0") -o OUTFILE KERNELVERSION"
		exit 1
	fi

	outfile=$2
	outfile_extra=$2-extra
	kernel=$3
}

# Verify that each file required by the installed hooks exists and exit with an
# error if they don't.
check_hook_files()
{
	for file in "/etc/postmarketos-mkinitfs/files"/*.files; do
		[ -f "$file" ] || continue
		while IFS= read -r line; do
			if ! [ -f "$line" ]; then
				echo "ERROR: File ${line} specified in ${file} does not exist!"
				exit 1
			fi
		done < "$file"
	done
}

create_folders()
{
	for dir in /bin /sbin /usr/bin /usr/sbin /proc /sys /dev /tmp /lib \
		/boot /sysroot /etc; do
		mkdir -p "$tmpdir$dir"
	done
}

get_modules_by_globs()
{
	globs="
		# base.modules
		kernel/drivers/block/loop.ko
		kernel/fs/overlayfs

		# cryptsetup.modules
		kernel/crypto/*
		kernel/arch/*/crypto/*
		kernel/drivers/md/dm-crypt.ko

		# required for modprobe
		modules.*
	"

	for glob in $globs; do
		for file in /lib/modules/$kernel/$glob; do
			if [ -d "$file" ]; then
				find "$file" -type f
			elif [ -e "$file" ]; then
				echo "$file"
			fi
		done
	done
}

# Read a modules-load.d style file into a single line. The file is expected to
# have empty lines, lines starting with "#" that are comments or one word in
# the line. The resulting parsed line is printed to stdout.
# $1: file to parse
parse_file_as_line()
{
	_first="true"
	while IFS= read -r line; do
		case "$line" in
			""|"#"*)
				# Comment or empty line, ignore
				;;
			*)
				if [ "$_first" = "true" ]; then
					_first="false"
				else
					printf " "
				fi
				printf "%s" "$line"
				;;
		esac
	done < "$1"
}

# Parse modules by name from deviceinfo and from these files:
# /etc/postmarketos-mkinitfs/modules/*.modules. The postmarketos-mkinitfs
# package installs a 00-default.modules there.
# Resolved kernel module paths get printed to stdout, informative logging to
# stderr.
# NOTE: This does not work with busybox' modprobe. That's why
#       postmarketos-mkinitfs depends on kmod.
get_modules_by_name()
{
	{
		echo "Scanning kernel module dependencies..."
		echo "NOTE: ** modprobe warnings below can be ignored ** if your device does not run the"
		echo "mainline kernel yet (most devices!) or if the related kernel options are enabled"
		echo "with 'y' instead of 'm' (module)."
	} >&2

	MODULES="$deviceinfo_modules_initfs"
	echo " - deviceinfo: $deviceinfo_modules_initfs" >&2

	for file in "/etc/postmarketos-mkinitfs/modules/"*.modules; do
		[ -f "$file" ] || continue
		_modules_file="$(parse_file_as_line "$file")"
		echo " - $(basename "$file"): $_modules_file" >&2
		MODULES="$MODULES $_modules_file"
	done

	# shellcheck disable=SC2086
	modprobe \
		-a \
		--dry-run \
		--show-depends \
		--set-version="$kernel" \
		$MODULES \
		| sort -u \
		| cut -d ' ' -f 2
}

get_modules()
{
	get_modules_by_globs
	get_modules_by_name
}

# Get the paths to all binaries and their dependencies
get_binaries()
{
	BINARIES="
		/bin/busybox
		/bin/busybox-extras
		/usr/sbin/telnetd
		/sbin/kpartx
	"

	for file in "/etc/postmarketos-mkinitfs/files"/*.files; do
		[ -f "$file" ] || continue
		while IFS= read -r line; do
			BINARIES="${BINARIES} ${line}"
		done < "$file"
	done
	# shellcheck disable=SC2086
	sudo -u nobody lddtree -l $BINARIES | sort -u
}

# Collect non-binary files for osk-sdl and its dependencies
# This gets called as $(get_osk_config), so the exit code can be checked/handled.
get_osk_config()
{
	fontpath=$(awk '/^keyboard-font = /{print $3}' /etc/osk.conf)
	if [ ! -f "$fontpath" ]; then
		echo "ERROR: failed to parse 'keyboard-font' from osk-sdl config!"
		exit 1
	fi
	ret="
		/etc/osk.conf
		/etc/ts.conf
		/etc/pointercal
		/etc/fb.modes
		/etc/directfbrc
		$fontpath
	"
	echo "${ret}"
}

get_binaries_extra()
{
	BINARIES_EXTRA="
		$(find /usr/lib/directfb-* -name '*.so')
		/lib/libz.so.1
		/sbin/cryptsetup
		/sbin/dmsetup
		/sbin/e2fsck
		/usr/bin/charging-sdl
		/usr/bin/osk-sdl
		/usr/lib/libGL.so.1
		/usr/lib/libts*
		/usr/lib/ts/*
		/usr/sbin/parted
		/usr/sbin/resize2fs
		/usr/sbin/thd
	"

	if [ -n "$deviceinfo_mesa_driver" ]; then
		BINARIES_EXTRA="
			$BINARIES_EXTRA
			/usr/lib/libEGL.so.1
			/usr/lib/libgbm.so.1
			/usr/lib/libudev.so.1
			/usr/lib/xorg/modules/dri/${deviceinfo_mesa_driver}_dri.so
		"
	fi

	tmp1=$(mktemp /tmp/mkinitfs.XXXXXX)
	get_binaries > "$tmp1"
	tmp2=$(mktemp /tmp/mkinitfs.XXXXXX)

	# shellcheck disable=SC2086
	sudo -u nobody lddtree -l $BINARIES_EXTRA | sort -u > "$tmp2"
	ret=$(comm -13 "$tmp1" "$tmp2")
	rm "$tmp1" "$tmp2"
	echo "${ret}"
}

# Copy files to the destination specified
# $1: files
# $2: destination
copy_files()
{
	for file in $1; do
		[ -e "$file" ] || continue
		cp -a --parents "$file" "$2"
	done
}

create_device_nodes()
{
	mknod -m 666 "$tmpdir/dev/null" c 1 3
	mknod -m 644 "$tmpdir/dev/random" c 1 8
	mknod -m 644 "$tmpdir/dev/urandom" c 1 9
}

replace_init_variables()
{
	sed -i "s:@INITRAMFS_EXTRA@:${outfile_extra}:g" "$tmpdir/init"
}

# Create a cpio image of the specified folder
# $1: folder
# $2: outfile
create_cpio_image()
{
	if ! cd "$1"; then
		echo "ERROR: failed to cd to '$1'"
		exit 1
	fi
	[ -z "$deviceinfo_initfs_compression" ] && deviceinfo_initfs_compression='gzip -1'
	find . -print0 \
		| cpio --quiet -o -0 -H newc \
		| $deviceinfo_initfs_compression > "$2"
}

# Required command check with useful error message
# $1: command (e.g. "mkimage")
# $2: package (e.g. "uboot-tools")
# $3: related deviceinfo variable (e.g. "generate_bootimg")
require_package()
{
	[ "$(command -v "$1")" = "" ] || return
	echo "ERROR: 'deviceinfo_$3' is set, but the package '$2' was not"
	echo "installed! Please add '$2' to the depends= line of your device's"
	echo "APKBUILD. See also: <https://postmarketos.org/deviceinfo>"
	exit 1
}

# Legacy u-boot images
create_uboot_files()
{
	arch="arm"
	if [ "${deviceinfo_arch}" = "aarch64" ]; then
		arch="arm64"
	fi

	[ "${deviceinfo_generate_legacy_uboot_initfs}" = "true" ] || return
	require_package "mkimage" "uboot-tools" "generate_legacy_uboot_initfs"

	echo "==> initramfs: creating uInitrd"
	# shellcheck disable=SC2039
	mkimage -A $arch -T ramdisk -C none -n uInitrd -d "$outfile" \
		"${outfile/initramfs-/uInitrd-}" || exit 1

	echo "==> kernel: creating uImage"
	# shellcheck disable=SC2039
	kernelfile="${outfile/initramfs-/vmlinuz-}"
	if [ "${deviceinfo_append_dtb}" = "true" ]; then
		kernelfile="${kernelfile}-dtb"
	fi

	if [ -z "$deviceinfo_legacy_uboot_load_address" ]; then
		deviceinfo_legacy_uboot_load_address="80008000"
	fi

	# shellcheck disable=SC2039
	mkimage -A $arch -O linux -T kernel -C none -a "$deviceinfo_legacy_uboot_load_address" \
		-e "$deviceinfo_legacy_uboot_load_address" \
		-n postmarketos -d "$kernelfile" "${outfile/initramfs-/uImage-}" || exit 1
}

# Android devices
create_bootimg()
{
	[ "${deviceinfo_generate_bootimg}" = "true" ] || return
	require_package "mkbootimg-osm0sis" "mkbootimg" "generate_bootimg"

	echo "==> initramfs: creating boot.img"
	_base="${deviceinfo_flash_offset_base}"
	[ -z "$_base" ] && _base="0x10000000"

	# shellcheck disable=SC2039
	kernelfile="${outfile/initramfs-/vmlinuz-}"
	if [ "${deviceinfo_append_dtb}" = "true" ]; then
		kernelfile="${kernelfile}-dtb"
	fi

	if [ "${deviceinfo_bootimg_mtk_mkimage}" = "true" ]; then
		kernelfile="${kernelfile}-mtk"
	fi

	_second=""
	if [ "${deviceinfo_bootimg_dtb_second}" = "true" ]; then
		if [ -z "${deviceinfo_dtb}" ]; then
			echo "ERROR: deviceinfo_bootimg_dtb_second is set, but"
			echo "'deviceinfo_dtb' is missing. Set 'deviceinfo_dtb'"
			echo "to the device tree blob for your device."
			echo "See also: <https://postmarketos.org/deviceinfo>"
			exit 1
		fi
		dtb="/usr/share/dtb/${deviceinfo_dtb}.dtb"
		_second="--second $dtb"
		if ! [ -e "$dtb" ]; then
			echo "ERROR: File not found: $dtb. Please set 'deviceinfo_dtb'"
			echo "to the relative path to the device tree blob for your"
			echo "device (without .dtb)."
			echo "See also: <https://postmarketos.org/deviceinfo>"
			exit 1
		fi
	fi
	_dt=""
	if [ "${deviceinfo_bootimg_qcdt}" = "true" ]; then
		_dt="--dt /boot/dt.img"
		if ! [ -e "/boot/dt.img" ]; then
			echo "ERROR: File not found: /boot/dt.img, but"
			echo "'deviceinfo_bootimg_qcdt' is set. Please verify that your"
			echo "device is a QCDT device by analyzing the boot.img file"
			echo "(e.g. 'pmbootstrap bootimg_analyze path/to/twrp.img')"
			echo "and based on that, set the deviceinfo variable to false or"
			echo "adjust your linux APKBUILD to properly generate the dt.img"
			echo "file. See also: <https://postmarketos.org/deviceinfo>"
			exit 1
		fi
	fi
	# shellcheck disable=SC2039 disable=SC2086
	mkbootimg-osm0sis \
		--kernel "${kernelfile}" \
		--ramdisk "$outfile" \
		--base "${_base}" \
		--second_offset "${deviceinfo_flash_offset_second}" \
		--cmdline "${deviceinfo_kernel_cmdline}" \
		--kernel_offset "${deviceinfo_flash_offset_kernel}" \
		--ramdisk_offset "${deviceinfo_flash_offset_ramdisk}" \
		--tags_offset "${deviceinfo_flash_offset_tags}" \
		--pagesize "${deviceinfo_flash_pagesize}" \
		${_second} \
		${_dt} \
		-o "${outfile/initramfs-/boot.img-}" || exit 1
	if [ "${deviceinfo_bootimg_blobpack}" = "true" ]; then
		echo "==> initramfs: creating blob"
		# shellcheck disable=SC2039
		blobpack "${outfile/initramfs-/blob-}" LNX \
			"${outfile/initramfs-/boot.img-}" || exit 1
	fi
	if [ "${deviceinfo_bootimg_append_seandroidenforce}" = "true" ]; then
		echo "==> initramfs: appending 'SEANDROIDENFORCE' to boot.img"
		# shellcheck disable=SC2039 disable=SC2039
		echo -n "SEANDROIDENFORCE" >> "${outfile/initramfs-/boot.img-}"
	fi
}

# Append the correct device tree to the linux image file or copy the dtb to the boot partition
append_or_copy_dtb()
{
	[ -n "${deviceinfo_dtb}" ] || return
	echo "==> kernel: device-tree blob operations"
	dtb=""
	for filename in $deviceinfo_dtb; do
		if ! [ -e "/usr/share/dtb/$filename.dtb" ]; then
			echo "ERROR: File not found: /usr/share/dtb/$filename.dtb"
			exit 1
		fi
		dtb="$dtb /usr/share/dtb/$filename.dtb"
	done
	# shellcheck disable=SC2039
	kernel="${outfile/initramfs-/vmlinuz-}"
	if [ "${deviceinfo_append_dtb}" = "true" ]; then
		echo "==> kernel: appending device-tree ${deviceinfo_dtb}"
		# shellcheck disable=SC2086
		cat "$kernel" $dtb > "${kernel}-dtb"
	else
		echo "==> kernel: copying dtb ${deviceinfo_dtb} to boot partition"
		# shellcheck disable=SC2086
		cp $dtb "$(dirname "${outfile}")"
	fi
}

# Add Mediatek header to kernel & initramfs
add_mtk_header()
{
	[ "${deviceinfo_bootimg_mtk_mkimage}" = "true" ] || return
	require_package "mtk-mkimage" "mtk-mkimage" "bootimg_mtk_mkimage"

	echo "==> initramfs: adding Mediatek header"
	mv "$outfile" "$outfile-orig"
	mtk-mkimage ROOTFS "$outfile-orig" "$outfile"
	rm "$outfile-orig"

	echo "==> kernel: adding Mediatek header"
	# shellcheck disable=SC2039
	kernel="${outfile/initramfs-/vmlinuz-}"
	rm -f "${kernel}-mtk"
	mtk-mkimage KERNEL "$kernel" "${kernel}-mtk"
}

# Create the initramfs-extra archive
# $1: outfile
generate_initramfs_extra()
{
	echo "==> initramfs: creating $1"

	osk_conf="$(get_osk_config)"
	if [ $? -eq 1 ]; then
		echo "ERROR: Font specified in /etc/osk.conf does not exist!"
		exit 1
	fi

	# Set up initramfs-extra in temp folder
	tmpdir_extra=$(mktemp -d /tmp/mkinitfs.XXXXXX)
	mkdir -p "$tmpdir_extra"
	copy_files "$(get_binaries_extra)" "$tmpdir_extra"
	copy_files "$osk_conf" "$tmpdir_extra"
	create_cpio_image "$tmpdir_extra" "$1.new"
	rm -rf "$tmpdir_extra"

	# Replace old initramfs-extra *after* we are done to make sure
	# it does not become corrupted if something goes wrong.
	mv "$1.new" "$1"
}
