.github/scripts/freebsd_jail_build.sh
#!/bin/sh

set -eu

# ----------------------------
# Config / Inputs (env-driven)
# ----------------------------
FREEBSD_VER_TAG="${FREEBSD_VERSION:-14_3}"
SNAP_LABEL="${FREEBSD_SNAPSHOT:-base-20250809}"
ZPOOL="${ZPOOL:-zroot}"
JAILS_DS="${JAILS_DS:-${ZPOOL}/z/jails}"
JAILS_MP="${JAILS_MP:-/z/jails}"

GITHUB_RUN_ID="${GITHUB_RUN_ID:-localrun}"
GITHUB_RUN_ATTEMPT="${GITHUB_RUN_ATTEMPT:-1}"

BUILD_MODE="${BUILD_MODE:-Release}"
CMAKE_ARGS="${CMAKE_ARGS:-}"

BASE_DS="${JAILS_DS}/${FREEBSD_VER_TAG}"
RUN_DS="${JAILS_DS}/ci-${FREEBSD_VER_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
RUN_MP="${JAILS_MP}/ci-${FREEBSD_VER_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
JAIL="ci_${FREEBSD_VER_TAG}_${GITHUB_RUN_ID}_${GITHUB_RUN_ATTEMPT}"

# ----------------------------
# Helpers
# ----------------------------
log() { printf '%s %s\n' "[freebsd-jail]" "$*" >&2; }

# Stop a jail if it's running; kill any lingering processes by JID if needed
stop_stale_jail() {
  jname="$1"
  if jls -j "$jname" >/dev/null 2>&1; then
    log "Stopping stale jail: $jname"
    if ! sudo jail -r "$jname" 2>/dev/null; then
      JID="$(jls -j "$jname" jid -h 2>/dev/null || true)"
      if [ -n "$JID" ]; then
        sudo killall -j "$JID" -TERM 2>/dev/null || true
        sleep 1
        sudo killall -j "$JID" -KILL 2>/dev/null || true
      fi
      sudo jail -r "$jname" 2>/dev/null || true
    fi
  fi
}

# Unmount everything under a mountpoint (deepest-first)
umount_tree() {
  rootmp="$1"
  # List mounted paths under $rootmp, deepest-first
  # shellcheck disable=SC2016
  for m in $(mount | awk -v p="$rootmp" '$3 ~ ("^"p) { print length, $3 }' | sort -rn | cut -d" " -f2-); do
    sudo umount -f "$m" 2>/dev/null || true
  done
}

# Destroy a cloned dataset (after umount)
destroy_dataset() {
  ds="$1"
  if sudo zfs list -H "$ds" >/dev/null 2>&1; then
    mp="$(sudo zfs get -H -o value mountpoint "$ds" 2>/dev/null || echo "")"
    if [ -n "$mp" ] && [ "$mp" != "-" ]; then
      umount_tree "$mp"
    fi
    log "Destroying dataset: $ds"
    sudo zfs destroy -r "$ds"
  fi
}

# Remove stale jail + dataset that match our run IDs
cleanup_stale_before_start() {
  stop_stale_jail "$JAIL"
  destroy_dataset "$RUN_DS"
}

# Full cleanup for traps: stop jail, unmount, destroy dataset
cleanup_on_exit() {
  # Be idempotent; ignore errors
  stop_stale_jail "$JAIL"
  destroy_dataset "$RUN_DS"
}

