#!/bin/sh
# vim: set ts=4:
#---help---
# Usage: efi-mkuki [options] <vmlinuz> [<microcode>...] [<initrd>]
#        efi-mkuki <-h | -V>
#
# Create an EFI Unified Kernel Image (UKI) - a single EFI PE executable
# combining an EFI stub loader, a kernel image, an initramfs image, the kernel
# command line, and optionally a CPU microcode update image.
#
# Arguments:
#   <vmlinuz>            Location of Linux kernel image file.
#   <microcode>...       Location of microcode file(s) (optional).
#   <initrd>             Location of initramdisk file.
#
# Options:
#   -c <cmdline | file>  Kernel cmdline, or location of file with kernel cmdline
#                        (if begins with "/" or "."). Defaults to /proc/cmdline.
#
#   -o <file>            Write output into <file>. Defaults to <vmlinuz>.efi.
#
#   -r <file>            Location of osrel file. Defaults to /etc/os-release.
#
#   -s <file>            Location of splash image (optional).
#
#   -S <file>            Location of EFI stub file. Defaults to
#                        linux<march>.efi.stub in /usr/lib/gummiboot where
#                        <march> is UEFI machine type (x64, ia32, aa64, arm).
#
#   -h                   Show this message and exit.
#
#   -V                   Print version.
#
# Please report bugs at <https://github.com/jirutka/efi-mkuki/issues>.
#---help---
set -eu

if ( set -o pipefail 2>/dev/null ); then
	set -o pipefail
fi

PROGNAME='efi-mkuki'
VERSION='0.1.0'
EFISTUB_DIR='/usr/lib/gummiboot'

help() {
	sed -n '/^#---help---/,/^#---help---/p' "$0" | sed 's/^# \?//; 1d;$d;'
}

die() {
	echo "$PROGNAME: $@" >&2
	exit 2
}

# Defaults
cmdline=/proc/cmdline
output=
osrel=/etc/os-release
splash=/dev/null
efistub=

while getopts ':c:o:r:s:S:hV' OPT; do
	case "$OPT" in
		c) cmdline=$OPTARG;;
		o) output=$OPTARG;;
		r) osrel=$OPTARG;;
		s) splash=$OPTARG;;
		S) efistub=$OPTARG;;
		h) help; exit 0;;
		V) echo "$PROGNAME $VERSION"; exit 0;;
		\?) die "unknown option: -$OPTARG";;
	esac
done
shift $((OPTIND - 1))

[ $# -ge 1 ] || die "invalid number of arguments, see '$PROGNAME -h'"

if ! [ "$efistub" ]; then
	case "$(uname -m)" in
		aarch64) march=aa64;;
		arm*) march=arm;;
		x86 | i686) march=ia32;;
		x86_64) march=x64;;
		*) die "unknown architecture: $(uname -m)";;
	esac
	efistub="$EFISTUB_DIR/linux$march.efi.stub"
fi
[ -f "$efistub" ] || die "EFI stub '$efistub' does not exist!"

tmpdir=$(mktemp -dt $PROGNAME.XXXXXX)
trap "rm -f $tmpdir/*; rmdir $tmpdir" EXIT HUP INT TERM

case "$cmdline" in
	/* | .*) grep '^[^#]' "$cmdline" | tr -s '\n' ' ' > "$tmpdir"/cmdline;;
	*) printf '%s\n' "$cmdline" > "$tmpdir"/cmdline;;
esac
cmdline="$tmpdir/cmdline"

linux=$1; shift

initrd=${1:-"/dev/null"}
if [ $# -gt 1 ]; then
	initrd="$tmpdir/initrd"
	cat "$@" > "$initrd"
fi

[ "$output" ] || output="$linux.efi"

objcopy \
	--add-section .osrel="$osrel"     --change-section-vma .osrel=0x20000    \
	--add-section .cmdline="$cmdline" --change-section-vma .cmdline=0x30000  \
	--add-section .splash="$splash"   --change-section-vma .splash=0x40000   \
	--add-section .linux="$linux"     --change-section-vma .linux=0x2000000  \
	--add-section .initrd="$initrd"   --change-section-vma .initrd=0x3000000 \
	"$efistub" "$output"
