#!/bin/sh
# Runs INSIDE the rootfs builder (guest/base/rootfs/Dockerfile); started by the build driver
# (scripts/xvm/lib/guest-build.mjs, RFC 018), which unpacks into /in:
#   rootfs/{apk.lock,busybox-applets.txt,etc/}  (guest/base/rootfs: the root set every image shares)
#   home/                                        (guest/base/home)
#   MODE=home:    rootfs/mkroot.sh and home/ only
#   image/{apk.lock,env,profile,hot-files.txt,...}  (guest/images/<image>: the image's root files)
#   apk/*.apk (checked): the root set's and the image's (the builder: also its "build" set)
#   agent/xvm-agent
#   MODE=java:    jdk/jdk.tar (the jlinked JDK + CDS archives and the compile server made by the
#                 builder guest)
#   MODE=builder: jdkbuild/ (guest/images/java/jdk: the in-guest build script, its inputs, the
#                 training sets train/ and train-broken/, guest/images/java/compiled as compiled/);
#                 image/ is the Java image's
#   MODE=python:  image/drop.txt (E56's subset) beside the Python image's other root files
# Writes a tar of the outputs on stdout; everything else goes to stderr.
# Env: MODE (java | python: lib/images.mjs's mkroot; builder: Java's jdk step; home: the base step
#      `guest home`), SOURCE_DATE_EPOCH, ROOTFS_UUID, HOME_UUID, HOME_HASH_SEED, HOME_SIZE_MIB,
#      SCRATCH_SIZE_MIB (builder), IMAGE_ID and IMAGE_VERSION (an image's root: the marker of
#      RFC 018 §6; a builder root and the home template have none).
#
# Every image's root: busybox, the agent, the root set of rootfs/apk.lock (musl, zlib), /etc
#   (rootfs/etc), the image's env as /etc/xvm/env (RFC 017 R8: the agent reads it) and profile as
#   /etc/profile, and two lines appended to /etc/os-release (RFC 018 §6, os-release(5)):
#   IMAGE_ID=<image> and IMAGE_VERSION=<version>, so `grep IMAGE /etc/os-release` names it.
# MODE=java (RFC 005 §4.1–§4.3): the Java image's root. 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.
#   Outputs rootfs.erofs, rootfs.tar, files.txt, extents.txt, erofs-super.txt, tools.txt.
# MODE=builder (§4.2): the builder guest's root. The same userland plus every "build" package of
#   image/apk.lock (the full JDK with jmods, binutils for strip), jdkbuild/ 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.
# MODE=python (RFC 017 §8.3 R5): the Python image's root. The same userland without the JDK, plus
#   image/apk.lock's packages trimmed by image/drop.txt (R2: E56's subset; "pkg" not installed,
#   "keep" re-added from a dropped package, "path" removed with its __pycache__ bytecode), ordered
#   by its hot-files.txt.
#   Outputs rootfs.erofs, rootfs.tar, files.txt, extents.txt, erofs-super.txt, dropped-files.txt,
#   tools.txt.
# MODE=home (RFC 018 I3): the home template every image boots with (mkhome below), a base step
#   like the kernel and the agent; every image's manifest names the same file, so the site's chunk
#   store holds it once. Outputs home.ext4, home-super.txt, home-files.txt, tools.txt.
set -eu
exec 3>&1 1>&2
MODE=${MODE:?MODE: java, python, builder or home}
: "${SOURCE_DATE_EPOCH:?}" "${ROOTFS_UUID:?}" "${HOME_UUID:?}" "${HOME_HASH_SEED:?}" "${HOME_SIZE_MIB:?}"
case $MODE in builder | home) ;; *) : "${IMAGE_ID:?}" "${IMAGE_VERSION:?}" ;; esac
R=/root-tree
O=/out
mkdir -p "$R" "$O" /x

# MODE=home: the home template, /dev/vdb of every image: 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/base/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.
mkhome() {
  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
}

if [ "$MODE" = home ]; then
  mkhome
  cp /packages.txt "$O/tools.txt"
  tar -cSf - -C "$O" . >&3
  exit 0
fi

# 1. Packages: unpack each locked .apk (v2: gzip'd tar segments); keep the payload. /tmp/apks.txt:
#    the root set, and in the builder the image's "build" set (installed like the root set);
#    /tmp/pyapks.txt: the Python image's own (installed trimmed, step 7p).
case $MODE in
  java | python | builder) ;;
  *) echo "MODE=$MODE?"; exit 1 ;;
