From 98b830da6e9c3cd390bc4612e4ca1d5608f563a5 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 16 Sep 2026 09:29:19 +0200 Subject: [PATCH] 0.1.0-0-pre.7 --- Android/.gitignore | 3 +- Android/README.md | 17 ++- .../games/common/SaseGameActivity.java | 9 +- Android/game-reflex-poc/build.gradle | 4 +- Android/game-snake-poc/build.gradle | 4 +- Cargo.toml | 5 +- ROADMAP.md | 4 +- .../apps/game-android-entrypoint/Cargo.toml | 50 ++++++++ .../apps/game-android-entrypoint/src/lib.rs | 53 +++++++++ .../unit_tests/selection.rs | 8 ++ crates/engines/engine-v1-sdl/Cargo.toml | 9 +- deltas/0.1.0/0-pre.7.md | 112 ++++++++++++++++++ docs/000-README.md | 3 +- .../006-ANDROID_RUST_NATIVE_BUILD.md | 52 ++++++++ docs/rules/RULES_RUST.md | 8 +- history/0.1.0/0-pre.6.fix.1.md | 21 ++++ scripts/audit_rust_general_rules.py | 13 +- scripts/build_android_rust.py | 79 ++++++++++++ 18 files changed, 434 insertions(+), 20 deletions(-) create mode 100644 crates/apps/game-android-entrypoint/Cargo.toml create mode 100644 crates/apps/game-android-entrypoint/src/lib.rs create mode 100644 crates/apps/game-android-entrypoint/unit_tests/selection.rs create mode 100644 deltas/0.1.0/0-pre.7.md create mode 100644 docs/development/006-ANDROID_RUST_NATIVE_BUILD.md create mode 100644 history/0.1.0/0-pre.6.fix.1.md create mode 100644 scripts/build_android_rust.py diff --git a/Android/.gitignore b/Android/.gitignore index a287092..5979f15 100644 --- a/Android/.gitignore +++ b/Android/.gitignore @@ -1,7 +1,8 @@ # file: Android/.gitignore -# version: 1 +# version: 2 .gradle/ **/build/ local.properties libs/*.aar +**/src/main/jniLibs/ diff --git a/Android/README.md b/Android/README.md index 250e39f..ed9145e 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,5 +1,5 @@ - + # Android @@ -55,3 +55,18 @@ Le module `common` consomme cet artefact et `SaseGameActivity` étend désormais Les APK peuvent être compilés côté Java/SDL3 une fois l'AAR disponible, mais ne disposent pas encore du `cdylib` Rust contenant le point d'entrée de jeu. Le lancement fonctionnel sur appareil/émulateur appartient donc au delta suivant. + +## Rust Android natif + +Le crate `game-android-entrypoint` produit `libgame_android_entrypoint.so`. Il est compilé séparément pour chaque jeu avec une feature Cargo et copié dans le `jniLibs` du module Android concerné. + +Pour `arm64-v8a` : + +```bash +rustup target add aarch64-linux-android +cargo install cargo-ndk +python3 ../scripts/build_android_rust.py reflex +python3 ../scripts/build_android_rust.py snake +``` + +Le script extrait temporairement `libSDL3.so` de l'AAR uniquement pour fournir le chemin de linkage à `rustc`; l'AAR reste responsable du packaging SDL3 dans l'APK. diff --git a/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java b/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java index 718153c..f386733 100644 --- a/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java +++ b/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java @@ -1,5 +1,5 @@ // file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java -// version: 2 +// version: 3 package com.sasedev.games.common; @@ -9,6 +9,13 @@ package com.sasedev.games.common; *

The SDL3 Android AAR supplies the common {@code SDLActivity} host. Game-specific activities extend this class.

