#!/bin/sh
set -eu

out="alpine-avf"
rootfs="$out/rootfs"
branch="${ALPINE_BRANCH:-v3.23}"
arch=aarch64
mirror="https://dl-cdn.alpinelinux.org/alpine"
size="${ROOT_SIZE:-4G}"
tmp=

packages="alpine-base linux-virt mkinitfs e2fsprogs e2fsprogs-extra doas ifupdown-ng ttyd \
avahi avahi-openrc dbus dbus-openrc"

die() { echo "$*" >&2; exit 1; }

cleanup() {
	if [ -n "$tmp" ] && [ -d "$tmp" ]; then
		rm -rf "$tmp" || true
	fi
	for m in dev/pts dev sys proc; do
		if mountpoint -q "$rootfs/$m"; then
			umount "$rootfs/$m" || true
		fi
	done
}

# cleanup unmounts best-effort only; rm -rf or mkfs -d on a rootfs that still
# has /dev bind-mounted would eat the host's /dev, so verify before either.
assert_unmounted() {
	for m in dev/pts dev sys proc; do
		! mountpoint -q "$rootfs/$m" \
			|| die "$rootfs/$m still mounted; unmount it and rerun"
	done
}

trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

[ "$(id -u)" = 0 ] || die "run as root"
[ "$(uname -m)" = "$arch" ] || [ -e "/proc/sys/fs/binfmt_misc/qemu-$arch" ] \
	|| die "need a $arch host or qemu-user-static binfmt"
for c in curl grep head mktemp sha256sum tar sed gzip od tr dd basename mkfs.ext4 openssl mount umount mountpoint chroot; do
	command -v "$c" >/dev/null 2>&1 || die "need $c"
done

# Validate inputs before the destructive cleanup below, so a typo fails loudly
# here instead of after curl wipes the old output or deep inside mkfs.ext4.
case "$branch" in
	"" | *[!a-zA-Z0-9.-]*) die "ALPINE_BRANCH must look like v3.23, latest-stable, or edge (got '$branch')" ;;
esac
case "${size%[sSkKmMgGtT]}" in
	"" | *[!0-9]*) die "ROOT_SIZE must be digits with an optional s/K/M/G/T suffix (got '$size')" ;;
esac

echo "cleaning $out"
cleanup
assert_unmounted
rm -rf "$out"; mkdir -p "$rootfs"

echo "fetching alpine $branch minirootfs"
rel="$mirror/$branch/releases/$arch"
file="$(curl -fsSL "$rel/latest-releases.yaml" | grep -oE "alpine-minirootfs-[0-9.]+-$arch\.tar\.gz" | head -1)"
[ -n "$file" ] || die "cannot determine minirootfs filename"
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/$file" "$rel/$file"
curl -fsSL -o "$tmp/$file.sha256" "$rel/$file.sha256"
( cd "$tmp" && sha256sum -c "$file.sha256" )
tar -xzf "$tmp/$file" -C "$rootfs"
rm -rf "$tmp"

echo "installing packages"
printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >"$rootfs/etc/resolv.conf"
printf '%s\n%s\n' "$mirror/$branch/main" "$mirror/$branch/community" >"$rootfs/etc/apk/repositories"
mount -t proc none "$rootfs/proc"
mount -t sysfs none "$rootfs/sys"
mount --bind /dev "$rootfs/dev"
mount --bind /dev/pts "$rootfs/dev/pts"
chroot "$rootfs" /bin/sh -c "apk update && apk add $packages"

echo "configuring"
echo alpine >"$rootfs/etc/hostname"
mkdir -p "$rootfs/mnt/internal" "$rootfs/mnt/shared"

cat >"$rootfs/etc/modules" <<'EOF'
virtio_blk
virtio_pci
virtio_net
virtio_console
virtiofs
vmw_vsock_virtio_transport
EOF

chroot "$rootfs" passwd -d root
chroot "$rootfs" adduser -D -u 1000 -s /bin/ash user
chroot "$rootfs" addgroup user video
chroot "$rootfs" passwd -d user
echo 'permit nopass user' >"$rootfs/etc/doas.conf"

