Обновление структуры проекта, использование pydantic для конфигураций, улучшение отчёта о патчах
Build mod / build (push) Successful in 6m39s

This commit is contained in:
2025-10-01 23:07:37 +03:00
parent 5ba590cc31
commit fbc8b3e017
25 changed files with 406 additions and 315 deletions
+74 -134
View File
@@ -1,5 +1,4 @@
from pathlib import Path
from typing import List
from typing import List, Dict, Any
import httpx
import typer
@@ -14,80 +13,36 @@ from rich.progress import Progress
from rich.prompt import Prompt
from rich.table import Table
from utils.config import *
from utils.tools import *
# --- Paths ---
TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")
console = Console()
app = typer.Typer()
# ======================= CONFIG =========================
class ToolsConfig(BaseModel):
apktool_jar_url: str
apktool_wrapper_url: str
# ======================= 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()
class XmlNamespaces(BaseModel):
android: str
app: str
class BaseSection(BaseModel):
tools: ToolsConfig
xml_ns: XmlNamespaces
class Config(BaseModel):
base: BaseSection
patches: dict
def load_config() -> Config:
try:
return Config.model_validate_json(Path("config.json").read_text())
except FileNotFoundError:
console.print("[red]Файл config.json не найден")
raise typer.Exit(1)
except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e)
raise typer.Exit(1)
# ======================= UTILS =========================
def ensure_dirs():
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES]:
d.mkdir(exist_ok=True)
def run(cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]]
try:
prog() if hide_output else prog & FG
except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr)
raise typer.Exit(1)
def download(url: str, dest: Path):
console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
with client.stream("GET", url) as response:
response.raise_for_status()
total = int(response.headers.get("Content-Length", 0))
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f, Progress(console=console) as progress:
task = progress.add_task("Загрузка", total=total if total else None)
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
progress.update(task, advance=len(chunk))
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 =========================
@@ -95,13 +50,20 @@ def download(url: str, dest: Path):
def init():
"""Создание директорий и скачивание инструментов"""
ensure_dirs()
conf = load_config()
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(conf.base.tools.apktool_jar_url, TOOLS / "apktool.jar")
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
if not (TOOLS / "apktool").exists():
download(conf.base.tools.apktool_wrapper_url, TOOLS / "apktool")
download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
(TOOLS / "apktool").chmod(0o755)
try:
@@ -116,9 +78,9 @@ def init():
@app.command()
def info(patch_name: str = ""):
"""Вывод информации о патче"""
conf = load_config().model_dump()
conf = load_config(console).model_dump()
if patch_name:
patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=['']))
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__}")
@@ -128,30 +90,12 @@ def info(patch_name: str = ""):
if f.name.startswith("todo_") or f.name == "__init__.py":
continue
name = f.stem
if conf['patches'].get(name,{}).get('enabled',True):
if conf["patches"].get(name, {}).get("enabled", True):
console.print(f" [yellow]{name}: [green]✔ enabled")
else:
console.print(f" [yellow]{name}: [red]✘ disabled")
# ======================= PATCHING =========================
class Patch:
def __init__(self, name: str, module):
self.name = name
self.module = module
self.applied = False
self.priority = getattr(module, "priority", 0)
def apply(self, conf: dict) -> bool:
try:
self.applied = bool(self.module.apply(conf))
return self.applied
except Exception as e:
console.print(f"[red]Ошибка в патче {self.name}: {e}")
traceback.print_exc()
return False
def select_apk() -> Path:
apks = [f for f in ORIGINAL.glob("*.apk")]
if not apks:
@@ -173,14 +117,12 @@ def select_apk() -> Path:
def decompile(apk: Path):
console.print("[yellow]Декомпиляция apk...")
run(
console,
[
"java",
"-jar",
str(TOOLS / "apktool.jar"),
"d",
"-f",
"-o",
str(DECOMPILED),
"-jar", str(TOOLS / "apktool.jar"),
"d", "-f",
"-o", str(DECOMPILED),
str(apk),
]
)
@@ -188,52 +130,51 @@ def decompile(apk: Path):
def compile(apk: Path, patches: List[Patch]):
console.print("[yellow]Сборка apk...")
out_apk = MODIFIED / apk.name
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),
"-jar", str(TOOLS / "apktool.jar"),
"b", str(DECOMPILED),
"-o", str(out_apk),
]
)
run(["zipalign", "-v", "4", str(out_apk), str(aligned)])
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),
"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),
]
)
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_str = " ".join(
f"{k}:{v}" for k, v in meta.get("versionInfo", {}).items()
)
console.print("[green]✔ APK успешно собран и подписан")
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
f.write(f"anixart mod {version_str}\n")
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
for p in patches:
f.write(f"{p.name}: {'applied' if p.applied else 'failed'}\n")
f.write(f"{'' if p.applied else ''} {p.name}\n")
@app.command()
@@ -242,22 +183,21 @@ def build(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
):
"""Декомпиляция, патчи и сборка apk"""
conf = load_config().model_dump()
conf = load_config(console)
apk = select_apk()
decompile(apk)
patch_settings = conf.get("patches", {})
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
settings = patch_settings.get(name, {})
if not settings.get("enabled", True):
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
module = importlib.import_module(f"patches.{name}")
patch_objs.append(Patch(name, module))
patch_objs.sort(key=lambda p: p.priority, reverse=True)
@@ -266,7 +206,7 @@ def build(
with Progress() as progress:
task = progress.add_task("Патчи", total=len(patch_objs))
for p in patch_objs:
ok = p.apply(patch_settings.get(p.name, {}) | conf.get("base", {}))
ok = p.apply(conf.base)
progress.console.print(f"{'' if ok else ''} {p.name}")
progress.advance(task)