#!/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 .pw.toml # that points at a stable download URL + sha256, then refresh. Here the metadata # is written as a TEMPLATE (.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/ # which is exactly what minecraft fetches via PACKWIZ_URL on install. # # Usage: # tooling/add-custom-mod.sh [--name "Nice Name"] [--side S] [--force] # tooling/add-custom-mod.sh [--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 [--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" < 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-pack.sh renders all pack templates (BASE_DOMAIN only) + rebuilds the # packwiz index. Single source of pack-render logic, shared with render-config. log "rendering metadata + refreshing index" tooling/render-pack.sh log "done — ${#JARS[@]} mod(s) added to the pack" echo " commit: custom/*.jar + pack/mods/*.pw.toml.tmpl (rendered .pw.toml/index are gitignored)"