#!/bin/sh
# Runs INSIDE the rootfs builder (guest/rootfs/Dockerfile); started by scripts/build-guest.sh,
# which unpacks into /in:
#   rootfs/{apk.lock,busybox-applets.txt,hot-files.txt,etc/}  home/  apk/*.apk (checked)
#   agent/xvm-agent
#   MODE=root:    jdk/jdk.tar (the jlinked JDK + CDS archives and the compile server made by the
#                 builder guest)
#   MODE=builder: jdkbuild/ (guest/jdk: the in-guest build script, its inputs, the training
#                 sets train/ and train-broken/, guest/compiled as compiled/)
# Writes a tar of the outputs on stdout; everything else goes to stderr.
# Env: MODE (root | builder), SOURCE_DATE_EPOCH, ROOTFS_UUID, HOME_UUID, HOME_HASH_SEED,
#      HOME_SIZE_MIB, SCRATCH_SIZE_MIB (builder).
#
# MODE=root (RFC 005 §4.1–§4.3): the shipped root. busybox, the agent, the "world" packages of
#   apk.lock (musl, zlib: the jlinked JDK's runtime closure), the JDK under /usr/lib/jvm/xvm,
#   the compile server under /usr/lib/xvm (G4), /usr/bin/java -> the JDK's launcher,
#   /usr/bin/javac -> the agent binary (the §5.4 shim, by argv[0]), /etc; ordered tar -> EROFS;
#   home.ext4. Outputs rootfs.erofs, rootfs.tar, files.txt, extents.txt, erofs-super.txt,
#   home.ext4, home-super.txt, home-files.txt, tools.txt.
# MODE=builder (§4.2): the builder guest's root. The same userland plus every "build" package of
#   apk.lock (the full JDK with jmods, binutils for strip), guest/jdk as /usr/lib/xvm-build (with
#   source-date-epoch), empty /usr/lib/jvm/xvm and /usr/lib/xvm (the bind-mount targets, so the
#   archives are dumped at the final paths); EROFS straight from the tree. Plus scratch.ext4,
#   the writable disk the build writes to. Outputs builder.erofs, builder-files.txt, scratch.ext4.
set -eu
exec 3>&1 1>&2
MODE=${MODE:-root}
: "${SOURCE_DATE_EPOCH:?}" "${ROOTFS_UUID:?}" "${HOME_UUID:?}" "${HOME_HASH_SEED:?}" "${HOME_SIZE_MIB:?}"
R=/root-tree
O=/out
mkdir -p "$R" "$O" /x

# 1. Packages: unpack each locked .apk of the set(s) (v2: gzip'd tar segments); keep the payload.
case $MODE in
  root) SETS=root ;;
  builder) SETS="root build" ;;
  *) echo "MODE=$MODE?"; exit 1 ;;
esac
for s in $SETS; do
  awk -v s="$s" '!/^#/ && NF && $6 == s { print $1 "-" $2 ".apk" }' /in/rootfs/apk.lock
done > /tmp/apks.txt
while read -r apk; do
  mkdir -p "/x/$apk"
  tar -xzf "/in/apk/$apk" -C "/x/$apk" 2>/dev/null || tar -xzf "/in/apk/$apk" -C "/x/$apk"
  rm -f "/x/$apk"/.PKGINFO "/x/$apk"/.SIGN.* "/x/$apk"/.pre-* "/x/$apk"/.post-* "/x/$apk"/.trigger
done < /tmp/apks.txt

# 2. Skeleton. / is read-only EROFS: every mount point the agent uses must exist (§3.1).
cd "$R"
for d in bin sbin usr/bin usr/sbin usr/lib etc proc sys dev run tmp root home/student var mnt; do
  mkdir -p "$d"
done
chmod 0755 . bin sbin usr usr/bin usr/sbin usr/lib etc proc sys dev run var mnt home home/student
chmod 0700 root
chmod 1777 tmp
ln -s ../tmp var/tmp
ln -s ../run var/run

