Spaces:
Sleeping
Sleeping
Download app.py from coidada/AutoStub-gen: direct link, hf CLI and curl.
- Browser
- Download file 30.2 kB
-
https://huggingface.co/spaces/coidada/AutoStub-gen/resolve/main/app.py
- Command line
-
hf download hf://spaces/coidada/AutoStub-gen/app.py
-
curl -L -o app.py https://huggingface.co/spaces/coidada/AutoStub-gen/resolve/main/app.py
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 & 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) |