0.1.0-0-pre.7
This commit is contained in:
3
Android/.gitignore
vendored
3
Android/.gitignore
vendored
@@ -1,7 +1,8 @@
|
|||||||
# file: Android/.gitignore
|
# file: Android/.gitignore
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
.gradle/
|
.gradle/
|
||||||
**/build/
|
**/build/
|
||||||
local.properties
|
local.properties
|
||||||
libs/*.aar
|
libs/*.aar
|
||||||
|
**/src/main/jniLibs/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: Android/README.md -->
|
<!-- file: Android/README.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Android
|
# 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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java
|
// file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
package com.sasedev.games.common;
|
package com.sasedev.games.common;
|
||||||
|
|
||||||
@@ -9,6 +9,13 @@ package com.sasedev.games.common;
|
|||||||
* <p>The SDL3 Android AAR supplies the common {@code SDLActivity} host. Game-specific activities extend this class.</p>
|
* <p>The SDL3 Android AAR supplies the common {@code SDLActivity} host. Game-specific activities extend this class.</p>
|
||||||
*/
|
*/
|
||||||
public abstract class SaseGameActivity extends org.libsdl.app.SDLActivity {
|
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.
|
* Returns the stable identifier of the game hosted by the activity.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: Android/game-reflex-poc/build.gradle
|
// file: Android/game-reflex-poc/build.gradle
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.6'
|
versionName '0.1.0-0-pre.7'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: Android/game-snake-poc/build.gradle
|
// file: Android/game-snake-poc/build.gradle
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.6'
|
versionName '0.1.0-0-pre.7'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 13
|
# version: 14
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -12,10 +12,11 @@ members = [
|
|||||||
"crates/apps/game-reflex-poc-desktop",
|
"crates/apps/game-reflex-poc-desktop",
|
||||||
"crates/apps/game-snake-poc-desktop",
|
"crates/apps/game-snake-poc-desktop",
|
||||||
"crates/common/game-logging-lib",
|
"crates/common/game-logging-lib",
|
||||||
|
"crates/apps/game-android-entrypoint",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0-0-pre.6.fix.1"
|
version = "0.1.0-0-pre.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/games"
|
repository = "https://git.sasedev.com/Sasedev/games"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# Roadmap
|
# 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.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.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.
|
- [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.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.8` — bridge Java/JNI minimal et input tactile Android.
|
||||||
- [ ] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates.
|
- [ ] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates.
|
||||||
|
|||||||
50
crates/apps/game-android-entrypoint/Cargo.toml
Normal file
50
crates/apps/game-android-entrypoint/Cargo.toml
Normal file
@@ -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"
|
||||||
53
crates/apps/game-android-entrypoint/src/lib.rs
Normal file
53
crates/apps/game-android-entrypoint/src/lib.rs
Normal file
@@ -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<G>(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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/engines/engine-v1-sdl/Cargo.toml
|
# file: crates/engines/engine-v1-sdl/Cargo.toml
|
||||||
# version: 4
|
# version: 5
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "engine-v1-sdl"
|
name = "engine-v1-sdl"
|
||||||
@@ -12,9 +12,14 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
engine-v1-common = { path = "../engine-v1-common" }
|
engine-v1-common = { path = "../engine-v1-common" }
|
||||||
sdl3 = { workspace = true, features = ["use-pkg-config"] }
|
|
||||||
tracing.workspace = true
|
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]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
|||||||
112
deltas/0.1.0/0-pre.7.md
Normal file
112
deltas/0.1.0/0-pre.7.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!-- file: deltas/0.1.0/0-pre.7.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/000-README.md -->
|
<!-- file: docs/000-README.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Documentation games.sasedev
|
# 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/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/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/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é
|
## Historique validé
|
||||||
|
|
||||||
|
|||||||
52
docs/development/006-ANDROID_RUST_NATIVE_BUILD.md
Normal file
52
docs/development/006-ANDROID_RUST_NATIVE_BUILD.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!-- file: docs/development/006-ANDROID_RUST_NATIVE_BUILD.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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`.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<!-- file: docs/rules/RULES_RUST.md -->
|
<!-- file: docs/rules/RULES_RUST.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Règles Rust générales
|
# Règles Rust générales
|
||||||
|
|
||||||
## Base
|
## Base
|
||||||
|
|
||||||
- **RUST-BASE-001** — L'édition Rust est Rust 2024.
|
- **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-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.
|
- **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** — Le code `unsafe` est interdit sauf future exception normative extrêmement ciblée et justifiée.
|
- **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-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-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é.
|
- **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é.
|
||||||
|
|||||||
21
history/0.1.0/0-pre.6.fix.1.md
Normal file
21
history/0.1.0/0-pre.6.fix.1.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- file: history/0.1.0/0-pre.6.fix.1.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# file: scripts/audit_rust_general_rules.py
|
# file: scripts/audit_rust_general_rules.py
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
"""Audit mechanically verifiable Rust normalization rules used by games.sasedev."""
|
"""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"))
|
violations.append(Violation("RUST-IMPORT-114", relative, current_idx, "module use block is not alphabetically ordered"))
|
||||||
# Crate façade constraints.
|
# Crate façade constraints.
|
||||||
if path.name in {"lib.rs", "main.rs"}:
|
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]:
|
if attribute not in lines[:24]:
|
||||||
violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`"))
|
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]] = []
|
exports: list[tuple[int, str]] = []
|
||||||
for idx, line in enumerate(lines, 1):
|
for idx, line in enumerate(lines, 1):
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
|
|||||||
79
scripts/build_android_rust.py
Normal file
79
scripts/build_android_rust.py
Normal file
@@ -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())
|
||||||
Reference in New Issue
Block a user