#!/bin/sh
# The JDK of the xvm root (RFC 005 §4.2), built INSIDE the builder guest: the riscv64 JDK has to
# run for jlink's native strip step and for every CDS dump, and E2 found toolchains crash under
# qemu-user, so scripts/build-guest.sh boots the guest kernel under qemu-system-riscv64 with the
# builder root (guest/rootfs/mkroot.sh MODE=builder: busybox, the agent, the full Alpine JDK with
# jmods, binutils, this directory as /usr/lib/xvm-build) and a scratch ext4 on /home/student, and
# the agent runs this script as root (scripts/guest-qemu.py jdk-build).
#
#   1. jlink (jlink.args: D8's modules, --no-man-pages --no-header-files --compress=zip-0,
#      D7's flags + D63's -XX:ArchiveRelocationMode=0 + -Xss512k -Xmx64m baked with
#      --add-options; no --strip-debug since D63: the JDK classes keep their line numbers) ->
#      $W/xvm, bind-mounted on /usr/lib/jvm/xvm, the path it has in the final root.
#   2. The static CDS archive lib/server/classes.jsa (-Xshare:dump with the default classlist),
#      dumped with G1 so that archived heap objects are included (JDK 25 writes them only from
#      G1; SerialGC loads them at run time).
#      Before that, the native code is stripped of its symbol tables (strip --strip-unneeded):
#      the jmods carry them and jlink's --strip-debug removes only debug sections (libjvm.so
#      25.1 MB vs 20.4 MB in Alpine's JRE package).
#   3. javac's AppCDS archive lib/server/javac.jsa, a *static* archive: the classes one javac run
#      over the training set loads (train/: the goal-G1 corpus that compiles, -Xlint:all;
#      -XX:DumpLoadedClassList, -Xint, the list put in a canonical order so that two builds dump
#      the same archive), dumped through the javac launcher (G1, -Xshare:dump), so that
#      the archive records javac's module options (-m jdk.compiler). E1's dynamic archive
#      (-XX:ArchiveClassesAtExit on top of classes.jsa) maps, but every javac run then prints
#      five "[error][cds] Mismatched values for property jdk.module.main ..." lines on stdout
#      (an archive named with -XX:SharedArchiveFile reports at error level, and the base archive
#      was dumped without -m), and the full module graph is off. The AOT cache of JDK 25
#      (-XX:AOTCache) cannot be combined with the baked -Xshare:on (RFC 005 §14).
#   4. The compile server (RFC 005 §5.2, G4): compiled/src (xvm.compiled.*) compiled with this
#      JDK's javac, packed into xvm-compiled.jar (fixed entry order, jar --date, mtime
#      SOURCE_DATE_EPOCH) in $W/xvmlib, bind-mounted on /usr/lib/xvm, its final path, with
#      xvm-compiled.args and train.sh. The server maps javac.jsa (step 3) rather than an archive
#      of its own (RFC 005 §15: same start-up cost, 27 MB less root). A verification run starts
#      it as the agent would and compiles the training corpus through the shim (the agent
#      binary as `javac`): every program and broken variant with -d out, a few with -Xlint:all,
#      --release 21, an @argfile, -version.
#   5. Evidence in $W/info: jlink and dump logs, -Xlog:cds of java, javac and the server, the
#      DT_NEEDED closure of every ELF file, sizes and checksums.
#   6. $OUT (/home/student/jdk.tar): xvm/ (the JDK), xvmlib/ (/usr/lib/xvm) and info/; the
#      host reads it back with the agent's file_read.
set -eu
B=/usr/lib/xvm-build
W=/home/student/jdk-build
X=/usr/lib/jvm/xvm
L=/usr/lib/xvm
OUT=/home/student/jdk.tar
I=$W/info
SDE=$(cat "$B/source-date-epoch")
SRC=$(ls -d /usr/lib/jvm/java-*-openjdk | head -1)
up() { cut -d' ' -f1 /proc/uptime; }
step() { echo "== [$(up) s] $*"; }
# On failure, the tails of the step logs (the scratch disk is thrown away).
trap 'rc=$?; if [ "$rc" != 0 ]; then echo "build.sh failed ($rc)"; for f in "$I"/*.out "$I"/*.log; do
  [ -f "$f" ] && { echo "--- $f"; tail -15 "$f"; }; done; fi' EXIT

