Files
ulicraft-server-v1/tooling/add-custom-mod.sh
Oier Bravo Urtasun f7858d4ec4 refactor(pack): template custom mod metadata so BASE_DOMAIN isn't frozen
The download URL is the only domain-dependent field in the external-file
metadata. Make mods/*.pw.toml build output rendered from committed
mods/*.pw.toml.tmpl (url uses ${BASE_DOMAIN}); render-config.sh and
add-custom-mod.sh render them + packwiz refresh at deploy/add time.

- pack/.packwizignore: keep *.pw.toml.tmpl out of the index
- gitignore rendered pack/mods/*.pw.toml + pack/index.toml (build output)
- seed a minimal index.toml before refresh (packwiz won't bootstrap one)
- add-custom-mod renders inline (BASE_DOMAIN only) — no coupling to
  dnsmasq's HOST_LAN_IP

Domain changes now just need a re-render, not a metadata rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:07:35 +02:00

141 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# add-custom-mod.sh — add LOCAL jar(s) to the packwiz pack as "external files".
#
# Implements the "Other external files" flow from
# https://packwiz.infra.link/tutorials/creating/adding-mods/ : a <slug>.pw.toml
# that points at a stable download URL + sha256, then refresh. Here the metadata
# is written as a TEMPLATE (<slug>.pw.toml.tmpl) with a ${BASE_DOMAIN}
# placeholder and rendered by render-config.sh, so the domain is never frozen.
#
# Since the jars are local (no public URL), we HOST them on the caddy pack
# server: each jar is copied to the top-level ./custom/ dir (OUTSIDE ./pack, so
# packwiz refresh won't index it as a direct file). Caddy bind-mounts ./custom
# at /srv/pack/custom, so it is served at
# http://pack.${BASE_DOMAIN}/custom/<filename>
# which is exactly what minecraft fetches via PACKWIZ_URL on install.
#
# Usage:
# tooling/add-custom-mod.sh <mod.jar> [--name "Nice Name"] [--side S] [--force]
# tooling/add-custom-mod.sh <dir/> [--side S] [--force] # all *.jar in dir
# # (--name not allowed)
# S = both|client|server (default both)
#
# Single-jar name defaults to the filename without .jar; folder mode derives
# each name from its filename. packwiz refresh runs ONCE at the end.
# Cautious: halts on missing tool/var, bad side, whitespace in a filename, an
# empty folder, or an existing metadata/custom jar (unless --force).
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
# ---- helpers ---------------------------------------------------------------
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
slugify() { # lowercase, non-alnum -> hyphen, squeeze + trim hyphens
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/-+/-/g; s/^-//; s/-$//'
}
# ---- args ------------------------------------------------------------------
TARGET=""; NAME=""; SIDE="both"; FORCE=0
while [ $# -gt 0 ]; do
case "$1" in
--name) NAME="${2:?--name needs a value}"; shift 2 ;;
--side) SIDE="${2:?--side needs a value}"; shift 2 ;;
--force) FORCE=1; shift ;;
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$TARGET" ] || die "only one jar or dir at a time (got extra: $1)"; TARGET="$1"; shift ;;
esac
done
[ -n "$TARGET" ] || die "no jar/dir given. usage: tooling/add-custom-mod.sh <mod.jar|dir/> [--side both|client|server]"
[ -e "$TARGET" ] || die "path not found: $TARGET"
case "$SIDE" in both|client|server) ;; *) die "invalid --side '$SIDE' (both|client|server)" ;; esac
# ---- preflight -------------------------------------------------------------
command -v packwiz >/dev/null 2>&1 || die "missing required tool: packwiz (https://packwiz.infra.link)"
command -v sha256sum >/dev/null 2>&1 || die "missing required tool: sha256sum"
command -v envsubst >/dev/null 2>&1 || die "missing required tool: envsubst (gettext)"
[ -f .env ] || die ".env not found (copy .env.example and fill it)"
# shellcheck disable=SC1091
set -a; . ./.env; set +a
: "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}"
[ -f pack/pack.toml ] || die "pack/pack.toml not found — run from a packwiz pack (see plan/04-packwiz.md)"
# ---- collect jars ----------------------------------------------------------
declare -a JARS=()
if [ -d "$TARGET" ]; then
[ -z "$NAME" ] || die "--name not allowed with a folder (names are derived per-jar)"
shopt -s nullglob
for f in "$TARGET"/*.jar; do JARS+=("$f"); done
shopt -u nullglob
[ "${#JARS[@]}" -gt 0 ] || die "no .jar files in folder: $TARGET"
else
JARS=("$TARGET")
fi
mkdir -p custom pack/mods
# ---- per-jar processing (no refresh; refresh once at the end) --------------
add_one() { # $1 = jar path. Uses NAME (single-jar only), SIDE, FORCE, BASE_DOMAIN.
local jar="$1" filename name slug meta dest url hash
[ -f "$jar" ] || die "not a file: $jar"
filename="$(basename -- "$jar")"
case "$filename" in *.jar) ;; *) die "not a .jar: $filename" ;; esac
case "$filename" in *[[:space:]]*) die "filename has whitespace: '$filename' — rename it (URL-unsafe)" ;; esac
name="$NAME"; [ -n "$name" ] || name="${filename%.jar}"
slug="$(slugify "$name")"
[ -n "$slug" ] || die "could not derive a slug from name '$name'"
# Metadata is a TEMPLATE: the download URL keeps a literal ${BASE_DOMAIN}
# placeholder, rendered to the real .pw.toml by render-config.sh at deploy so
# the domain is never frozen into a committed file.
meta="pack/mods/${slug}.pw.toml.tmpl"
dest="custom/${filename}"
url_tmpl='http://pack.${BASE_DOMAIN}/custom/'"${filename}"
if [ "$FORCE" -ne 1 ]; then
[ -e "$meta" ] && die "metadata exists: $meta (use --force to overwrite)"
[ -e "$dest" ] && die "hosted jar exists: $dest (use --force to overwrite)"
fi
hash="$(sha256sum -- "$jar" | cut -d' ' -f1)"
# Skip the copy when the source already IS the hosted file (regen-in-place,
# e.g. `add-custom-mod.sh custom --force` to rewrite metadata).
[ "$dest" -ef "$jar" ] || cp -- "$jar" "$dest"
cat > "$meta" <<EOF
name = "${name}"
filename = "${filename}"
side = "${SIDE}"
[download]
url = "${url_tmpl}"
hash-format = "sha256"
hash = "${hash}"
EOF
echo " + ${filename} -> http://pack.${BASE_DOMAIN}/custom/${filename} (${SIDE}, sha256 ${hash:0:12}…)"
}
log "adding ${#JARS[@]} jar(s), side=${SIDE}"
for j in "${JARS[@]}"; do add_one "$j"; done
# ---- render + refresh once -------------------------------------------------
# Render every metadata template -> .pw.toml (inject ${BASE_DOMAIN} only — no
# coupling to dnsmasq's HOST_LAN_IP), then rebuild the packwiz index. This is
# the same render render-config.sh does at deploy; kept inline so adding a mod
# works with just BASE_DOMAIN set.
log "rendering metadata + refreshing index"
for t in pack/mods/*.pw.toml.tmpl; do
envsubst '${BASE_DOMAIN}' < "$t" > "${t%.tmpl}"
done
# index.toml is gitignored build output; seed a minimal one so packwiz refresh
# (which won't bootstrap a missing index) has something to populate.
[ -f pack/index.toml ] || printf 'hash-format = "sha256"\n' > pack/index.toml
( cd pack && packwiz refresh )
log "done — ${#JARS[@]} mod(s) added to the pack"
echo " commit: custom/*.jar + pack/mods/*.pw.toml.tmpl (rendered .pw.toml/index are gitignored)"