forked from anixart-mod/patcher
224 lines
7.3 KiB
Python
224 lines
7.3 KiB
Python
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 *
|
||
|
||
# --- Paths ---
|
||
|
||
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}")
|
||
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__}")
|
||
else:
|
||
console.print("[cyan]Список патчей:")
|
||
for f in PATCHES.glob("*.py"):
|
||
if f.name.startswith("todo_") or f.name == "__init__.py":
|
||
continue
|
||
name = f.stem
|
||
if conf["patches"].get(name, {}).get("enabled", True):
|
||
console.print(f" [yellow]{name}: [green]✔ enabled")
|
||
else:
|
||
console.print(f" [yellow]{name}: [red]✘ disabled")
|
||
|
||
|
||
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")
|
||
|
||
|
||
@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()
|