#!/bin/sh
# SPDX-FileCopyrightText: 2023-2024 Helmut Grohne <helmut@subdivi.de>
# SPDX-License-Identifier: MIT

# We generally use single quotes to avoid variable expansion:
# shellcheck disable=SC2016

set -eu

die() {
	echo "$*" >&2
	exit 1
}
usage() {
	test -n "$*" && echo "error: $*" >&2
	die "usage: $0 [--architecture=|--mirror=|--release=|--rootsize=] <IMAGE> [--] [mmdebstrap OPTIONS]"
}

DEBARCH=$(dpkg --print-architecture)
FAT_SIZE_KIB=$((126*1024))
HOSTNAME=host
IMAGE=
MIRROR=
EFILABEL=efisys
ROOTLABEL=rootfs

RELEASE=unstable
EXT4_SIZE_KIB=$((10*1024*1024))

opt_architecture() {
	DEBARCH="$1"
}
opt_mirror() {
	MIRROR="$1"
}
opt_release() {
	RELEASE="$1"
}
opt_rootsize() {
	EXT4_SIZE_KIB="$(numfmt --to=none --to-unit=Ki --from=iec "$1")" ||
		die "failed to parse size '$1'"
}

positional=1
positional_1() {
	IMAGE="$1"
}
positional_2() {
	die "too many positional options"
}

while test "$#" -gt 0; do
	case "$1" in
		--architecture=*|--mirror=*|--release=*|--rootsize=*)
			optname="${1%%=*}"
			"opt_${optname#--}" "${1#*=}"
		;;
		--architecture|--mirror|--release|--rootsize)
			test "$#" -ge 2 || usage "missing argument for $1"
			"opt_${1#--}" "$2"
			shift
		;;
		--)
			shift
			break
		;;
		--*)
			usage "unrecognized argument $1"
		;;
		*)
			"positional_$positional" "$1"
			positional=$((positional + 1))
		;;
	esac
	shift
done

test -z "$IMAGE" && usage "missing positional arguments"

# Disk layout:
#  * GPT partition table in the front. Oversize to align.
GPT_FRONT_SIZE_KIB=1024
#  * FAT partition containing the EFI boot loader. Size configured.
FAT_OFFSET_KIB=$GPT_FRONT_SIZE_KIB
#  * Main ext4 root partition. Compute as remaining size.
EXT4_OFFSET_KIB=$((FAT_OFFSET_KIB + FAT_SIZE_KIB))
TOT_SIZE_KIB=$((EXT4_SIZE_KIB + EXT4_OFFSET_KIB - GPT_BACK_SIZE_KIB))
#  * GPT backup table in the back. Oversize to align.
GPT_BACK_SIZE_KIB=1024

test "$EXT4_SIZE_KIB" -lt "$((1024*64))" &&
	die "implausibly small disk size $TOT_SIZE_KIB"

case "$DEBARCH" in
	amd64)
		EFIARCH=x64
		ROOTGUID=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
	;;
	arm64)
		EFIARCH=aa64
		ROOTGUID=B921B045-1DF0-41C3-AF44-4C6F280D3FAE
	;;
	armhf)
		EFIARCH=arm
		ROOTGUID=69DAD710-2CE4-4E3C-B16C-21A1D49ABED3
	;;
	i386)
		EFIARCH=ia32
		ROOTGUID=44479540-F297-41B2-9AF7-D131D5F0458A
	;;
	riscv64)
		EFIARCH=riscv64
		ROOTGUID=72EC70A6-CF74-40E6-BD49-4BDA08E8F224
	;;
	*)
		die "unsupported architecture: $DEBARCH"
	;;
esac

test -e "$IMAGE" &&
	die "output file $IMAGE already exists"

if command -v ukify >/dev/null; then
	UKIFY=ukify
	UKIFY_VERBOSE=
else
	UKIFY=/usr/share/debusine-worker/mini-ukify
	UKIFY_VERBOSE=--verbose
fi

WORKDIR=

cleanup() {
	test -n "$WORKDIR" && rm -Rf "$WORKDIR"
}
cleanup_abort() {
	cleanup
	echo aborted >&2
	exit 2
}
trap cleanup EXIT
trap cleanup_abort HUP INT TERM QUIT

WORKDIR=$(mktemp -d)

truncate --size="${TOT_SIZE_KIB}K" "$IMAGE"

# Stuff most of our options before the user options to allow them to override
# ours.
#
# We skip replacing start-stop-daemon, because policy-rc.d nowadays works well
# enough and then we have to not restore it before calling mkfs.ext4.
set -- \
	--mode=root \
	--variant=important \
	--architecture="$DEBARCH" \
	--skip=chroot/start-stop-daemon \
	"$@"

case $MIRROR in http://snapshot.debian.org/archive/*|https://snapshot.debian.org/archive/*)
	set -- --aptopt='Acquire::Check-Valid-Until "false"' "$@"
	;;
esac

test -n "$MIRROR" && set -- "$MIRROR" "$@"
set -- \
	'--customize-hook=/usr/share/debvm/customize-kernel.sh' \
	"--customize-hook=printf 'LABEL=%s / ext4 defaults 0 0' '$ROOTLABEL' >"'"$1/etc/fstab"' \
	'--include=?exact-name(systemd-boot)' \
	'--include=systemd-sysv' \
	'--customize-hook=passwd --root "$1" --delete root' \
	"$RELEASE" \
	/dev/null \
	"$@"