# 3. busybox (static) and its applet links from the committed list (no install scripts).
install -m 0755 /x/busybox-static-*/bin/busybox.static bin/busybox
grep -v '^#' /in/rootfs/busybox-applets.txt | while read -r p; do
  [ -n "$p" ] || continue
  p=${p#/}
  [ "$p" = bin/busybox ] && continue
  case $p in
    bin/*) ln -s busybox "$p" ;;
    sbin/*) ln -s ../bin/busybox "$p" ;;
    usr/bin/* | usr/sbin/*) ln -s ../../bin/busybox "$p" ;;
    *) echo "unexpected applet path $p"; exit 1 ;;
  esac
done

# 4. Every other package's payload, as apk would place it (scripts are not run). Files of a
#    package replace busybox applet links of the same name (binutils' strings, ar, ... in the
#    builder).
while read -r apk; do
  case $apk in busybox-static-*) continue ;; esac
  ( cd "/x/$apk" && find . ! -type d ) | while read -r f; do rm -f "$R/${f#./}"; done
  tar -C "/x/$apk" -cf - . | tar -C "$R" -xf -
done < /tmp/apks.txt

# 5. The agent is PID 1 (§3.1): /sbin/init -> xvm-agent. busybox's own init is not linked.
install -m 0755 /in/agent/xvm-agent sbin/xvm-agent
rm -f sbin/init
ln -s xvm-agent sbin/init

# 6. /etc
cp -R /in/rootfs/etc/. etc/
chmod 0644 etc/*

if [ "$MODE" = builder ]; then
  # 7b. The in-guest build (guest/jdk) and the bind-mount target at the final JDK path.
  mkdir -p usr/lib/xvm-build usr/lib/jvm/xvm usr/lib/xvm
  cp -R /in/jdkbuild/. usr/lib/xvm-build/
  echo "$SOURCE_DATE_EPOCH" > usr/lib/xvm-build/source-date-epoch
  chmod -R a+rX usr/lib/xvm-build
  chmod 0755 usr/lib/xvm-build/*.sh
  chown -R 0:0 .
  find . | LC_ALL=C sort > "$O/builder-files.txt"
  mkfs.erofs -b4096 -T"$SOURCE_DATE_EPOCH" -U "$ROOTFS_UUID" -x-1 --all-root "$O/builder.erofs" "$R"
  E2FSPROGS_FAKE_TIME=$SOURCE_DATE_EPOCH mkfs.ext4 -q -F -b 4096 -L scratch -U "$HOME_UUID" \
      -E "hash_seed=$HOME_HASH_SEED,lazy_itable_init=0,lazy_journal_init=0,root_owner=1000:1000,nodiscard" \
      "$O/scratch.ext4" "${SCRATCH_SIZE_MIB:-512}M"
  cp /packages.txt "$O/tools.txt"
  tar -cSf - -C "$O" . >&3
  exit 0
fi

# 7. The JDK (§4.2): the tree the builder guest produced, at its final path. /usr/bin/java is the
#    real launcher (a symlink; the launcher resolves it through /proc/self/exe, so JAVA_HOME and
#    the LD_LIBRARY_PATH prefix it checks are /usr/lib/jvm/xvm); /usr/bin/javac is the agent
#    binary, which acts as §5.4's shim when its argv[0] is javac. /usr/lib/xvm: the compile
#    server (jar, launcher argfile, train.sh), made in the builder guest; it maps javac.jsa.
mkdir -p usr/lib/jvm usr/lib/xvm
tar -xf /in/jdk/jdk.tar -C usr/lib/jvm xvm
tar -xf /in/jdk/jdk.tar -C /x xvmlib
cp -R /x/xvmlib/. usr/lib/xvm/
ln -s ../lib/jvm/xvm/bin/java usr/bin/java
ln -s ../../sbin/xvm-agent usr/bin/javac
chown -R 0:0 .

# 8. Ordered tar (§4.5), then EROFS (§4.3): uncompressed, 4 KiB blocks, fixed time and UUID, no
#    xattrs, every inode root-owned. Member order: every directory (name order), then the files
#    of hot-files.txt in first-touch order, then everything else in name order. mkfs.erofs
#    places data in path order unless told otherwise; `--sort=none` keeps the tar stream's
#    order, and only with `-Enoinline_data` (tail packing puts file tails in the metadata area
#    and the data back in path order; erofs-utils 1.9.1, E11). So the boot's working set packs
#    into the first chunks, every file starts block-aligned (§4.3: page-cache pages equal image
#    blocks), at ≤ 4 KiB of zeros per file. extents.txt shows where each file landed.
find . -type d | LC_ALL=C sort > /tmp/order.txt
grep -v '^#' /in/rootfs/hot-files.txt | sed -n 's|^/|./|p' | while read -r f; do
  if [ -e "$f" ] || [ -L "$f" ]; then [ -d "$f" ] || echo "$f"; else echo "hot-files.txt: $f not in the tree" >&2; fi
done | awk '!seen[$0]++' > /tmp/hot.txt
find . ! -type d | LC_ALL=C sort | grep -vxF -f /tmp/hot.txt > /tmp/rest.txt || true
cat /tmp/hot.txt /tmp/rest.txt >> /tmp/order.txt
tar --no-recursion --numeric-owner --owner=0 --group=0 --mtime="@$SOURCE_DATE_EPOCH" \
    --format=posix --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \
    -cf "$O/rootfs.tar" -T /tmp/order.txt
mkfs.erofs -b4096 -T"$SOURCE_DATE_EPOCH" -U "$ROOTFS_UUID" -x-1 --all-root \
    --sort=none -Enoinline_data --tar=f "$O/rootfs.erofs" "$O/rootfs.tar"
tar -tvf "$O/rootfs.tar" --numeric-owner --utc | awk '{ $4=""; $5=""; print }' | sed 's/  */ /g' > "$O/files.txt"
dump.erofs -s "$O/rootfs.erofs" > "$O/erofs-super.txt" 2>&1 || true
# extents.txt: "<physical byte offset> <length> <path>" for every extent of every regular file
# (scripts/build-guest.sh trace maps block reads to files with it).
find . -type f | LC_ALL=C sort | sed 's|^\.||' | while read -r f; do
  dump.erofs -e --path="$f" "$O/rootfs.erofs" 2>/dev/null \
    | awk -v f="$f" '/^ *[0-9]+: .*\|.*:.*\|/ { split($0, a, ":"); split(a[3], b, "|"); split(b[1], c, /[.][.]/); gsub(/ /, "", c[1]); gsub(/ /, "", b[2]); print c[1], b[2], f }'
done | sort -n > "$O/extents.txt"

# 9. /dev/vdb: an empty ext4 for /home/student (§3.2, D50: 64 MiB), no partition table, eager
#    init so no ext4lazyinit runs in the guest, fixed UUID / hash seed / time, root dir owned
#    by 1000. The skeleton (guest/home/; `dot-x` there becomes `.x`) is written with debugfs and
#    every timestamp pinned: mke2fs -d would copy the builder's ctimes, which are not
#    reproducible.
E2FSPROGS_FAKE_TIME=$SOURCE_DATE_EPOCH mkfs.ext4 -q -F -b 4096 -L home -U "$HOME_UUID" \
    -E "hash_seed=$HOME_HASH_SEED,lazy_itable_init=0,lazy_journal_init=0,root_owner=1000:1000,nodiscard" \
    "$O/home.ext4" "${HOME_SIZE_MIB}M"
T=$SOURCE_DATE_EPOCH
{
  ( cd /in/home && find . -mindepth 1 | LC_ALL=C sort ) | while read -r src; do
    src=${src#./}
    p=$(printf '%s' "$src" | sed 's|^dot-|.|; s|/dot-|/.|g')
    if [ -d "/in/home/$src" ]; then echo "mkdir $p"; m=040755; else echo "write /in/home/$src $p"; m=0100644; fi
    [ -x "/in/home/$src" ] && [ ! -d "/in/home/$src" ] && m=0100755
    printf 'sif %s uid 1000\nsif %s gid 1000\nsif %s mode %s\n' "$p" "$p" "$p" "$m"
    for t in atime mtime ctime crtime; do echo "sif $p $t @$T"; done
  done
  for d in / lost+found; do for t in atime mtime ctime; do echo "sif $d $t @$T"; done; done
} > /tmp/home.debugfs
E2FSPROGS_FAKE_TIME=$T debugfs -w -f /tmp/home.debugfs "$O/home.ext4" > /tmp/debugfs.log 2>&1
if grep -i 'error\|not found\|invalid' /tmp/debugfs.log; then exit 1; fi
dumpe2fs -h "$O/home.ext4" > "$O/home-super.txt" 2>/dev/null || true
debugfs -R "ls -l /" "$O/home.ext4" > "$O/home-files.txt" 2>/dev/null || true

cp /packages.txt "$O/tools.txt"
tar -cSf - -C "$O" . >&3