grep -q '^ttyS0::' "$rootfs/etc/inittab" \
	|| echo 'ttyS0::respawn:/sbin/getty -L 0 ttyS0 vt100' >>"$rootfs/etc/inittab"

cat >"$rootfs/etc/network/interfaces" <<'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
EOF

mkdir -p "$rootfs/etc/ttyd"
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=localhost \
	-keyout "$rootfs/etc/ttyd/server.key" -out "$rootfs/etc/ttyd/server.crt" 2>/dev/null
chmod 0600 "$rootfs/etc/ttyd/server.key"

cat >"$rootfs/etc/init.d/ttyd" <<'EOF'
#!/sbin/openrc-run
description="ttyd terminal for the AVF Terminal app"
supervisor=supervise-daemon
command="/usr/bin/ttyd"
command_args="--ssl --ssl-cert /etc/ttyd/server.crt --ssl-key /etc/ttyd/server.key --ssl-ca /mnt/internal/ca.crt -t disableLeaveAlert=true -W login -f user"
respawn_delay=2
depend() { need avf-mounts; after net avahi-daemon; }
EOF
chmod +x "$rootfs/etc/init.d/ttyd"

rm -f "$rootfs"/etc/avahi/services/*.service
cat >"$rootfs/etc/avahi/services/ttyd.service" <<'EOF'
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name>ttyd</name>
  <service><type>_http._tcp</type><port>7681</port></service>
</service-group>
EOF
sed -i 's/^#*use-ipv4=.*/use-ipv4=yes/; s/^#*use-ipv6=.*/use-ipv6=no/' "$rootfs/etc/avahi/avahi-daemon.conf"

cat >"$rootfs/etc/init.d/avf-mounts" <<'EOF'
#!/sbin/openrc-run
description="Load AVF virtio modules and mount virtiofs shares"
depend() { after modules; before net; }
start() {
	ebegin "Mounting AVF virtiofs shares"
	for m in virtio_net virtiofs vmw_vsock_virtio_transport; do modprobe "$m" 2>/dev/null || true; done
	mkdir -p /mnt/internal /mnt/shared
	# ownership only shows through if a mount fails and the bare dir is used
	chown 1000:1000 /mnt/internal /mnt/shared 2>/dev/null || true
	if ! mountpoint -q /mnt/internal && ! mount -t virtiofs internal /mnt/internal; then
		eend 1 "failed to mount /mnt/internal"
		return 1
	fi
	if ! mountpoint -q /mnt/shared && ! mount -t virtiofs android /mnt/shared; then
		ewarn "failed to mount /mnt/shared"
	fi
	eend 0
}
EOF
chmod +x "$rootfs/etc/init.d/avf-mounts"

cat >"$rootfs/etc/init.d/avf-clock" <<'EOF'
#!/sbin/openrc-run
description="Set the AVF guest clock from the Terminal app CA certificate"
depend() { need avf-mounts; before net; }
start() {
	ebegin "Setting clock from /mnt/internal/ca.crt"
	n=0
	while [ ! -e /mnt/internal/ca.crt ] && [ "$n" -lt 10 ]; do
		sleep 1
		n=$((n + 1))
	done
	if [ ! -e /mnt/internal/ca.crt ]; then
		eend 1 "/mnt/internal/ca.crt not found"
		return 1
	fi
	ts="$(date -u -r /mnt/internal/ca.crt '+%Y-%m-%d %H:%M:%S' 2>/dev/null)" || {
		eend 1 "could not read /mnt/internal/ca.crt mtime"
		return 1
	}
	date -u -s "$ts" >/dev/null || {
		eend 1 "could not set clock"
		return 1
	}
	eend 0
}
EOF
chmod +x "$rootfs/etc/init.d/avf-clock"

cat >"$rootfs/etc/init.d/avf-resize" <<'EOF'
#!/sbin/openrc-run
description="Grow the root filesystem to fill its block device"
depend() { after root; before localmount; }
start() {
	ebegin "Growing root filesystem to fill the disk"
	dev="$(awk '$2 == "/" { print $1; exit }' /proc/mounts)"
	[ -b "$dev" ] || { ewarn "root device not found"; return 0; }
	resize2fs "$dev"
	eend $?
}
EOF
chmod +x "$rootfs/etc/init.d/avf-resize"