*/ public abstract class SaseGameActivity extends org.libsdl.app.SDLActivity { + + /** {@inheritDoc} */ + @Override + protected final String[] getLibraries() { + return new String[] {"SDL3", "game_android_entrypoint"}; + } + /** * Returns the stable identifier of the game hosted by the activity. * diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index fc41a62..fc2ae09 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: 3 +// version: 4 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.6' + versionName '0.1.0-0-pre.7' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index f9e3e4c..ee800db 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: 3 +// version: 4 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.6' + versionName '0.1.0-0-pre.7' } compileOptions { diff --git a/Cargo.toml b/Cargo.toml index 08d7652..b9b0115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 13 +# version: 14 [workspace] resolver = "3" @@ -12,10 +12,11 @@ members = [ "crates/apps/game-reflex-poc-desktop", "crates/apps/game-snake-poc-desktop", "crates/common/game-logging-lib", + "crates/apps/game-android-entrypoint", ] [workspace.package] -version = "0.1.0-0-pre.6.fix.1" +version = "0.1.0-0-pre.7" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/ROADMAP.md b/ROADMAP.md index 744163e..4588fcc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap @@ -10,7 +10,7 @@ - [x] `0-pre.3` — première boucle moteur exécutable Desktop : temps, input abstrait, capacités de monétisation optionnelles et état de jeu minimal via les runners. - [x] `0-pre.4` — architecture de tests hors `src/`, stratégie de tests ciblés et socle `tracing`/`tracing-subscriber`/`tracing-appender`. - [x] `0-pre.5` — intégration SDL3 Desktop réelle et premier rendu POC Reflex. -- [ ] `0-pre.6` — fondation Gradle Android, SDL3 AAR, Activity Java commune et politique de fenêtre Desktop redimensionnable. +- [x] `0-pre.6` — fondation Gradle Android, SDL3 AAR, Activity Java commune et politique de fenêtre Desktop redimensionnable. - [ ] `0-pre.7` — compilation Rust Android `cdylib`, point d'entrée SDL Android et premier lancement APK sur appareil/émulateur. - [ ] `0-pre.8` — bridge Java/JNI minimal et input tactile Android. - [ ] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates. diff --git a/crates/apps/game-android-entrypoint/Cargo.toml b/crates/apps/game-android-entrypoint/Cargo.toml new file mode 100644 index 0000000..b007a53 --- /dev/null +++ b/crates/apps/game-android-entrypoint/Cargo.toml @@ -0,0 +1,50 @@ +# file: crates/apps/game-android-entrypoint/Cargo.toml +# version: 1 + +[package] +name = "game-android-entrypoint" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[lib] +crate-type = ["cdylib"] + +[features] +default = [] +reflex = ["dep:game-reflex-poc"] +snake = ["dep:game-snake-poc"] + +[dependencies] +engine-v1-common = { path = "../../engines/engine-v1-common" } +engine-v1-sdl = { path = "../../engines/engine-v1-sdl" } +game-reflex-poc = { path = "../../games/game-reflex-poc", optional = true } +game-snake-poc = { path = "../../games/game-snake-poc", optional = true } + +[lints.rust] +missing_docs = "warn" +unreachable_pub = "deny" +unsafe_code = "allow" + +[lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +implicit_return = "deny" +needless_return = "allow" +useless_vec = "deny" +question_mark = "deny" +question_mark_used = "deny" +needless_match = "allow" +manual_ok_err = "allow" +manual_unwrap_or = "allow" +manual_map = "allow" +match_like_matches_macro = "allow" +single_match = "allow" +manual_unwrap_or_default = "allow" +manual_find = "allow" +explicit_counter_loop = "allow" +get_first = "allow" +implicit_saturating_sub = "allow" diff --git a/crates/apps/game-android-entrypoint/src/lib.rs b/crates/apps/game-android-entrypoint/src/lib.rs new file mode 100644 index 0000000..ed827ed --- /dev/null +++ b/crates/apps/game-android-entrypoint/src/lib.rs @@ -0,0 +1,53 @@ +// file: crates/apps/game-android-entrypoint/src/lib.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![allow(unsafe_code)] + +//! Minimal SDL3 Android entry point. +//! +//! This crate is the only normalized Rust FFI export exception in the current workspace. +//! It uses one unsafe export attribute to expose `SDL_main` to `SDLActivity`; it contains no unsafe block and no unsafe function. + +#[cfg(test)] +#[path = "../unit_tests/selection.rs"] +mod tests; + +#[unsafe(export_name = "SDL_main")] +extern "C" fn sdl_main(_argc: core::ffi::c_int, _argv: *mut *mut core::ffi::c_char) -> core::ffi::c_int { + return run_selected_game(); +} + +#[cfg(feature = "reflex")] +fn run_selected_game() -> core::ffi::c_int { + let runtime = engine_v1_sdl::SdlRuntime::new("Reflex POC", 405, 720, std::time::Duration::from_millis(16)); + let mut game = game_reflex_poc::ReflexState::new(); + return run_game(&runtime, &mut game); +} + +#[cfg(all(not(feature = "reflex"), feature = "snake"))] +fn run_selected_game() -> core::ffi::c_int { + let runtime = engine_v1_sdl::SdlRuntime::new("Snake POC", 405, 720, std::time::Duration::from_millis(16)); + let mut game = game_snake_poc::SnakeState::new(); + return run_game(&runtime, &mut game); +} + +#[cfg(not(any(feature = "reflex", feature = "snake")))] +fn run_selected_game() -> core::ffi::c_int { + return 2; +} + +#[cfg(any(feature = "reflex", feature = "snake"))] +fn run_game(runtime: &engine_v1_sdl::SdlRuntime, game: &mut G) -> core::ffi::c_int +where + G: engine_v1_common::EngineGame, +{ + match runtime.run(game) { + std::result::Result::Ok(()) => return 0, + std::result::Result::Err(error) => { + eprintln!("Android SDL3 runtime error: {error}"); + return 1; + }, + } +} diff --git a/crates/apps/game-android-entrypoint/unit_tests/selection.rs b/crates/apps/game-android-entrypoint/unit_tests/selection.rs new file mode 100644 index 0000000..b07f130 --- /dev/null +++ b/crates/apps/game-android-entrypoint/unit_tests/selection.rs @@ -0,0 +1,8 @@ +// file: crates/apps/game-android-entrypoint/unit_tests/selection.rs +// version: 1 + +#[test] +fn no_feature_entrypoint_has_failure_status() { + #[cfg(not(any(feature = "reflex", feature = "snake")))] + assert_eq!(crate::run_selected_game(), 2); +} diff --git a/crates/engines/engine-v1-sdl/Cargo.toml b/crates/engines/engine-v1-sdl/Cargo.toml index d9310e9..31f998c 100644 --- a/crates/engines/engine-v1-sdl/Cargo.toml +++ b/crates/engines/engine-v1-sdl/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/engines/engine-v1-sdl/Cargo.toml -# version: 4 +# version: 5 [package] name = "engine-v1-sdl" @@ -12,9 +12,14 @@ publish.workspace = true [dependencies] engine-v1-common = { path = "../engine-v1-common" } -sdl3 = { workspace = true, features = ["use-pkg-config"] } tracing.workspace = true +[target.'cfg(target_os = "android")'.dependencies] +sdl3.workspace = true + +[target.'cfg(not(target_os = "android"))'.dependencies] +sdl3 = { workspace = true, features = ["use-pkg-config"] } + [lints] workspace = true diff --git a/deltas/0.1.0/0-pre.7.md b/deltas/0.1.0/0-pre.7.md new file mode 100644 index 0000000..56147c6 --- /dev/null +++ b/deltas/0.1.0/0-pre.7.md @@ -0,0 +1,112 @@ + + + +# Delta 0.1.0-0-pre.7 + +## Base + +Base validée : `0.1.0-0-pre.6.fix.1`. + +## Validation du delta précédent + +Le 2026-09-16, l'utilisateur a validé les gates Rust légères ainsi que les trois builds Android `:common`, Reflex et Snake. Les trois commandes Gradle ont terminé par `BUILD SUCCESSFUL`. + +## Objectif + +Introduire le `cdylib` Rust Android, le symbole `SDL_main`, le staging `jniLibs` et préparer le premier lancement réel sur appareil ou émulateur. + +## Changements + +- nouveau crate `crates/apps/game-android-entrypoint` ; +- `crate-type = ["cdylib"]` ; +- features `reflex` et `snake` activées par build applicatif ; +- export contrôlé du symbole `SDL_main` ; +- `engine-v1-sdl` utilise `use-pkg-config` uniquement hors Android ; +- `SaseGameActivity` charge `SDL3` puis `game_android_entrypoint` ; +- script `scripts/build_android_rust.py` pour extraire la bibliothèque SDL3 de l'AAR uniquement pour le linker Rust puis générer `jniLibs` via `cargo-ndk` ; +- `jniLibs` générés exclus de Git ; +- `versionName` Android porté à `0.1.0-0-pre.7`. + +## Exception FFI + +Le symbole `SDL_main` nécessite un nom ABI stable. Rust 2024 classe `export_name` comme attribut unsafe. Une exception normative unique est donc introduite dans `game-android-entrypoint` : un seul `#[unsafe(export_name = "SDL_main")]`, sans bloc ou fonction unsafe. + +## Préflight Android Rust + +Vérifier le NDK et les outils : + +```bash +cargo ndk --version +rustup target list --installed | grep aarch64-linux-android +``` + +Si nécessaire : + +```bash +rustup target add aarch64-linux-android +cargo install cargo-ndk +``` + +SDL demande NDK r28c ou ultérieur. `cargo-ndk` détecte normalement le NDK Android Studio ; `ANDROID_NDK_HOME` peut être utilisé pour forcer un chemin. + +## Validation Rust hôte + +Le delta modifie du Rust : + +```bash +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 deltas history +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +Test ciblé de la crate d'entrée sans feature : + +```bash +cargo test -p game-android-entrypoint --no-default-features +``` + +## Build natif Android + +Depuis la racine : + +```bash +python3 scripts/build_android_rust.py reflex +python3 scripts/build_android_rust.py snake +``` + +Puis : + +```bash +cd Android +gradle :game-reflex-poc:assembleDebug +gradle :game-snake-poc:assembleDebug +cd .. +``` + +## Smoke appareil/émulateur + +Avec une cible ADB disponible : + +```bash +adb devices +adb install -r Android/game-reflex-poc/build/outputs/apk/debug/game-reflex-poc-debug.apk +adb shell am start -n com.sasedev.games.reflex/.ReflexActivity +``` + +Puis répéter pour Snake : + +```bash +adb install -r Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk +adb shell am start -n com.sasedev.games.snake/.SnakeActivity +``` + +Le critère de réussite est l'ouverture de la surface SDL sans dialogue `SDL Error`, crash natif ou `UnsatisfiedLinkError`. + +## Règle de transition + +Si les gates Rust, les deux builds natifs, les APK et les deux smokes appareil/émulateur sont propres, passer automatiquement à `0.1.0-0-pre.8`. + +Si une gate échoue à cause du projet, produire `0.1.0-0-pre.7.fix.1`. diff --git a/docs/000-README.md b/docs/000-README.md index 7d4ad79..19cb1d4 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev @@ -50,6 +50,7 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R - [`development/003-TRACING_AND_DIAGNOSTICS.md`](development/003-TRACING_AND_DIAGNOSTICS.md) — socle `tracing`, subscriber et appender communs. - [`development/004-SDL3_DESKTOP_PREREQUISITES.md`](development/004-SDL3_DESKTOP_PREREQUISITES.md) — prérequis SDL3 Desktop, stratégie de liaison système et vérification `pkg-config`. - [`development/005-DESKTOP_WINDOW_POLICY.md`](development/005-DESKTOP_WINDOW_POLICY.md) — taille initiale, redimensionnement et séparation future entre fenêtre physique et résolution virtuelle. +- [`development/006-ANDROID_RUST_NATIVE_BUILD.md`](development/006-ANDROID_RUST_NATIVE_BUILD.md) — build `cdylib` Rust Android, cargo-ndk, packaging `jniLibs` et symbole `SDL_main`. ## Historique validé diff --git a/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md b/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md new file mode 100644 index 0000000..d6487b2 --- /dev/null +++ b/docs/development/006-ANDROID_RUST_NATIVE_BUILD.md @@ -0,0 +1,52 @@ + + + +# Build Rust Android natif + +## Contrat SDL + +`SDLActivity` charge `SDL3`, puis `game_android_entrypoint`, et invoque le symbole `SDL_main`. + +Le crate Rust `game-android-entrypoint` est un `cdylib`. Il est compilé avec exactement une feature métier lors du packaging d'une application : + +- `reflex` ; +- `snake`. + +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`. + +## 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. + +Installer au minimum : + +```bash +rustup target add aarch64-linux-android +cargo install cargo-ndk +``` + +`cargo-ndk` détecte un NDK installé par Android Studio ou utilise `ANDROID_NDK_HOME` lorsqu'il est défini. + +## Build + +Depuis la racine : + +```bash +python3 scripts/build_android_rust.py reflex +python3 scripts/build_android_rust.py snake +``` + +Puis : + +```bash +cd Android +gradle :game-reflex-poc:assembleDebug +gradle :game-snake-poc:assembleDebug +cd .. +``` + +Les bibliothèques générées sous `Android/*/src/main/jniLibs/` sont des artefacts de build et ne sont pas versionnées. + +## 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/docs/rules/RULES_RUST.md b/docs/rules/RULES_RUST.md index ecdbe88..585b455 100644 --- a/docs/rules/RULES_RUST.md +++ b/docs/rules/RULES_RUST.md @@ -1,14 +1,14 @@ - + # Règles Rust générales ## Base - **RUST-BASE-001** — L'édition Rust est Rust 2024. -- **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` active `missing_docs`, `unreachable_pub` et interdit `unsafe_code`. -- **RUST-BASE-003** — Les lints communs sont déclarés au workspace et hérités par les crates. -- **RUST-BASE-004** — Le code `unsafe` est interdit sauf future exception normative extrêmement ciblée et justifiée. +- **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` active `missing_docs`, `unreachable_pub` et interdit `unsafe_code`, sauf exception FFI explicitement définie par cette règle. +- **RUST-BASE-003** — Les lints communs sont déclarés au workspace et hérités par les crates ; une crate FFI exceptée peut déclarer localement le même profil avec uniquement le niveau `unsafe_code` ajusté. +- **RUST-BASE-004** — Les blocs `unsafe` et fonctions `unsafe` sont interdits. L’unique exception actuelle est `crates/apps/game-android-entrypoint/src/lib.rs`, autorisé à porter exactement un attribut `#[unsafe(export_name = "SDL_main")]` afin d’exposer le symbole exigé par SDL Android. Cette exception n’autorise aucun déréférencement de pointeur brut ni autre attribut unsafe. - **RUST-BASE-005** — Tout fichier Rust possède les en-têtes `// file: ...` et `// version: N`. - **RUST-DEP-001** — Une dépendance tierce partagée déclare uniquement sa contrainte de version canonique sous `[workspace.dependencies]`, sauf exception normative explicitement documentée. - **RUST-DEP-002** — Les features d'une dépendance tierce sont activées dans le `Cargo.toml` de la crate qui en a réellement besoin, via `workspace = true`; elles ne sont pas activées globalement au workspace par commodité. diff --git a/history/0.1.0/0-pre.6.fix.1.md b/history/0.1.0/0-pre.6.fix.1.md new file mode 100644 index 0000000..c8bf19d --- /dev/null +++ b/history/0.1.0/0-pre.6.fix.1.md @@ -0,0 +1,21 @@ + + + +# Historique 0.1.0-0-pre.6.fix.1 + +## Statut + +Validé par l'utilisateur le 2026-09-16. + +## Gates validées + +- `cargo fmt --all -- --check` ; +- audits Rust/workspace et Markdown ; +- `cargo check --workspace` ; +- `gradle :common:assembleDebug` ; +- `gradle :game-reflex-poc:assembleDebug` ; +- `gradle :game-snake-poc:assembleDebug`. + +## Portée durable + +Le jalon valide le projet Gradle Android multi-module, l'AAR SDL3 externe, la frontière Java commune et le packaging SDL3 au niveau des APK. diff --git a/scripts/audit_rust_general_rules.py b/scripts/audit_rust_general_rules.py index 04ef3fe..1e1b1e6 100755 --- a/scripts/audit_rust_general_rules.py +++ b/scripts/audit_rust_general_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_rust_general_rules.py -# version: 1 +# version: 2 """Audit mechanically verifiable Rust normalization rules used by games.sasedev.""" @@ -513,9 +513,18 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: violations.append(Violation("RUST-IMPORT-114", relative, current_idx, "module use block is not alphabetically ordered")) # Crate façade constraints. if path.name in {"lib.rs", "main.rs"}: - for attribute in ("#![warn(missing_docs)]", "#![deny(unreachable_pub)]", "#![forbid(unsafe_code)]"): + ffi_export_exception = relative == "crates/apps/game-android-entrypoint/src/lib.rs" + required_attributes = ["#![warn(missing_docs)]", "#![deny(unreachable_pub)]"] + required_attributes.append("#![allow(unsafe_code)]" if ffi_export_exception else "#![forbid(unsafe_code)]") + for attribute in required_attributes: if attribute not in lines[:24]: violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`")) + if ffi_export_exception: + export_attribute = '#[unsafe(export_name = "SDL_main")]' + if text.count(export_attribute) != 1: + violations.append(Violation("RUST-FFI-101", relative, 1, "Android entrypoint must contain exactly one SDL_main export attribute")) + if "unsafe {" in text or re.search(r"\bunsafe\s+fn\b", text) is not None or re.search(r"\bunsafe\s+extern\b", text) is not None: + violations.append(Violation("RUST-FFI-102", relative, 1, "Android entrypoint exception permits an unsafe export attribute only; unsafe blocks/functions remain forbidden")) exports: list[tuple[int, str]] = [] for idx, line in enumerate(lines, 1): stripped = line.strip() diff --git a/scripts/build_android_rust.py b/scripts/build_android_rust.py new file mode 100644 index 0000000..fee3e16 --- /dev/null +++ b/scripts/build_android_rust.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# file: scripts/build_android_rust.py +# version: 1 + +"""Build one Android Rust game entrypoint and stage it as Gradle jniLibs.""" + +from __future__ import annotations + +import argparse +import os +import pathlib +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_sdl3_aar_name(properties_path: pathlib.Path) -> str: + """Read the configured SDL3 AAR filename.""" + for raw_line in properties_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if line.startswith("sdl3AarName="): + 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) + 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 + +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 + aar_name = read_sdl3_aar_name(root / "Android" / "gradle.properties") + 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 + link_dir = root / "Android" / "build" / "rust-link" / arguments.abi + try: + extract_sdl3_link_library(aar_path, arguments.abi, link_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, + ] + if arguments.release: + command.append("--release") + completed = subprocess.run(command, cwd=root, env=environment, check=False) + return completed.returncode + +if __name__ == "__main__": + raise SystemExit(main())