Files
ulicraft-server-v1/tooling/sync-server-mods.sh
Oier Bravo Urtasun 27237bc7c1 feat(mods): replace packwiz with distribution-fed custom mod volume
Drop packwiz entirely. The server now loads a filtered mod subset from a
local ./server-mods volume instead of fetching a packwiz pack at boot.

New flow:
- tooling/classify-mods.py (tomllib only, no network) reads each jar's
  META-INF/neoforge.mods.toml and seeds mods-sides.json with a side
  (client|server|both; default both). Existing entries are preserved.
- mods-sides.json: committed, keyed by jar filename, hand-editable override
  surface. Initial seed: 65 both, 11 client, 0 server (76 jars).
- tooling/sync-server-mods.sh mirrors the both/server jars from
  $DISTRIBUTION_WEB_ROOT into ./server-mods/ (authoritative; prunes strays;
  halts on a missing jar). Replaces render-pack.sh.
- compose: minecraft mounts ./server-mods:/mods:ro with REMOVE_OLD_MODS=TRUE
  (itzg auto-syncs /mods -> /data/mods). Removed PACKWIZ_URL, the pack.
  extra_host (auth. kept for authlib), and depends_on caddy (drasl kept).

Both classify-mods.py and sync-server-mods.sh resolve their source dir as:
CLI arg / MODS_DIST_DIR env / $DISTRIBUTION_WEB_ROOT (in that order).

Removed: tooling/render-pack.sh + add-custom-mod.sh (packwiz tooling),
pack/ (packwiz source), custom/ (76 jars now sourced from the distribution),
the pack. caddy vhost + its mounts/alias, and the packwiz .gitignore block.

Client distribution is UNCHANGED: HeliosLauncher still installs the full
modset from distribution.json. Docs (CLAUDE.md, plan/04/05/14, runtime)
updated; plan/04-packwiz.md stubbed as superseded by plan/04-mods.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:34:38 +02:00

109 lines
3.9 KiB
Bash
Executable File

#!/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 <forgemods-required-dir-or-dist-root>
# 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)."