#!/usr/bin/env bash # sync-server-mods.sh — mirror the server's mod subset into ./server-mods/. # Replaces the old render-pack.sh (packwiz). Copies every jar mapped `both` or # `server` in mods-sides.json from the distribution's forgemods/required dir into # ./server-mods/, and removes any stray server-mods/*.jar not in the selection # (authoritative mirror). The minecraft container mounts ./server-mods at /mods # (REMOVE_OLD_MODS=TRUE), so /data/mods ends up matching exactly. # # Cautious by design: halts (non-zero) on a missing mods-sides.json or a selected # jar absent from the source dir — never a silent partial sync. # # Source dir resolution (first match wins): # 1. CLI arg: sync-server-mods.sh # 2. env MODS_DIST_DIR # 3. $DISTRIBUTION_WEB_ROOT from .env # The path may point at the dist ROOT (…/distribution[/dist]) or directly at # …/servers/ulicraft-1.21.1/forgemods/required — both are accepted. set -euo pipefail cd "$(dirname "$0")/.." # repo root [ -f .env ] && { set -a; . ./.env; set +a; } REQUIRED_SUBPATH="servers/ulicraft-1.21.1/forgemods/required" SIDES_JSON="mods-sides.json" DEST="server-mods" # ── Resolve source dir ────────────────────────────────────────────── if [ "${1:-}" != "" ]; then SRC="$1"; ORIGIN="CLI arg" elif [ "${MODS_DIST_DIR:-}" != "" ]; then SRC="$MODS_DIST_DIR"; ORIGIN="MODS_DIST_DIR env" elif [ "${DISTRIBUTION_WEB_ROOT:-}" != "" ]; then SRC="$DISTRIBUTION_WEB_ROOT"; ORIGIN="\$DISTRIBUTION_WEB_ROOT" else echo "no source dir: pass a path, set MODS_DIST_DIR, or DISTRIBUTION_WEB_ROOT in .env" >&2 exit 1 fi if [ -d "$SRC/$REQUIRED_SUBPATH" ]; then REQ="$SRC/$REQUIRED_SUBPATH" elif [ "$(basename "$SRC")" = "required" ] && [ -d "$SRC" ]; then REQ="$SRC" else echo "forgemods/required dir not found (from $ORIGIN=$SRC)" >&2 echo "tried: $SRC/$REQUIRED_SUBPATH and $SRC" >&2 exit 1 fi [ -f "$SIDES_JSON" ] || { echo "$SIDES_JSON not found — run tooling/classify-mods.py first" >&2; exit 1; } # ── Selected jars (both|server) ───────────────────────────────────── select_jars() { if command -v jq >/dev/null 2>&1; then jq -r 'to_entries[] | select(.value == "both" or .value == "server") | .key' "$SIDES_JSON" else python3 - "$SIDES_JSON" <<'PY' import json, sys with open(sys.argv[1], encoding="utf-8") as fh: sides = json.load(fh) for name, side in sides.items(): if side in ("both", "server"): print(name) PY fi } mapfile -t SELECTED < <(select_jars | sort) [ "${#SELECTED[@]}" -gt 0 ] || { echo "no jars selected (both/server) in $SIDES_JSON — nothing to sync" >&2; exit 1; } # ── Halt if any selected jar is missing from the source ───────────── missing=() for jar in "${SELECTED[@]}"; do [ -f "$REQ/$jar" ] || missing+=("$jar") done if [ "${#missing[@]}" -gt 0 ]; then echo "ERROR: ${#missing[@]} selected jar(s) missing from source ($REQ):" >&2 printf ' %s\n' "${missing[@]}" >&2 echo "refusing to do a partial sync — fix mods-sides.json or the distribution." >&2 exit 1 fi # ── Mirror: copy selected, prune everything else ──────────────────── mkdir -p "$DEST" # Prune stray jars not in the current selection. declare -A keep for jar in "${SELECTED[@]}"; do keep["$jar"]=1; done pruned=0 shopt -s nullglob for existing in "$DEST"/*.jar; do base="$(basename "$existing")" if [ -z "${keep[$base]:-}" ]; then rm -f -- "$existing" pruned=$((pruned + 1)) fi done shopt -u nullglob # Copy selection (cp -p preserves mtime so unchanged jars don't re-touch). copied=0 for jar in "${SELECTED[@]}"; do cp -p -- "$REQ/$jar" "$DEST/$jar" copied=$((copied + 1)) done echo "source: $REQ (from $ORIGIN)" echo "synced $copied jar(s) into $DEST/ (pruned $pruned stray)."