#!/bin/sh
# ps3-hdd1-boot DEFAULT [RELEASE...]: the internal install's boot files on GameOS's dev_hdd1 (FAT,
# /dev/ps3dc), the only internal partition Petitboot reads (docs/GAMEOS-HDD.md "The internal install").
# Run by ps3-update-kboot when /etc/default/ps3-boot says PS3_BOOT_TARGET=hdd1 (the initramfs's install
# sets it), with the default release first and every installed release, newest first.
#
# On dev_hdd1: /linux/boot/vmlinux-<release> for DEFAULT and the newest other release, and kboot.conf's
# two fixed entries, walnut (DEFAULT) and walnut-prev (the other one; Petitboot keys its default on the
# label, so a kernel update can't strand it on a label that is gone). The generic initramfs comes from
# /usr/lib/ps3/boot/initrd-walnut.img when the package ships one; the one it replaces stays as
# initrd-walnut-prev.img for walnut-prev. Other kboot.conf lines (another install's) are kept, older
# vmlinux-* files of ours removed. Nothing else on dev_hdd1 is touched, and ps3da/ps3db never.
#
# dev_hdd1 is written only after blkid finds a vfat file system on it; it is mounted rw for these
# copies only and made read-only again (the initramfs leaves it read-only). Every file goes through
# a .tmp copy and a rename, kboot.conf last, so an interrupted run leaves the old menu working.
# GameOS's data lives on the same partition, so nothing is copied before the free space is checked
# against what the run adds; when it is short, only kernels the current menu doesn't use are freed
# first, and if that is not enough the run stops without writing a menu or a new file (kernels no menu uses may be gone). Our *.tmp files (also
# those of an interrupted run) are removed at the start and on every exit.
# PS3_BOOT_ROOT: a prefix for /boot and /usr (host tests); PS3_HDD1_DIR: a directory standing in for
# the mounted dev_hdd1 (host tests: no blkid, mount or blockdev); PS3_HDD1_FREE_KB with it: the free
# space to assume (host tests).
set -e
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
R=${PS3_BOOT_ROOT:-}
DEV=${PS3_HDD1_DEV:-/dev/ps3dc}
die() { echo "ps3-hdd1-boot: $*" >&2; exit 1; }
[ $# -ge 1 ] || die "usage: ps3-hdd1-boot DEFAULT [RELEASE...]"
[ -r "$R/etc/default/ps3-boot" ] && . "$R/etc/default/ps3-boot"
case "${PS3_ROOT_ARGS:-}" in *ps3ssd.image=/*) ;; *) die "PS3_ROOT_ARGS has no ps3ssd.image=: not an internal install" ;; esac
# the generic initramfs carries no modules: only kernels with ufs and dm built in can boot through it
fits() { grep -qx CONFIG_UFS_FS=y "$R/boot/config-$1" 2>/dev/null && grep -qx CONFIG_BLK_DEV_DM=y "$R/boot/config-$1"; }
cur=$1
shift
[ -e "$R/boot/vmlinux-$cur" ] || die "no /boot/vmlinux-$cur"
if ! fits "$cur"; then
	n=
	for r in "$@"; do fits "$r" && { n=$r; break; }; done
	[ -n "$n" ] || die "no installed kernel has ufs and dm built in (the generic initramfs needs them): dev_hdd1 left as it is"
	echo "ps3-hdd1-boot: $cur lacks built-in ufs/dm, walnut boots $n"
	cur=$n
fi
# walnut-prev: the newest other kernel of the same flavour (release or debug), so it is the previous
# version to go back to; another flavour only when there is none
flavour() { case $1 in *-debug*) echo debug ;; *) echo release ;; esac; }
prev=
for r in "$@"; do [ "$r" != "$cur" ] && fits "$r" && [ "$(flavour "$r")" = "$(flavour "$cur")" ] && { prev=$r; break; }; done
[ -n "$prev" ] || for r in "$@"; do [ "$r" != "$cur" ] && fits "$r" && { prev=$r; break; }; done

cleanup_tmp() { rm -f "$M"/linux/boot/*.tmp "$M/kboot.conf.new" 2>/dev/null || true; }
if [ -n "${PS3_HDD1_DIR:-}" ]; then
	M=$PS3_HDD1_DIR
	trap cleanup_tmp EXIT
else
	[ -b "$DEV" ] || die "no $DEV (dev_hdd1)"
	t=$(blkid -o value -s TYPE "$DEV" 2>/dev/null || true)
	[ "$t" = vfat ] || die "$DEV is \"$t\", not GameOS's FAT dev_hdd1: nothing written"
	grep -q "^$DEV " /proc/mounts && die "$DEV is mounted: unmount it first"
	M=$(mktemp -d /run/ps3-hdd1.XXXXXX)
	ro=$(blockdev --getro "$DEV")
	trap 'cleanup_tmp; umount "$M" 2>/dev/null; rmdir "$M"; [ "$ro" = 1 ] && blockdev --setro "$DEV"' EXIT
	blockdev --setrw "$DEV"
	mount -t vfat -o rw,noatime,nodev,nosuid,noexec "$DEV" "$M"
fi
cleanup_tmp

# free space: what this run adds (bytes) against what the partition has, plus 4 MiB for FAT's clusters
size() { wc -c < "$1" 2>/dev/null || echo 0; }
free_kb() {
	if [ -n "${PS3_HDD1_DIR:-}" ] && [ -n "${PS3_HDD1_FREE_KB:-}" ]; then echo "$PS3_HDD1_FREE_KB"
	else df -Pk "$M" | awk 'NR==2 {print $4}'; fi
}
# kernels the current menu names: ours (the walnut and walnut-prev lines) and every other line's (another install's,
# any label, single or double quotes); the other lines survive this run, so their kernels are never removed
kernels_of() { sed -n "s/^$1=[\"']\{0,1\}\/linux\/boot\/vmlinux-\([^ \"']*\).*/\1/p" "$M/kboot.conf" 2>/dev/null || true; }
ours=$(kernels_of walnut; kernels_of walnut-prev)
ours=$(echo $ours)
foreign=$(sed -n "/^walnut\(-prev\)\{0,1\}=/d; s/^[^=#]*=[\"']\{0,1\}\/linux\/boot\/vmlinux-\([^ \"']*\).*/\1/p" "$M/kboot.conf" 2>/dev/null | tr '\n' ' ' || true)
needed=0
for r in $cur $prev; do
	cmp -s "$R/boot/vmlinux-$r" "$M/linux/boot/vmlinux-$r" 2>/dev/null || needed=$((needed + $(size "$R/boot/vmlinux-$r")))
done
new=$R/usr/lib/ps3/boot/initrd-walnut.img
if [ -e "$new" ] && ! cmp -s "$new" "$M/linux/boot/initrd-walnut.img" 2>/dev/null; then
	needed=$((needed + $(size "$new")))
	[ -e "$M/linux/boot/initrd-walnut.img" ] && needed=$((needed + $(size "$M/linux/boot/initrd-walnut.img")))
fi
short() { [ $(( $(free_kb) * 1024 )) -lt $((needed + 4194304)) ]; }
if short; then
	# free the kernels the current menu doesn't use (the old menu stays bootable)
	used="$ours $foreign"
	for f in "$M"/linux/boot/vmlinux-*; do
		[ -e "$f" ] || continue
		v=${f##*/vmlinux-}
		case " $used $cur $prev " in *" $v "*) continue ;; esac
		short || break
		echo "ps3-hdd1-boot: dev_hdd1 is short of space: removing the unused $v"
		rm -f "$f"
		sync
	done
fi
short && die "dev_hdd1 has $(free_kb) kB free, this needs $((needed / 1024 + 4096)) kB: nothing written"

put() {	# put SRC DEST: through DEST.tmp, then a rename
	cp "$1" "$M$2.tmp"
	sync
	mv -f "$M$2.tmp" "$M$2"
}
# the display setting (the same values as in ps3-update-kboot): PS3_UNDERSCAN=1 keeps the TV-safe
# inset, after a kernel's own arguments so it wins over them
case $(printf '%s' "${PS3_UNDERSCAN:-0}" | tr '[:upper:]' '[:lower:]') in
	1|yes|true|on) display_args=ps3fb.fullscreen=0 ;;
	0|no|false|off) display_args= ;;
	*) echo "ps3-hdd1-boot: PS3_UNDERSCAN=$PS3_UNDERSCAN is not 0 or 1: the full frame" >&2; display_args= ;;
