import xml.etree.ElementTree as ET from pathlib import Path from typing import Optional import yaml from pydantic import BaseModel, Field, computed_field from rich.console import Console from utils.tools import DECOMPILED, MODIFIED, TOOLS, run class APKMeta(BaseModel): """Метаданные APK файла""" version_code: int = Field(default=0) version_name: str = Field(default="unknown") package: str = Field(default="unknown") path: Path @computed_field @property def safe_version(self) -> str: """Версия, безопасная для использования в именах файлов""" return self.version_name.lower().replace(" ", "-").replace(".", "-") @computed_field @property def output_name(self) -> str: """Имя выходного файла""" return f"Anixarts-v{self.safe_version}.apk" @computed_field @property def aligned_name(self) -> str: """Имя выровненного файла""" return f"Anixarts-v{self.safe_version}-aligned.apk" @computed_field @property def signed_name(self) -> str: """Имя подписанного файла""" return f"Anixarts-v{self.safe_version}-mod.apk" class SigningConfig(BaseModel): """Конфигурация подписи APK""" keystore: Path = Field(default=Path("keystore.jks")) keystore_pass_file: Path = Field(default=Path("keystore.pass")) v1_signing: bool = Field(default=False) v2_signing: bool = Field(default=True) v3_signing: bool = Field(default=True) class APKProcessor: """Класс для работы с APK файлами""" def __init__(self, console: Console, tools_dir: Path = TOOLS): self.console = console self.tools_dir = tools_dir self.apktool_jar = tools_dir / "apktool.jar" def decompile(self, apk: Path, output: Path = DECOMPILED) -> None: """Декомпилирует APK файл""" self.console.print("[yellow]Декомпиляция APK...") run( self.console, [ "java", "-jar", str(self.apktool_jar), "d", "-f", "-o", str(output), str(apk), ], ) self.console.print("[green]✔ Декомпиляция завершена") def compile(self, source: Path, output: Path) -> None: """Компилирует APK из исходников""" self.console.print("[yellow]Сборка APK...") run( self.console, [ "java", "-jar", str(self.apktool_jar), "b", str(source), "-o", str(output), ], ) self.console.print("[green]✔ Сборка завершена") def align(self, input_apk: Path, output_apk: Path) -> None: """Выравнивает APK с помощью zipalign""" self.console.print("[yellow]Выравнивание APK...") run( self.console, ["zipalign", "-f", "-v", "4", str(input_apk), str(output_apk)] ) self.console.print("[green]✔ Выравнивание завершено") def sign( self, input_apk: Path, output_apk: Path, config: Optional[SigningConfig] = None, ) -> None: """Подписывает APK""" if config is None: config = SigningConfig() self.console.print("[yellow]Подпись APK...") run( self.console, [ "apksigner", "sign", "--v1-signing-enabled", str(config.v1_signing).lower(), "--v2-signing-enabled", str(config.v2_signing).lower(), "--v3-signing-enabled", str(config.v3_signing).lower(), "--ks", str(config.keystore), "--ks-pass", f"file:{config.keystore_pass_file}", "--out", str(output_apk), str(input_apk), ], ) self.console.print("[green]✔ Подпись завершена") def _get_package_name_from_manifest(self, decompiled_path: Path) -> str: """Читает имя пакета напрямую из AndroidManifest.xml""" manifest_path = decompiled_path / "AndroidManifest.xml" if not manifest_path.exists(): return "unknown" try: tree = ET.parse(manifest_path) root = tree.getroot() return root.get("package", "unknown") except Exception: return "unknown" def get_meta(self, decompiled: Path = DECOMPILED) -> APKMeta: """Извлекает метаданные из декомпилированного APK""" apktool_yml = decompiled / "apktool.yml" if not apktool_yml.exists(): raise FileNotFoundError(f"Файл {apktool_yml} не найден") with open(apktool_yml, encoding="utf-8") as f: meta = yaml.safe_load(f) version_info = meta.get("versionInfo", {}) package_name = self._get_package_name_from_manifest(decompiled) if package_name == "unknown": package_info = meta_yaml.get("packageInfo", {}) package_name = package_info.get("renameManifestPackage") or "unknown" return APKMeta( version_code=version_info.get("versionCode", 0), version_name=version_info.get("versionName", "unknown"), package=package_name, path=decompiled, ) def _extract_package_from_manifest(self, decompiled: Path) -> str | None: """Извлекает имя пакета из AndroidManifest.xml""" manifest = decompiled / "AndroidManifest.xml" if not manifest.exists(): return None try: import re content = manifest.read_text(encoding="utf-8") match = re.search(r'package="([^"]+)"', content) if match: return match.group(1) except Exception: pass return None def build_and_sign( self, source: Path = DECOMPILED, output_dir: Path = MODIFIED, signing_config: Optional[SigningConfig] = None, cleanup: bool = True, ) -> tuple[Path, APKMeta]: """ Полный цикл сборки: компиляция, выравнивание, подпись. Возвращает путь к подписанному APK и метаданные. """ meta = self.get_meta(source) out_apk = output_dir / meta.output_name aligned_apk = output_dir / meta.aligned_name signed_apk = output_dir / meta.signed_name for f in [out_apk, aligned_apk, signed_apk]: f.unlink(missing_ok=True) self.compile(source, out_apk) self.align(out_apk, aligned_apk) self.sign(aligned_apk, signed_apk, signing_config) if cleanup: out_apk.unlink(missing_ok=True) aligned_apk.unlink(missing_ok=True) idsig = signed_apk.with_suffix(".apk.idsig") idsig.unlink(missing_ok=True) self.console.print(f"[green]✔ APK готов: {signed_apk.name}") return signed_apk, meta