diff --git a/README.md b/README.md index 32b12cc..0e1f34b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,34 @@ Send the players `https:///setup-windows-packwiz.html` or `setup-linux-packwiz.html`. Those are generated with your real pack URL and server address already filled in. +## Deploying from an ISO or USB stick + +```bash +./build.sh --iso # -> dist/mc-packwiz-deploy-.iso +``` + +Burn or `dd` it, mount it on the target, then: + +```bash +mkdir -p ~/mc-packwiz-deploy +cp -r /run/media/*/mc-packwiz-deploy/. ~/mc-packwiz-deploy/ +cd ~/mc-packwiz-deploy && chmod +x *.sh *.py +$EDITOR deploy.conf # set MIRROR_URL and ACCEPT_EULA at minimum +./install.sh --check # verify the machine is ready +./install.sh +``` + +`install.sh` reads everything from `deploy.conf`, so there are no flags to type +at a console. It installs missing prerequisites with `dnf`, optionally caps the +ZFS ARC, runs the deploy, and can start the server. `--check` validates +prerequisites and the mount without changing anything. + +The one thing it will not do is guess about the EULA: `ACCEPT_EULA` must be set +to `yes` in the config, or it stops and points at the licence. + +It also refuses to run when the share is not a mountpoint, since deploying onto +an unmounted path would fill the root disk instead of the pool. + ## Re-running `deploy.sh` is safe to run repeatedly, and re-running is the intended way to diff --git a/build.sh b/build.sh index 5b1132b..d87aea2 100755 --- a/build.sh +++ b/build.sh @@ -3,6 +3,7 @@ # Build the release zip. # # ./build.sh -> dist/mc-packwiz-deploy-.zip +# ./build.sh --iso -> also dist/mc-packwiz-deploy-.iso # ./build.sh -o /tmp -> writes there instead # ./build.sh -v 1.2.0 -> override the version # @@ -15,12 +16,15 @@ set -euo pipefail VERSION="0.1.0" NAME="mc-packwiz-deploy" OUTDIR="" +MAKE_ISO=0 HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # Everything the zip ships. Anything not listed here stays out, so stray notes # or test output in the working tree can't leak into a release. FILES=( README.md + install.sh + deploy.conf.example deploy.sh mc-service-setup.sh packwiz-setup.sh @@ -35,6 +39,7 @@ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; exi while [ $# -gt 0 ]; do case "$1" in -o|--outdir) OUTDIR="${2:-}"; shift 2 ;; + --iso) MAKE_ISO=1; shift ;; -v|--version) VERSION="${2:-}"; VERSION_FORCED=1; shift 2 ;; -h|--help) usage 0 ;; *) echo "unknown option: $1" >&2; usage 1 ;; @@ -104,3 +109,54 @@ unzip -l "$ZIP" | sed 's/^/ /' if command -v sha256sum >/dev/null; then sha256sum "$ZIP" | tee "$ZIP.sha256" | sed 's/^/ /' fi + +# -------------------------------------------------------------------- iso --- + +if (( MAKE_ISO )); then + ISO="$OUTDIR/${NAME}-${VERSION}.iso" + + mkisofs="" + for c in xorrisofs genisoimage mkisofs; do + command -v "$c" >/dev/null && { mkisofs="$c"; break; } + done + [ -n "$mkisofs" ] || die "no ISO tool found — sudo dnf install xorriso" + + # A ready-to-edit config goes on the media so the target host only needs + # the file opened and one command run. + cp "$HERE/deploy.conf.example" "$DEST/deploy.conf" + + cat > "$DEST/README.FIRST.txt" <<'TXT' +Minecraft + packwiz hosting kit +=============================== + +The media is read-only, so install.sh copies itself to your home directory +before doing anything. + + 1. Mount this media, then copy the kit somewhere writable: + + mkdir -p ~/mc-packwiz-deploy + cp -r /run/media/*/mc-packwiz-deploy/. ~/mc-packwiz-deploy/ + cd ~/mc-packwiz-deploy && chmod +x *.sh *.py + + 2. Edit deploy.conf. At minimum set MIRROR_URL and ACCEPT_EULA. + + 3. Check the machine is ready: + + ./install.sh --check + + 4. Install: + + ./install.sh + +Requires a ZFS dataset already mounted at the SHARE path in deploy.conf. +install.sh refuses to run otherwise, rather than filling the root disk. +TXT + + rm -f "$ISO" + "$mkisofs" -quiet -o "$ISO" -V "MCPACKWIZ" -J -r "$STAGE" \ + || die "$mkisofs failed" + + info "built $ISO" + ls -lh "$ISO" | sed 's/^/ /' + command -v sha256sum >/dev/null && sha256sum "$ISO" | tee "$ISO.sha256" | sed 's/^/ /' +fi diff --git a/deploy.conf.example b/deploy.conf.example new file mode 100644 index 0000000..d7616eb --- /dev/null +++ b/deploy.conf.example @@ -0,0 +1,34 @@ +# Deployment settings. Copy to deploy.conf next to install.sh and edit. +# install.sh reads this, so there are no flags to remember on the target host. + +# --- identity --------------------------------------------------------------- +PACK_NAME="AbdelpakDelta" # display name; the directory slug is derived +AUTHOR="themiro" +MC_VERSION="1.21.1" +NEOFORGE="latest" # or pin e.g. 21.1.248 + +# --- where things live ------------------------------------------------------ +SHARE="/minecraft" # ZFS dataset mountpoint + +# --- how clients reach it --------------------------------------------------- +# The public URL of the mods directory. Everything else is derived from it: +# the pack URL, the guide URLs, and the address shown to players. +MIRROR_URL="https://games.abdelbaki.eu/abdelpakDelta/mods" + +# --- ports ------------------------------------------------------------------ +MC_PORT="27960" # Minecraft +HTTP_PORT="18080" # pack/mod HTTP server, behind your proxy +HTTP_BIND="0.0.0.0" # 127.0.0.1 if the proxy is on this host + +# --- memory ----------------------------------------------------------------- +XMX="10G" +XMS="10G" + +# --- Minecraft EULA --------------------------------------------------------- +# Setting this to yes writes eula=true and is your acceptance of +# https://aka.ms/MinecraftEULA. Leave it as no and the install stops there. +ACCEPT_EULA="no" + +# --- optional --------------------------------------------------------------- +START_SERVER="no" # yes to start Minecraft at the end +CAP_ZFS_ARC="2147483648" # bytes; empty to leave ARC alone diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3538a1f --- /dev/null +++ b/install.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# +# Single entry point for deploying from removable media. +# +# ./install.sh reads deploy.conf next to this script +# ./install.sh -c other.conf reads a different config +# ./install.sh --check verify prerequisites and exit +# +# Copies itself off read-only media, installs prerequisites, and runs the +# deploy. Everything is configured in deploy.conf, so there are no flags to +# remember at the console. + +set -euo pipefail + +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CONF="$HERE/deploy.conf" +CHECK_ONLY=0 +WORKDIR="" + +die() { printf '\n\033[1;31merror:\033[0m %s\n\n' "$*" >&2; exit 1; } +step() { printf '\n\033[1m=== %s ===\033[0m\n\n' "$*"; } +info() { echo ">>> $*"; } +warn() { echo " warning: $*" >&2; } + +while [ $# -gt 0 ]; do + case "$1" in + -c|--config) CONF="${2:-}"; shift 2 ;; + --check) CHECK_ONLY=1; shift ;; + -w|--workdir) WORKDIR="${2:-}"; shift 2 ;; + -h|--help) awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; exit 0 ;; + *) die "unknown option: $1" ;; + esac +done + +[ "$(id -u)" -ne 0 ] || die "run this as your normal user, not root — it calls sudo itself" + +# ------------------------------------------------------------------ config --- + +[ -f "$CONF" ] || die "no config at $CONF + Copy deploy.conf.example to deploy.conf and edit it first." + +# shellcheck source=/dev/null +. "$CONF" + +for v in PACK_NAME AUTHOR MIRROR_URL SHARE; do + [ -n "${!v:-}" ] || die "$v is not set in $CONF" +done + +MC_VERSION="${MC_VERSION:-1.21.1}" +NEOFORGE="${NEOFORGE:-latest}" +MC_PORT="${MC_PORT:-25565}" +HTTP_PORT="${HTTP_PORT:-18080}" +HTTP_BIND="${HTTP_BIND:-0.0.0.0}" +XMX="${XMX:-10G}" +XMS="${XMS:-$XMX}" +ACCEPT_EULA="${ACCEPT_EULA:-no}" +START_SERVER="${START_SERVER:-no}" +CAP_ZFS_ARC="${CAP_ZFS_ARC:-}" + +# ------------------------------------------------------------- prequisites --- + +step "Checking prerequisites" + +MISSING=() +need() { command -v "$1" >/dev/null || MISSING+=("$2"); } +need java java-21-openjdk-headless +need curl curl +need go golang +need python3 python3 +need zfs zfs +need findmnt util-linux +need mkfifo coreutils + +if [ "${#MISSING[@]}" -gt 0 ]; then + info "missing: ${MISSING[*]}" + if (( CHECK_ONLY )); then + die "prerequisites missing (see above)" + fi + info "installing with dnf" + sudo dnf install -y "${MISSING[@]}" \ + || die "could not install: ${MISSING[*]} + Install them by hand and re-run." +else + info "all present" +fi + +jver="$(java -version 2>&1 | head -1 | grep -oE '"[0-9]+' | tr -d '"' || echo 0)" +[ "${jver:-0}" -ge 21 ] || warn "java is $jver; NeoForge for 1.21.x wants 21" + +if ! mountpoint -q "$SHARE"; then + die "$SHARE is not a mountpoint — the ZFS dataset is not mounted. + Import and mount the pool first: + sudo zpool import -a && sudo zfs mount -a + Then re-run. Deploying onto an unmounted path would fill the root disk." +fi +info "$SHARE is mounted ($(findmnt -no SOURCE "$SHARE"))" + +if (( CHECK_ONLY )); then + step "Prerequisites OK" + exit 0 +fi + +# --------------------------------------------------------------- workspace --- + +# The media is very likely read-only, and the deploy needs somewhere writable +# for the go build and the pack. Copy the kit onto the host first. +if [ -z "$WORKDIR" ]; then + if [ -w "$HERE" ]; then + WORKDIR="$HERE" + else + WORKDIR="$HOME/mc-packwiz-deploy" + info "media is read-only — copying the kit to $WORKDIR" + mkdir -p "$WORKDIR" + cp -f "$HERE"/*.sh "$HERE"/*.py "$WORKDIR"/ 2>/dev/null || true + cp -f "$CONF" "$WORKDIR/deploy.conf" + chmod +x "$WORKDIR"/*.sh "$WORKDIR"/*.py 2>/dev/null || true + fi +fi +cd "$WORKDIR" +[ -x ./deploy.sh ] || die "deploy.sh not found in $WORKDIR" + +# ----------------------------------------------------------------- zfs arc --- + +if [ -n "$CAP_ZFS_ARC" ]; then + if [ ! -f /etc/modprobe.d/zfs.conf ] || ! grep -q zfs_arc_max /etc/modprobe.d/zfs.conf; then + step "Capping the ZFS ARC at $CAP_ZFS_ARC bytes" + echo "options zfs zfs_arc_max=$CAP_ZFS_ARC" | sudo tee /etc/modprobe.d/zfs.conf >/dev/null + warn "the ARC cap needs a reboot (and 'sudo dracut --force') to take effect" + else + info "ZFS ARC already capped in /etc/modprobe.d/zfs.conf" + fi +fi + +# ------------------------------------------------------------------ deploy --- + +EULA_FLAG="" +case "${ACCEPT_EULA,,}" in + yes|true|1) EULA_FLAG="--accept-eula" ;; + *) die "ACCEPT_EULA is not set to yes in $CONF. + Read https://aka.ms/MinecraftEULA, then set ACCEPT_EULA=\"yes\" to accept it." ;; +esac + +step "Deploying $PACK_NAME" + +./deploy.sh \ + -n "$PACK_NAME" \ + -a "$AUTHOR" \ + -u "$MIRROR_URL" \ + -m "$MC_VERSION" \ + -N "$NEOFORGE" \ + -s "$SHARE" \ + -p "$MC_PORT" \ + -H "$HTTP_PORT" \ + -B "$HTTP_BIND" \ + -X "$XMX" \ + -x "$XMS" \ + $EULA_FLAG + +# ------------------------------------------------------------------- start --- + +case "${START_SERVER,,}" in + yes|true|1) + step "Starting Minecraft" + sudo systemctl start minecraft + sleep 3 + if systemctl is-active --quiet minecraft; then + info "minecraft.service is running" + else + warn "minecraft.service did not come up — journalctl -u minecraft -n 50" + fi + ;; +esac + +# ------------------------------------------------------------------- done ---- + +SITE="${MIRROR_URL%/*}" +HOSTONLY="$(sed -e 's#^[a-z]*://##' -e 's#/.*##' -e 's#:.*##' <<<"$SITE")" +[ "$MC_PORT" = "25565" ] && SHOWN="$HOSTONLY" || SHOWN="$HOSTONLY:$MC_PORT" + +step "Done" +cat < this) +Players connect to: $SHOWN +Setup guides: $SITE/setup-windows-packwiz.html + $SITE/setup-linux-packwiz.html + +Still to do by hand: + 1. Point your reverse proxy at ${HTTP_BIND}:${HTTP_PORT} + 2. Drop mod jars into $SHARE/mods + 3. Run $WORKDIR/mc-refresh-restart.sh to register them + + sudo systemctl start minecraft + journalctl -u minecraft -f + echo "op YourName" | sudo tee /run/minecraft/console +EOF