Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- Dockerfile +36 -0
- README.md +39 -5
- app.py +565 -0
Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
# Только JDK 17 — он умеет запускать и 1.12.2 и 1.20.1
|
| 7 |
+
# JDK 17 с флагом --add-opens совместим с Gradle 4.x
|
| 8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
openjdk-17-jdk-headless \
|
| 10 |
+
wget unzip curl \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
| 14 |
+
|
| 15 |
+
# Только один Gradle — 4.10.3 (работает для обеих версий через wrapper)
|
| 16 |
+
RUN wget -q https://services.gradle.org/distributions/gradle-4.10.3-bin.zip \
|
| 17 |
+
&& unzip -q gradle-4.10.3-bin.zip -d /opt/gradle \
|
| 18 |
+
&& rm gradle-4.10.3-bin.zip
|
| 19 |
+
ENV PATH="/opt/gradle/gradle-4.10.3/bin:${PATH}"
|
| 20 |
+
|
| 21 |
+
# Python зависимости
|
| 22 |
+
RUN pip install --no-cache-dir \
|
| 23 |
+
fastapi==0.104.1 \
|
| 24 |
+
uvicorn==0.24.0 \
|
| 25 |
+
python-multipart==0.0.6
|
| 26 |
+
|
| 27 |
+
WORKDIR /app
|
| 28 |
+
COPY app.py .
|
| 29 |
+
|
| 30 |
+
RUN useradd -m -u 1000 user && \
|
| 31 |
+
mkdir -p /tmp/fb && \
|
| 32 |
+
chown -R user:user /tmp/fb /app
|
| 33 |
+
USER user
|
| 34 |
+
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
CMD ["python3", "app.py"]
|
README.md
CHANGED
|
@@ -1,11 +1,45 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ForgeBuilder
|
| 3 |
+
emoji: 🔨
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
short_description: ZIP to JAR for Minecraft Forge mods
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# ForgeBuilder 🔨
|
| 13 |
+
|
| 14 |
+
**Собери Minecraft Forge мод прямо в браузере — без установки Java и Gradle.**
|
| 15 |
+
|
| 16 |
+
## Как использовать
|
| 17 |
+
|
| 18 |
+
1. Выбери версию Minecraft (1.12.2 или 1.20.1)
|
| 19 |
+
2. Загрузи ZIP с исходниками мода (должен содержать `build.gradle` и `src/`)
|
| 20 |
+
3. Нажми "Собрать JAR"
|
| 21 |
+
4. Скачай готовый `.jar` файл
|
| 22 |
+
|
| 23 |
+
## Требования к ZIP
|
| 24 |
+
|
| 25 |
+
Структура должна быть такой:
|
| 26 |
+
```
|
| 27 |
+
ваш-мод.zip
|
| 28 |
+
├── build.gradle ← обязательно
|
| 29 |
+
├── src/
|
| 30 |
+
│ └── main/
|
| 31 |
+
│ ├── java/ ← ваш Java код
|
| 32 |
+
│ └── resources/ ← текстуры, lang файлы
|
| 33 |
+
└── gradlew ← необязательно
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Ограничения
|
| 37 |
+
|
| 38 |
+
- Максимальный размер ZIP: 50 MB
|
| 39 |
+
- JAR хранится 1 час после сборки
|
| 40 |
+
- Одновременно обрабатывается не более 3 сборок
|
| 41 |
+
|
| 42 |
+
## Безопасность
|
| 43 |
+
|
| 44 |
+
Все загружаемые файлы проверяются на наличие вредоносного кода.
|
| 45 |
+
Исходные файлы удаляются сразу после сборки.
|
app.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ForgeBuilder — Hugging Face Spaces
|
| 3 |
+
Полный бэкенд + встроенный фронтенд в одном файле.
|
| 4 |
+
Запускается как: python app.py
|
| 5 |
+
"""
|
| 6 |
+
import asyncio, os, shutil, subprocess, time, uuid, zipfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, Request
|
| 9 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 10 |
+
import uvicorn
|
| 11 |
+
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
# ── Dirs ──────────────────────────────────────────────────────────────────────
|
| 15 |
+
BASE = Path("/tmp/fb")
|
| 16 |
+
JOBS_DIR = BASE / "jobs"
|
| 17 |
+
JARS_DIR = BASE / "jars"
|
| 18 |
+
CACHE_DIR = BASE / "gradle-cache"
|
| 19 |
+
for d in [JOBS_DIR, JARS_DIR, CACHE_DIR]:
|
| 20 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 21 |
+
|
| 22 |
+
jobs: dict[str, dict] = {}
|
| 23 |
+
|
| 24 |
+
VERSIONS = {
|
| 25 |
+
"1.12.2": {"jdk": "/usr/lib/jvm/java-8-openjdk-amd64", "forge": "1.12.2-14.23.5.2860", "mappings": "stable_39"},
|
| 26 |
+
"1.20.1": {"jdk": "/usr/lib/jvm/java-17-openjdk-amd64", "forge": "1.20.1-47.2.0", "mappings": "official"},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
MAX_ZIP = 50 * 1024 * 1024
|
| 30 |
+
BAD_EXT = {".exe",".sh",".bat",".ps1",".cmd",".php",".rb"}
|
| 31 |
+
|
| 32 |
+
# ── HTML (весь фронтенд встроен сюда) ────────────────────────────────────────
|
| 33 |
+
HTML = r"""<!DOCTYPE html>
|
| 34 |
+
<html lang="ru">
|
| 35 |
+
<head>
|
| 36 |
+
<meta charset="UTF-8">
|
| 37 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 38 |
+
<title>ForgeBuilder — ZIP → JAR</title>
|
| 39 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Unbounded:wght@700;900&display=swap" rel="stylesheet">
|
| 40 |
+
<style>
|
| 41 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 42 |
+
:root{
|
| 43 |
+
--bg:#09090d;--surf:#111118;--surf2:#18181f;--brd:#252530;--brd2:#35353f;
|
| 44 |
+
--acc:#e8572a;--acc2:#ff7a4d;--grn:#27ae60;--red:#e74c3c;--ylw:#f39c12;
|
| 45 |
+
--txt:#e4e4f0;--mut:#5e5e78;
|
| 46 |
+
--mono:'JetBrains Mono',monospace;--disp:'Unbounded',sans-serif;
|
| 47 |
+
}
|
| 48 |
+
body{background:var(--bg);color:var(--txt);font-family:var(--mono);font-size:14px;line-height:1.6;min-height:100vh}
|
| 49 |
+
.wrap{max-width:820px;margin:0 auto;padding:0 18px}
|
| 50 |
+
|
| 51 |
+
/* header */
|
| 52 |
+
header{padding:44px 0 36px;border-bottom:1px solid var(--brd);margin-bottom:44px}
|
| 53 |
+
.logo{font-family:var(--disp);font-size:clamp(26px,6vw,46px);font-weight:900;letter-spacing:-.03em;line-height:1;margin-bottom:10px}
|
| 54 |
+
.logo span{color:var(--acc)}
|
| 55 |
+
.tag{color:var(--mut);font-size:12px;letter-spacing:.1em;text-transform:uppercase}
|
| 56 |
+
|
| 57 |
+
/* cards */
|
| 58 |
+
.card{background:var(--surf);border:1px solid var(--brd);border-radius:12px;padding:28px;margin-bottom:18px}
|
| 59 |
+
.ctitle{font-family:var(--disp);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--mut);margin-bottom:18px;display:flex;align-items:center;gap:8px}
|
| 60 |
+
.ctitle::before{content:'';width:3px;height:13px;background:var(--acc);border-radius:2px;display:inline-block}
|
| 61 |
+
|
| 62 |
+
/* version */
|
| 63 |
+
.vgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 64 |
+
.vbtn{background:var(--surf2);border:1px solid var(--brd);border-radius:8px;padding:15px 18px;cursor:pointer;color:var(--txt);font-family:var(--mono);text-align:left;transition:all .15s}
|
| 65 |
+
.vbtn:hover{border-color:var(--acc)}
|
| 66 |
+
.vbtn.on{border-color:var(--acc);background:#1a1016;box-shadow:0 0 0 1px var(--acc)}
|
| 67 |
+
.vnum{font-family:var(--disp);font-size:19px;font-weight:700;color:var(--acc2);display:block;margin-bottom:3px}
|
| 68 |
+
.vlbl{font-size:11px;color:var(--mut);text-transform:uppercase;letter-spacing:.07em}
|
| 69 |
+
.vbtn.on .vlbl{color:var(--acc)}
|
| 70 |
+
|
| 71 |
+
/* drop */
|
| 72 |
+
.dz{border:2px dashed var(--brd2);border-radius:10px;padding:44px 20px;text-align:center;cursor:pointer;transition:all .2s;background:var(--surf2);position:relative}
|
| 73 |
+
.dz:hover,.dz.over{border-color:var(--acc);background:#14100d}
|
| 74 |
+
.dz input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
|
| 75 |
+
.dico{font-size:36px;margin-bottom:10px;display:block}
|
| 76 |
+
.dtitle{font-family:var(--disp);font-size:15px;font-weight:700;margin-bottom:5px}
|
| 77 |
+
.dsub{color:var(--mut);font-size:12px}
|
| 78 |
+
.finfo{display:none;align-items:center;gap:10px;background:var(--surf2);border:1px solid var(--grn);border-radius:8px;padding:12px 16px;margin-top:10px}
|
| 79 |
+
.finfo.show{display:flex}
|
| 80 |
+
.fick{color:var(--grn)}
|
| 81 |
+
.fname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
|
| 82 |
+
.fsz{color:var(--mut);font-size:12px}
|
| 83 |
+
|
| 84 |
+
/* warn */
|
| 85 |
+
.warn{background:#161200;border:1px solid var(--ylw);border-radius:6px;padding:10px 14px;font-size:12px;color:var(--ylw);margin-top:10px;display:flex;gap:8px}
|
| 86 |
+
|
| 87 |
+
/* info grid */
|
| 88 |
+
.igrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:16px}
|
| 89 |
+
.iitem{background:var(--surf2);border:1px solid var(--brd);border-radius:8px;padding:14px}
|
| 90 |
+
.ilbl{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);margin-bottom:5px}
|
| 91 |
+
.ival{font-size:13px;font-weight:600;color:var(--acc2)}
|
| 92 |
+
|
| 93 |
+
/* build btn */
|
| 94 |
+
.bbtn{width:100%;padding:17px;background:var(--acc);color:#fff;border:none;border-radius:10px;font-family:var(--disp);font-size:14px;font-weight:700;letter-spacing:.05em;cursor:pointer;transition:all .15s;text-transform:uppercase}
|
| 95 |
+
.bbtn:hover:not(:disabled){background:var(--acc2);transform:translateY(-1px)}
|
| 96 |
+
.bbtn:disabled{opacity:.38;cursor:not-allowed;transform:none}
|
| 97 |
+
|
| 98 |
+
/* progress */
|
| 99 |
+
#psec{display:none}
|
| 100 |
+
#psec.show{display:block}
|
| 101 |
+
.plbl{display:flex;justify-content:space-between;font-size:12px;color:var(--mut);margin-bottom:7px}
|
| 102 |
+
.ptrack{background:var(--surf2);border-radius:4px;height:5px;overflow:hidden}
|
| 103 |
+
.pbar{height:100%;background:linear-gradient(90deg,var(--acc),var(--acc2));border-radius:4px;width:0%;transition:width .5s ease}
|
| 104 |
+
.logbox{background:#050509;border:1px solid var(--brd);border-radius:8px;padding:16px;height:260px;overflow-y:auto;font-size:12px;line-height:1.9;margin-top:14px}
|
| 105 |
+
.logbox::-webkit-scrollbar{width:3px}
|
| 106 |
+
.logbox::-webkit-scrollbar-thumb{background:var(--brd2);border-radius:2px}
|
| 107 |
+
.ll{display:flex;gap:10px}
|
| 108 |
+
.lt{color:var(--mut);min-width:56px}
|
| 109 |
+
.lm{flex:1}
|
| 110 |
+
.lm.ok{color:var(--grn)}.lm.er{color:var(--red)}.lm.wn{color:var(--ylw)}.lm.in{color:#89b4fa}
|
| 111 |
+
|
| 112 |
+
/* result */
|
| 113 |
+
#rsec{display:none}
|
| 114 |
+
#rsec.show{display:block}
|
| 115 |
+
.rsucc{background:#08130d;border:1px solid var(--grn);border-radius:10px;padding:28px;text-align:center}
|
| 116 |
+
.rerr{background:#130808;border:1px solid var(--red);border-radius:10px;padding:28px;text-align:center}
|
| 117 |
+
.rico{font-size:44px;display:block;margin-bottom:10px}
|
| 118 |
+
.rtitle{font-family:var(--disp);font-size:20px;font-weight:900;margin-bottom:7px}
|
| 119 |
+
.rsucc .rtitle{color:var(--grn)}.rerr .rtitle{color:var(--red)}
|
| 120 |
+
.rsub{color:var(--mut);font-size:13px;margin-bottom:22px}
|
| 121 |
+
.dbtn{display:inline-flex;align-items:center;gap:9px;padding:13px 26px;background:var(--grn);color:#050509;border:none;border-radius:8px;font-family:var(--disp);font-size:13px;font-weight:700;cursor:pointer;text-decoration:none;transition:opacity .15s}
|
| 122 |
+
.dbtn:hover{opacity:.82}
|
| 123 |
+
.rbtn{display:inline-flex;align-items:center;gap:7px;padding:11px 22px;background:transparent;color:var(--txt);border:1px solid var(--brd2);border-radius:8px;font-family:var(--mono);font-size:13px;cursor:pointer;margin-top:14px;transition:border-color .15s}
|
| 124 |
+
.rbtn:hover{border-color:var(--acc)}
|
| 125 |
+
|
| 126 |
+
/* spinner */
|
| 127 |
+
.sp{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:5px}
|
| 128 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 129 |
+
|
| 130 |
+
footer{border-top:1px solid var(--brd);margin-top:56px;padding:28px 0;color:var(--mut);font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
|
| 131 |
+
@media(max-width:480px){.vgrid{grid-template-columns:1fr}.card{padding:18px}}
|
| 132 |
+
</style>
|
| 133 |
+
</head>
|
| 134 |
+
<body>
|
| 135 |
+
<div class="wrap">
|
| 136 |
+
<header>
|
| 137 |
+
<div class="logo">Forge<span>Builder</span></div>
|
| 138 |
+
<div class="tag">ZIP с исходниками → готовый JAR // Forge 1.12.2 & 1.20.1</div>
|
| 139 |
+
</header>
|
| 140 |
+
|
| 141 |
+
<div class="card">
|
| 142 |
+
<div class="ctitle">Шаг 1 — Версия Minecraft</div>
|
| 143 |
+
<div class="vgrid">
|
| 144 |
+
<button class="vbtn on" data-ver="1.12.2" onclick="selVer(this)">
|
| 145 |
+
<span class="vnum">1.12.2</span>
|
| 146 |
+
<span class="vlbl">Forge 14.23.5.2860 · JDK 8</span>
|
| 147 |
+
</button>
|
| 148 |
+
<button class="vbtn" data-ver="1.20.1" onclick="selVer(this)">
|
| 149 |
+
<span class="vnum">1.20.1</span>
|
| 150 |
+
<span class="vlbl">Forge 47.2.0 · JDK 17</span>
|
| 151 |
+
</button>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="card">
|
| 156 |
+
<div class="ctitle">Шаг 2 — Загрузи ZIP с исходниками</div>
|
| 157 |
+
<div class="dz" id="dz">
|
| 158 |
+
<input type="file" id="fi" accept=".zip" onchange="onFile(this.files[0])">
|
| 159 |
+
<span class="dico">📦</span>
|
| 160 |
+
<div class="dtitle">Перетащи ZIP или нажми здесь</div>
|
| 161 |
+
<div class="dsub">Максимум 50 MB · Должен содержать build.gradle + src/</div>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="finfo" id="finfo">
|
| 164 |
+
<span class="fick">✓</span>
|
| 165 |
+
<span class="fname" id="fname">—</span>
|
| 166 |
+
<span class="fsz" id="fsz">—</span>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="warn">⚠ Загружай только свой код. Вредоносные файлы автоматически блокируются.</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="card">
|
| 172 |
+
<div class="ctitle">Шаг 3 — Сборка</div>
|
| 173 |
+
<div class="igrid">
|
| 174 |
+
<div class="iitem"><div class="ilbl">Версия</div><div class="ival" id="iver">1.12.2 Forge</div></div>
|
| 175 |
+
<div class="iitem"><div class="ilbl">Время сборки</div><div class="ival">3 — 8 минут</div></div>
|
| 176 |
+
<div class="iitem"><div class="ilbl">Платформа</div><div class="ival">HF Spaces</div></div>
|
| 177 |
+
<div class="iitem"><div class="ilbl">JAR хранится</div><div class="ival">1 час</div></div>
|
| 178 |
+
</div>
|
| 179 |
+
<button class="bbtn" id="bbtn" onclick="startBuild()" disabled>Собрать JAR</button>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div class="card" id="psec">
|
| 183 |
+
<div class="ctitle">Сборка...</div>
|
| 184 |
+
<div class="plbl"><span id="pstep">Запуск...</span><span id="ppct">0%</span></div>
|
| 185 |
+
<div class="ptrack"><div class="pbar" id="pbar"></div></div>
|
| 186 |
+
<div class="logbox" id="logbox"></div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div id="rsec"></div>
|
| 190 |
+
|
| 191 |
+
<footer>
|
| 192 |
+
<span>ForgeBuilder v1.0 — бесплатно для всех моддеров</span>
|
| 193 |
+
<span>Hugging Face Spaces · 16 GB RAM</span>
|
| 194 |
+
</footer>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
+
<script>
|
| 198 |
+
let ver='1.12.2', file=null, jobId=null, poll=null;
|
| 199 |
+
|
| 200 |
+
function selVer(b){
|
| 201 |
+
document.querySelectorAll('.vbtn').forEach(x=>x.classList.remove('on'));
|
| 202 |
+
b.classList.add('on'); ver=b.dataset.ver;
|
| 203 |
+
document.getElementById('iver').textContent=ver+' Forge';
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
function onFile(f){
|
| 207 |
+
if(!f)return;
|
| 208 |
+
if(!f.name.endsWith('.zip')){alert('Нужен ZIP!');return}
|
| 209 |
+
if(f.size>50*1024*1024){alert('Максимум 50 MB');return}
|
| 210 |
+
file=f;
|
| 211 |
+
document.getElementById('fname').textContent=f.name;
|
| 212 |
+
document.getElementById('fsz').textContent=fmtSz(f.size);
|
| 213 |
+
document.getElementById('finfo').classList.add('show');
|
| 214 |
+
document.getElementById('bbtn').disabled=false;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
function fmtSz(b){
|
| 218 |
+
if(b<1024)return b+' B';
|
| 219 |
+
if(b<1048576)return (b/1024).toFixed(1)+' KB';
|
| 220 |
+
return (b/1048576).toFixed(1)+' MB';
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
const dz=document.getElementById('dz');
|
| 224 |
+
dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('over')});
|
| 225 |
+
dz.addEventListener('dragleave',()=>dz.classList.remove('over'));
|
| 226 |
+
dz.addEventListener('drop',e=>{e.preventDefault();dz.classList.remove('over');onFile(e.dataTransfer.files[0])});
|
| 227 |
+
|
| 228 |
+
async function startBuild(){
|
| 229 |
+
if(!file)return;
|
| 230 |
+
document.getElementById('psec').classList.add('show');
|
| 231 |
+
document.getElementById('rsec').classList.remove('show');
|
| 232 |
+
document.getElementById('rsec').innerHTML='';
|
| 233 |
+
document.getElementById('bbtn').disabled=true;
|
| 234 |
+
document.getElementById('bbtn').innerHTML='<span class="sp"></span>Собирается...';
|
| 235 |
+
document.getElementById('logbox').innerHTML='';
|
| 236 |
+
setP(0,'Загрузка файла...');
|
| 237 |
+
|
| 238 |
+
const form=new FormData();
|
| 239 |
+
form.append('file',file);
|
| 240 |
+
form.append('version',ver);
|
| 241 |
+
addLog('Отправка ZIP на сервер...','in');
|
| 242 |
+
|
| 243 |
+
try{
|
| 244 |
+
const r=await fetch('/build',{method:'POST',body:form});
|
| 245 |
+
if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||'Ошибка '+r.status)}
|
| 246 |
+
const d=await r.json();
|
| 247 |
+
jobId=d.job_id;
|
| 248 |
+
addLog('Job: '+jobId,'in');
|
| 249 |
+
setP(12,'Запуск Gradle...');
|
| 250 |
+
pollJob();
|
| 251 |
+
}catch(e){showErr(e.message)}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function pollJob(){
|
| 255 |
+
poll=setInterval(async()=>{
|
| 256 |
+
try{
|
| 257 |
+
const r=await fetch('/status/'+jobId);
|
| 258 |
+
const d=await r.json();
|
| 259 |
+
if(d.logs)d.logs.forEach(l=>{
|
| 260 |
+
const c=l.includes('ERROR')||l.includes('FAILED')?'er':
|
| 261 |
+
l.includes('WARN')?'wn':
|
| 262 |
+
l.includes('SUCCESS')?'ok':'';
|
| 263 |
+
addLog(l,c);
|
| 264 |
+
if(l.includes('Compiling'))setP(72,'Компиляция Java...');
|
| 265 |
+
else if(l.includes('jar'))setP(88,'Создание JAR...');
|
| 266 |
+
else if(l.includes('Downloading')||l.includes('Resolving'))setP(40,'Загрузка зависимостей...');
|
| 267 |
+
});
|
| 268 |
+
if(d.progress)setP(d.progress,d.step||'');
|
| 269 |
+
if(d.status==='done'){clearInterval(poll);showOk(d.jar_url,d.jar_name,d.build_time)}
|
| 270 |
+
else if(d.status==='error'){clearInterval(poll);showErr(d.error||'BUILD FAILED')}
|
| 271 |
+
}catch(e){addLog('Потеря связи: '+e.message,'er')}
|
| 272 |
+
},2200);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function setP(pct,lbl){
|
| 276 |
+
document.getElementById('pbar').style.width=pct+'%';
|
| 277 |
+
document.getElementById('ppct').textContent=pct+'%';
|
| 278 |
+
if(lbl)document.getElementById('pstep').textContent=lbl;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
function addLog(msg,cls){
|
| 282 |
+
const b=document.getElementById('logbox');
|
| 283 |
+
const now=new Date();
|
| 284 |
+
const t=now.getHours().toString().padStart(2,'0')+':'+now.getMinutes().toString().padStart(2,'0')+':'+now.getSeconds().toString().padStart(2,'0');
|
| 285 |
+
const d=document.createElement('div');d.className='ll';
|
| 286 |
+
d.innerHTML=`<span class="lt">${t}</span><span class="lm ${cls||''}">${esc(msg)}</span>`;
|
| 287 |
+
b.appendChild(d);b.scrollTop=b.scrollHeight;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
| 291 |
+
|
| 292 |
+
function showOk(url,name,t){
|
| 293 |
+
reset();
|
| 294 |
+
const s=document.getElementById('rsec');
|
| 295 |
+
s.innerHTML=`<div class="card"><div class="rsucc">
|
| 296 |
+
<span class="rico">✅</span>
|
| 297 |
+
<div class="rtitle">BUILD SUCCESSFUL</div>
|
| 298 |
+
<div class="rsub">Время сборки: ${t||'—'} · Файл будет удалён через 1 час</div>
|
| 299 |
+
<a class="dbtn" href="${url}" download="${name}">⬇ Скачать ${name}</a>
|
| 300 |
+
</div></div>`;
|
| 301 |
+
s.classList.add('show');s.scrollIntoView({behavior:'smooth',block:'center'});
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
function showErr(msg){
|
| 305 |
+
reset();
|
| 306 |
+
const s=document.getElementById('rsec');
|
| 307 |
+
s.innerHTML=`<div class="card"><div class="rerr">
|
| 308 |
+
<span class="rico">❌</span>
|
| 309 |
+
<div class="rtitle">BUILD FAILED</div>
|
| 310 |
+
<div class="rsub">${esc(msg)}</div>
|
| 311 |
+
<button class="rbtn" onclick="resetUI()">↺ Попробовать снова</button>
|
| 312 |
+
</div></div>`;
|
| 313 |
+
s.classList.add('show');s.scrollIntoView({behavior:'smooth',block:'center'});
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
function reset(){
|
| 317 |
+
document.getElementById('bbtn').disabled=false;
|
| 318 |
+
document.getElementById('bbtn').textContent='Собрать JAR';
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
function resetUI(){
|
| 322 |
+
document.getElementById('rsec').classList.remove('show');
|
| 323 |
+
document.getElementById('rsec').innerHTML='';
|
| 324 |
+
document.getElementById('psec').classList.remove('show');
|
| 325 |
+
reset();
|
| 326 |
+
}
|
| 327 |
+
</script>
|
| 328 |
+
</body>
|
| 329 |
+
</html>"""
|
| 330 |
+
|
| 331 |
+
# ── Health check (HF Spaces) ────────────────────────────────────────────────
|
| 332 |
+
@app.get("/health")
|
| 333 |
+
async def health():
|
| 334 |
+
return {"status": "ok"}
|
| 335 |
+
|
| 336 |
+
# ── Serve HTML ──────────────────────────────────────────────────────────────
|
| 337 |
+
@app.get("/", response_class=HTMLResponse)
|
| 338 |
+
async def index():
|
| 339 |
+
return HTML
|
| 340 |
+
|
| 341 |
+
# ── Security: validate ZIP ────────────────────────────────────────────────────
|
| 342 |
+
def validate_zip(path: Path):
|
| 343 |
+
try:
|
| 344 |
+
with zipfile.ZipFile(path) as zf:
|
| 345 |
+
names = zf.namelist()
|
| 346 |
+
if not any("build.gradle" in n for n in names):
|
| 347 |
+
return False, "ZIP не содержит build.gradle"
|
| 348 |
+
if not any("src/" in n or n.startswith("src/") for n in names):
|
| 349 |
+
return False, "ZIP не содержит папку src/"
|
| 350 |
+
for n in names:
|
| 351 |
+
if ".." in n or n.startswith("/"):
|
| 352 |
+
return False, f"Подозрительный путь: {n}"
|
| 353 |
+
if Path(n).suffix.lower() in BAD_EXT:
|
| 354 |
+
return False, f"Запрещённый файл: {n}"
|
| 355 |
+
if sum(i.file_size for i in zf.infolist()) > 200_000_000:
|
| 356 |
+
return False, "Распакованный размер > 200 MB"
|
| 357 |
+
return True, ""
|
| 358 |
+
except zipfile.BadZipFile:
|
| 359 |
+
return False, "Повреждённый ZIP"
|
| 360 |
+
|
| 361 |
+
# ── POST /build ───────────────────────────────────────────────────────────────
|
| 362 |
+
@app.post("/build")
|
| 363 |
+
async def build(file: UploadFile = File(...), version: str = Form(...)):
|
| 364 |
+
if version not in VERSIONS:
|
| 365 |
+
raise HTTPException(400, f"Неподдерживаемая версия: {version}")
|
| 366 |
+
data = await file.read()
|
| 367 |
+
if len(data) > MAX_ZIP:
|
| 368 |
+
raise HTTPException(400, "Файл > 50 MB")
|
| 369 |
+
if not file.filename.endswith(".zip"):
|
| 370 |
+
raise HTTPException(400, "Нужен ZIP файл")
|
| 371 |
+
|
| 372 |
+
jid = str(uuid.uuid4())[:8]
|
| 373 |
+
job_dir = JOBS_DIR / jid
|
| 374 |
+
zip_p = job_dir / "src.zip"
|
| 375 |
+
job_dir.mkdir(parents=True)
|
| 376 |
+
zip_p.write_bytes(data)
|
| 377 |
+
|
| 378 |
+
ok, err = validate_zip(zip_p)
|
| 379 |
+
if not ok:
|
| 380 |
+
shutil.rmtree(job_dir, ignore_errors=True)
|
| 381 |
+
raise HTTPException(400, err)
|
| 382 |
+
|
| 383 |
+
jobs[jid] = {"status":"queued","progress":5,"step":"В очереди...","logs":[],"error":None,"jar_url":None,"jar_name":None,"build_time":None}
|
| 384 |
+
asyncio.create_task(run_build(jid, version, zip_p))
|
| 385 |
+
return {"job_id": jid}
|
| 386 |
+
|
| 387 |
+
# ── GET /status/{jid} ─────────────────────────────────────────────────────────
|
| 388 |
+
@app.get("/status/{jid}")
|
| 389 |
+
async def status(jid: str):
|
| 390 |
+
if jid not in jobs:
|
| 391 |
+
raise HTTPException(404, "Job не найден")
|
| 392 |
+
j = jobs[jid]
|
| 393 |
+
logs = j["logs"][:]
|
| 394 |
+
j["logs"] = []
|
| 395 |
+
return {**j, "logs": logs}
|
| 396 |
+
|
| 397 |
+
# ── GET /download/{jid}/{name} ────────────────────────────────────────────────
|
| 398 |
+
@app.get("/download/{jid}/{name}")
|
| 399 |
+
async def download(jid: str, name: str):
|
| 400 |
+
p = JARS_DIR / jid / name
|
| 401 |
+
if not p.exists():
|
| 402 |
+
raise HTTPException(404, "Файл не найден или истёк срок")
|
| 403 |
+
return FileResponse(p, filename=name, media_type="application/java-archive")
|
| 404 |
+
|
| 405 |
+
# ── Build worker ──────────────────────────────────────────────────────────────
|
| 406 |
+
async def run_build(jid: str, version: str, zip_p: Path):
|
| 407 |
+
j = jobs[jid]
|
| 408 |
+
cfg = VERSIONS[version]
|
| 409 |
+
jdir = zip_p.parent
|
| 410 |
+
t0 = time.time()
|
| 411 |
+
|
| 412 |
+
def log(m): j["logs"].append(m)
|
| 413 |
+
def st(s,p,step=""): j["status"]=s; j["progress"]=p; j["step"]=step
|
| 414 |
+
|
| 415 |
+
try:
|
| 416 |
+
# Extract
|
| 417 |
+
st("running", 15, "Распаковка ZIP...")
|
| 418 |
+
log("[INFO] Распаковка ZIP...")
|
| 419 |
+
src = jdir / "extracted"
|
| 420 |
+
src.mkdir()
|
| 421 |
+
with zipfile.ZipFile(zip_p) as zf:
|
| 422 |
+
zf.extractall(src)
|
| 423 |
+
|
| 424 |
+
# Find project root (where build.gradle lives)
|
| 425 |
+
roots = list(src.rglob("build.gradle"))
|
| 426 |
+
if not roots:
|
| 427 |
+
raise RuntimeError("build.gradle не найден")
|
| 428 |
+
root = roots[0].parent
|
| 429 |
+
log(f"[INFO] Корень проекта: {root.name}")
|
| 430 |
+
|
| 431 |
+
# Verify src/main/java
|
| 432 |
+
if not (root / "src" / "main" / "java").exists():
|
| 433 |
+
raise RuntimeError("src/main/java не найдена")
|
| 434 |
+
|
| 435 |
+
# Copy to writable build dir
|
| 436 |
+
st("running", 22, "Подготовка окружения...")
|
| 437 |
+
build_dir = jdir / "build_workspace"
|
| 438 |
+
shutil.copytree(root, build_dir)
|
| 439 |
+
|
| 440 |
+
# Set JAVA_HOME for this version
|
| 441 |
+
env = os.environ.copy()
|
| 442 |
+
env["JAVA_HOME"] = "/usr/lib/jvm/java-17-openjdk-amd64"
|
| 443 |
+
env["PATH"] = "/usr/lib/jvm/java-17-openjdk-amd64/bin:" + env.get("PATH","")
|
| 444 |
+
env["GRADLE_USER_HOME"] = str(CACHE_DIR)
|
| 445 |
+
env["GRADLE_OPTS"] = "-Xmx1g -Xms128m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED"
|
| 446 |
+
|
| 447 |
+
# Always use system gradle for reliability
|
| 448 |
+
# gradle4 = Gradle 4.10.3 for 1.12.2, gradle8 = Gradle 8.1.1 for 1.20.1
|
| 449 |
+
gradle = ["gradle"]
|
| 450 |
+
log("[INFO] Используем Gradle 4.10.3 + JDK 17"))
|
| 451 |
+
|
| 452 |
+
# Inject Forge plugin if missing
|
| 453 |
+
bg = (build_dir / "build.gradle").read_text(errors="replace")
|
| 454 |
+
if "minecraftforge" not in bg and "ForgeGradle" not in bg:
|
| 455 |
+
log("[WARN] Forge плагин не найден в build.gradle — добавляем автоматически")
|
| 456 |
+
(build_dir / "build.gradle").write_text(
|
| 457 |
+
_inject_forge(version, cfg) + "\n\n" + bg
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Run setupDecompWorkspace only if cache is cold
|
| 461 |
+
deobf_cache = CACHE_DIR / f"caches/minecraft/{version}"
|
| 462 |
+
if not deobf_cache.exists():
|
| 463 |
+
st("running", 30, "Загрузка Forge MDK (первый раз ~10 мин)...")
|
| 464 |
+
log("[INFO] Первая сборка: скачиваем Forge и Minecraft деобфускацию...")
|
| 465 |
+
await _run_gradle(gradle + ["setupDecompWorkspace", "--no-daemon"], build_dir, env, j)
|
| 466 |
+
|
| 467 |
+
# Build
|
| 468 |
+
st("running", 55, "Компиляция Java...")
|
| 469 |
+
log("[INFO] Запуск: gradle build")
|
| 470 |
+
await _run_gradle(gradle + ["build", "--no-daemon", "--stacktrace"], build_dir, env, j)
|
| 471 |
+
|
| 472 |
+
# Collect JARs
|
| 473 |
+
st("running", 92, "Сбор JAR файлов...")
|
| 474 |
+
out = JARS_DIR / jid
|
| 475 |
+
out.mkdir(parents=True)
|
| 476 |
+
jars = [f for f in (build_dir / "build" / "libs").glob("*.jar")
|
| 477 |
+
if not any(s in f.name for s in ["-sources","-dev","-javadoc"])]
|
| 478 |
+
if not jars:
|
| 479 |
+
raise RuntimeError("JAR не создан — проверь build.gradle")
|
| 480 |
+
|
| 481 |
+
jar = jars[0]
|
| 482 |
+
shutil.copy(jar, out / jar.name)
|
| 483 |
+
secs = int(time.time() - t0)
|
| 484 |
+
log(f"[SUCCESS] BUILD SUCCESSFUL — {jar.name} ({jar.stat().st_size//1024} KB)")
|
| 485 |
+
log(f"[INFO] Время: {secs} сек")
|
| 486 |
+
|
| 487 |
+
j["status"] = "done"
|
| 488 |
+
j["progress"] = 100
|
| 489 |
+
j["step"] = "Готово!"
|
| 490 |
+
j["jar_url"] = f"/download/{jid}/{jar.name}"
|
| 491 |
+
j["jar_name"] = jar.name
|
| 492 |
+
j["build_time"] = f"{secs} сек"
|
| 493 |
+
|
| 494 |
+
asyncio.create_task(_cleanup(jid))
|
| 495 |
+
|
| 496 |
+
except Exception as e:
|
| 497 |
+
log(f"[ERROR] {e}")
|
| 498 |
+
j["status"] = "error"
|
| 499 |
+
j["error"] = str(e)
|
| 500 |
+
finally:
|
| 501 |
+
shutil.rmtree(jdir, ignore_errors=True)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
async def _run_gradle(cmd, cwd, env, j):
|
| 505 |
+
"""Stream gradle output into job logs."""
|
| 506 |
+
proc = await asyncio.create_subprocess_exec(
|
| 507 |
+
*cmd, cwd=str(cwd), env=env,
|
| 508 |
+
stdout=asyncio.subprocess.PIPE,
|
| 509 |
+
stderr=asyncio.subprocess.STDOUT,
|
| 510 |
+
)
|
| 511 |
+
while True:
|
| 512 |
+
line = await proc.stdout.readline()
|
| 513 |
+
if not line:
|
| 514 |
+
break
|
| 515 |
+
txt = line.decode("utf-8", errors="replace").rstrip()
|
| 516 |
+
if txt and any(kw in txt for kw in [
|
| 517 |
+
"BUILD","Task","error","ERROR","warn","WARN",
|
| 518 |
+
"Compiling","Download","Resolving","Exception","FAILED","jar"
|
| 519 |
+
]):
|
| 520 |
+
j["logs"].append(txt)
|
| 521 |
+
await proc.wait()
|
| 522 |
+
if proc.returncode != 0:
|
| 523 |
+
raise RuntimeError("Gradle вернул ошибку. Проверь логи выше.")
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def _inject_forge(version: str, cfg: dict) -> str:
|
| 527 |
+
if version == "1.12.2":
|
| 528 |
+
return f"""
|
| 529 |
+
buildscript {{
|
| 530 |
+
repositories {{ maven {{ url = 'https://maven.minecraftforge.net' }} mavenCentral() }}
|
| 531 |
+
dependencies {{ classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' }}
|
| 532 |
+
}}
|
| 533 |
+
apply plugin: 'net.minecraftforge.gradle.forge'
|
| 534 |
+
apply plugin: 'java'
|
| 535 |
+
minecraft {{
|
| 536 |
+
version = "{cfg['forge']}"
|
| 537 |
+
runDir = "run"
|
| 538 |
+
mappings = "{cfg['mappings']}"
|
| 539 |
+
}}
|
| 540 |
+
"""
|
| 541 |
+
else: # 1.20.1
|
| 542 |
+
return f"""
|
| 543 |
+
plugins {{
|
| 544 |
+
id 'net.minecraftforge.gradle' version '6.0.+'
|
| 545 |
+
id 'java'
|
| 546 |
+
}}
|
| 547 |
+
minecraft {{
|
| 548 |
+
mappings channel: 'official', version: '1.20.1'
|
| 549 |
+
runs {{ client {{ workingDirectory project.file('run') }} }}
|
| 550 |
+
}}
|
| 551 |
+
dependencies {{
|
| 552 |
+
minecraft 'net.minecraftforge:forge:{cfg['forge']}'
|
| 553 |
+
}}
|
| 554 |
+
"""
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
async def _cleanup(jid: str, delay: int = 3600):
|
| 558 |
+
await asyncio.sleep(delay)
|
| 559 |
+
shutil.rmtree(JARS_DIR / jid, ignore_errors=True)
|
| 560 |
+
jobs.pop(jid, None)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
| 564 |
+
if __name__ == "__main__":
|
| 565 |
+
uvicorn.run(app, host="0.0.0.0", port=7860, timeout_keep_alive=120, workers=1)
|