esac
{
  awk '!/^#/ && NF && $6 == "root" { print $1 "-" $2 ".apk" }' /in/rootfs/apk.lock
  [ "$MODE" != builder ] || awk '!/^#/ && NF && $6 == "build" { print $1 "-" $2 ".apk" }' /in/image/apk.lock
} > /tmp/apks.txt
: > /tmp/pyapks.txt
[ "$MODE" != python ] || awk '!/^#/ && NF { print $1 "-" $2 ".apk" }' /in/image/apk.lock > /tmp/pyapks.txt
cat /tmp/apks.txt /tmp/pyapks.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

# 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; the image's /etc/xvm/env, its default exec environment (RFC 017 R8: the agent reads
#    it), and /etc/profile; the marker (RFC 018 §6) at the end of /etc/os-release.
cp -R /in/rootfs/etc/. etc/
install -m 0644 /in/image/profile etc/profile
chmod 0644 etc/*
mkdir -p etc/xvm
chmod 0755 etc/xvm
install -m 0644 /in/image/env etc/xvm/env
if [ "$MODE" != builder ]; then
  [ -z "$(tail -c 1 etc/os-release)" ] || echo >> etc/os-release
  printf 'IMAGE_ID=%s\nIMAGE_VERSION=%s\n' "$IMAGE_ID" "$IMAGE_VERSION" >> etc/os-release
fi

if [ "$MODE" = builder ]; then
  # 7b. The in-guest build (guest/images/java/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

HOT=/in/image/hot-files.txt
if [ "$MODE" = python ]; then
  # 7p. Python (RFC 017 R1–R3): every package of python/apk.lock into /py, trimmed by drop.txt,
  #     then over the tree. P, D and S in drop.txt's paths stand for the stdlib, its lib-dynload
  #     and an extension module's suffix; a removed X.py takes its __pycache__/X.cpython-314*.pyc.
  P=usr/lib/python3.14
  D=$P/lib-dynload
  S=.cpython-314-riscv64-linux-musl.so
  drop() { awk -v k="$1" '$1 == k { $1 = ""; sub(/^ /, ""); print }' /in/image/drop.txt; }
  mkdir -p /py
  awk '!/^#/ && NF { print $1, $1 "-" $2 ".apk" }' /in/image/apk.lock | while read -r name apk; do
    drop pkg | grep -qxF "$name" && continue
    tar -C "/x/$apk" -cf - . | tar -C /py -xf -
  done
  drop keep | while read -r name glob; do
    apk=$(awk -v n="$name" '$1 == n { print $1 "-" $2 ".apk" }' /in/image/apk.lock)
    [ -n "$apk" ] || { echo "drop.txt: keep $name: not in image/apk.lock"; exit 1; }
    # shellcheck disable=SC2086
    (cd "/x/$apk" && tar -cf - $glob) | tar -C /py -xf -
  done
  : > "$O/dropped-files.txt"
  drop path | sed "s|^P/|$P/|; s|^D/|$D/|; s|\.S\$|$S|" | while read -r g; do
    (cd /py && for f in $g; do
       [ -e "$f" ] || continue
       n=$(find "$f" ! -type d | wc -l); b=$(find "$f" -type f -exec stat -c %s {} + | awk '{ s += $1 } END { print s + 0 }')
       case $f in *.py)
         m=${f%.py}; dd=$(dirname "$m"); bb=$(basename "$m")
         for c in "$dd"/__pycache__/"$bb".cpython-314*.pyc; do
           [ -e "$c" ] && { b=$((b + $(stat -c %s "$c"))); n=$((n + 1)); rm -f "$c"; }
         done ;;
       esac
       rm -rf "$f"; echo "$b $n /$f" >> "$O/dropped-files.txt"
     done)
  done
  find /py -depth -type d -empty -delete
  ( cd /py && find . ! -type d ) | while read -r f; do rm -f "$R/${f#./}"; done
  tar -C /py -cf - . | tar -C "$R" -xf -
  [ -x usr/bin/python3 ] || { echo "python mode: no usr/bin/python3 in the tree"; exit 1; }
  chown -R 0:0 .
else
# 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 .
fi

# 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 '^#' "$HOT" | 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
# (the image's harness's 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"

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