AutoStub-gen / app.py
coidada's picture
Update app.py
d7c16e0 verified
Raw History Blame Contribute Delete
30.2 kB
import gradio as gr
import subprocess
import tempfile
import shutil
import os
import glob
import zipfile
import hashlib
from concurrent.futures import ThreadPoolExecutor
# ── Пути к SDK инструментам ──────────────────────────────────────────────
BUILD_TOOLS = "/opt/android-sdk/build-tools/33.0.2"
JAVA_HOME = "/usr/lib/jvm/java-17-openjdk-amd64"
# Kotlin-компилятор. Компилирует .kt (и вместе с .java — совместно) в JVM
# bytecode (.class). Дальше d8 уже не отличает, откуда взялся класс.
KOTLINC = "/opt/kotlinc/bin/kotlinc"
# kotlin-stdlib нужен в classpath на этапе компиляции и в --lib на этапе d8,
# иначе рантайм не найдёт kotlin.jvm.internal.Intrinsics и т.п.
KOTLIN_STDLIB_JAR = "/opt/kotlinc/lib/kotlin-stdlib.jar"
# Путь к предустановленным стабам
DEFAULT_STUBS_DIR = "/opt/default-stubs"
# Постоянный кэш очищенных стаб-JAR (переживает запуски компиляции).
STUB_CACHE_DIR = os.path.join(tempfile.gettempdir(), "stub_clean_cache")
os.makedirs(STUB_CACHE_DIR, exist_ok=True)
# Выбираем android.jar (приоритет отдается скачанному, иначе SDK-версия)
if os.path.exists(os.path.join(DEFAULT_STUBS_DIR, "android.jar")):
ANDROID_JAR = os.path.join(DEFAULT_STUBS_DIR, "android.jar")
else:
ANDROID_JAR = "/opt/android-sdk/platforms/android-33/android.jar"
# Пакеты, конфликтующие с JDK bootclasspath / android.jar —
# вырезаются из стаб-jar приложения
CONFLICTING_PREFIXES = (
"java/",
"javax/",
"jdk/",
"org/jetbrains/annotations/",
"org/intellij/",
# Стаб-JAR-ы (например app_stubs.jar) иногда сами содержат Kotlin-рантайм,
# часто более новой/несовместимой версии metadata, чем наш kotlinc.
# Наш собственный kotlin-stdlib.jar уже добавляется в classpath отдельно,
# поэтому дублирующийся kotlin.* из стабов только ломает компиляцию.
"kotlin/",
"kotlinx/",
)
EXAMPLE_CODE = """\
package com.example.hook
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import android.content.Context
class MainHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.target.app") return
XposedBridge.hookAllMethods(
lpparam.classLoader.loadClass("com.target.app.SomeClass"),
"targetMethod",
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val ctx = param.thisObject as Context
// ... ваша логика ...
}
}
)
}
}
"""
# ---------------------------------------------------------------------------
# Sanitize stub JAR — удаляем конфликтующие с JDK/android.jar пакеты
# ---------------------------------------------------------------------------
def sanitize_stub_jar(src_jar: str, out_dir: str, log_lines: list, use_cache: bool = False) -> str:
"""
Копирует src_jar в новый .jar, выбрасывая все entries, чьи пути
начинаются с CONFLICTING_PREFIXES. Возвращает путь к очищенному jar.
"""
basename = os.path.basename(src_jar)
stem = os.path.splitext(basename)[0]
if use_cache:
try:
st = os.stat(src_jar)
key = hashlib.md5(
f"{src_jar}:{st.st_mtime_ns}:{st.st_size}".encode()
).hexdigest()[:12]
cached = os.path.join(STUB_CACHE_DIR, f"{stem}_{key}.jar")
if os.path.exists(cached):
log_lines.append(f"♻️ Reused cached clean JAR: {basename}")
return cached
out_jar = cached
except OSError:
use_cache = False
out_jar = os.path.join(out_dir, stem + "_clean.jar")
else:
out_jar = os.path.join(out_dir, stem + "_clean.jar")
removed = []
kept = 0
tmp_out = out_jar + ".tmp"
with zipfile.ZipFile(src_jar, "r") as src, \
zipfile.ZipFile(tmp_out, "w", zipfile.ZIP_STORED) as dst:
for entry in src.infolist():
name = entry.filename
if any(name.startswith(p) for p in CONFLICTING_PREFIXES):
removed.append(name)
else:
dst.writestr(entry, src.read(name))
kept += 1
os.replace(tmp_out, out_jar)
if removed:
log_lines.append(f"🧹 Sanitized — removed {len(removed)} conflicting entries, kept {kept}")
bad_pkgs = sorted({e.split("/")[0] + "/" for e in removed})
log_lines.append(f" Removed top-level packages: {', '.join(bad_pkgs)}")
else:
log_lines.append(f"✅ JAR is clean — no conflicting packages found ({kept} entries)")
return out_jar
def sanitize_all(all_stubs: list, auto_stubs: list, workdir: str, log_lines: list) -> list:
"""Санитизирует список стаб-JAR параллельно. Порядок сохраняется."""
auto_set = set(auto_stubs)
def work(stub_path):
sub: list = []
is_default = stub_path in auto_set
label = " [Pre-installed]" if is_default else ""
sub.append(f"📦 {os.path.basename(stub_path)}{label}")
clean = sanitize_stub_jar(stub_path, workdir, sub, use_cache=is_default)
return clean, sub
clean_stubs: list = []
if not all_stubs:
return clean_stubs
with ThreadPoolExecutor(max_workers=min(8, len(all_stubs))) as ex:
for clean, sub in ex.map(work, all_stubs):
clean_stubs.append(clean)
log_lines.extend(sub)
return clean_stubs
# ---------------------------------------------------------------------------
# Извлечение исходников из загруженных файлов (.java / .kt / .zip)
# ---------------------------------------------------------------------------
def extract_sources(upload_paths: list[str], src_dir: str, log_lines: list) -> tuple[list[str], list[str]]:
"""
Принимает список файлов (.java, .kt, .zip).
Распаковывает ZIP-архивы, копирует .java/.kt файлы в src_dir.
Возвращает (java_files, kotlin_files).
"""
for upload_path in upload_paths:
ext = os.path.splitext(upload_path)[1].lower()
basename = os.path.basename(upload_path)
if ext == ".zip":
with zipfile.ZipFile(upload_path, "r") as zf:
zf.extractall(src_dir)
log_lines.append(f"📦 ZIP extracted: {basename}")
elif ext in (".java", ".kt"):
dest = os.path.join(src_dir, basename)
shutil.copy(upload_path, dest)
log_lines.append(f"📄 Source copied: {basename}")
else:
log_lines.append(f"⚠️ Unknown extension '{ext}' for {basename} — treating as .kt")
dest = os.path.join(src_dir, basename + ".kt")
shutil.copy(upload_path, dest)
java_files = []
kotlin_files = []
for root, _, files in os.walk(src_dir):
for f in files:
if f.endswith(".java"):
java_files.append(os.path.join(root, f))
elif f.endswith(".kt"):
kotlin_files.append(os.path.join(root, f))
log_lines.append(
f"🔍 Found {len(java_files)} .java file(s) and {len(kotlin_files)} .kt file(s)"
)
return java_files, kotlin_files
def pack_classes_jar(classes_dir: str, jar_path: str) -> None:
with zipfile.ZipFile(jar_path, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _, files in os.walk(classes_dir):
for filename in files:
if filename.endswith(".class"):
path = os.path.join(root, filename)
zf.write(path, os.path.relpath(path, classes_dir))
def merge_classpath_jars(jar_paths: list[str], out_path: str, log_lines: list[str]) -> str | None:
"""Объединяет classpath JAR-ы в один, удаляя дубли между стабами."""
if not jar_paths:
return None
seen: set[str] = set()
duplicate_classes = 0
written = 0
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_STORED) as dst:
for jar_path in jar_paths:
with zipfile.ZipFile(jar_path, "r") as src:
for entry in src.infolist():
name = entry.filename
if entry.is_dir():
continue
if name.startswith("META-INF/versions/"):
continue
if name in seen:
if name.endswith(".class"):
duplicate_classes += 1
continue
seen.add(name)
upper_name = name.upper()
if upper_name.startswith("META-INF/") and upper_name.endswith((".SF", ".RSA", ".DSA")):
continue
dst.writestr(entry, src.read(name))
written += 1
log_lines.append(
f"🧩 Classpath merged: {len(jar_paths)} JAR(s), "
f"{written} entries, removed {duplicate_classes} duplicate class(es)"
)
return out_path
# ---------------------------------------------------------------------------
# Основная функция компиляции (Java/Kotlin -> DEX / Class)
# ---------------------------------------------------------------------------
def compile_to_dex(
code: str,
filename: str,
source_files: list[str],
stub_jar_paths: list[str],
) -> tuple[str, str | None, str | None]:
workdir = tempfile.mkdtemp(prefix="kt_dex_")
log_lines: list[str] = []
try:
src_dir = os.path.join(workdir, "src")
classes_dir = os.path.join(workdir, "classes")
dex_dir = os.path.join(workdir, "dex_out")
os.makedirs(src_dir)
os.makedirs(classes_dir)
os.makedirs(dex_dir)
base_name = os.path.splitext(os.path.basename(filename or "Main"))[0] or "Main"
# ── Сбор исходников ──
if source_files:
log_lines.append(f"📂 Source files ({len(source_files)}):")
for sf in source_files:
log_lines.append(f" • {os.path.basename(sf)}")
java_files, kotlin_files = extract_sources(source_files, src_dir, log_lines)
else:
src_path = os.path.join(src_dir, f"{base_name}.kt")
with open(src_path, "w", encoding="utf-8") as f:
f.write(code)
java_files, kotlin_files = [], [src_path]
log_lines.append(f"📝 Source saved from editor: {base_name}.kt")
all_source_files = java_files + kotlin_files
if not all_source_files:
log_lines.append("❌ No .java/.kt files found — aborting")
return "\n".join(log_lines), None, None
# ── Сбор и подготовка стаб-файлов ──
auto_stubs = []
if os.path.isdir(DEFAULT_STUBS_DIR):
for f in sorted(os.listdir(DEFAULT_STUBS_DIR)):
if f.endswith(".jar") and f != "android.jar":
auto_stubs.append(os.path.join(DEFAULT_STUBS_DIR, f))
valid_stubs = [p for p in stub_jar_paths if p and os.path.isfile(p)]
all_stubs = auto_stubs + valid_stubs
if all_stubs:
log_lines.append("")
log_lines.append(f"── Sanitizing {len(all_stubs)} stub JAR(s) (parallel + cached) ──")
clean_stubs = sanitize_all(all_stubs, auto_stubs, workdir, log_lines)
else:
clean_stubs = []
log_lines.append("⚠️ No stub JARs provided — compiling without extra classpath entries")
log_lines.append("")
classpath_parts = [ANDROID_JAR] + clean_stubs
if kotlin_files:
classpath_parts.append(KOTLIN_STDLIB_JAR)
classpath = ":".join(classpath_parts)
# ── Step 1: компиляция в JVM bytecode (.class) ──
log_lines.append("═" * 50)
log_lines.append("STEP 1 — Compiling source → JVM bytecode (.class)")
log_lines.append("═" * 50)
if kotlin_files:
# kotlinc умеет компилировать .kt и .java совместно за один вызов
# (это важно, если Kotlin-код ссылается на Java-классы и наоборот).
kotlinc_cmd = [
KOTLINC,
"-cp", classpath,
"-d", classes_dir,
"-jvm-target", "1.8",
"-Xno-call-assertions", # чуть легче итоговый байткод
"-nowarn",
] + kotlin_files + java_files
log_lines.append("$ " + " ".join(kotlinc_cmd))
result = subprocess.run(
kotlinc_cmd,
capture_output=True,
text=True,
timeout=300,
env={**os.environ, "JAVA_HOME": JAVA_HOME},
)
if result.stdout:
log_lines.append(result.stdout)
if result.stderr:
log_lines.append(result.stderr)
if result.returncode != 0:
log_lines.append(f"\n❌ kotlinc failed (exit {result.returncode})")
return "\n".join(log_lines), None, None
else:
# Только .java — используем javac напрямую (быстрее, чем гонять kotlinc)
javac_cmd = [
f"{JAVA_HOME}/bin/javac",
"-source", "8",
"-target", "8",
"-encoding", "UTF-8",
"-proc:none",
"-nowarn",
"-d", classes_dir,
"-classpath", classpath,
] + java_files
log_lines.append("$ " + " ".join(javac_cmd))
result = subprocess.run(
javac_cmd,
capture_output=True,
text=True,
timeout=120,
env={**os.environ, "JAVA_HOME": JAVA_HOME},
)
if result.stdout:
log_lines.append(result.stdout)
if result.stderr:
log_lines.append(result.stderr)
if result.returncode != 0:
log_lines.append(f"\n❌ javac failed (exit {result.returncode})")
return "\n".join(log_lines), None, None
class_files = glob.glob(os.path.join(classes_dir, "**", "*.class"), recursive=True)
total_size = sum(os.path.getsize(cf) for cf in class_files)
log_lines.append(
f"\n✅ Compilation succeeded — {len(class_files)} .class file(s) ({total_size:,} bytes total)"
)
if not class_files:
log_lines.append("❌ No .class files produced")
return "\n".join(log_lines), None, None
# Подготовка .class файлов для отдельного скачивания
if len(class_files) == 1:
class_out_path = os.path.join(workdir, os.path.basename(class_files[0]))
shutil.copy(class_files[0], class_out_path)
else:
class_out_path = os.path.join(workdir, f"{base_name}_classes.zip")
with zipfile.ZipFile(class_out_path, "w") as zf:
for cf in class_files:
rel_path = os.path.relpath(cf, classes_dir)
zf.write(cf, rel_path)
# ── Step 2: d8 — JVM bytecode → Dalvik (.dex) ──
log_lines.append("")
log_lines.append("═" * 50)
log_lines.append("STEP 2 — Converting JVM bytecode → Dalvik (.dex) with d8")
log_lines.append("═" * 50)
d8_lib_stubs = list(clean_stubs)
if kotlin_files and os.path.isfile(KOTLIN_STDLIB_JAR):
# kotlin-stdlib должен присутствовать и на этапе dex-конвертации,
# чтобы d8 разрешил ссылки на Intrinsics/kotlin.* классы.
d8_lib_stubs.append(KOTLIN_STDLIB_JAR)
build_cmd = [
f"{BUILD_TOOLS}/d8",
"--release",
"--min-api", "21",
"--lib", ANDROID_JAR,
]
for stub in d8_lib_stubs:
build_cmd += ["--lib", stub]
build_cmd += ["--output", dex_dir] + class_files
log_lines.append("$ " + " ".join(build_cmd))
result2 = subprocess.run(build_cmd, capture_output=True, text=True, timeout=180)
if result2.stdout:
log_lines.append(result2.stdout)
if result2.stderr:
log_lines.append(result2.stderr)
if result2.returncode != 0:
log_lines.append(f"\n❌ d8 failed (exit {result2.returncode})")
return "\n".join(log_lines), None, class_out_path
dex_files = glob.glob(os.path.join(dex_dir, "*.dex"))
if not dex_files:
log_lines.append("❌ No .dex files produced")
return "\n".join(log_lines), None, class_out_path
log_lines.append(f"\n✅ d8 succeeded — {len(dex_files)} dex file(s) produced")
# Упаковка результата DEX
if len(dex_files) == 1:
dex_out_path = os.path.join(workdir, f"{base_name}.dex")
shutil.copy(dex_files[0], dex_out_path)
else:
dex_out_path = os.path.join(workdir, f"{base_name}_multidex.zip")
with zipfile.ZipFile(dex_out_path, "w") as zf:
for df in sorted(dex_files):
zf.write(df, os.path.basename(df))
log_lines.append(f"\n📦 Packed into {os.path.basename(dex_out_path)}")
log_lines.append("\n🎉 Compilation complete!")
return "\n".join(log_lines), dex_out_path, class_out_path
except Exception as e:
log_lines.append(f"❌ Unexpected error: {e}")
return "\n".join(log_lines), None, None
# ---------------------------------------------------------------------------
# Функция объединения (Fat DEX)
# ---------------------------------------------------------------------------
def run_fat_dex_merge(primary_inputs_raw, libs_raw, filename_base: str) -> tuple[str, str | None]:
primary_inputs = normalize_file_list(primary_inputs_raw)
libs = normalize_file_list(libs_raw)
if not primary_inputs:
return "⚠️ Please provide primary input files (.class, .jar, or .zip with classes).", None
workdir = tempfile.mkdtemp(prefix="fat_dex_")
log_lines: list[str] = []
try:
dex_dir = os.path.join(workdir, "dex_out")
os.makedirs(dex_dir)
base_name = os.path.splitext(os.path.basename(filename_base or "classes"))[0] or "classes"
auto_stubs = []
if os.path.isdir(DEFAULT_STUBS_DIR):
for f in sorted(os.listdir(DEFAULT_STUBS_DIR)):
if f.endswith(".jar") and f != "android.jar":
auto_stubs.append(os.path.join(DEFAULT_STUBS_DIR, f))
# kotlin-stdlib тоже подмешиваем автоматически, если скомпилированные
# классы могут ссылаться на Kotlin-рантайм.
if os.path.isfile(KOTLIN_STDLIB_JAR):
auto_stubs.append(KOTLIN_STDLIB_JAR)
all_libs = auto_stubs + libs
clean_libs = []
if all_libs:
log_lines.append(f"── Sanitizing {len(all_libs)} library JAR(s) (parallel + cached) ──")
clean_libs = sanitize_all(all_libs, auto_stubs, workdir, log_lines)
log_lines.append("")
log_lines.append("═" * 50)
log_lines.append("Merging and compiling into Fat DEX")
log_lines.append("═" * 50)
d8_cmd = [
f"{BUILD_TOOLS}/d8",
"--release",
"--min-api", "21",
"--lib", ANDROID_JAR,
"--output", dex_dir,
] + primary_inputs + clean_libs
log_lines.append("$ " + " ".join(d8_cmd))
result = subprocess.run(d8_cmd, capture_output=True, text=True, timeout=120)
if result.stdout:
log_lines.append(result.stdout)
if result.stderr:
log_lines.append(result.stderr)
if result.returncode != 0:
log_lines.append(f"\n❌ d8 merger failed (exit {result.returncode})")
return "\n".join(log_lines), None
dex_files = glob.glob(os.path.join(dex_dir, "*.dex"))
if not dex_files:
log_lines.append("❌ No .dex files produced")
return "\n".join(log_lines), None
log_lines.append(f"\n✅ Merge succeeded — {len(dex_files)} dex file(s) produced")
if len(dex_files) == 1:
out_path = os.path.join(workdir, f"{base_name}.dex")
shutil.copy(dex_files[0], out_path)
else:
out_path = os.path.join(workdir, f"{base_name}_multidex.zip")
with zipfile.ZipFile(out_path, "w") as zf:
for df in sorted(dex_files):
zf.write(df, os.path.basename(df))
log_lines.append(f"\n📦 Packed into {os.path.basename(out_path)}")
log_lines.append("\n🎉 Fat DEX creation complete!")
return "\n".join(log_lines), out_path
except Exception as e:
log_lines.append(f"❌ Unexpected error during Fat DEX merge: {e}")
return "\n".join(log_lines), None
# ---------------------------------------------------------------------------
# Gradio handlers
# ---------------------------------------------------------------------------
def handle_file_upload(file_list) -> str:
if not file_list:
return ""
for item in file_list:
path = item if isinstance(item, str) else item.name
ext = os.path.splitext(path)[1].lower()
if ext == ".zip":
return f"// ZIP archive: {os.path.basename(path)}\n// Contents will be extracted at compile time."
if ext in (".java", ".kt"):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"// Error reading file: {e}"
return ""
def normalize_file_list(raw) -> list[str]:
if raw is None:
return []
if isinstance(raw, str):
return [raw]
result = []
for item in raw:
if item is None:
continue
result.append(item if isinstance(item, str) else item.name)
return result
def run_compilation(
code: str,
filename: str,
source_files_raw,
stub_jars_raw,
):
source_files = normalize_file_list(source_files_raw)
stub_jars = normalize_file_list(stub_jars_raw)
if not source_files and (not code or not code.strip()):
return "⚠️ No code provided.", None, None
log, dex_path, class_path = compile_to_dex(
code,
filename or "Main",
source_files,
stub_jars,
)
return log, dex_path, class_path
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
CSS = """
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght=400;600;700&family=Space+Grotesk:wght=400;600;700&display=swap');
:root {
--bg: #0d0e14; --surface: #13141e; --border: #22253a;
--accent: #3ecf8e; --accent2: #7c6af7; --text: #e2e4f0; --muted: #6b7090;
}
body, .gradio-container {
background: var(--bg) !important;
font-family: 'Space Grotesk', sans-serif !important;
color: var(--text) !important;
}
.gr-panel, .gr-box, .gr-form {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
}
textarea, input[type="text"] {
background: #0a0b10 !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 13px !important;
border-radius: 8px !important;
}
.gr-button-primary {
background: linear-gradient(135deg, var(--accent), #2aad6f) !important;
border: none !important; color: #0d0e14 !important;
font-weight: 700 !important; font-size: 15px !important;
border-radius: 10px !important; padding: 12px 32px !important;
transition: all 0.2s !important;
}
.gr-button-primary:hover {
transform: translateY(-1px) !important;
box-shadow: 0 8px 24px rgba(62, 207, 142, 0.35) !important;
}
"""
HEADER_HTML = """
<div style="text-align:center;padding:32px 0 16px;border-bottom:1px solid #22253a;margin-bottom:24px;">
<h1 style="font-family:'JetBrains Mono',monospace;font-size:32px;font-weight:700;margin:0;">
Kotlin/Java → DEX &amp; Merger Tool
</h1>
<p style="color:#6b7090;font-size:14px;margin-top:8px;">
Compile Kotlin or Java to Dalvik bytecode · SDK 33 · API 21+ · Create Fat DEX
</p>
</div>
"""
with gr.Blocks(css=CSS, title="Kotlin/Java → DEX Toolkit") as demo:
gr.HTML(HEADER_HTML)
with gr.Tabs():
# ── Вкладка 1: Компилятор ───
with gr.Tab("☕ Kotlin/Java Compiler"):
with gr.Row():
with gr.Column(scale=3):
code_input = gr.Textbox(
label="Source Code (.kt / .java)",
value=EXAMPLE_CODE,
lines=20,
)
with gr.Row():
file_upload = gr.File(
label="Source files — .kt / .java / .zip (можно несколько)",
file_types=[".kt", ".java", ".zip"],
file_count="multiple",
type="filepath",
)
filename_input = gr.Textbox(
label="Output filename (base)",
value="Main",
max_lines=1,
)
stub_upload = gr.File(
label="Дополнительные Classpath JARs (pre-installed jna, app_stubs и kotlin-stdlib уже подключены)",
file_types=[".jar"],
file_count="multiple",
type="filepath",
)
compile_btn = gr.Button("⚡ Compile", variant="primary", size="lg")
with gr.Column(scale=2):
log_output = gr.Textbox(
label="Compiler Log",
lines=20,
interactive=False,
)
dex_output = gr.File(label="Download compiled .dex", interactive=False)
class_output = gr.File(label="Download JVM output (.class/.zip)", interactive=False)
file_upload.change(
fn=handle_file_upload,
inputs=[file_upload],
outputs=[code_input],
)
compile_btn.click(
fn=run_compilation,
inputs=[code_input, filename_input, file_upload, stub_upload],
outputs=[log_output, dex_output, class_output],
)
# ── Вкладка 2: Fat DEX Merger ───
with gr.Tab("🛠️ Fat DEX Merger"):
gr.Markdown("### Объединение скомпилированных классов (.class/.zip/.jar) и библиотек/stubs (.jar) в единый Fat DEX")
with gr.Row():
with gr.Column(scale=3):
primary_classes_input = gr.File(
label="1. Основные файлы классов (.class, .zip или .jar с вашими классами)",
file_types=[".class", ".zip", ".jar"],
file_count="multiple",
type="filepath",
)
stubs_to_merge_input = gr.File(
label="2. Дополнительные JAR библиотеки для слияния (pre-installed jna, app_stubs, kotlin-stdlib добавляются автоматически)",
file_types=[".jar"],
file_count="multiple",
type="filepath",
)
merge_filename_input = gr.Textbox(
label="Имя выходного DEX файла (base)",
value="classes",
max_lines=1,
)
merge_btn = gr.Button("⚡ Merge to Fat DEX", variant="primary", size="lg")
with gr.Column(scale=2):
merge_log_output = gr.Textbox(
label="Merger Log",
lines=20,
interactive=False,
)
merged_dex_output = gr.File(label="Download Fat DEX", interactive=False)
merge_btn.click(
fn=run_fat_dex_merge,
inputs=[primary_classes_input, stubs_to_merge_input, merge_filename_input],
outputs=[merge_log_output, merged_dex_output],
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True)