Compare commits
10
Commits
c279f35e14
...
6e91749f59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e91749f59 | ||
|
|
853405afe7 | ||
|
|
f7858d4ec4 | ||
|
|
39be74d028 | ||
|
|
c40cbe7594 | ||
|
|
58f0312018 | ||
|
|
8c3d85f691 | ||
|
|
2405eee57d | ||
|
|
e1ccdda961 | ||
|
|
5172316919 |
No files matched your search
@@ -17,3 +17,13 @@ ENABLE_MDNS=false
|
||||
RCON_PASSWORD=change-me-to-something-random
|
||||
# Only needed if using CurseForge-exclusive mods:
|
||||
# CF_API_KEY=your-curseforge-api-key
|
||||
|
||||
# ── Blessing Skin auth variant (docker-compose.blessingskin.yml) ──────
|
||||
# Only needed when running the Blessing Skin override instead of Drasl.
|
||||
BS_DB_NAME=blessingskin
|
||||
BS_DB_USER=blessingskin
|
||||
BS_DB_PASSWORD=change-me-bs-db
|
||||
BS_DB_ROOT_PASSWORD=change-me-bs-root
|
||||
# Generate once and paste here so it survives container recreates:
|
||||
# docker run --rm azusamikan/blessing-skin-server-docker:latest php artisan key:generate --show
|
||||
BS_APP_KEY=base64:replace-with-generated-key
|
||||
@@ -6,6 +6,11 @@ runtime/authlib-injector.jar
|
||||
# rendered configs (from tooling/render-config.sh)
|
||||
drasl/config/config.toml
|
||||
dnsmasq/dnsmasq.conf
|
||||
# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh
|
||||
# (domain-dependent build output); source of truth is the .tmpl + custom/ jars.
|
||||
pack/mods/*.pw.toml
|
||||
pack/index.toml
|
||||
pack/CustomSkinLoader/CustomSkinLoader.json
|
||||
|
||||
# vendored / mirrored binaries
|
||||
mirror/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Core ingress — Blessing Skin variant. Replaces 00-core.caddy when the
|
||||
# docker-compose.blessingskin.yml override is in play (mounted at the same
|
||||
# target path). auth.* now points at Blessing Skin instead of Drasl.
|
||||
#
|
||||
# Blessing Skin serves BOTH its web UI (/) and the Yggdrasil API (/api/yggdrasil)
|
||||
# on the same vhost, so a single reverse_proxy covers everything.
|
||||
http://auth.{$BASE_DOMAIN} {
|
||||
reverse_proxy blessing-skin:80
|
||||
}
|
||||
|
||||
http://pack.{$BASE_DOMAIN} {
|
||||
root * /srv/pack
|
||||
file_server browse
|
||||
}
|
||||
@@ -3,7 +3,7 @@ http://auth.{$BASE_DOMAIN} {
|
||||
reverse_proxy drasl:25585
|
||||
}
|
||||
|
||||
http://packwiz.{$BASE_DOMAIN} {
|
||||
http://pack.{$BASE_DOMAIN} {
|
||||
root * /srv/pack
|
||||
file_server browse
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,103 @@
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Blessing Skin auth variant — use INSTEAD of Drasl.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Layer this over the base file:
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.blessingskin.yml up -d
|
||||
#
|
||||
# What it does vs the base stack:
|
||||
# - drasl → left running but UNUSED + unreachable (Caddy no longer
|
||||
# routes to it; it has no host port). Compose merges
|
||||
# depends_on, so it can't be removed from an override —
|
||||
# idling it is the clean option. `docker compose ... stop
|
||||
# drasl` after up if you want it down.
|
||||
# - caddy → auth.* now reverse-proxies blessing-skin:80
|
||||
# - mariadb → new; Blessing Skin's datastore (no SQLite support)
|
||||
# - blessing-skin → new; PHP skin server + yggdrasil-api plugin
|
||||
# - minecraft → authlib-injector now points at /api/yggdrasil,
|
||||
# ONLINE_MODE=TRUE (real session validation against BSS)
|
||||
#
|
||||
# First-run is a WEB WIZARD — see plan/03b-blessing-skin.md. The yggdrasil-api
|
||||
# plugin must be installed + enabled from the BSS admin panel before the
|
||||
# /api/yggdrasil endpoint (and thus Minecraft login) works.
|
||||
#
|
||||
# DB creds + APP_KEY come from .env (see .env.example: BS_* vars).
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
caddy:
|
||||
# Swap the core ingress conf for the Blessing Skin one. Compose MERGES
|
||||
# volumes by container path, so mounting the BSS conf at the SAME target
|
||||
# (/etc/caddy/conf.d/00-core.caddy) shadows the base 00-core.caddy; all
|
||||
# other base mounts (pack, custom, static, ca) are kept automatically.
|
||||
volumes:
|
||||
- ./caddy/conf.d/00-core-blessingskin.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
depends_on:
|
||||
- blessing-skin
|
||||
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
container_name: mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_DATABASE: ${BS_DB_NAME}
|
||||
MARIADB_USER: ${BS_DB_USER}
|
||||
MARIADB_PASSWORD: ${BS_DB_PASSWORD}
|
||||
MARIADB_ROOT_PASSWORD: ${BS_DB_ROOT_PASSWORD}
|
||||
TZ: "Europe/Madrid"
|
||||
volumes:
|
||||
- bs_db:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
blessing-skin:
|
||||
image: azusamikan/blessing-skin-server-docker:latest
|
||||
container_name: blessing-skin
|
||||
restart: unless-stopped
|
||||
# Internal-only — reached via Caddy at auth.${BASE_DOMAIN}.
|
||||
environment:
|
||||
APP_URL: "http://auth.${BASE_DOMAIN}"
|
||||
# Persist APP_KEY across recreates (else sessions/encryption break).
|
||||
# Generate once: docker run --rm azusamikan/blessing-skin-server-docker:latest php artisan key:generate --show
|
||||
APP_KEY: ${BS_APP_KEY}
|
||||
DB_DRIVER: "mysql"
|
||||
DB_HOST: "mariadb"
|
||||
DB_PORT: "3306"
|
||||
DB_DATABASE: ${BS_DB_NAME}
|
||||
DB_USERNAME: ${BS_DB_USER}
|
||||
DB_PASSWORD: ${BS_DB_PASSWORD}
|
||||
TZ: "Europe/Madrid"
|
||||
volumes:
|
||||
- bs_storage:/app/storage # uploaded skins/capes + textures
|
||||
- bs_plugins:/app/plugins # yggdrasil-api plugin lives here
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- mcnet
|
||||
|
||||
minecraft:
|
||||
environment:
|
||||
# authlib-injector API root for Blessing Skin's yggdrasil-api plugin
|
||||
# is <site>/api/yggdrasil (NOT /authlib-injector like Drasl).
|
||||
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=http://auth.${BASE_DOMAIN}/api/yggdrasil"
|
||||
# authlib-injector needs real auth: online-mode TRUE so the server
|
||||
# validates sessions against Blessing Skin. (Drasl variant used FALSE.)
|
||||
ONLINE_MODE: "TRUE"
|
||||
# Keep FALSE for safety on 1.21+. The yggdrasil-api plugin DOES sign
|
||||
# profile keys, so you MAY flip this to TRUE if signed chat is wanted.
|
||||
ENFORCE_SECURE_PROFILE: "FALSE"
|
||||
# Merged with the base depends_on (drasl, caddy); just adds blessing-skin so
|
||||
# Minecraft starts after the skin server is up.
|
||||
depends_on:
|
||||
- blessing-skin
|
||||
|
||||
volumes:
|
||||
bs_db:
|
||||
bs_storage:
|
||||
bs_plugins:
|
||||
+11
-4
@@ -26,7 +26,15 @@ services:
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./caddy/conf.d/00-core.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
||||
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./mirror/launcher:/srv/launcher:ro
|
||||
- ./caddy/caddy-root-ca.crt:/srv/ca-pub/ca.crt:ro
|
||||
- ./pack:/srv/pack:ro
|
||||
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
|
||||
# doesn't index them as direct files — they're external download targets
|
||||
# referenced by pack/mods/*.pw.toml. Served at pack.${BASE_DOMAIN}/custom/.
|
||||
- ./custom:/srv/pack/custom:ro
|
||||
networks:
|
||||
mcnet:
|
||||
# Only the stack's own subdomains in the base. The upstream spoof-host
|
||||
@@ -35,8 +43,7 @@ services:
|
||||
# containers, breaking the ONLINE NeoForge install during pre-bake.
|
||||
aliases:
|
||||
- "auth.${BASE_DOMAIN}"
|
||||
- "packwiz.${BASE_DOMAIN}"
|
||||
|
||||
- "pack.${BASE_DOMAIN}"
|
||||
drasl:
|
||||
image: unmojang/drasl:latest
|
||||
container_name: drasl
|
||||
@@ -81,7 +88,7 @@ services:
|
||||
# ── Server config ───────────────────────────────────────────
|
||||
DIFFICULTY: "normal"
|
||||
MODE: "survival"
|
||||
MOTD: "LAN party — kitchen sink"
|
||||
MOTD: "Uli LAN party"
|
||||
MAX_PLAYERS: "10"
|
||||
VIEW_DISTANCE: "10"
|
||||
SIMULATION_DISTANCE: "8"
|
||||
@@ -89,7 +96,7 @@ services:
|
||||
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
||||
|
||||
# ── Mod source: packwiz pack served by caddy ────────────────
|
||||
PACKWIZ_URL: "http://packwiz.${BASE_DOMAIN}/pack.toml"
|
||||
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
|
||||
|
||||
# ── RCON for remote admin ───────────────────────────────────
|
||||
ENABLE_RCON: "TRUE"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Template sources for pack metadata/config — rendered to their real files by
|
||||
# tooling/render-pack.sh. packwiz must NOT index the templates themselves.
|
||||
*.tmpl
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"enable": true,
|
||||
"loadlist": [
|
||||
{
|
||||
"name": "Ulicraft Skins",
|
||||
"type": "MojangAPI",
|
||||
"apiRoot": "https://auth.${BASE_DOMAIN}/account/",
|
||||
"sessionRoot": "https://auth.${BASE_DOMAIN}/session/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
hash-format = "sha256"
|
||||
files = []
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "balm-neoforge-1.21.1-21.0.58"
|
||||
filename = "balm-neoforge-1.21.1-21.0.58.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/balm-neoforge-1.21.1-21.0.58.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "aa2463747bad4c2351869226ea7bce7839510dfd2ac387f8d1588b1ce9309c67"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "CustomSkinLoader_Universal-14.28"
|
||||
filename = "CustomSkinLoader_Universal-14.28.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/CustomSkinLoader_Universal-14.28.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "0008fb0a91dd1e84d52162cfe96addb5f6aed3b76d426ea7305d7624e31f1894"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "inventoryessentials-neoforge-1.21.1-21.1.15"
|
||||
filename = "inventoryessentials-neoforge-1.21.1-21.1.15.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/inventoryessentials-neoforge-1.21.1-21.1.15.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "b7a5c5526a45256551543419ba2b5d54e56c585d76a645a8cea8cb8ea843ae2c"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "Jade-1.21.1-NeoForge-15.10.5"
|
||||
filename = "Jade-1.21.1-NeoForge-15.10.5.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/Jade-1.21.1-NeoForge-15.10.5.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "067bb4b007e1d6f6b79f0afe99c91252aa825472b99a76d33a60d24442f9e92d"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "JadeAddons-1.21.1-NeoForge-6.1.0"
|
||||
filename = "JadeAddons-1.21.1-NeoForge-6.1.0.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/JadeAddons-1.21.1-NeoForge-6.1.0.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "5f242668ad710092964d4d9afda8a94fa2401665ea13eea67d55d07e4dff8a64"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "jei-1.21.1-neoforge-19.27.0.340"
|
||||
filename = "jei-1.21.1-neoforge-19.27.0.340.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/jei-1.21.1-neoforge-19.27.0.340.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "8aaf547432f1b4958239b036356b910692fe40f858c3073d996f56bbf7c99826"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "JustEnoughProfessions-neoforge-1.21.1-4.0.5"
|
||||
filename = "JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "b320b47f1791f56df9e1a05138f0e312e6e54541945ff15f2f1fd398cd1d4d9d"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "JustEnoughResources-NeoForge-1.21.1-1.6.0.17"
|
||||
filename = "JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "7a47d69b5530704690d5a5f0726cd74a2d6c57df17dd4a8589c4ab5212f91460"
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "MouseTweaks-neoforge-mc1.21-2.26.1"
|
||||
filename = "MouseTweaks-neoforge-mc1.21-2.26.1.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "http://pack.${BASE_DOMAIN}/custom/MouseTweaks-neoforge-mc1.21-2.26.1.jar"
|
||||
hash-format = "sha256"
|
||||
hash = "68e6f4201c5de97b77929a7215c9552495696ca6a3bf3ae4eacc34e135f6cc8b"
|
||||
+1
-1
@@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "7b81c9e9d9e6e21a9865a2dded0dfd77e7683d679b98fe5b0114eaed9a31f17e"
|
||||
hash = "9bca0b1a4434050835cab46116d7c14aafec971ff5a95a1ee0c4232c8bcbc355"
|
||||
|
||||
[versions]
|
||||
minecraft = "1.21.1"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Blessing Skin — auth + skin server (Drasl alternative)
|
||||
|
||||
## Summary
|
||||
|
||||
Drop-in alternative to Drasl (`plan/03-drasl.md`). Blessing Skin Server (BSS) is
|
||||
a PHP skin station; the **yggdrasil-api plugin** gives it a Yggdrasil API that
|
||||
authlib-injector talks to — same end result as Drasl, different internals.
|
||||
|
||||
Run it as an override **instead of** Drasl:
|
||||
|
||||
```
|
||||
docker compose -f docker-compose.yml -f docker-compose.blessingskin.yml up -d
|
||||
```
|
||||
|
||||
The override (`docker-compose.blessingskin.yml`) repoints Caddy's `auth.*` vhost
|
||||
at `blessing-skin:80`, adds `mariadb` + `blessing-skin`, and switches the
|
||||
Minecraft authlib endpoint to `/api/yggdrasil`. Drasl stays defined but idle and
|
||||
unreachable (compose merges `depends_on`, so an override can't remove it);
|
||||
`docker compose … stop drasl` after up if you want it down.
|
||||
|
||||
## Drasl vs Blessing Skin — what changes
|
||||
|
||||
| | Drasl | Blessing Skin |
|
||||
|---|---|---|
|
||||
| Backend | single Go binary | PHP app + **MariaDB** (no SQLite) |
|
||||
| Config | rendered `config.toml` | **web install wizard** + DB |
|
||||
| Yggdrasil API | built in | **yggdrasil-api plugin** (install + enable) |
|
||||
| authlib endpoint | `…/authlib-injector` | `…/api/yggdrasil` |
|
||||
| OIDC/Keycloak | supported (future) | not native (OAuth plugins exist) |
|
||||
| Server online-mode | repo used FALSE | **TRUE** (real session validation) |
|
||||
|
||||
## Setup (first run)
|
||||
|
||||
1. `cp .env.example .env`, fill the `BS_*` vars. Generate `BS_APP_KEY`:
|
||||
```
|
||||
docker run --rm azusamikan/blessing-skin-server-docker:latest \
|
||||
php artisan key:generate --show
|
||||
```
|
||||
2. Bring the stack up with the override (command above).
|
||||
3. Open `http://auth.${BASE_DOMAIN}` → BSS install wizard. DB host = `mariadb`,
|
||||
port `3306`, name/user/pass = the `BS_*` values. Create the admin account.
|
||||
4. Admin panel → **Plugins → Plugin Market** → install **yggdrasil-api** →
|
||||
enable it. (Needs internet; for air-gap, pre-place the plugin jar/zip into
|
||||
the `bs_plugins` volume.) After enabling, the API root is live at
|
||||
`http://auth.${BASE_DOMAIN}/api/yggdrasil`.
|
||||
5. Each player: register on the BSS site, add a character whose name == their
|
||||
in-game player name, upload a skin.
|
||||
6. Restart minecraft if it came up before the plugin was enabled.
|
||||
|
||||
## Client (Prism / authlib-injector)
|
||||
|
||||
- authlib-injector URL: `http://auth.${BASE_DOMAIN}/api/yggdrasil`
|
||||
- Login = BSS **email + password** (BSS uses email as the account id).
|
||||
- Must match the server's `-javaagent` arg exactly (same host + path).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`/api/yggdrasil`, not `/authlib-injector`.** Wrong path → silent "Invalid
|
||||
session". Server `JVM_OPTS` and every client must agree.
|
||||
- **ONLINE_MODE=TRUE.** authlib-injector only authenticates when online-mode is
|
||||
on; the server then validates the join against BSS. (The Drasl variant in this
|
||||
repo set FALSE — do not copy that here.)
|
||||
- **Secure profile (1.21+).** Override ships `ENFORCE_SECURE_PROFILE=FALSE` for
|
||||
safety. yggdrasil-api *does* sign profile keys, so TRUE may work — flip and
|
||||
test if signed chat matters; revert on "Invalid signature".
|
||||
- **APP_KEY must persist.** Set `BS_APP_KEY` in `.env`. Losing it invalidates
|
||||
all sessions and breaks encrypted data.
|
||||
- **DB is the source of truth.** `bs_db` volume holds accounts; `bs_storage`
|
||||
holds skins; `bs_plugins` holds the yggdrasil-api plugin. Back these up.
|
||||
- **Air-gap:** the plugin market needs internet. Install + enable yggdrasil-api
|
||||
while online (it persists in `bs_plugins`), or stage the plugin manually.
|
||||
|
||||
## Image note
|
||||
|
||||
`azusamikan/blessing-skin-server-docker:latest` is a community image and the
|
||||
env-var names (`DB_*`, `APP_KEY`) may differ slightly by tag — if auto-config
|
||||
doesn't take, the web wizard is the reliable path. Official source:
|
||||
https://github.com/bs-community/blessing-skin-server ,
|
||||
plugin: https://github.com/bs-community/yggdrasil-api , authlib-injector setup:
|
||||
https://github.com/bs-community/blessing-skin-manual/blob/master/man/yggdrasil-api/authlib-injector.md
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/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-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)"
|
||||
@@ -19,3 +19,7 @@ render() { # $1=template $2=output
|
||||
|
||||
render drasl/config/config.toml.tmpl drasl/config/config.toml
|
||||
render dnsmasq/dnsmasq.conf.tmpl dnsmasq/dnsmasq.conf
|
||||
|
||||
# pack templates (mods/*.pw.toml.tmpl, CustomSkinLoader, …) + packwiz index.
|
||||
# Delegated to render-pack.sh (BASE_DOMAIN only — single source of pack render).
|
||||
tooling/render-pack.sh
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-pack.sh — render every pack template (pack/**/*.tmpl) by injecting
|
||||
# ${BASE_DOMAIN}, then rebuild the packwiz index.
|
||||
#
|
||||
# Pack templates are domain-dependent build output (download URLs, skin API
|
||||
# roots, …). This renderer needs ONLY ${BASE_DOMAIN} — no dnsmasq HOST_LAN_IP —
|
||||
# so it can run while curating mods before the full deploy config exists.
|
||||
# Both tooling/render-config.sh (deploy) and tooling/add-custom-mod.sh call it.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
||||
command -v envsubst >/dev/null 2>&1 || { echo "missing required tool: envsubst (gettext)" >&2; exit 1; }
|
||||
command -v packwiz >/dev/null 2>&1 || { echo "missing required tool: packwiz" >&2; exit 1; }
|
||||
[ -f pack/pack.toml ] || { echo "pack/pack.toml not found" >&2; exit 1; }
|
||||
|
||||
shopt -s globstar nullglob
|
||||
tmpls=(pack/**/*.tmpl)
|
||||
shopt -u globstar nullglob
|
||||
|
||||
for t in "${tmpls[@]}"; do
|
||||
# Substitute ONLY ${BASE_DOMAIN} so any other literal $-syntax is preserved.
|
||||
envsubst '${BASE_DOMAIN}' < "$t" > "${t%.tmpl}"
|
||||
echo "rendered ${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 )
|
||||
echo "packwiz index refreshed (${#tmpls[@]} template(s))"
|
||||
Reference in new issue
Block a user