umount "$X" 2>/dev/null || true
umount "$L" 2>/dev/null || true
rm -rf "$W" "$OUT"
mkdir -p "$W" "$I"
cp "$SRC/release" "$I/source-release.txt"
cp "$B/jlink.args" "$I/jlink.args"
uname -a > "$I/uname.txt"

step "jlink ($SRC)"
"$SRC/bin/jlink" -J-Xmx256m --module-path "$SRC/jmods" "@$B/jlink.args" --output "$W/xvm" > "$I/jlink.out" 2>&1 \
  || { cat "$I/jlink.out"; exit 1; }
find "$W/xvm" -type f | while read -r f; do
  if [ "$(head -c 4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then strip --strip-unneeded "$f"; fi
done
mount -o bind "$W/xvm" "$X"
"$X/bin/java" -XX:-UseSerialGC -XX:+UseG1GC -Xshare:off --list-modules > "$I/modules.txt"
"$X/bin/java" -XX:-UseSerialGC -XX:+UseG1GC -Xshare:off -XX:+PrintFlagsFinal -version 2>&1 \
  | grep -E ' (TieredStopAtLevel|CICompilerCount|UseSerialGC|UseG1GC|UsePerfData|ThreadStackSize|MaxHeapSize) ' \
  > "$I/flags-check.txt" || true

step "static CDS archive (classes.jsa, G1 dump)"
"$X/bin/java" -XX:-UseSerialGC -XX:+UseG1GC -Xmx128m -Xshare:dump \
  -Xlog:cds=info,cds+heap=info:file="$I/cds-dump.log" > "$I/cds-dump.out" 2>&1 \
  || { tail -30 "$I/cds-dump.out"; exit 1; }

step "javac AppCDS archive (javac.jsa: classlist of a training javac, static dump via javac)"
rm -rf "$W/train" "$W/train-out"
cp -R "$B/train" "$W/train"
cd "$W/train"
find . -name '*.java' | LC_ALL=C sort > "$I/train-files.txt"
# shellcheck disable=SC2046
"$X/bin/javac" -J-Xint -J-XX:DumpLoadedClassList="$W/javac.classlist.raw" \
  -Xlint:all -d "$W/train-out" $(cat "$I/train-files.txt") > "$I/javac-train.out" 2>&1 \
  || { tail -30 "$I/javac-train.out"; exit 1; }
cd /
# The order in which javac loads its classes, and with C1 the constant-pool entries it resolves,
# vary from run to run (E14: same set, different order). The canonical list (comments, class
# lines sorted by name with ids renumbered, then the @-lines sorted) dumps the same archive
# every time; -Xint keeps the @cp lines independent of JIT timing.
{
  grep '^#' "$W/javac.classlist.raw"
  grep -v '^[#@]' "$W/javac.classlist.raw" | sed 's/ id: [0-9]*$//' | LC_ALL=C sort -u | awk '{ print $0 " id: " NR - 1 }'
  grep '^@' "$W/javac.classlist.raw" | LC_ALL=C sort -u
} > "$I/javac.classlist"
"$X/bin/javac" -J-Xshare:dump -J-XX:SharedClassListFile="$I/javac.classlist" \
  -J-XX:SharedArchiveFile="$X/lib/server/javac.jsa" -J-XX:-UseSerialGC -J-XX:+UseG1GC -J-Xmx128m \
  -J-Xlog:cds=info:file="$I/javac-dump.log" > "$I/javac-dump.out" 2>&1 \
  || { tail -30 "$I/javac-dump.out"; exit 1; }

step "compile server: xvm-compiled.jar"
mkdir -p "$W/xvmlib" "$W/compiled-classes" "$W/bin"
mount -o bind "$W/xvmlib" "$L"
# shellcheck disable=SC2046
"$SRC/bin/javac" -d "$W/compiled-classes" $(find "$B/compiled/src" -name '*.java' | LC_ALL=C sort) \
  > "$I/compiled-javac.out" 2>&1 || { cat "$I/compiled-javac.out"; exit 1; }
DATE=$(date -u -d "@$SDE" +%Y-%m-%dT%H:%M:%SZ)
( cd "$W/compiled-classes" && find xvm -type f | LC_ALL=C sort > "$I/compiled-jar-files.txt" \
  && "$SRC/bin/jar" --create --no-manifest --date="$DATE" --file "$L/xvm-compiled.jar" @"$I/compiled-jar-files.txt" )
cp "$B/compiled/xvm-compiled.args" "$L/xvm-compiled.args"
cp "$B/compiled/train.sh" "$L/train.sh"
chmod 0644 "$L/xvm-compiled.jar" "$L/xvm-compiled.args"
chmod 0755 "$L/train.sh"
# CDS records a class path jar's size and mtime; the root's tar gives every file this mtime.
touch -d "@$SDE" "$L/xvm-compiled.jar"
ln -sf /sbin/xvm-agent "$W/bin/javac"

# The server as the agent starts it (compiled.rs), plus the training options; waits for READY.
server() {  # LOG EXTRA-OPTIONS...
  log=$1; shift
  rm -f /run/xvm/javac.sock
  "$X/bin/java" "$@" "@$L/xvm-compiled.args" > "$log" 2>&1 &
  n=0
  until grep -q '^READY' "$log"; do
    n=$((n + 1))
    if [ "$n" -gt 1800 ] || ! kill -0 $! 2>/dev/null; then cat "$log"; echo "server did not start"; exit 1; fi
    sleep 1
  done
}

step "compile server: verification run (javac.jsa mapped, the training corpus through the shim)"
server "$I/compiled-verify-server.log" -Xlog:class+load=info
rm -rf "$W/ctrain"; mkdir -p "$W/ctrain"
cp -R "$B/train/." "$W/ctrain/"
cp -R "$B/train-broken" "$W/ctrain/broken"
cd "$W/ctrain"
# Broken programs exit 1: every compile is checked by its echo line, not by set -e.
tc() { rc=0; "$W/bin/javac" "$@" || rc=$?; echo "javac $* -> $rc"; }
{
  for f in $(find . -name '*.java' | LC_ALL=C sort); do tc -d out "$f"; done
  tc -Xlint:all -d out Warnings.java
  tc --release 21 -d out ModernJava.java
  printf -- '-d out\n-Xlint:all\nHello.java\n' > args.txt
  tc @args.txt
  tc -version
} > "$I/compiled-verify.out" 2>&1
cd /
/sbin/xvm-agent compiled-ctl Q >> "$I/compiled-verify.out" 2>&1
wait
grep -c ' -> ' "$I/compiled-verify.out" > "$I/compiled-verify-count.txt" || true
rm -rf "$W/ctrain"
ls -ln --full-time "$L" > "$I/xvmlib-ls.txt" 2>&1 || ls -ln "$L" > "$I/xvmlib-ls.txt"

step "evidence"
cd "$W/train-out"
"$X/bin/java" -Xlog:cds -Xlog:class+load=info -cp . Hello > "$I/java-hello-xlog.txt" 2>&1
"$X/bin/javac" -J-XX:SharedArchiveFile="$X/lib/server/javac.jsa" -J-Xlog:cds -J-Xlog:class+load=info \
  -d /tmp/xvm-verify "$W/train/Hello.java" > "$I/javac-hello-xlog.txt" 2>&1
rm -rf /tmp/xvm-verify
cd /
find "$X" -type f | while read -r f; do
  if [ "$(head -c 4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then
    echo "${f#"$X"/}: $(readelf -d "$f" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p' | tr '\n' ' ')"
  fi
done | LC_ALL=C sort > "$I/needed.txt"
( cd "$X" && find . -type f -exec ls -l {} + | awk '{ print $5, $9 }' | LC_ALL=C sort -k2 ) > "$I/sizes.txt"
( cd "$X" && find . -type f | LC_ALL=C sort | xargs sha256sum ) > "$I/sha256.txt"
du -sk "$X" > "$I/du.txt"

( cd "$L" && find . -type f | LC_ALL=C sort | xargs sha256sum ) > "$I/xvmlib-sha256.txt"
umount "$X"
umount "$L"
step "tar"
tar -cf "$OUT" -C "$W" xvm xvmlib info
ls -l "$OUT"
sha256sum "$OUT"
sync
step "done"