esac
args_of() {
	if [ -e "$R/usr/lib/ps3/boot/$1.args" ]; then a=$(cat "$R/usr/lib/ps3/boot/$1.args"); else a=${PS3_LEGACY_ARGS:-}; fi
	echo "$a $display_args"
}
mkdir -p "$M/linux/boot"
for r in $cur $prev; do
	cmp -s "$R/boot/vmlinux-$r" "$M/linux/boot/vmlinux-$r" 2>/dev/null || put "$R/boot/vmlinux-$r" "/linux/boot/vmlinux-$r"
done
I=/linux/boot/initrd-walnut.img
P=/linux/boot/initrd-walnut-prev.img
if [ -e "$new" ] && ! cmp -s "$new" "$M$I" 2>/dev/null; then
	[ -e "$M$I" ] && put "$M$I" "$P"
	put "$new" "$I"
fi
[ -e "$M$I" ] || die "no initramfs: $I on dev_hdd1 and no $new"
pinit=$I
[ -e "$M$P" ] && pinit=$P

# one space between the arguments: Petitboot passes an empty one on as "(null)"
squeeze() { printf '%s' "$*" | tr -s ' ' | sed 's/^ //; s/ $//'; }
{
	echo "# Petitboot's menu on dev_hdd1: the internal install's entries, written by ps3-hdd1-boot"
	echo "walnut='$(squeeze "/linux/boot/vmlinux-$cur initrd=$I $PS3_ROOT_ARGS ${PS3_EXTRA_ARGS:-} $(args_of "$cur")")'"
	[ -n "$prev" ] && echo "walnut-prev='$(squeeze "/linux/boot/vmlinux-$prev initrd=$pinit $PS3_ROOT_ARGS ${PS3_EXTRA_ARGS:-} $(args_of "$prev")")'"
	[ -e "$M/kboot.conf" ] && grep -v -e '^walnut=' -e '^walnut-prev=' \
		-e '^# Petitboot.s menu on dev_hdd1: the internal install' "$M/kboot.conf" || true
} > "$M/kboot.conf.new"
sync
mv -f "$M/kboot.conf.new" "$M/kboot.conf"
for f in "$M"/linux/boot/vmlinux-*; do
	[ -e "$f" ] || continue
	case " $cur $prev $foreign " in *" ${f##*/vmlinux-} "*) ;; *) rm -f "$f" ;; esac
done
sync
echo "ps3-hdd1-boot: dev_hdd1: walnut $cur${prev:+, walnut-prev $prev}"