# Set up basic networking:
#  * hostname (not inheriting from the builder)
#  * /etc/hosts (not inheriting from the builder)
#  * systemd-networkd
#  * systemd-resolved
ETC_HOSTS_TEMPLATE='127.0.0.1 localhost\n127.0.1.1 %s\n::1 ip6-localhost ip6-loopback\n'
set -- \
	"--customize-hook=echo '$HOSTNAME' >"'"$1/etc/hostname"' \
	"--customize-hook=printf '$ETC_HOSTS_TEMPLATE' '$HOSTNAME' >"'"$1/etc/hosts"' \
	"--include=libnss-resolve" \
	"--customize-hook=/usr/share/debvm/customize-resolved.sh" \
	"--customize-hook=/usr/share/debvm/customize-networkd.sh" \
	"$@"

# By default, Debian mounts the EFI System Partition to /boot/efi. Keep that
# default.
BOOT_EFI_MOUNT_TEMPLATE='[Unit]\nDescription=EFI System Partition\n\n[Mount]\nWhat=LABEL=%s\nWhere=/boot/efi\nType=vfat\nOptions=umask=0077,rw,nodev,nosuid,noexec,nosymfollow\n\n[Install]\nWantedBy=local-fs.target\n'
set -- \
	'--customize-hook=mkdir "$1/boot/efi"' \
	"--customize-hook=printf '$BOOT_EFI_MOUNT_TEMPLATE' '$EFILABEL' >"'"$1/etc/systemd/system/boot-efi.mount"' \
	'--customize-hook=systemctl --root "$1" enable boot-efi.mount' \
	"$@"

# Pass the remaining hooks last. These collect data from the generated tree and
# create the resulting filesystem. All user-supplied customizations should have
# run before these.
#
# We download the artifacts that end up being turned into the EFI.
EXT4_OPTIONS="offset=$((EXT4_OFFSET_KIB * 1024)),assume_storage_prezeroed=1"
set -- "$@" \
	"--customize-hook=download vmlinuz '$WORKDIR/kernel'" \
	"--customize-hook=download initrd.img '$WORKDIR/initrd'" \
	"--customize-hook=download '/usr/lib/systemd/boot/efi/linux$EFIARCH.efi.stub' '$WORKDIR/stub'" \
	'--customize-hook=mount --bind "$1" "$1/mnt"' \
	'--customize-hook=mount --bind "$1/mnt/mnt" "$1/mnt/dev"' \
	'--customize-hook=rm "$1/usr/sbin/policy-rc.d"' \
	'--customize-hook=/sbin/mkfs.ext4 -d "$1/mnt" -L '"'$ROOTLABEL' -E '$EXT4_OPTIONS' '$IMAGE' '${EXT4_SIZE_KIB}K'" \
	'--customize-hook=umount --lazy "$1/mnt"'

set -- mmdebstrap "$@"

# We need to write the resulting $IMAGE from inside the namespace used by
# mmdebstrap. If we were to have mmdebstrap --mode=unshare set up that
# namespace, we could end up being unable to write it if a leading directory
# component in $IMAGE were not not grant execute permission to any of the
# subuids mapped. This typically is the case when using PrivateTmp=yes. With
# that setting, there is a 0700 directory owned by the current user that
# happens to not be mapped by mmdebstrap.
#
# Instead, we take matters into our own hands and set up the namespaces outside
# mmdebstrap asking it to assume root. In addition to what mmdebstrap would do
# by itself, we also map the current user to 65536.  The precise uid does not
# matter, but having it mapped allows CAP_DAC_OVERRIDE to be used for writing
# the image.
if test "$(id -u)" != 0; then
	set -- \
		unshare \
		--user \
		--mount \
		--ipc \
		--pid \
		--uts \
		--fork \
		--kill-child=TERM \
		--map-user=65536 \
		--map-users=auto \
		--map-group=65536 \
		--map-groups=auto \
		--propagation=private \
		--mount-proc \
		--setuid=0 \
		--setgid=0 \
		"$@"
fi
echo "+ $*" >&2
"$@" || die "mmdebstrap failed"

CMDLINE="root=LABEL=$ROOTLABEL rw"
case "$DEBARCH" in
	amd64|i386)
		CMDLINE="$CMDLINE console=ttyS0"
	;;
esac

set -- "$UKIFY" build \
	$UKIFY_VERBOSE \
	--efi-arch "$EFIARCH" \
	--linux "$WORKDIR/kernel" \
	--initrd "$WORKDIR/initrd" \
	--cmdline "$CMDLINE" \
	--stub "$WORKDIR/stub" \
	--output "$WORKDIR/efiimg"
echo "+ $*" >&2
"$@" ||
	die "failed to generate UKI"

rm -f "$WORKDIR/kernel" "$WORKDIR/initrd" "$WORKDIR/stub"

truncate -s "${FAT_SIZE_KIB}K" "$WORKDIR/fat"
/sbin/mkfs.fat -n "$EFILABEL" -F 32 --invariant "$WORKDIR/fat"
mmd -i "$WORKDIR/fat" EFI EFI/BOOT
mcopy -i "$WORKDIR/fat" "$WORKDIR/efiimg" "::EFI/BOOT/boot$EFIARCH.efi"

rm -f "$WORKDIR/efiimg"

/sbin/sfdisk "$IMAGE" <<EOF
label: gpt
unit: sectors

start=${FAT_OFFSET_KIB}KiB, size=${FAT_SIZE_KIB}KiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
start=$((FAT_OFFSET_KIB + FAT_SIZE_KIB))KiB, type=$ROOTGUID
EOF

dd if="$WORKDIR/fat" of="$IMAGE" conv=notrunc,sparse bs=1024 "seek=$FAT_OFFSET_KIB" status=none
