#!/usr/bin/env python3 # file: scripts/build_android_rust.py # version: 3 """Build one Android Rust game entrypoint and stage SDL3 and Rust jniLibs.""" from __future__ import annotations import argparse import os import pathlib import shutil import subprocess import sys import zipfile ABI_TO_TRIPLE = { "arm64-v8a": "aarch64-linux-android", "armeabi-v7a": "armv7-linux-androideabi", "x86_64": "x86_64-linux-android", "x86": "i686-linux-android", } GAME_TO_MODULE = {"reflex": "game-reflex-poc", "snake": "game-snake-poc"} def read_gradle_property(properties_path: pathlib.Path, property_name: str) -> str: """Read one required Android Gradle property.""" prefix = f"{property_name}=" for raw_line in properties_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if line.startswith(prefix): return line.split("=", 1)[1].strip() raise RuntimeError(f"{property_name} is missing from Android/gradle.properties") def read_sdl3_aar_name(properties_path: pathlib.Path) -> str: """Read the configured SDL3 AAR filename.""" return read_gradle_property(properties_path, "sdl3AarName") def find_sdl3_member(archive: zipfile.ZipFile, abi: str) -> str: """Return the SDL3 shared-library member for one Android ABI.""" candidates = ( f"prefab/modules/SDL3/libs/android.{abi}/libSDL3.so", f"jni/{abi}/libSDL3.so", ) members = set(archive.namelist()) for candidate in candidates: if candidate in members: return candidate suffix = f"/libs/android.{abi}/libSDL3.so" fallback = sorted(member for member in members if member.endswith(suffix)) if len(fallback) == 1: return fallback[0] raise RuntimeError( f"SDL3 shared library for ABI {abi} is missing from the AAR; " f"expected Prefab member {candidates[0]}" ) def extract_sdl3_library( aar_path: pathlib.Path, abi: str, link_dir: pathlib.Path, runtime_dir: pathlib.Path, ) -> pathlib.Path: """Extract SDL3 for both Rust linking and APK runtime staging.""" link_dir.mkdir(parents=True, exist_ok=True) runtime_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(aar_path) as archive: member = find_sdl3_member(archive, abi) payload = archive.read(member) link_target = link_dir / "libSDL3.so" runtime_target = runtime_dir / "libSDL3.so" link_target.write_bytes(payload) runtime_target.write_bytes(payload) return link_target def main() -> int: """Build the requested game entrypoint for one Android ABI.""" parser = argparse.ArgumentParser() parser.add_argument("game", choices=sorted(GAME_TO_MODULE)) parser.add_argument("--abi", choices=sorted(ABI_TO_TRIPLE), default="arm64-v8a") parser.add_argument("--release", action="store_true") arguments = parser.parse_args() root = pathlib.Path(__file__).resolve().parent.parent properties_path = root / "Android" / "gradle.properties" aar_name = read_sdl3_aar_name(properties_path) ndk_version = read_gradle_property(properties_path, "androidNdkVersion") aar_path = root / "Android" / "libs" / aar_name if not aar_path.is_file(): print(f"missing SDL3 AAR: {aar_path}", file=sys.stderr) return 2 module = GAME_TO_MODULE[arguments.game] jni_root = root / "Android" / module / "src" / "main" / "jniLibs" runtime_dir = jni_root / arguments.abi link_dir = root / "Android" / "build" / "rust-link" / arguments.abi try: extract_sdl3_library(aar_path, arguments.abi, link_dir, runtime_dir) except (OSError, RuntimeError, zipfile.BadZipFile) as error: print(str(error), file=sys.stderr) return 2 environment = os.environ.copy() android_home = environment.get("ANDROID_HOME") if not android_home: print("ANDROID_HOME is required to resolve the project NDK", file=sys.stderr) return 2 ndk_home = pathlib.Path(android_home) / "ndk" / ndk_version if not ndk_home.is_dir(): print(f"configured Android NDK is missing: {ndk_home}", file=sys.stderr) return 2 environment["ANDROID_NDK_HOME"] = str(ndk_home) rustflags = environment.get("RUSTFLAGS", "").strip() link_flag = f"-L native={link_dir}" environment["RUSTFLAGS"] = f"{rustflags} {link_flag}".strip() environment["CARGO_NDK_PLATFORM"] = "21" command = [ "cargo", "ndk", "-t", arguments.abi, "-o", str(jni_root), "build", "-p", "game-android-entrypoint", "--no-default-features", "--features", arguments.game, ] if arguments.release: command.append("--release") completed = subprocess.run(command, cwd=root, env=environment, check=False) if completed.returncode != 0: return completed.returncode expected = runtime_dir / "libgame_android_entrypoint.so" if not expected.is_file(): print(f"missing Rust Android library after cargo-ndk build: {expected}", file=sys.stderr) return 3 return 0 if __name__ == "__main__": raise SystemExit(main())