chroot "$rootfs" /bin/sh <<'EOF'
set -e
for s in devfs dmesg mdev hwdrivers cgroups; do rc-update add "$s" sysinit || true; done
for s in modules sysctl hostname bootmisc syslog seedrng localmount networking; do rc-update add "$s" boot || true; done
for s in killprocs mount-ro savecache; do rc-update add "$s" shutdown || true; done
rc-update add avf-mounts boot
rc-update add avf-clock boot
rc-update add avf-resize boot
rc-update add dbus default
rc-update add avahi-daemon default
rc-update add ttyd default
EOF

echo "building image"
echo 'features="base virtio ext4"' >"$rootfs/etc/mkinitfs/mkinitfs.conf"
kver="$(basename "$(ls -d "$rootfs"/lib/modules/*-virt | head -1)")"
[ -n "$kver" ] || die "no -virt kernel modules under $rootfs/lib/modules"
chroot "$rootfs" mkinitfs -o /boot/initramfs-avf "$kver"
cp "$rootfs/boot/initramfs-avf" "$out/initrd.img"

# Alpine ships vmlinuz as an EFI zboot PE wrapping a gzip Image; crosvm needs the
# raw arm64 Image (magic "ARMd" / 41524d64 at offset 0x38).
vmlinuz="$rootfs/boot/vmlinuz-virt"
if [ "$(od -An -c -j4 -N4 "$vmlinuz" | tr -d ' ')" = "zimg" ]; then
	off="$(od -An -tu4 -j8 -N4 "$vmlinuz" | tr -d ' ')"
	sz="$(od -An -tu4 -j12 -N4 "$vmlinuz" | tr -d ' ')"
	dd if="$vmlinuz" bs=1M iflag=skip_bytes,count_bytes skip="$off" count="$sz" 2>/dev/null | gzip -dc >"$out/vmlinuz"
else
	cp "$vmlinuz" "$out/vmlinuz"
fi
[ "$(od -An -tx1 -j56 -N4 "$out/vmlinuz" | tr -d ' \n')" = "41524d64" ] \
	|| die "extracted kernel is not a raw arm64 Image"

cleanup
assert_unmounted
rm -f "$rootfs/etc/resolv.conf"
# ^orphan_file: Alpine's mke2fs enables it by default; the image must still
# pass the stock VM's e2fsck (the stage 2 gate in install-alpine-avf) and
# online resize2fs under crosvm, so keep the feature set conservative.
mkfs.ext4 -q -F -L ROOT -O ^orphan_file -d "$rootfs" "$out/root_part" "$size"

# keep the stock layout; "name" stays "debian" (anything else is untested
# against the Terminal app).
cat >"$out/vm_config.json" <<EOF
{
    "name": "debian",
    "disks": [ { "partitions": [
        { "label": "ROOT", "path": "\$PAYLOAD_DIR/root_part", "writable": true, "guid": "$(cat /proc/sys/kernel/random/uuid)" }
    ], "writable": true } ],
    "sharedPath": [ { "sharedPath": "/storage/emulated" }, { "sharedPath": "\$APP_DATA_DIR/files" } ],
    "kernel": "\$PAYLOAD_DIR/vmlinuz",
    "initrd": "\$PAYLOAD_DIR/initrd.img",
    "params": "root=/dev/vda1 rootfstype=ext4 rw console=ttyS0",
    "protected": false,
    "cpu_topology": "match_host",
    "platform_version": "~1.0",
    "memory_mib": 4096,
    "debuggable": true,
    "connect_console": true,
    "console_out": true,
    "console_input_device": "ttyS0",
    "network": true,
    "auto_memory_balloon": false,
    "gpu": { "backend": "2d" }
}
EOF

# The Terminal app parses the LAST space-separated token of build_id as a 4-digit
# year and reinstalls the stock Debian (InstalledImage.isOlderThanCurrentVersion)
# if it is below the app's release year. Keep the trailing year; do not drop it.
echo "alpine $(date -u +%Y%m%dT%H%M%SZ) $(date -u +%Y)" >"$out/build_id"
echo "done: $out"
