from typing import List, Dict, Any import typer import importlib import traceback import yaml from plumbum import local, ProcessExecutionError from rich.console import Console from rich.progress import Progress from rich.prompt import Prompt from utils.config import * from utils.tools import * console = Console() app = typer.Typer() # ======================= PATCHING ========================= class Patch: def __init__(self, name: str, module): self.name = name self.module = module self.applied = False self.priority = getattr(module, "priority", 0) try: self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()) except Exception as e: console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}") console.print(f"[yellow]Используются значения по умолчанию") self.config = module.Config() def apply(self, conf: Dict[str, Any]) -> bool: try: self.applied = bool(self.module.apply(self.config, conf)) return self.applied except Exception as e: console.print(f"[red]Ошибка в патче {self.name}: {e}") traceback.print_exc() return False # ========================= INIT ========================= @app.command() def init(): """Создание директорий и скачивание инструментов""" ensure_dirs() conf = load_config(console) for f in PATCHES.glob("*.py"): if f.name.startswith("todo_") or f.name == "__init__.py": continue patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""])) json_string = patch.config.model_dump_json() (CONFIGS / f"{patch.name}.json").write_text(json_string) if not (TOOLS / "apktool.jar").exists(): download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar") if not (TOOLS / "apktool").exists(): download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool") (TOOLS / "apktool").chmod(0o755) try: local["java"]["-version"]() console.print("[green]Java найдена") except ProcessExecutionError: console.print("[red]Java не установлена") raise typer.Exit(1) # ========================= INFO ========================= @app.command() def info(patch_name: str = ""): """Вывод информации о патче""" conf = load_config(console).model_dump() if patch_name: patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=[""])) console.print(f"[green]Информация о патче {patch.name}:") console.print(f" [yellow]Приоритет: {patch.priority}") console.print(f" [yellow]Описание: {patch.module.__doc__}") console.print(f"[blue]Поля конфигурации") for field_name, field_info in type(patch.config).model_fields.items(): field_data = { 'type': field_info.annotation.__name__, 'description': field_info.description, 'default': field_info.default, 'json_schema_extra': field_info.json_schema_extra, } console.print(f'{field_name} {field_data}') console.print("\n[blue]" + "="*50 + "\n") else: conf = load_config(console) console.print("[cyan]Список патчей:") patch_list = [] for f in PATCHES.glob("*.py"): if f.name == "__init__.py": continue if f.name.startswith("todo_"): try: priority = __import__(f"patches.{f.stem}.priority", fromlist=[""]) except: priority = None patch_list.append((priority, f" [{priority}] [yellow]{f.stem}: [yellow]⚠ в разработке")) continue patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""])) if patch.config.enabled: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [green]✔ включен")) else: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [red]✘ выключен")) for _, patch in sorted(patch_list, key=lambda x: (x[0] is None, x[0]), reverse=True): console.print(patch) # ========================= UTIL ========================= def select_apk() -> Path: apks = [f for f in ORIGINAL.glob("*.apk")] if not apks: console.print("[red]Нет apk-файлов в папке original") raise typer.Exit(1) if len(apks) == 1: console.print(f"[green]Выбран {apks[0].name}") return apks[0] options = {str(i): apk for i, apk in enumerate(apks, 1)} for k, v in options.items(): console.print(f"{k}. {v.name}") choice = Prompt.ask("Выберите номер", choices=list(options.keys())) return options[choice] def decompile(apk: Path): console.print("[yellow]Декомпиляция apk...") run( console, [ "java", "-jar", str(TOOLS / "apktool.jar"), "d", "-f", "-o", str(DECOMPILED), str(apk), ] ) def compile(apk: Path, patches: List[Patch]): console.print("[yellow]Сборка apk...") with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f: meta = yaml.safe_load(f) version_info = meta.get("versionInfo", {}) version_code = version_info.get("versionCode", 0) version_name = version_info.get("versionName", "unknown") filename_version = version_name.lower().replace(" ", "-").replace(".", "-") out_apk = MODIFIED / f"Anixarty-mod-v{filename_version}.apk" aligned = out_apk.with_stem(out_apk.stem + "-aligned") signed = out_apk.with_stem(out_apk.stem + "-mod") run( console, [ "java", "-jar", str(TOOLS / "apktool.jar"), "b", str(DECOMPILED), "-o", str(out_apk), ] ) run( console, ["zipalign", "-v", "4", str(out_apk), str(aligned)] ) run( console, [ "apksigner", "sign", "--v1-signing-enabled", "false", "--v2-signing-enabled", "true", "--v3-signing-enabled", "true", "--ks", "keystore.jks", "--ks-pass", "file:keystore.pass", "--out", str(signed), str(aligned), ] ) console.print("[green]✔ APK успешно собран и подписан") with open(MODIFIED / "report.log", "w", encoding="utf-8") as f: f.write(f"Anixarty mod v {version_name} ({version_code})\n") for p in patches: f.write(f"{'✔' if p.applied else '✘'} {p.name}\n") # ========================= BUILD ========================= @app.command() def build( force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"), ): """Декомпиляция, патчи и сборка apk""" conf = load_config(console) apk = select_apk() decompile(apk) patch_objs: List[Patch] = [] conf.base |= {"verbose": verbose} for f in PATCHES.glob("*.py"): if f.name.startswith("todo_") or f.name == "__init__.py": continue name = f.stem module = importlib.import_module(f"patches.{name}") if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()): console.print(f"[yellow]≫ Пропускаем {name}") continue patch_objs.append(Patch(name, module)) patch_objs.sort(key=lambda p: p.priority, reverse=True) console.print("[cyan]Применение патчей") with Progress() as progress: task = progress.add_task("Патчи", total=len(patch_objs)) for p in patch_objs: ok = p.apply(conf.base) progress.console.print(f"{'✔' if ok else '✘'} {p.name}") progress.advance(task) successes = sum(p.applied for p in patch_objs) if successes == len(patch_objs): compile(apk, patch_objs) elif successes > 0 and ( force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y" ): compile(apk, patch_objs) else: console.print("[red]Сборка отменена") raise typer.Exit(1) if __name__ == "__main__": app()