From 19f31082eafe1c2bdd561c80bb81f02f5ea03ddc Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 21 Sep 2026 11:17:38 +0200 Subject: [PATCH] 0.3.3-0-pre.5 --- Android/README.md | 24 ++- Android/gradle/sasedev-rust-android.gradle | 44 ++++-- Cargo.toml | 4 +- deltas/0.3.3/0-pre.5.delete.txt | 1 + deltas/0.3.3/0-pre.5.md | 126 ++++++++++++++++ .../006-ANDROID_RUST_NATIVE_BUILD.md | 63 +++++--- ...03-V0_3_3_ANDROID_NATIVE_MULTI_ABI_PLAN.md | 10 +- docs/rules/RULES_COMMANDS.md | 4 +- ...5-V0_3_3_ANDROID_NATIVE_MULTI_ABI_AUDIT.md | 10 +- docs/testing/003-RC_VALIDATION_MATRIX.md | 20 +-- history/0.3.3/0-pre.4.md | 76 ++++++++++ scripts/audit_distribution_layout.py | 13 +- scripts/build_android_rust.py | 139 ------------------ 13 files changed, 322 insertions(+), 212 deletions(-) create mode 100644 deltas/0.3.3/0-pre.5.delete.txt create mode 100644 deltas/0.3.3/0-pre.5.md create mode 100644 history/0.3.3/0-pre.4.md delete mode 100644 scripts/build_android_rust.py diff --git a/Android/README.md b/Android/README.md index 139243f..2703d67 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,5 +1,5 @@ - + # Android @@ -50,8 +50,12 @@ Le build natif normal utilise directement le Gradle de l'environnement : ```bash (cd Android && gradle :game-snake-poc:assembleDebug) (cd Android && gradle :game-reflex-poc:assembleDebug) +(cd Android && gradle :game-snake-poc:bundleRelease) +(cd Android && gradle :game-reflex-poc:bundleRelease) ``` +`bundleRelease` ne requiert aucun secret dans le dépôt. Sans configuration de signature externe, il produit un AAB de validation non destiné à être téléversé tel quel sur un store. + Le dépôt ne versionne pas de Gradle Wrapper pour le projet Android natif. Le minimum est un contrat de compatibilité, pas une version exacte imposée à toutes les machines. ## SDL3 AAR @@ -74,7 +78,7 @@ Le module `common` compile contre cet artefact et `SaseGameActivity` étend `org Le crate `game-android-entrypoint` produit `libgame_android_entrypoint.so`. `Android/gradle/sasedev-rust-android.gradle` transfère au graphe Gradle les responsabilités historiques d'extraction SDL3, de résolution NDK, de `cargo ndk`, de sélection de feature et de staging `jniLibs`. -À `0.3.3-0-pre.4`, Snake et Reflex utilisent tous deux le même pipeline commun avec les quatre ABI Android encore supportées par la toolchain retenue : +À `0.3.3-0-pre.5`, Snake et Reflex utilisent tous deux le même pipeline commun avec les quatre ABI Android encore supportées par la toolchain retenue : ```text arm64-v8a @@ -85,17 +89,25 @@ x86 Les ABI historiques supprimées des toolchains Android modernes, notamment `armeabi`, `mips` et `mips64`, ne font pas partie du contrat. Le support maximal signifie ici toutes les ABI encore supportées simultanément par Android NDK/SDL3/Rust, et non des architectures abandonnées par l'écosystème. -Chaque `assembleDebug` doit déclencher, pour chaque ABI, un build `cargo ndk` avec exactement la feature du module concerné. Les outputs sont placés sous : +Chaque variante Android déclenche, pour chaque ABI, un build `cargo ndk` avec exactement la feature du module concerné. La variante Debug utilise le profil Cargo `dev`; la variante Release ajoute `--release`. Les outputs sont placés sous : ```text -Android//build/generated/sasedevNative/debug//jniLibs// +Android//build/generated/sasedevNative///jniLibs// ├── libSDL3.so └── libgame_android_entrypoint.so ``` -AGP fusionne ces sources `jniLibs` générées dans un APK Debug universal contenant les quatre architectures. Aucune copie durable n'est faite dans `src/main/jniLibs`. +AGP fusionne ces sources `jniLibs` générées dans l'APK Debug universal ou dans l'AAB Release. Aucune copie durable n'est faite dans `src/main/jniLibs`. -`scripts/build_android_rust.py` reste temporairement comme chemin historique jusqu'au jalon de fermeture `0-pre.5`; il ne fait plus partie du chemin normal `assembleDebug` de Snake ou Reflex. +Le builder historique `scripts/build_android_rust.py` est supprimé en `0-pre.5` : toutes ses responsabilités durables (résolution NDK, extraction SDL3, linkage, sélection de feature, `cargo ndk`, profil release et staging) appartiennent désormais au graphe Gradle. + +## Compatibilité 16 KB et minSdk + +Le plancher Android reste `minSdk 21` / Android 5.0, qui correspond au minimum Android documenté par SDL3. Le niveau Rust transmis à `cargo ndk` reste également `21`. + +La baseline utilise AGP `9.4.0` et NDK `r28c`. Les bibliothèques Rust produites par ce NDK doivent être alignées pour des pages de 16 KB ; SDL3 étant fourni sous forme précompilée, `libSDL3.so` doit être vérifiée dans l'artefact final au même titre que `libgame_android_entrypoint.so`. La gate `0-pre.5` contrôle les segments ELF avec `llvm-objdump`, puis l'alignement ZIP de l'APK avec `zipalign -P 16`. + +L'AAB doit contenir les deux bibliothèques pour les quatre ABI sous `base/lib//`. Si `bundletool` est disponible dans l'environnement de validation, `bundletool dump config --bundle=` doit également annoncer `PAGE_ALIGNMENT_16K`. ## Bridge JNI et tactile diff --git a/Android/gradle/sasedev-rust-android.gradle b/Android/gradle/sasedev-rust-android.gradle index 04fcdc7..925ea49 100644 --- a/Android/gradle/sasedev-rust-android.gradle +++ b/Android/gradle/sasedev-rust-android.gradle @@ -1,5 +1,5 @@ // file: Android/gradle/sasedev-rust-android.gradle -// version: 1 +// version: 2 import java.util.Collections import java.util.zip.ZipFile @@ -27,6 +27,9 @@ abstract class BuildSasedevRustAndroidTask extends DefaultTask { @Input abstract Property getNdkVersion() + @Input + abstract Property getReleaseBuild() + @Internal abstract DirectoryProperty getRepositoryRoot() @@ -92,25 +95,32 @@ abstract class BuildSasedevRustAndroidTask extends DefaultTask { def linkFlag = "-Lnative=${linkDirectory}" def rustFlags = inheritedRustFlags ? "${inheritedRustFlags} ${linkFlag}" : linkFlag + def cargoCommand = [ + "cargo", + "ndk", + "-t", + selectedAbi, + "-o", + output.absolutePath, + "build", + "-p", + "game-android-entrypoint", + ] + if (releaseBuild.get()) { + cargoCommand.add("--release") + } + cargoCommand.addAll([ + "--no-default-features", + "--features", + selectedFeature, + ]) + execOperations.exec { spec -> spec.workingDir(repository) spec.environment("ANDROID_NDK_HOME", configuredNdk.absolutePath) spec.environment("CARGO_NDK_PLATFORM", androidApi.get().toString()) spec.environment("RUSTFLAGS", rustFlags) - spec.commandLine( - "cargo", - "ndk", - "-t", - selectedAbi, - "-o", - output.absolutePath, - "build", - "-p", - "game-android-entrypoint", - "--no-default-features", - "--features", - selectedFeature, - ) + spec.commandLine(cargoCommand) } def rustLibrary = new File(runtimeDirectory, "libgame_android_entrypoint.so") @@ -153,7 +163,8 @@ if (!unsupportedAbis.isEmpty()) { } androidComponents { - onVariants(selector().withBuildType("debug")) { variant -> + onVariants(selector().all()) { variant -> + def selectedReleaseBuild = variant.buildType == "release" selectedAbis.each { selectedAbi -> def variantSuffix = variant.name.substring(0, 1).toUpperCase() + variant.name.substring(1) def abiSuffix = selectedAbi @@ -177,6 +188,7 @@ androidComponents { abi.set(selectedAbi) androidApi.set(selectedAndroidApi) ndkVersion.set(configuredNdkVersion) + releaseBuild.set(selectedReleaseBuild) repositoryRoot.set(repositoryDirectory) outputDirectory.set(layout.buildDirectory.dir("generated/sasedevNative/${variant.name}/${selectedAbi}/jniLibs")) } diff --git a/Cargo.toml b/Cargo.toml index c639e9f..5c535b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 83 +# version: 84 [workspace] resolver = "3" @@ -21,7 +21,7 @@ members = [ ] [workspace.package] -version = "0.3.3-0-pre.4" +version = "0.3.3-0-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/deltas/0.3.3/0-pre.5.delete.txt b/deltas/0.3.3/0-pre.5.delete.txt new file mode 100644 index 0000000..49c47af --- /dev/null +++ b/deltas/0.3.3/0-pre.5.delete.txt @@ -0,0 +1 @@ +scripts/build_android_rust.py diff --git a/deltas/0.3.3/0-pre.5.md b/deltas/0.3.3/0-pre.5.md new file mode 100644 index 0000000..a9b7942 --- /dev/null +++ b/deltas/0.3.3/0-pre.5.md @@ -0,0 +1,126 @@ + + + +# Delta 0.3.3-0-pre.5 + +## Objet + +Fermer le chemin Android natif historique après validation des quatre ABI : étendre l'ownership Gradle aux variantes Release/AAB, consolider `minSdk 21`, préparer la vérification 16 KB et supprimer `scripts/build_android_rust.py`. + +## Base + +Base : `0.3.3-0-pre.4`, validée par l'utilisateur le 2026-09-21 avec Temurin 25.0.4.1, Gradle 9.7.1, audits/Cargo/Clippy propres et builds Debug Snake/Reflex réussis sur `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`. + +`history/0.3.3/0-pre.4.md` consigne cette preuve. + +## Changements + +- version workspace portée à `0.3.3-0-pre.5` ; +- `Android/gradle/sasedev-rust-android.gradle` enregistre désormais ses sources `jniLibs` pour toutes les variantes, pas seulement Debug ; +- les variantes Release déclenchent `cargo ndk ... build --release`, tandis que Debug conserve le profil Cargo `dev` ; +- `bundleRelease` de Snake et Reflex devient donc propriétaire de la reconstruction native quatre ABI ; +- `minSdk 21` et `CARGO_NDK_PLATFORM=21` deviennent des invariants audités pour les deux applications ; +- le builder historique `scripts/build_android_rust.py` est supprimé via manifest ; +- l'audit de distribution interdit désormais le retour de ce script et de `src/main/jniLibs` générés ; +- les anciennes commandes RC dépendant du builder Python sont remplacées par les tâches Gradle natives ; +- documentation Android/build/plan/étude actualisée pour AAB, API 21 et contrôle 16 KB. + +Aucun secret de signature ni keystore n'est ajouté. + +## Référence 16 KB + +Le contrôle suit le chemin recommandé par Android : AGP `>= 8.5.1`, NDK `r28+`, vérification ELF par `llvm-objdump`, puis `zipalign -P 16` sur l'APK. La SDL3 de l'AAR est précompilée et doit donc être contrôlée explicitement avec la bibliothèque Rust. + +## Validation attendue + +Depuis la racine : + +```bash +java -version +(cd Android && gradle --version) + +cargo fmt --all +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 Web deltas history +python3 scripts/audit_distribution_layout.py +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +(cd Android && gradle :game-snake-poc:bundleRelease) +(cd Android && gradle :game-reflex-poc:bundleRelease) +``` + +Les AAB attendus sont sous `Android//build/outputs/bundle/release/`. Pour chaque bundle : + +```bash +for game in game-snake-poc game-reflex-poc; do + AAB="$(find "Android/${game}/build/outputs/bundle/release" \ + -maxdepth 1 -type f -name '*.aab' -print -quit)" + test -n "$AAB" + printf 'AAB=%s\n' "$AAB" + unzip -Z1 "$AAB" | grep '^base/lib/' | sort + for abi in arm64-v8a armeabi-v7a x86_64 x86; do + unzip -Z1 "$AAB" | grep -Fx "base/lib/${abi}/libSDL3.so" + unzip -Z1 "$AAB" | grep -Fx "base/lib/${abi}/libgame_android_entrypoint.so" + done +done +``` + +Revalider ensuite l'APK Debug et son alignement ZIP 16 KB avec le `zipalign` des Build Tools installés : + +```bash +ZIPALIGN="$(find "$ANDROID_HOME/build-tools" -mindepth 2 -maxdepth 2 -type f -name zipalign -print | sort -V | tail -n 1)" +test -x "$ZIPALIGN" + +for game in game-snake-poc game-reflex-poc; do + (cd Android && gradle ":${game}:assembleDebug") + APK="$(find "Android/${game}/build/outputs/apk/debug" -maxdepth 1 -type f -name '*.apk' -print -quit)" + "$ZIPALIGN" -c -P 16 -v 4 "$APK" +done +``` + +Contrôler les segments ELF des bibliothèques générées et de SDL3. Sur Linux avec le NDK configuré : + +```bash +OBJDUMP="$ANDROID_HOME/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump" +test -x "$OBJDUMP" + +for game in game-snake-poc game-reflex-poc; do + for variant in debug release; do + for abi in arm64-v8a armeabi-v7a x86_64 x86; do + for so in \ + "Android/${game}/build/generated/sasedevNative/${variant}/${abi}/jniLibs/${abi}/libSDL3.so" \ + "Android/${game}/build/generated/sasedevNative/${variant}/${abi}/jniLibs/${abi}/libgame_android_entrypoint.so"; do + test -f "$so" + printf '%s\n' "$so" + "$OBJDUMP" -p "$so" | grep 'LOAD' + done + done + done +done +``` + +Aucun `LOAD` ne doit annoncer un alignement inférieur à `2**14`. + +Si `bundletool` est installé dans l'environnement : + +```bash +bundletool dump config --bundle="Android/game-snake-poc/build/outputs/bundle/release/game-snake-poc-release.aab" | grep alignment +bundletool dump config --bundle="Android/game-reflex-poc/build/outputs/bundle/release/game-reflex-poc-release.aab" | grep alignment +``` + +La valeur attendue est `PAGE_ALIGNMENT_16K`. Son absence locale parce que `bundletool` n'est pas installé doit être consignée ; elle ne doit pas être remplacée par une affirmation fictive. + +Pour le plancher Android, relever les images disponibles : + +```bash +emulator -list-avds +sdkmanager --list_installed | grep 'system-images;android-' || true +``` + +Si une image API 21 exploitable est déjà disponible ou raisonnablement installable, effectuer un smoke Snake. Sinon, consigner explicitement cette lacune ; le contrat reste API 21 car SDL3 lui-même documente ce niveau comme minimum et le build Rust/manifest l'imposent déjà. + +## Suite + +Si les AAB quatre ABI et les contrôles 16 KB sont propres, la prochaine tranche est `0.3.3-2-beta.1` conformément au plan révisé. Une défaillance du chemin Release/AAB, de l'alignement ou de la fermeture Python produit d'abord `0.3.3-0-pre.5.fix.N`. diff --git a/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md b/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md index 096ab72..6bdcb94 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 @@ -54,35 +54,32 @@ Le projet Android natif n'impose pas `JAVA_HOME`. Le JDK courant du shell/IDE ex `cargo-ndk` détecte le NDK désigné pour le sous-processus via `ANDROID_NDK_HOME`. La tâche Gradle résout cette valeur à partir de `ANDROID_HOME` ou `ANDROID_SDK_ROOT` et de `androidNdkVersion`; aucun `ANDROID_NDK_HOME` global n'est requis. -## État 0.3.3-0-pre.4 +## État 0.3.3-0-pre.5 -La preuve mono-ABI de `0-pre.2` a validé que `assembleDebug` déclenche correctement le build Rust/SDL3 ARM64 sans orchestrateur Python préalable. +`0-pre.4` a validé les quatre ABI sur Snake et Reflex : `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`. Le même mécanisme Gradle est désormais enregistré pour toutes les variantes Android, et pas seulement Debug. -`0-pre.3` a validé le graphe sur les deux ABI 64 bits. `0-pre.4` élargit le même mécanisme à la matrice Android complète encore supportée : - -```text -arm64-v8a -armeabi-v7a -x86_64 -x86 -``` - -pour Snake et Reflex : +Pour Debug : ```text ::assembleDebug - -> buildDebugSasedevRustArm64V8a - -> buildDebugSasedevRustArmeabiV7a - -> buildDebugSasedevRustX8664 - -> buildDebugSasedevRustX86 - -> quatre sources jniLibs générées + -> buildDebugSasedevRust + -> cargo ndk build -> APK Debug universal ``` -Chaque arbre ABI contient : +Pour Release/AAB : ```text -build/generated/sasedevNative/debug//jniLibs// +::bundleRelease + -> buildReleaseSasedevRust + -> cargo ndk build --release + -> AAB Release multi-ABI +``` + +Chaque arbre généré contient : + +```text +build/generated/sasedevNative///jniLibs// ├── libSDL3.so └── libgame_android_entrypoint.so ``` @@ -94,17 +91,37 @@ java -version (cd Android && gradle --version) (cd Android && gradle :game-snake-poc:assembleDebug) (cd Android && gradle :game-reflex-poc:assembleDebug) +(cd Android && gradle :game-snake-poc:bundleRelease) +(cd Android && gradle :game-reflex-poc:bundleRelease) ``` -Il ne faut pas appeler `scripts/build_android_rust.py` avant ces tâches. Le script Python historique reste temporairement versionné jusqu'à la fermeture complète du chemin natif en `0-pre.5`, après preuve du chemin release/AAB. +Le builder historique `scripts/build_android_rust.py` est retiré : il n'existe plus de chemin natif Android qui stage des bibliothèques dans `src/main/jniLibs`. Les sorties natives sont exclusivement générées sous `build/`. -Les outputs natifs restent sous `Android//build/` et ne sont jamais versionnés. +### Pages mémoire 16 KB + +La validation porte sur les deux dimensions requises : + +1. segments ELF de chaque `.so` avec un alignement `LOAD` au moins `2**14`, contrôlés par `llvm-objdump -p` depuis le NDK configuré ; +2. alignement ZIP de l'APK avec `zipalign -c -P 16 -v 4`. + +AGP `9.4.0` est supérieur au plancher `8.5.1` documenté pour le packaging 16 KB et NDK `r28c` produit les nouvelles bibliothèques avec alignement 16 KB par défaut. Cette propriété ne dispense pas de contrôler `libSDL3.so`, qui est précompilée dans l'AAR. + +Pour l'AAB, les huit bibliothèques doivent être présentes sous `base/lib//`. Lorsque `bundletool` est disponible, `bundletool dump config --bundle=` doit indiquer `PAGE_ALIGNMENT_16K`. + +### Plancher Android + +Le contrat est consolidé à Android 5.0 / API 21 : + +- `minSdk 21` dans chaque application ; +- `sasedevRustAndroidApi = 21` pour `cargo ndk` ; +- SDL3 documente API 21 comme minimum Android ; +- un smoke API 21 est souhaité lorsqu'une image 32/64 bits exploitable est disponible, mais l'absence locale d'une telle image est consignée comme lacune de runtime et ne modifie pas artificiellement le contrat. ## Smoke appareil L'APK universal doit être inspecté mécaniquement avant installation. Il doit contenir les deux bibliothèques natives pour chacune des quatre ABI : `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`. -Le smoke réel reste effectué sur les architectures matériellement disponibles : AVD x86_64 et appareil ARM64. Les ABI 32 bits sont néanmoins exigées au build et dans l'APK ; leur exécution réelle dépend de la disponibilité d'un appareil/AVD 32 bits. Ces smokes ne changent pas le `minSdk`, qui reste un axe séparé et sera consolidé avec l'AAB/compatibilité dans `0-pre.5`. +Le smoke réel reste effectué sur les architectures matériellement disponibles : AVD x86_64 et appareil ARM64. Les ABI 32 bits sont néanmoins exigées au build et dans l'APK ; leur exécution réelle dépend de la disponibilité d'un appareil/AVD 32 bits. Le `minSdk 21` est consolidé en `0-pre.5`; les smokes disponibles servent de preuve runtime complémentaire et ne redéfinissent pas ce plancher. ## Exception FFI Rust diff --git a/docs/plans/003-V0_3_3_ANDROID_NATIVE_MULTI_ABI_PLAN.md b/docs/plans/003-V0_3_3_ANDROID_NATIVE_MULTI_ABI_PLAN.md index e81f756..3dd351a 100644 --- a/docs/plans/003-V0_3_3_ANDROID_NATIVE_MULTI_ABI_PLAN.md +++ b/docs/plans/003-V0_3_3_ANDROID_NATIVE_MULTI_ABI_PLAN.md @@ -1,5 +1,5 @@ - + # Plan 0.3.3 — Android SDL3 natif multi-ABI et packaging Gradle @@ -74,7 +74,7 @@ La logique durable doit rester répartie ainsi : - `Android/gradle/` : logique de build Android commune, y compris orchestration Rust native et staging des sorties générées ; - `assets/` : sources d'assets canoniques ; - `Android//build/generated/` : outputs jetables de packaging ; -- `scripts/build_android_rust.py` : mécanisme historique à retirer du chemin normal dès que la responsabilité Gradle est prouvée, puis fichier à supprimer seulement lorsque toutes ses responsabilités sont remplacées. +- le builder historique `scripts/build_android_rust.py` est supprimé en `0-pre.5` après migration de toutes ses responsabilités vers Gradle ; aucun équivalent Python ne doit être recréé. Aucune `.so` générée ne doit être écrite durablement dans `src/main/jniLibs/` par le nouveau chemin. @@ -164,9 +164,9 @@ Les smokes matériels restent ciblés sur les environnements disponibles ; l'abs ### `0-pre.5` — AAB, minSdk et fermeture du chemin historique -Ajouter/valider le chemin `bundleRelease` sans secret, inspecter le bundle et la compatibilité 16 KB, exécuter la stratégie de smoke du plancher Android et supprimer `scripts/build_android_rust.py` si toutes ses responsabilités ont effectivement migré. +Étendre l'ownership Gradle aux variantes Release : `bundleRelease` doit reconstruire les quatre ABI en profil Cargo release sans secret embarqué. Inspecter les AAB, les segments ELF et l'alignement APK 16 KB ; consolider `minSdk 21` ; supprimer `scripts/build_android_rust.py` et interdire le retour des `src/main/jniLibs` générés. -Mettre à jour les documents Android/JNI/build durables pour décrire le nouveau chemin réel et ajuster l'audit de distribution si le script historique n'est plus un path requis. +Mettre à jour les documents Android/JNI/build durables pour décrire le chemin réel et remplacer les anciennes commandes de validation encore dépendantes du builder Python. ### `2-beta.1` — validation large et packaging final @@ -220,6 +220,6 @@ La politique a été révisée après validation de `0-pre.2` : le projet Androi - le minSdk soutenu et la preuve de smoke disponible sont documentés sans surpromesse ; - l'appareil ARM64 réel et l'AVD x86_64 passent le smoke prévu ; - Reflex est soit branché naturellement au pipeline commun, soit explicitement borné selon les résultats de factorisation ; -- le script Python historique est supprimé s'il n'a plus aucune responsabilité durable ; +- le script Python historique Android est supprimé et l'audit interdit son retour ; - les documents Android, build, JNI et validation décrivent le chemin réellement livré ; - les gates workspace/beta applicables sont propres. diff --git a/docs/rules/RULES_COMMANDS.md b/docs/rules/RULES_COMMANDS.md index 32cd85a..602d81e 100644 --- a/docs/rules/RULES_COMMANDS.md +++ b/docs/rules/RULES_COMMANDS.md @@ -1,5 +1,5 @@ - + # Règles d'exécution des commandes @@ -103,7 +103,7 @@ ## Outils de build et scripts d'audit - **CMD-BUILD-001** — Les scripts Python du dépôt sont autorisés pour les audits, audits complémentaires, validations et validations complémentaires. -- **CMD-BUILD-002** — À partir de `0.3.x`, aucun chemin de build nouveau ou modifié n’est piloté par Python. Les orchestrateurs historiques `scripts/build_reflex_tauri_wasm.py` et `scripts/build_android_rust.py` restent tolérés uniquement comme mécanismes gelés de la baseline `0.1.0` jusqu’à la tranche qui réactive leur chemin ; ils ne sont ni copiés, ni généralisés, ni utilisés pour un nouveau POC. +- **CMD-BUILD-002** — À partir de `0.3.x`, aucun chemin de build nouveau ou modifié n’est piloté par Python. L'orchestrateur historique `scripts/build_reflex_tauri_wasm.py` reste toléré uniquement comme mécanisme gelé de la baseline `0.1.0` jusqu’à la tranche qui réactive son chemin ; il n'est ni copié, ni généralisé, ni utilisé pour un nouveau POC. Le builder Android historique `scripts/build_android_rust.py` a été retiré lorsque son chemin a été réactivé en `0.3.3`. - **CMD-BUILD-003** — Les builds utilisent l'outil natif approprié au périmètre : Cargo pour Rust, Gradle pour Android, Tauri CLI pour Tauri, ou l'outil officiellement retenu par la plateforme concernée. - **CMD-BUILD-004** — Les POC `0.3.x` doivent remplacer toute orchestration de build Python restante par des procédures explicites, reproductibles et testées avec les outils natifs. - **CMD-BUILD-005** — Les builds, tests unitaires, tests d'intégration et smoke tests de validation sont exécutés côté utilisateur ; les scripts d'audit peuvent vérifier statiquement leur préparation mais ne les simulent pas. diff --git a/docs/studies/025-V0_3_3_ANDROID_NATIVE_MULTI_ABI_AUDIT.md b/docs/studies/025-V0_3_3_ANDROID_NATIVE_MULTI_ABI_AUDIT.md index 0f3aa01..d0bb0f5 100644 --- a/docs/studies/025-V0_3_3_ANDROID_NATIVE_MULTI_ABI_AUDIT.md +++ b/docs/studies/025-V0_3_3_ANDROID_NATIVE_MULTI_ABI_AUDIT.md @@ -1,5 +1,5 @@ - + # Audit Android SDL3 natif multi-ABI pour 0.3.3 @@ -263,3 +263,11 @@ Avant d'ouvrir l'implémentation `0-pre.2`, la gate utilisateur doit donc releve ## Conclusion de cadrage Le scope tient dans `0.3.3` si chaque tranche reste centrée sur un résultat vertical : ownership Gradle mono-ABI, puis multi-ABI/universal, puis AAB/compatibilité et consolidation. Aucun besoin découvert n'impose aujourd'hui de créer une version supplémentaire avant de commencer l'implémentation. + +## Révision après validation de la matrice quatre ABI + +La gate `0-pre.4` du 2026-09-21 a confirmé que les quatre targets Rust Android sont installées et que Snake comme Reflex compilent avec succès `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`. Les deux APK Debug universal contiennent les huit bibliothèques attendues. + +La fermeture `0-pre.5` peut donc retirer le builder Python historique sans perdre de responsabilité fonctionnelle. La validation 16 KB suit la méthode Android actuelle : AGP 8.5.1+ pour le packaging, NDK r28+ pour l'alignement ELF par défaut, contrôle direct de tous les `.so` avec `llvm-objdump`, contrôle APK avec `zipalign -P 16`, et contrôle `PAGE_ALIGNMENT_16K` du bundle lorsque `bundletool` est disponible. SDL3 restant un prébuild, son alignement doit être contrôlé explicitement. + +Le plancher `minSdk 21` est consolidé parce qu'il correspond au minimum Android documenté par SDL3 ; un smoke sur API 21 reste une preuve runtime additionnelle lorsque l'image est raisonnablement disponible, pas une condition permettant de prétendre à un niveau inférieur. diff --git a/docs/testing/003-RC_VALIDATION_MATRIX.md b/docs/testing/003-RC_VALIDATION_MATRIX.md index 6891339..cf35bf1 100644 --- a/docs/testing/003-RC_VALIDATION_MATRIX.md +++ b/docs/testing/003-RC_VALIDATION_MATRIX.md @@ -1,5 +1,5 @@ - + # Matrice de validation RC 0.1.0 @@ -85,13 +85,8 @@ Critères : ## Android x86_64 / API 36 ```bash -python3 scripts/build_android_rust.py reflex --abi x86_64 -python3 scripts/build_android_rust.py snake --abi x86_64 - -cd Android -gradle :game-reflex-poc:assembleDebug -gradle :game-snake-poc:assembleDebug -cd .. +(cd Android && gradle :game-reflex-poc:assembleDebug) +(cd Android && gradle :game-snake-poc:assembleDebug) ``` Déployer les deux APK sur l'AVD de référence et revalider : @@ -105,13 +100,8 @@ Déployer les deux APK sur l'AVD de référence et revalider : ## Android ARM64 réel ```bash -python3 scripts/build_android_rust.py reflex --abi arm64-v8a -python3 scripts/build_android_rust.py snake --abi arm64-v8a - -cd Android -gradle :game-reflex-poc:assembleDebug -gradle :game-snake-poc:assembleDebug -cd .. +(cd Android && gradle :game-reflex-poc:assembleDebug) +(cd Android && gradle :game-snake-poc:assembleDebug) ``` Déployer sur l'appareil ARM64 de référence et revalider Reflex et Snake. diff --git a/history/0.3.3/0-pre.4.md b/history/0.3.3/0-pre.4.md new file mode 100644 index 0000000..e647f02 --- /dev/null +++ b/history/0.3.3/0-pre.4.md @@ -0,0 +1,76 @@ + + + +# Historique 0.3.3-0-pre.4 + +## Statut + +`0.3.3-0-pre.4` a été validé par l'utilisateur le 2026-09-21. La tranche confirme la matrice Android native complète encore supportée par la toolchain moderne pour Snake et Reflex : `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`. + +Aucun `0-pre.4.fix.N` n'est requis. La suite peut ouvrir `0-pre.5`. + +## Toolchain réellement validée + +```text +OpenJDK / Temurin 25.0.4.1 LTS +Gradle 9.7.1 +Launcher JVM 25.0.4.1 +Daemon JVM /usr/lib/jvm/temurin-25-jdk-amd64 +``` + +Targets Rust Android présents : + +```text +aarch64-linux-android +armv7-linux-androideabi +i686-linux-android +x86_64-linux-android +``` + +## Gates statiques et Rust + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +games.sasedev workspace audit: clean +Markdown table audit: clean (5 table(s), 239 file(s)) +Distribution layout audit: clean (50 required path(s), 5 forbidden path(s) absent) +cargo fmt --all: clean +cargo fmt --all -- --check: clean +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +``` + +## Builds quatre ABI + +Snake et Reflex exécutent tous deux avec succès les tâches natives Gradle pour : + +```text +arm64-v8a -> aarch64-linux-android +armeabi-v7a -> armv7-linux-androideabi +x86 -> i686-linux-android +x86_64 -> x86_64-linux-android +``` + +Les deux commandes `assembleDebug` terminent avec `BUILD SUCCESSFUL`. + +## Inspection des APK + +Les APK Debug universal Snake et Reflex contiennent chacun exactement les couples natifs attendus pour les quatre ABI : + +```text +lib/arm64-v8a/libSDL3.so +lib/arm64-v8a/libgame_android_entrypoint.so +lib/armeabi-v7a/libSDL3.so +lib/armeabi-v7a/libgame_android_entrypoint.so +lib/x86/libSDL3.so +lib/x86/libgame_android_entrypoint.so +lib/x86_64/libSDL3.so +lib/x86_64/libgame_android_entrypoint.so +``` + +Les répétitions visibles dans la sortie utilisateur proviennent des `grep -Fx` de contrôle exécutés après le listing initial. + +## Conséquence + +La matrice quatre ABI n'est plus seulement une intention documentaire : le même pipeline Gradle/Cargo/SDL3 la compile et la package pour les deux jeux. `0-pre.5` peut donc étendre ce pipeline aux variantes Release/AAB, vérifier la compatibilité 16 KB, consolider API 21 et supprimer le builder Python Android historique. diff --git a/scripts/audit_distribution_layout.py b/scripts/audit_distribution_layout.py index 827bb7f..43ff350 100644 --- a/scripts/audit_distribution_layout.py +++ b/scripts/audit_distribution_layout.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_distribution_layout.py -# version: 16 +# version: 17 """Audit the static distribution layout expected by supported POCs.""" @@ -17,6 +17,9 @@ FORBIDDEN_PATHS = ( "Android/gradlew.bat", "Android/gradle/wrapper/gradle-wrapper.jar", "Android/gradle/wrapper/gradle-wrapper.properties", + "Android/game-reflex-poc/src/main/jniLibs", + "Android/game-snake-poc/src/main/jniLibs", + "scripts/build_android_rust.py", ) @@ -66,7 +69,6 @@ REQUIRED_PATHS = ( "Android/game-snake-poc/build.gradle", "Android/settings.gradle", "Android/gradle/sasedev-rust-android.gradle", - "scripts/build_android_rust.py", "scripts/build_reflex_tauri_wasm.py", "assets/common", "assets/game-reflex-poc", @@ -334,9 +336,12 @@ def audit_android_native_gradle(root: pathlib.Path) -> list[str]: native_script = native_script_path.read_text(encoding="utf-8") native_fragments = ( - 'spec.commandLine(', + 'spec.commandLine(cargoCommand)', '"cargo",', '"ndk",', + 'cargoCommand.add("--release")', + 'releaseBuild.set(selectedReleaseBuild)', + 'onVariants(selector().all())', '"--no-default-features",', 'variant.sources.jniLibs.addGeneratedSourceDirectory', 'generated/sasedevNative/', @@ -363,6 +368,8 @@ def audit_android_native_gradle(root: pathlib.Path) -> list[str]: ) shared_fragments = ( 'ext.sasedevRustAndroidAbis = ["arm64-v8a", "armeabi-v7a", "x86_64", "x86"]', + 'ext.sasedevRustAndroidApi = 21', + 'minSdk 21', "abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'", 'apply from: rootProject.file("gradle/sasedev-rust-android.gradle")', ) diff --git a/scripts/build_android_rust.py b/scripts/build_android_rust.py deleted file mode 100644 index 075b07c..0000000 --- a/scripts/build_android_rust.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/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())