#!/usr/bin/env python3 """Robustes Deployment der Debug-APK auf ein physisches Android-Geraet. Features: - ADB automatisch finden (Argument, ENV, local.properties) - USB oder Wireless-Device robust erkennen - Optional Build via Gradle - Install mit Retry - Optional App-Start - Klare Exitcodes und kompakte Logs """ from __future__ import annotations import argparse import os import re import subprocess import sys import time from pathlib import Path from typing import List, Optional, Tuple PACKAGE_NAME = "de.bollwerk.app" MAIN_ACTIVITY = "de.bollwerk.app/.MainActivity" DEFAULT_APK_REL = Path("app/build/outputs/apk/debug/app-debug.apk") DEFAULT_GRADLEW = Path("gradlew.bat") def log(msg: str) -> None: print(msg, flush=True) def run_cmd(cmd: List[str], cwd: Optional[Path] = None, timeout: Optional[int] = None) -> subprocess.CompletedProcess: return subprocess.run( cmd, cwd=str(cwd) if cwd else None, text=True, capture_output=True, timeout=timeout, check=False, ) def normalize_sdk_dir(raw: str) -> str: # local.properties encodes ':' as '\:' on Windows. val = raw.strip() val = val.replace("\\:", ":") val = val.replace("\\\\", "\\") return val def read_sdk_from_local_properties(repo_root: Path) -> Optional[Path]: local_props = repo_root / "local.properties" if not local_props.exists(): return None for line in local_props.read_text(encoding="utf-8").splitlines(): if line.strip().startswith("sdk.dir="): _, value = line.split("=", 1) sdk = normalize_sdk_dir(value) p = Path(sdk) return p if p.exists() else None return None def find_adb(repo_root: Path, explicit_adb: Optional[str]) -> Path: candidates: List[Path] = [] if explicit_adb: candidates.append(Path(explicit_adb)) env_android_sdk = os.environ.get("ANDROID_SDK_ROOT") or os.environ.get("ANDROID_HOME") if env_android_sdk: candidates.append(Path(env_android_sdk) / "platform-tools" / "adb.exe") candidates.append(Path(env_android_sdk) / "platform-tools" / "adb") sdk_from_local = read_sdk_from_local_properties(repo_root) if sdk_from_local: candidates.append(sdk_from_local / "platform-tools" / "adb.exe") candidates.append(sdk_from_local / "platform-tools" / "adb") # Last fallback: rely on PATH. path_adb = shutil_which("adb") if path_adb: candidates.append(Path(path_adb)) for candidate in candidates: if candidate.exists(): return candidate raise FileNotFoundError( "ADB nicht gefunden. Setze --adb oder ANDROID_SDK_ROOT/ANDROID_HOME oder local.properties sdk.dir." ) def shutil_which(binary: str) -> Optional[str]: for folder in os.environ.get("PATH", "").split(os.pathsep): p = Path(folder) / binary if p.exists(): return str(p) if os.name == "nt": p_exe = Path(folder) / f"{binary}.exe" if p_exe.exists(): return str(p_exe) return None def parse_adb_devices(output: str) -> List[Tuple[str, str]]: devices: List[Tuple[str, str]] = [] for line in output.splitlines(): line = line.strip() if not line or line.startswith("List of devices"): continue # serial state [extra] parts = re.split(r"\s+", line) if len(parts) >= 2: devices.append((parts[0], parts[1])) return devices def resolve_target_serial(adb: Path, prefer_serial: Optional[str], prefer_usb: bool, timeout_sec: int) -> str: start = time.time() while True: cp = run_cmd([str(adb), "devices", "-l"]) if cp.returncode == 0: devices = parse_adb_devices(cp.stdout) if prefer_serial: for serial, state in devices: if serial == prefer_serial and state == "device": return serial else: live = [(s, st) for s, st in devices if st == "device"] if prefer_usb: usb = [s for s, _ in live if not re.match(r"\d+\.\d+\.\d+\.\d+:\d+", s)] if usb: return usb[0] if live: return live[0][0] if (time.time() - start) >= timeout_sec: break time.sleep(2) raise RuntimeError("Kein geeignetes ADB-Geraet gefunden (USB/Wireless, Debugging, Autorisierung pruefen).") def ensure_apk(repo_root: Path, apk_path: Path, gradlew_path: Path, skip_build: bool) -> None: if skip_build and apk_path.exists(): return if not gradlew_path.exists(): raise FileNotFoundError(f"Gradle Wrapper nicht gefunden: {gradlew_path}") log("[1/4] Build Debug-APK ...") cp = run_cmd([str(gradlew_path), "assembleDebug"], cwd=repo_root) if cp.returncode != 0: tail = "\n".join((cp.stdout + "\n" + cp.stderr).splitlines()[-40:]) raise RuntimeError(f"Gradle Build fehlgeschlagen.\n{tail}") if not apk_path.exists(): raise FileNotFoundError(f"APK nach Build nicht gefunden: {apk_path}") def adb_install(adb: Path, serial: str, apk_path: Path, retries: int) -> None: last_err = "" for attempt in range(1, retries + 1): log(f"[3/4] Installiere APK (Versuch {attempt}/{retries}) ...") cp = run_cmd([str(adb), "-s", serial, "install", "-r", str(apk_path)], timeout=180) out = (cp.stdout + "\n" + cp.stderr).strip() if cp.returncode == 0 and "Success" in out: log("Install OK") return last_err = out time.sleep(2) raise RuntimeError(f"APK-Installation fehlgeschlagen.\n{last_err}") def adb_launch(adb: Path, serial: str) -> None: cp = run_cmd([str(adb), "-s", serial, "shell", "am", "start", "-n", MAIN_ACTIVITY], timeout=60) out = (cp.stdout + "\n" + cp.stderr).strip() if cp.returncode != 0 or "Error" in out or "Exception" in out: raise RuntimeError(f"App-Start fehlgeschlagen.\n{out}") def main() -> int: parser = argparse.ArgumentParser(description="Deploy app-debug.apk robust auf ein physisches Android-Geraet") parser.add_argument("--repo-root", default=".", help="Pfad zum Repo-Root (Standard: aktuelles Verzeichnis)") parser.add_argument("--adb", default=None, help="Expliziter Pfad zu adb(.exe)") parser.add_argument("--serial", default=None, help="Geraete-Serial explizit (z. B. USB-Serial oder 192.168.x.x:port)") parser.add_argument("--prefer-usb", action="store_true", help="Bevorzuge USB-Geraet, wenn --serial nicht gesetzt") parser.add_argument("--timeout-device", type=int, default=30, help="Maximale Wartezeit auf Device-Erkennung in Sekunden") parser.add_argument("--retries-install", type=int, default=3, help="Install-Retries") parser.add_argument("--skip-build", action="store_true", help="Build ueberspringen, wenn APK bereits existiert") parser.add_argument("--no-launch", action="store_true", help="App nach Installation nicht starten") args = parser.parse_args() repo_root = Path(args.repo_root).resolve() apk_path = (repo_root / DEFAULT_APK_REL).resolve() gradlew_path = (repo_root / DEFAULT_GRADLEW).resolve() try: adb = find_adb(repo_root, args.adb) log(f"ADB: {adb}") ensure_apk(repo_root, apk_path, gradlew_path, skip_build=args.skip_build) log("[2/4] Suche Device ...") serial = resolve_target_serial(adb, args.serial, args.prefer_usb, args.timeout_device) log(f"Device: {serial}") adb_install(adb, serial, apk_path, retries=args.retries_install) if not args.no_launch: log("[4/4] Starte App ...") adb_launch(adb, serial) log("Launch OK") log("DEPLOY SUCCESS") return 0 except Exception as exc: log(f"DEPLOY FAILED: {exc}") return 1 if __name__ == "__main__": raise SystemExit(main())