#!/bin/bash
# Grows the root partition + filesystem to fill the disk on first boot.
# Self-disables after success.
#
# Uses sfdisk + partx + resize2fs (all in util-linux + e2fsprogs, both
# present in the minimal Arch/SteamOS base). Earlier versions called
# growpart from cloud-utils — but that package isn't in the SteamOS
# recovery image, so the unit failed with "growpart: command not found"
# and rootfs stayed at the dd'd image size (~14 GB) on any disk larger
# than that.
set -e

ROOT_DEV=$(findmnt -no SOURCE /)
DISK="/dev/$(lsblk -ndo PKNAME "$ROOT_DEV")"

# PS5 M.2 drive appear as /dev/nvme*. Do not modify partition table,
# because it contains m2_init PS5 offset. Only USB drive (/dev/sd*)
# are safe for automatic grow root partition.
if [[ "$(basename "$DISK")" == nvme* ]]; then
    echo "grow-rootfs: M.2 detected, skipping"
    systemctl disable grow-rootfs.service
    exit 0
fi

PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)

# Our images dd at their built size (~14 GB) onto a much larger USB. The GPT
# backup header is therefore at byte ~built-size, not at end of disk. Move
# the backup header to end-of-disk first so partition table growth works.
sgdisk -e "$DISK"

# If a previous flash left behind partitions BEYOND our rootfs + boot pair
# (e.g. a stale `linux-home` partset at part 3 from a prior SteamOS install
# on the same drive), the partition extend refuses to grow into the space
# already claimed by the leftover partition. Delete every partition with a
# number higher than 2 — we only ever create partitions 1 (rootfs) + 2 (boot).
# Important: sgdisk only rewrites the on-disk GPT; we must ALSO drop those
# stale entries from the kernel's in-memory partition table via partx -d,
# or BLKPG_RESIZE_PARTITION below returns EBUSY because the kernel still
# thinks sda1 would have to grow into space owned by phantom sda3.
for n in $(parted -ms "$DISK" print 2>/dev/null \
           | awk -F: 'NR>2 && $1+0>2 {print $1}'); do
    sgdisk -d "$n" "$DISK" 2>/dev/null || true
    partx -d --nr "$n" "$DISK" 2>/dev/null || true
done

# sfdisk `,+` extends partition $PART_NUM to consume all available free
# space. Works on a mounted/live partition because sfdisk only rewrites
# the GPT entry (kernel re-read of the partition table comes next).
echo ",+" | sfdisk --no-reread -N "$PART_NUM" "$DISK"

# Make the kernel pick up the new partition size without unmounting via
# BLKPG_RESIZE_PARTITION. partx -u also uses BLKPG, but on some images
# (notably SteamOS recovery loop-mounted btrfs) partx returns an error
# updating the mounted partition; the direct ioctl is more reliable.
python3 - "$DISK" "$PART_NUM" <<'PY'
import sys, os, fcntl, ctypes, subprocess
disk, pno = sys.argv[1], int(sys.argv[2])
out = subprocess.run(["sfdisk","--bytes","-q","-l",disk],
                     capture_output=True, text=True, check=True).stdout
start = sectors = None
for line in out.splitlines():
    if line.startswith(f"{disk}{pno} ") or line.startswith(f"{disk}p{pno} "):
        f = line.split()
        start, sectors = int(f[1]), int(f[3])
        break
if start is None:
    sys.exit(f"could not parse partition {pno} from sfdisk -l {disk}")
class P(ctypes.Structure):
    _fields_=[("start",ctypes.c_longlong),("length",ctypes.c_longlong),
              ("pno",ctypes.c_int),("devname",ctypes.c_char*64),
              ("volname",ctypes.c_char*64)]
class A(ctypes.Structure):
    _fields_=[("op",ctypes.c_int),("flags",ctypes.c_int),
              ("datalen",ctypes.c_int),("data",ctypes.c_void_p)]
p = P(start=start*512, length=sectors*512, pno=pno)
a = A(op=3, flags=0, datalen=ctypes.sizeof(p), data=ctypes.addressof(p))
fd = os.open(disk, os.O_RDONLY)
try:
    fcntl.ioctl(fd, 0x1269, a)   # BLKPG, BLKPG_RESIZE_PARTITION
finally:
    os.close(fd)
PY

# Belt-and-suspenders: also try partx in case BLKPG via python failed.
partx -u --nr "$PART_NUM" "$DISK" 2>/dev/null || true

# Online ext4 grow.
resize2fs "$ROOT_DEV"

systemctl disable grow-rootfs.service
