#!/bin/sh
# Build the PsionLX CF card image from workspace/card/root.
#
# Produces card/psionlx-cf-1gb.img, exactly 1,024,966,656 bytes: an MBR, a
# 70 MB FAT16 boot partition holding the bootloader's nBkProOs.img, and a
# 906.5 MB ext2 root.  Nothing here needs root -- the device nodes and the
# file ownership are written into the filesystem with debugfs afterwards,
# which is why the tree can live on a macOS host that has neither.
#
# Usage:  scripts/mkimage.sh [--no-chown-regen]
set -e

# Which card tree to build. The default is workspace/card, the build we
# actually use. PSION_CARD_DIR points it at another tree -- card-archival is
# the cut-down one that carries only the applications visible in Psion's
# photographs.
CARD_DIR="${PSION_CARD_DIR:-card}"
W="$(cd "$(dirname "$0")/../$CARD_DIR" && pwd)"
E=/opt/homebrew/opt/e2fsprogs/sbin
MKE="$E/mke2fs"; DBG="$E/debugfs"; FSCK="$E/e2fsck"
PY=/usr/local/bin/python3

P1_START=2048        # LBA of the FAT16 boot partition
P2_BLOCKS=928240     # 1 KiB ext2 blocks == 1856480 sectors == 906.5 MB
EXPECT=1024966656
# Pinned so two builds of the same tree compare equal; mke2fs would
# otherwise generate a random UUID every run.  Nothing looks the
# filesystem up by UUID -- fstab uses /dev/hda2.
FS_UUID=5053494f-4e4c-5800-0000-6e6574626b70

cd "$W"

# ---------------------------------------------------------------- ownership
# chown.debugfs is derived from the host tree, which carries no target uids.
# Regenerate it so /home/lx ends up owned by lx (uid 1000): the entire
# desktop runs as that user and must be able to create ~/.matchbox and
# ~/.gpe.  Getting this wrong looks like three unrelated bugs at once.
if [ "$1" != "--no-chown-regen" ]; then
  "$PY" - "$W/root" "$W/chown.debugfs" <<'PY'
import os, sys
root, out = sys.argv[1], sys.argv[2]
OWN = {'home/lx': (1000, 1000), 'home/gpe': (100, 65535)}
def own_for(rel):
    for pre, ug in OWN.items():
        if rel == pre or rel.startswith(pre + '/'):
            return ug
    return (0, 0)
# Setuid/setgid bits, taken from Psion's own rootfs. macOS drops them when a
# non-root user copies a file, and mke2fs -d takes the mode from the host
# tree, so every one of them was being lost: su could not change user at all
# ("su: cannot set groups: Operation not permitted"), which took out gpe-su
# and everything behind it -- WLAN config, the card ejector, gpe-conf's admin
# pages -- plus Bluetooth (hciconfig/hciattach) and apm --suspend.
SETUID = {
    'bin/tinylogin':        0o4755,
    'sbin/hciconfig':       0o4755,
    'sbin/hciattach':       0o4755,
    'usr/bin/apm':          0o4577,
    'usr/bin/gpe-shield':   0o4755,
    'usr/bin/gpe-package':  0o4755,
    'usr/bin/irsw':         0o4755,
    'usr/bin/prismstumbler':0o4755,
    'usr/bin/gpe-conf':     0o4755,
}
lines, seen = [], set()
for dirpath, dirnames, filenames in os.walk(root):
    dirnames.sort(); filenames.sort()
    for name in dirnames + filenames:
        rel = os.path.relpath(os.path.join(dirpath, name), root)
        u, g = own_for(rel)
        for d in (f'sif "/{rel}" uid {u}', f'sif "/{rel}" gid {g}'):
            if d not in seen:
                seen.add(d); lines.append(d)
        if rel in SETUID and os.path.isfile(os.path.join(root, rel)):
            d = f'sif "/{rel}" mode 0{SETUID[rel] | 0o100000:o}'
            if d not in seen:
                seen.add(d); lines.append(d)
open(out, 'w').write('\n'.join(lines) + '\n')
print(f'  chown.debugfs: {len(lines)} directives')
PY
fi

