| FROM eclipse-temurin:17-jre-jammy | |
| USER root | |
| RUN apt-get update && \ | |
| apt-get install -y --no-install-recommends \ | |
| bash \ | |
| curl \ | |
| jq \ | |
| ca-certificates \ | |
| coreutils \ | |
| findutils \ | |
| python3 && \ | |
| rm -rf /var/lib/apt/lists/* | |
| # ββ Bake Vineflower into the image at build time ββββββββββββββββββββββββββββββ | |
| RUN curl -fL \ | |
| -o /app/vineflower.jar \ | |
| https://github.com/Vineflower/vineflower/releases/download/1.11.2/vineflower-1.11.2.jar && \ | |
| java -jar /app/vineflower.jar --help > /dev/null 2>&1 || true | |
| WORKDIR /app | |
| EXPOSE 7860 | |
| CMD ["bash", "-lc", "\ | |
| set -u; \ | |
| BASE=/data/minecraft; \ | |
| TMP=/tmp/mc-sync; \ | |
| MANIFEST_URL=https://piston-meta.mojang.com/mc/game/version_manifest_v2.json; \ | |
| MANIFEST_PATH=$BASE/version_manifest_v2.json; \ | |
| LOCKDIR=$BASE/.lock; \ | |
| LOG=$BASE/download.log; \ | |
| PROGRESS=$BASE/progress.txt; \ | |
| JOBS=${JOBS:-8}; \ | |
| DECOMPILE_JOBS=${DECOMPILE_JOBS:-4}; \ | |
| VINEFLOWER=/app/vineflower.jar; \ | |
| mkdir -p $BASE/server $BASE/client $TMP; \ | |
| printf 'starting\\n' > $PROGRESS; \ | |
| \ | |
| cat > /app/web.py <<'PY'\n\ | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\n\ | |
| import os, html\n\ | |
| BASE='/data/minecraft'\n\ | |
| class H(BaseHTTPRequestHandler):\n\ | |
| def do_GET(self):\n\ | |
| self.send_response(200)\n\ | |
| self.send_header('Content-Type','text/html; charset=utf-8')\n\ | |
| self.end_headers()\n\ | |
| prog='starting'\n\ | |
| log=''\n\ | |
| try:\n\ | |
| with open(os.path.join(BASE,'progress.txt'),'r',encoding='utf-8',errors='ignore') as f: prog=f.read()\n\ | |
| except: pass\n\ | |
| try:\n\ | |
| with open(os.path.join(BASE,'download.log'),'r',encoding='utf-8',errors='ignore') as f: log=''.join(f.readlines()[-200:])\n\ | |
| except: pass\n\ | |
| self.wfile.write(f'''<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"refresh\" content=\"2\"><title>Minecraft Sync</title><style>body{{font-family:Arial;background:#111;color:#eee;padding:20px}}pre{{white-space:pre-wrap;background:#000;padding:12px;border-radius:8px}}.c{{max-width:1100px;margin:auto}}</style></head><body><div class=\"c\"><h2>Minecraft Official Artifact Sync + Decompile</h2><h3>Progress</h3><pre>{html.escape(prog)}</pre><h3>Recent Logs</h3><pre>{html.escape(log)}</pre></div></body></html>'''.encode())\n\ | |
| def log_message(self,*a):\n\ | |
| return\n\ | |
| ThreadingHTTPServer(('0.0.0.0',7860),H).serve_forever()\n\ | |
| PY\n\ | |
| python3 /app/web.py & \ | |
| \ | |
| log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a $LOG; printf '%s\\n' \"$*\" > $PROGRESS; }; \ | |
| cleanup_tmp(){ rm -rf $TMP/*; }; \ | |
| acquire_lock(){ if mkdir $LOCKDIR 2>/dev/null; then trap 'rm -rf \"$LOCKDIR\"; rm -rf \"$TMP\"/*' EXIT INT TERM; return 0; fi; log 'Another sync appears to be running'; exit 1; }; \ | |
| download_atomic(){ \ | |
| url=\"$1\"; dest=\"$2\"; expected_sha1=\"${3:-}\"; \ | |
| mkdir -p \"$(dirname \"$dest\")\"; \ | |
| part=\"${dest}.part\"; \ | |
| rm -f \"$part\"; \ | |
| if ! curl -fL --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 3600 -A 'Mozilla/5.0' -o \"$part\" \"$url\"; then rm -f \"$part\"; return 1; fi; \ | |
| if [ ! -s \"$part\" ]; then rm -f \"$part\"; return 1; fi; \ | |
| if [ -n \"$expected_sha1\" ]; then \ | |
| actual_sha1=\"$(sha1sum \"$part\" | awk '{print $1}')\"; \ | |
| if [ \"$actual_sha1\" != \"$expected_sha1\" ]; then \ | |
| log \"SHA1 mismatch for $dest expected=$expected_sha1 actual=$actual_sha1\"; \ | |
| rm -f \"$part\"; return 1; \ | |
| fi; \ | |
| fi; \ | |
| mv -f \"$part\" \"$dest\"; return 0; \ | |
| }; \ | |
| is_valid_existing(){ \ | |
| file=\"$1\"; expected_sha1=\"${2:-}\"; \ | |
| [ -s \"$file\" ] || return 1; \ | |
| if [ -n \"$expected_sha1\" ]; then \ | |
| actual_sha1=\"$(sha1sum \"$file\" | awk '{print $1}')\"; \ | |
| [ \"$actual_sha1\" = \"$expected_sha1\" ] || return 1; \ | |
| fi; \ | |
| return 0; \ | |
| }; \ | |
| \ | |
| # ββ Download worker ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| cat > /app/worker.sh <<'SH'\n\ | |
| #!/bin/bash\n\ | |
| set -u\n\ | |
| BASE=\"$1\"\n\ | |
| TMP=\"$2\"\n\ | |
| LOG=\"$3\"\n\ | |
| PROGRESS=\"$4\"\n\ | |
| ITEM=\"$5\"\n\ | |
| log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a \"$LOG\"; printf '%s\\n' \"$*\" > \"$PROGRESS\"; }\n\ | |
| download_atomic(){\n\ | |
| url=\"$1\"; dest=\"$2\"; expected_sha1=\"${3:-}\"\n\ | |
| mkdir -p \"$(dirname \"$dest\")\"\n\ | |
| part=\"${dest}.part\"\n\ | |
| rm -f \"$part\"\n\ | |
| if ! curl -fL --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 3600 -A 'Mozilla/5.0' -o \"$part\" \"$url\"; then rm -f \"$part\"; return 1; fi\n\ | |
| if [ ! -s \"$part\" ]; then rm -f \"$part\"; return 1; fi\n\ | |
| if [ -n \"$expected_sha1\" ]; then\n\ | |
| actual_sha1=\"$(sha1sum \"$part\" | awk '{print $1}')\"\n\ | |
| [ \"$actual_sha1\" = \"$expected_sha1\" ] || { log \"SHA1 mismatch for $dest\"; rm -f \"$part\"; return 1; }\n\ | |
| fi\n\ | |
| mv -f \"$part\" \"$dest\"\n\ | |
| }\n\ | |
| is_valid_existing(){\n\ | |
| file=\"$1\"; expected_sha1=\"${2:-}\"\n\ | |
| [ -s \"$file\" ] || return 1\n\ | |
| if [ -n \"$expected_sha1\" ]; then\n\ | |
| actual_sha1=\"$(sha1sum \"$file\" | awk '{print $1}')\"\n\ | |
| [ \"$actual_sha1\" = \"$expected_sha1\" ] || return 1\n\ | |
| fi\n\ | |
| return 0\n\ | |
| }\n\ | |
| id=\"$(printf '%s' \"$ITEM\" | cut -d'|' -f1)\"\n\ | |
| meta_url=\"$(printf '%s' \"$ITEM\" | cut -d'|' -f2-)\"\n\ | |
| tmp_json=\"$TMP/${id}.json\"\n\ | |
| log \"$id fetching version metadata\"\n\ | |
| download_atomic \"$meta_url\" \"$tmp_json\" '' || { log \"$id failed metadata\"; exit 0; }\n\ | |
| for side in client server; do\n\ | |
| dl_url=\"$(jq -r \".downloads.${side}.url // empty\" \"$tmp_json\")\"\n\ | |
| dl_sha1=\"$(jq -r \".downloads.${side}.sha1 // empty\" \"$tmp_json\")\"\n\ | |
| [ -n \"$dl_url\" ] || { log \"$id [$side] no official download entry, skipping\"; continue; }\n\ | |
| dir=\"$BASE/$side/$id\"\n\ | |
| json_dest=\"$dir/$id.json\"\n\ | |
| jar_dest=\"$dir/${side}.jar\"\n\ | |
| mkdir -p \"$dir\"\n\ | |
| cp -f \"$tmp_json\" \"$json_dest\"\n\ | |
| if is_valid_existing \"$jar_dest\" \"$dl_sha1\"; then log \"$id [$side] already complete, skipping\"; continue; fi\n\ | |
| [ ! -e \"$jar_dest\" ] || rm -f \"$jar_dest\"\n\ | |
| log \"$id [$side] downloading jar\"\n\ | |
| if download_atomic \"$dl_url\" \"$jar_dest\" \"$dl_sha1\"; then log \"$id [$side] download complete\"; else rm -f \"$jar_dest\"; log \"$id [$side] download failed\"; fi\n\ | |
| done\n\ | |
| SH\n\ | |
| chmod +x /app/worker.sh; \ | |
| \ | |
| # ββ Decompile worker βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Layout per version/side: | |
| # /data/minecraft/{client|server}/{id}/ | |
| # {id}.json <- version metadata | |
| # {client|server}.jar | |
| # src/ <- vineflower output (java source tree) | |
| # .decompiled <- stamp: written only after a clean run | |
| cat > /app/decompile_worker.sh <<'SH'\n\ | |
| #!/bin/bash\n\ | |
| set -u\n\ | |
| LOG=\"$1\"\n\ | |
| PROGRESS=\"$2\"\n\ | |
| JAR=\"$3\"\n\ | |
| log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a \"$LOG\"; printf '%s\\n' \"$*\" > \"$PROGRESS\"; }\n\ | |
| \n\ | |
| dir=\"$(dirname \"$JAR\")\"\n\ | |
| side=\"$(basename \"$JAR\" .jar)\"\n\ | |
| id=\"$(basename \"$dir\")\"\n\ | |
| label=\"$id [$side]\"\n\ | |
| src_dir=\"$dir/src\"\n\ | |
| stamp=\"$dir/.decompiled\"\n\ | |
| \n\ | |
| # Skip if already successfully decompiled\n\ | |
| if [ -f \"$stamp\" ] && [ -d \"$src_dir\" ] && [ -n \"$(ls -A \"$src_dir\" 2>/dev/null)\" ]; then\n\ | |
| log \"$label decompile already done, skipping\"\n\ | |
| exit 0\n\ | |
| fi\n\ | |
| \n\ | |
| log \"$label starting decompile -> $src_dir\"\n\ | |
| rm -rf \"$src_dir\"\n\ | |
| mkdir -p \"$src_dir\"\n\ | |
| \n\ | |
| if java -jar /app/vineflower.jar \\\n\ | |
| --folder \\\n\ | |
| --log-level=WARN \\\n\ | |
| --thread-count=2 \\\n\ | |
| --decompile-generics=1 \\\n\ | |
| --decompile-enums=1 \\\n\ | |
| --decompile-inner=1 \\\n\ | |
| --use-lvt-names=1 \\\n\ | |
| --use-method-parameters=1 \\\n\ | |
| --override-annotation=1 \\\n\ | |
| --pattern-matching=1 \\\n\ | |
| --remove-synthetic=0 \\\n\ | |
| --remove-bridge=0 \\\n\ | |
| \"$JAR\" \"$src_dir\" >> \"$LOG\" 2>&1; then\n\ | |
| touch \"$stamp\"\n\ | |
| log \"$label decompile complete\"\n\ | |
| else\n\ | |
| log \"$label decompile FAILED (partial output left in $src_dir)\"\n\ | |
| exit 1\n\ | |
| fi\n\ | |
| SH\n\ | |
| chmod +x /app/decompile_worker.sh; \ | |
| \ | |
| # ββ Main flow ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| acquire_lock; \ | |
| \ | |
| log 'Starting Minecraft artifact sync'; \ | |
| log 'Downloading global version manifest'; \ | |
| if ! download_atomic \"$MANIFEST_URL\" \"$MANIFEST_PATH\" ''; then log 'Failed to download manifest'; exit 1; fi; \ | |
| TOTAL=\"$(jq '.versions | length' \"$MANIFEST_PATH\")\"; \ | |
| log \"Manifest contains $TOTAL versions\"; \ | |
| \ | |
| log \"Phase 1/2 β downloading all jars (parallel jobs=$JOBS)\"; \ | |
| jq -r '.versions[] | \"\\(.id)|\\(.url)\"' \"$MANIFEST_PATH\" \ | |
| | xargs -I{} -P \"$JOBS\" bash /app/worker.sh \"$BASE\" \"$TMP\" \"$LOG\" \"$PROGRESS\" '{}'; \ | |
| \ | |
| log 'Phase 1/2 complete β all jars downloaded'; \ | |
| \ | |
| log \"Phase 2/2 β decompiling all jars with Vineflower (parallel jobs=$DECOMPILE_JOBS, 2 threads each)\"; \ | |
| find \"$BASE/client\" \"$BASE/server\" \ | |
| -mindepth 2 -maxdepth 2 \ | |
| -name '*.jar' \ | |
| ! -name '*.part' \ | |
| | sort \ | |
| | xargs -I{} -P \"$DECOMPILE_JOBS\" bash /app/decompile_worker.sh \"$LOG\" \"$PROGRESS\" '{}'; \ | |
| \ | |
| log 'Phase 2/2 complete β all decompilation finished'; \ | |
| log 'Sync + decompile complete'; \ | |
| wait \ | |
| "] |