This commit is contained in:
+228
@@ -0,0 +1,228 @@
|
||||
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
|
||||
Reference in New Issue
Block a user