# ---------------------------------------------------------------- root fs
echo "  building rootfs.ext2 ..."
rm -f rootfs.ext2
"$MKE" -q -t ext2 -b 1024 -d root -F -L PSIONLX \
       -O ^resize_inode,^dir_index -U "$FS_UUID" rootfs.ext2 "$P2_BLOCKS"
"$DBG" -w -f mknods.debugfs rootfs.ext2 >/dev/null 2>&1   # /dev nodes
"$DBG" -w -f chown.debugfs  rootfs.ext2 >/dev/null 2>&1 || true
"$FSCK" -fn rootfs.ext2 2>&1 | tail -1 | sed 's/^/  /'

# ---------------------------------------------------------------- assemble
"$PY" - "$P1_START" "$EXPECT" <<'PY'
import os, struct, sys
SEC = 512; H, S = 16, 32            # the CHS geometry the bootloader expects
p1_start, expect = int(sys.argv[1]), int(sys.argv[2])
boot, root, out = "boot.fat", "rootfs.ext2", "psionlx-cf-1gb.img"
n1 = os.path.getsize(boot) // SEC
p2_start = p1_start + n1
n2 = os.path.getsize(root) // SEC

def chs(lba):
    c = lba // (H * S); h = (lba // S) % H; s = lba % S + 1
    if c > 1023: c, h, s = 1023, H - 1, S
    return bytes([h, ((c >> 2) & 0xC0) | (s & 0x3F), c & 0xFF])

def entry(bootable, ptype, start, count):
    return (bytes([0x80 if bootable else 0]) + chs(start) + bytes([ptype])
            + chs(start + count - 1) + struct.pack("<II", start, count))

mbr = bytearray(512)
mbr[446:462] = entry(True,  0x06, p1_start, n1)     # FAT16
mbr[462:478] = entry(False, 0x83, p2_start, n2)     # Linux
mbr[510:512] = b"\x55\xAA"

with open(out, "wb") as f:
    f.write(mbr)
    for path, start in ((boot, p1_start), (root, p2_start)):
        f.seek(start * SEC)
        with open(path, "rb") as g:
            while (chunk := g.read(1 << 20)):
                f.write(chunk)

size = os.path.getsize(out)
print(f"  image: {size:,} bytes", "OK" if size == expect else f"EXPECTED {expect:,}")
PY

# ---------------------------------------------------------------- verify
echo "  verifying:"
printf '    /home/lx ownership : %s\n' \
  "$("$DBG" -R 'stat /home/lx' rootfs.ext2 2>/dev/null | grep -m1 -o 'User: *[0-9]* *Group: *[0-9]*')"
for f in /etc/matchbox/session /etc/gpe/psion.mbdock /etc/gpe/buttonbox.conf \
         /etc/X11/Xinit.d/99psion-session /usr/share/gpe/splash.png; do
  s=$("$DBG" -R "stat $f" rootfs.ext2 2>/dev/null | grep -m1 -o 'Size: [0-9]*' | awk '{print $2}')
  printf '    %-42s %s\n' "$f" "${s:-MISSING}"
done

# The kit holds more than one image: the build we use, and the cut-down
# archival one. Name the copy accordingly and re-checksum everything, rather
# than overwriting the other image's entry.
KIT_NAME="${PSION_IMAGE_NAME:-psionlx-cf-1gb.img}"
# Only when the kit directory is actually there. Building from the published
# release there is no PsionLX-CF-kit, and the image in card/ is the product --
# this step used to fail the run at the very end after a successful build.
if [ -d ../../PsionLX-CF-kit ]; then
  cp psionlx-cf-1gb.img "../../PsionLX-CF-kit/$KIT_NAME"
  ( cd ../../PsionLX-CF-kit && shasum -a 256 *.img > SHA256SUMS.txt )
  echo "  copied to PsionLX-CF-kit/$KIT_NAME and checksummed"
else
  echo "  image is at $(pwd)/psionlx-cf-1gb.img"
  shasum -a 256 psionlx-cf-1gb.img
fi
