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>
109 lines
3.9 KiB
Bash
Executable File
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)."
|