229 lines
7.4 KiB
Python
229 lines
7.4 KiB
Python
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"Anixarty-v{self.safe_version}.apk"
|
||
|
||
@computed_field
|
||
@property
|
||
def aligned_name(self) -> str:
|
||
"""Имя выровненного файла"""
|
||
return f"Anixarty-v{self.safe_version}-aligned.apk"
|
||
|
||
@computed_field
|
||
@property
|
||
def signed_name(self) -> str:
|
||
"""Имя подписанного файла"""
|
||
return f"Anixarty-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
|