diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index fc2ae09..5de0e6e 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-reflex-poc/build.gradle -// version: 4 +// version: 5 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.7' + versionName '0.1.0-0-pre.7.fix.1' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index ee800db..43d9832 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-snake-poc/build.gradle -// version: 4 +// version: 5 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.7' + versionName '0.1.0-0-pre.7.fix.1' } compileOptions { diff --git a/Cargo.toml b/Cargo.toml index b9b0115..b344783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 14 +# version: 15 [workspace] resolver = "3" @@ -16,7 +16,7 @@ members = [ ] [workspace.package] -version = "0.1.0-0-pre.7" +version = "0.1.0-0-pre.7.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/deltas/0.1.0/0-pre.7.fix.1.md b/deltas/0.1.0/0-pre.7.fix.1.md new file mode 100644 index 0000000..db946ea --- /dev/null +++ b/deltas/0.1.0/0-pre.7.fix.1.md @@ -0,0 +1,129 @@ + + + +# Delta 0.1.0-0-pre.7.fix.1 + +## Base + +Base déclarée : `0.1.0-0-pre.7`, non validée intégralement. + +## Validation du delta précédent + +L'utilisateur a exécuté les validations de `0.1.0-0-pre.7` le 2026-09-16. + +Résultats propres : + +- `cargo fmt --all` ; +- `cargo fmt --all -- --check` ; +- audit Rust/workspace ; +- audit Markdown : `1 table(s), 51 file(s)` ; +- `cargo check --workspace` ; +- `cargo clippy --workspace --all-targets --all-features -- -D warnings` ; +- `cargo test -p game-android-entrypoint --no-default-features` ; +- `cargo-ndk 4.1.2` installé ; +- target Rust `aarch64-linux-android` installé ; +- builds Gradle Reflex et Snake propres. + +## Échec du build natif Android + +Les deux commandes : + +```bash +python3 scripts/build_android_rust.py reflex +python3 scripts/build_android_rust.py snake +``` + +ont échoué avant cross-compilation avec : + +```text +jni/arm64-v8a/libSDL3.so is missing from Android/libs/SDL3-3.4.16.aar +``` + +La cause est une hypothèse incorrecte du script. + +L'AAR SDL3 officiel expose ses bibliothèques natives via Prefab, sous une structure de type : + +```text +prefab/modules/SDL3/libs/android.arm64-v8a/libSDL3.so +``` + +et non obligatoirement sous `jni/arm64-v8a/`. + +## Correctif + +Le script `build_android_rust.py` : + +- recherche d'abord la structure Prefab officielle ; +- conserve un fallback `jni//libSDL3.so` ; +- sait détecter une unique variante compatible sous `*/libs/android./libSDL3.so` ; +- extrait `libSDL3.so` dans le répertoire de liaison temporaire ; +- stage aussi `libSDL3.so` dans le `jniLibs//` de l'application ; +- vérifie après `cargo ndk` que `libgame_android_entrypoint.so` existe réellement. + +Les `versionName` Android passent à `0.1.0-0-pre.7.fix.1`. + +## ADB et NDK + +L'absence initiale de `adb` n'est pas considérée comme un défaut du projet. + +Préflight recommandé : + +```bash +"${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358" "platform-tools" +export PATH="${ANDROID_HOME}/platform-tools:${PATH}" +adb version +``` + +Le NDK r28c correspond à `28.2.13676358`. + +## Validation à exécuter par l'utilisateur + +Le fix modifie un script Python et des fichiers Gradle, mais aucun fichier Rust. + +```bash +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history +cargo check --workspace +``` + +Build natif : + +```bash +python3 scripts/build_android_rust.py reflex +python3 scripts/build_android_rust.py snake +``` + +Vérifier : + +```bash +test -f Android/game-reflex-poc/src/main/jniLibs/arm64-v8a/libSDL3.so +test -f Android/game-reflex-poc/src/main/jniLibs/arm64-v8a/libgame_android_entrypoint.so +test -f Android/game-snake-poc/src/main/jniLibs/arm64-v8a/libSDL3.so +test -f Android/game-snake-poc/src/main/jniLibs/arm64-v8a/libgame_android_entrypoint.so +``` + +Puis : + +```bash +cd Android +gradle :game-reflex-poc:assembleDebug +gradle :game-snake-poc:assembleDebug +cd .. +``` + +Le smoke appareil est demandé seulement si `adb` et un appareil/émulateur sont disponibles : + +```bash +adb devices +``` + +puis installation/lancement des deux APK. + +## Règle de transition + +Si les gates légères, les deux builds Rust Android et les deux APK sont propres, `0.1.0-0-pre.7.fix.1` peut être validé même sans appareil disponible. + +Si un appareil/émulateur est disponible, le premier smoke runtime Android doit également être exécuté avant de passer à `0.1.0-0-pre.8`. + +En cas d'échec imputable au projet, produire `0.1.0-0-pre.7.fix.2`. diff --git a/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md b/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md index d6487b2..40f6049 100644 --- a/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md +++ b/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md @@ -1,5 +1,5 @@ - + # Build Rust Android natif @@ -14,6 +14,28 @@ Le crate Rust `game-android-entrypoint` est un `cdylib`. Il est compilé avec ex Le même nom de bibliothèque native peut être utilisé dans les deux APK puisque chaque module Android possède son propre répertoire `jniLibs`. +## SDL3 AAR et Prefab + +L'AAR officiel SDL3 expose ses bibliothèques natives avec Prefab. + +Pour `arm64-v8a`, la structure attendue est notamment : + +```text +prefab/modules/SDL3/libs/android.arm64-v8a/libSDL3.so +``` + +Le script `scripts/build_android_rust.py` accepte cette structure officielle et conserve un fallback `jni//libSDL3.so` pour compatibilité. + +Pour chaque application, le script : + +1. extrait `libSDL3.so` depuis l'AAR ; +2. l'utilise comme bibliothèque de liaison pour le build Rust ; +3. copie `libSDL3.so` dans le `jniLibs//` du module Android ; +4. lance `cargo ndk` ; +5. vérifie la présence de `libgame_android_entrypoint.so`. + +Ainsi, `SDLActivity` dispose au runtime de `libSDL3.so` et de la bibliothèque Rust applicative. + ## Toolchain SDL Android requiert actuellement SDK 35 ou ultérieur, NDK r28c ou ultérieur et API minimale 21. La baseline projet conserve `compileSdk 36`, `minSdk 21` et l'AAR SDL3 3.4.16. @@ -25,8 +47,28 @@ rustup target add aarch64-linux-android cargo install cargo-ndk ``` +NDK de référence : + +```text +r28c = 28.2.13676358 +``` + +Avec le SDK manager : + +```bash +"${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358" "platform-tools" +``` + `cargo-ndk` détecte un NDK installé par Android Studio ou utilise `ANDROID_NDK_HOME` lorsqu'il est défini. +Pour utiliser directement `adb` : + +```bash +export PATH="${ANDROID_HOME}/platform-tools:${PATH}" +``` + +Cet export peut être ajouté au fichier de configuration shell local de la machine. + ## Build Depuis la racine : @@ -47,6 +89,19 @@ cd .. Les bibliothèques générées sous `Android/*/src/main/jniLibs/` sont des artefacts de build et ne sont pas versionnées. +## Smoke appareil + +Préflight : + +```bash +adb version +adb devices +``` + +Puis installation/lancement des APK si un appareil ou émulateur est disponible. + +L'absence de `adb` ou d'appareil n'est pas un défaut fonctionnel du projet ; elle bloque uniquement le smoke appareil. + ## Exception FFI Rust Rust 2024 exige un attribut unsafe pour imposer un nom de symbole d'export. Le projet autorise donc exclusivement `#[unsafe(export_name = "SDL_main")]` dans `game-android-entrypoint`. Cette exception ne permet ni bloc `unsafe`, ni fonction `unsafe`, ni déréférencement des pointeurs `argc/argv`. diff --git a/scripts/build_android_rust.py b/scripts/build_android_rust.py index fee3e16..f75ba03 100644 --- a/scripts/build_android_rust.py +++ b/scripts/build_android_rust.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # file: scripts/build_android_rust.py -# version: 1 +# version: 2 -"""Build one Android Rust game entrypoint and stage it as Gradle jniLibs.""" +"""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 @@ -29,16 +30,42 @@ def read_sdl3_aar_name(properties_path: pathlib.Path) -> str: return line.split("=", 1)[1].strip() raise RuntimeError("sdl3AarName is missing from Android/gradle.properties") -def extract_sdl3_link_library(aar_path: pathlib.Path, abi: str, output_dir: pathlib.Path) -> pathlib.Path: - """Extract the SDL3 shared library used only for the Rust linker search path.""" - expected = f"jni/{abi}/libSDL3.so" - output_dir.mkdir(parents=True, exist_ok=True) +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: - if expected not in archive.namelist(): - raise RuntimeError(f"{expected} is missing from {aar_path}") - target = output_dir / "libSDL3.so" - target.write_bytes(archive.read(expected)) - return target + 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.""" @@ -53,27 +80,44 @@ def main() -> int: 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_link_library(aar_path, arguments.abi, link_dir) + 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 - module = GAME_TO_MODULE[arguments.game] - output = root / "Android" / module / "src" / "main" / "jniLibs" environment = os.environ.copy() 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(output), "build", - "-p", "game-android-entrypoint", "--no-default-features", "--features", arguments.game, + "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) - return completed.returncode + 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())