# Generate an env file inside jail that exports safe GitHub env vars
write_gha_env() {
  out="$1"
  # Allow common CI vars; deny obvious secrets
  # Adjust allow/deny as needed for your org.
  ALLOW='GITHUB_* RUNNER_* CI ACTIONS_* INPUT_* MATRIX_* BUILD_MODE CMAKE_ARGS'
  DENY='*TOKEN* *SECRET* *PASSWORD* *PASS* *KEY* *CERT* AWS_* AZURE_* GCP_*'

  # Function to test name against allow/deny (POSIX sh compatible inline)
  is_allowed() {
    name="$1"
    case "$name" in
      GITHUB_*|RUNNER_*|CI|ACTIONS_*|INPUT_*|MATRIX_*|BUILD_MODE|CMAKE_ARGS) : ;;
      *) return 1 ;;
    esac
    case "$name" in
      *TOKEN*|*SECRET*|*PASSWORD*|*PASS*|*KEY*|*CERT*|AWS_*|AZURE_*|GCP_* ) return 1 ;;
    esac
    return 0
  }

  tmp="$(mktemp)"
  {
    echo "# Autogenerated; sourced by /root/build.sh"
    # Ensure our two common knobs are present even if not in env
    printf "export BUILD_MODE='%s'\n" "$BUILD_MODE"
    # Escape single quotes in CMAKE_ARGS
    esc_ca=$(printf "%s" "$CMAKE_ARGS" | sed "s/'/'\"'\"'/g")
    printf "export CMAKE_ARGS='%s'\n" "$esc_ca"

    env | while IFS='=' read -r name value; do
      if is_allowed "$name"; then
        esc=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g")
        printf "export %s='%s'\n" "$name" "$esc"
      fi
    done
  } > "$tmp"
  sudo mkdir -p "$(dirname "$out")"
  sudo cp "$tmp" "$out"
  sudo chmod 0644 "$out"
  rm -f "$tmp"
}

# ----------------------------
# Trap: ensure cleanup on any exit
# ----------------------------
trap cleanup_on_exit EXIT INT TERM HUP

# ----------------------------
# Prep: nuke stale, then clone snapshot
# ----------------------------
cleanup_stale_before_start

log "Cloning ${BASE_DS}@${SNAP_LABEL} -> ${RUN_DS}"
sudo zfs clone "${BASE_DS}@${SNAP_LABEL}" "${RUN_DS}"

# ----------------------------
# Bootstrapping jail root: DNS + mounts
# ----------------------------
# DNS for networking inside jail
sudo mkdir -p "${RUN_MP}/etc"
sudo cp /etc/resolv.conf "${RUN_MP}/etc/resolv.conf"
[ -f /etc/hosts ]         && sudo cp /etc/hosts         "${RUN_MP}/etc/hosts"
[ -f /etc/nsswitch.conf ] && sudo cp /etc/nsswitch.conf "${RUN_MP}/etc/nsswitch.conf"

# Core mounts
sudo mount -t devfs   devfs "${RUN_MP}/dev"
sudo mount -t fdescfs fdesc "${RUN_MP}/dev/fd"
sudo mount -t procfs  proc  "${RUN_MP}/proc"
sudo mount -t tmpfs   tmpfs "${RUN_MP}/tmp"

# Workspace / temp / ccache / NFS from host
RUNNER_WORKSPACE="${RUNNER_WORKSPACE:-$PWD}"
RUNNER_TEMP="${RUNNER_TEMP:-/tmp/runner_temp}"
mkdir -p "${RUNNER_TEMP}" "${HOME}/.ccache"

sudo mkdir -p "${RUN_MP}/work" "${RUN_MP}/runner_temp" "${RUN_MP}/ccache" "${RUN_MP}/mnt/opensource"
sudo mount -t nullfs -o rw "${RUNNER_WORKSPACE}"   "${RUN_MP}/work"
sudo mount -t nullfs -o rw "${RUNNER_TEMP}"        "${RUN_MP}/runner_temp"
sudo mount -t nullfs -o rw "${HOME}/github-ccache" "${RUN_MP}/ccache"
sudo mount -t nullfs -o rw "/mnt/opensource"       "${RUN_MP}/mnt/opensource"

# ----------------------------
# Start jail (FUSE enabled)
# ----------------------------
log "Starting jail: ${JAIL}"
sudo jail -c name="${JAIL}" host.hostname="${JAIL}" \
     path="${RUN_MP}" persist \
     mount.devfs devfs_ruleset=5 enforce_statfs=1 \
     allow.mount allow.mount.fusefs \
     allow.raw_sockets \
     ip4=inherit ip6=inherit

# ----------------------------
# Inject env + build script, then run it
# ----------------------------
write_gha_env "${RUN_MP}/root/gha_env.sh"

sudo cp ".docker/build-freebsd.sh" "${RUN_MP}/root/build.sh"
sudo chmod +x "${RUN_MP}/root/build.sh"

# Run inside the jail with env sourced
log "Executing build inside jail…"
sudo jexec "${JAIL}" /bin/sh -lc '. /root/gha_env.sh; exec /root/build.sh'

log "Build finished successfully."
# Cleanup happens automatically via trap on EXIT.
