0.3.3-0-pre.3

This commit is contained in:
2026-09-21 10:32:31 +02:00
parent f770f99a4a
commit 5854ac3d07
16 changed files with 1004 additions and 107 deletions

View File

@@ -1,9 +1,9 @@
<!-- file: Android/README.md -->
<!-- version: 9 -->
<!-- version: 11 -->
# Android
Le frontend Android est un projet Gradle multi-module séparé du workspace Cargo.
Le frontend Android natif est un projet Gradle multi-module séparé du workspace Cargo.
## Modules
@@ -20,24 +20,43 @@ Chaque module `game-*` est une application Android indépendante ; il dépend du
## Toolchain de référence
La baseline stable `0.3.1` conserve :
Le pipeline Android natif `0.3.3` utilise :
- Android Gradle Plugin `9.4.0` ;
- Gradle `9.6.0` attendu par AGP 9.4 ;
- JDK `17` ;
- Gradle `>= 9.6.0`, résolu depuis l'environnement de développement ;
- JDK `>= 17` pour exécuter Gradle/AGP, sans `JAVA_HOME` imposé par le projet ;
- niveau Java source/target `17` pour la glue Android ;
- `compileSdk 36` ;
- `targetSdk 36` ;
- `minSdk 21` ;
- NDK `28.2.13676358` (`r28c`) ;
- SDL3 Android AAR `3.4.16`.
Le chemin historique compile déjà `game-android-entrypoint` via `cargo-ndk` et `scripts/build_android_rust.py`. `0.3.3` réactive ce produit Android natif afin de transférer cette orchestration dans Gradle, sans changer les responsabilités Rust/SDL3/Java/JNI.
`Android/settings.gradle` déclare `minimumGradleVersion = 9.6.0` et refuse explicitement une version plus ancienne. Une version système plus récente reste autorisée et devient la version réellement utilisée pour le build.
Le JDK n'est pas épinglé par le projet Android natif. Le shell/IDE fournit le JDK courant ; la gate doit afficher `java -version` et `gradle --version` afin de consigner la combinaison réellement validée. Le POC Tauri Android historique conserve ses propres contraintes de JDK et de wrapper sous sa crate ; elles ne pilotent pas le chemin SDL3 natif.
## Commandes Gradle
Depuis la racine du dépôt :
```bash
java -version
(cd Android && gradle --version)
```
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)
```
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
L'archive n'est pas vendorizée dans le dépôt.
Placer :
L'archive n'est pas vendorizée dans le dépôt. Placer :
```text
SDL3-3.4.16.aar
@@ -49,32 +68,34 @@ dans :
Android/libs/
```
Le module `common` consomme cet artefact et `SaseGameActivity` étend désormais `org.libsdl.app.SDLActivity`.
## Limite volontaire de ce jalon
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 module `common` compile contre cet artefact et `SaseGameActivity` étend `org.libsdl.app.SDLActivity`.
## 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é.
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`.
Pour `arm64-v8a` :
À `0.3.3-0-pre.3`, Snake et Reflex utilisent tous deux le même pipeline commun avec deux ABI :
```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
```text
arm64-v8a
x86_64
```
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.
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 :
```text
Android/<game>/build/generated/sasedevNative/debug/<abi>/jniLibs/<abi>/
├── libSDL3.so
└── libgame_android_entrypoint.so
```
AGP fusionne ces sources `jniLibs` générées dans un APK Debug universal contenant les deux architectures. 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.4`; il ne fait plus partie du chemin normal `assembleDebug` de Snake ou Reflex.
## Bridge JNI et tactile
À partir de `0.1.0-0-pre.9`, `SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust.
`SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust.
Les touch events standards restent traités via SDL3 et ne transitent pas par JNI.
@@ -97,6 +118,6 @@ Aucune ressource source n'est copiée durablement dans un module Android ou une
`Android/gradle.properties` définit `androidNdkVersion=28.2.13676358`.
Gradle utilise cette valeur comme `ndkVersion`. Le script Rust Android résout la même version sous `${ANDROID_HOME}/ndk/` et définit `ANDROID_NDK_HOME` uniquement pour le sous-processus `cargo ndk`.
Gradle utilise cette valeur comme `ndkVersion`. La tâche Rust Android résout la même version sous `${ANDROID_HOME}/ndk/` ou `${ANDROID_SDK_ROOT}/ndk/` et définit `ANDROID_NDK_HOME` uniquement pour le sous-processus `cargo ndk`.
Aucun `ANDROID_NDK_HOME` global n'est requis ; plusieurs NDK peuvent rester installés côte à côte.

View File

@@ -1,10 +1,14 @@
// file: Android/game-reflex-poc/build.gradle
// version: 45
// version: 46
plugins {
id 'com.android.application'
}
ext.sasedevRustGameFeature = "reflex"
ext.sasedevRustAndroidAbis = ["arm64-v8a", "x86_64"]
ext.sasedevRustAndroidApi = 21
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
@@ -19,6 +23,10 @@ android {
targetSdk 36
versionCode 2
versionName '0.2.0'
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
compileOptions {
@@ -34,3 +42,4 @@ dependencies {
ext.sasedevGameAssetDirectory = "game-reflex-poc"
apply from: rootProject.file("gradle/sasedev-assets.gradle")
apply from: rootProject.file("gradle/sasedev-rust-android.gradle")

View File

@@ -1,10 +1,14 @@
// file: Android/game-snake-poc/build.gradle
// version: 45
// version: 47
plugins {
id 'com.android.application'
}
ext.sasedevRustGameFeature = "snake"
ext.sasedevRustAndroidAbis = ["arm64-v8a", "x86_64"]
ext.sasedevRustAndroidApi = 21
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
@@ -19,6 +23,10 @@ android {
targetSdk 36
versionCode 2
versionName '0.2.0'
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
compileOptions {
@@ -34,3 +42,4 @@ dependencies {
ext.sasedevGameAssetDirectory = "game-snake-poc"
apply from: rootProject.file("gradle/sasedev-assets.gradle")
apply from: rootProject.file("gradle/sasedev-rust-android.gradle")

View File

@@ -0,0 +1,192 @@
// file: Android/gradle/sasedev-rust-android.gradle
// version: 1
import java.util.Collections
import java.util.zip.ZipFile
import javax.inject.Inject
import org.gradle.process.ExecOperations
abstract class BuildSasedevRustAndroidTask extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getSdl3Aar()
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
abstract ConfigurableFileCollection getRustInputs()
@Input
abstract Property<String> getGameFeature()
@Input
abstract Property<String> getAbi()
@Input
abstract Property<Integer> getAndroidApi()
@Input
abstract Property<String> getNdkVersion()
@Internal
abstract DirectoryProperty getRepositoryRoot()
@OutputDirectory
abstract DirectoryProperty getOutputDirectory()
@Inject
abstract ExecOperations getExecOperations()
@TaskAction
void buildNative() {
def selectedAbi = abi.get()
def selectedFeature = gameFeature.get()
def aar = sdl3Aar.get().asFile
def repository = repositoryRoot.get().asFile
def output = outputDirectory.get().asFile
def runtimeDirectory = new File(output, selectedAbi)
def linkDirectory = new File(temporaryDir, "link")
project.delete(output)
project.delete(linkDirectory)
runtimeDirectory.mkdirs()
linkDirectory.mkdirs()
byte[] sdl3Payload
new ZipFile(aar).withCloseable { archive ->
def candidates = [
"prefab/modules/SDL3/libs/android.${selectedAbi}/libSDL3.so",
"jni/${selectedAbi}/libSDL3.so",
]
def entry = candidates.collect { candidate -> archive.getEntry(candidate) }.find { candidate -> candidate != null }
if (entry == null) {
def suffix = "/libs/android.${selectedAbi}/libSDL3.so"
def fallback = Collections.list(archive.entries()).findAll { candidate ->
!candidate.directory && candidate.name.endsWith(suffix)
}
if (fallback.size() != 1) {
throw new GradleException(
"SDL3 shared library for ABI ${selectedAbi} is missing from ${aar}; " +
"expected Prefab member ${candidates[0]}"
)
}
entry = fallback[0]
}
sdl3Payload = archive.getInputStream(entry).withCloseable { stream -> stream.bytes }
}
def linkLibrary = new File(linkDirectory, "libSDL3.so")
def runtimeLibrary = new File(runtimeDirectory, "libSDL3.so")
linkLibrary.bytes = sdl3Payload
runtimeLibrary.bytes = sdl3Payload
def androidHome = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
if (!androidHome) {
throw new GradleException("ANDROID_HOME or ANDROID_SDK_ROOT is required to resolve the project NDK")
}
def configuredNdk = new File(androidHome, "ndk/${ndkVersion.get()}")
if (!configuredNdk.isDirectory()) {
throw new GradleException("configured Android NDK is missing: ${configuredNdk}")
}
def inheritedRustFlags = System.getenv("RUSTFLAGS")?.trim()
def linkFlag = "-Lnative=${linkDirectory}"
def rustFlags = inheritedRustFlags ? "${inheritedRustFlags} ${linkFlag}" : linkFlag
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,
)
}
def rustLibrary = new File(runtimeDirectory, "libgame_android_entrypoint.so")
if (!runtimeLibrary.isFile()) {
throw new GradleException("missing staged SDL3 Android library: ${runtimeLibrary}")
}
if (!rustLibrary.isFile()) {
throw new GradleException("missing Rust Android library after cargo-ndk build: ${rustLibrary}")
}
}
}
if (!project.ext.has("sasedevRustGameFeature")) {
throw new GradleException("sasedevRustGameFeature must select exactly one Cargo game feature")
}
if (!project.ext.has("sasedevRustAndroidAbis")) {
throw new GradleException("sasedevRustAndroidAbis must declare at least one Android ABI")
}
if (!project.ext.has("sasedevRustAndroidApi")) {
throw new GradleException("sasedevRustAndroidApi must declare the Rust Android API level")
}
def selectedGameFeature = project.ext.sasedevRustGameFeature as String
def selectedAbis = (project.ext.sasedevRustAndroidAbis as List).collect { configuredAbi -> configuredAbi as String }
def selectedAndroidApi = project.ext.sasedevRustAndroidApi as Integer
def configuredSdl3AarName = providers.gradleProperty("sdl3AarName").get()
def configuredNdkVersion = providers.gradleProperty("androidNdkVersion").get()
def repositoryDirectory = rootProject.projectDir.parentFile
def supportedAbis = ["arm64-v8a", "armeabi-v7a", "x86_64", "x86"] as Set
if (selectedAbis.isEmpty()) {
throw new GradleException("sasedevRustAndroidAbis must not be empty")
}
if (selectedAbis.toSet().size() != selectedAbis.size()) {
throw new GradleException("sasedevRustAndroidAbis must not contain duplicates: ${selectedAbis}")
}
def unsupportedAbis = selectedAbis.findAll { configuredAbi -> !supportedAbis.contains(configuredAbi) }
if (!unsupportedAbis.isEmpty()) {
throw new GradleException("unsupported Rust Android ABI(s): ${unsupportedAbis.join(', ')}")
}
androidComponents {
onVariants(selector().withBuildType("debug")) { variant ->
selectedAbis.each { selectedAbi ->
def variantSuffix = variant.name.substring(0, 1).toUpperCase() + variant.name.substring(1)
def abiSuffix = selectedAbi
.split(/[^A-Za-z0-9]+/)
.findAll { segment -> !segment.isEmpty() }
.collect { segment -> segment.substring(0, 1).toUpperCase() + segment.substring(1) }
.join("")
def nativeTask = tasks.register(
"build${variantSuffix}SasedevRust${abiSuffix}",
BuildSasedevRustAndroidTask,
) {
sdl3Aar.set(rootProject.layout.projectDirectory.file("libs/${configuredSdl3AarName}"))
rustInputs.from(rootProject.file("../Cargo.toml"))
rustInputs.from(rootProject.file("../.cargo/config.toml"))
rustInputs.from(rootProject.fileTree("../crates") {
include "**/*.rs"
include "**/Cargo.toml"
include "**/build.rs"
})
gameFeature.set(selectedGameFeature)
abi.set(selectedAbi)
androidApi.set(selectedAndroidApi)
ndkVersion.set(configuredNdkVersion)
repositoryRoot.set(repositoryDirectory)
outputDirectory.set(layout.buildDirectory.dir("generated/sasedevNative/${variant.name}/${selectedAbi}/jniLibs"))
}
if (variant.sources.jniLibs == null) {
throw new GradleException("AGP variant ${variant.name} does not expose jniLibs sources")
}
variant.sources.jniLibs.addGeneratedSourceDirectory(nativeTask) { task ->
task.outputDirectory
}
}
}
}

View File

@@ -1,5 +1,7 @@
// file: Android/settings.gradle
// version: 2
// version: 3
import org.gradle.util.GradleVersion
pluginManagement {
repositories {
@@ -9,6 +11,14 @@ pluginManagement {
}
}
def minimumGradleVersion = GradleVersion.version("9.6.0")
def currentGradleVersion = GradleVersion.current()
if (currentGradleVersion < minimumGradleVersion) {
throw new GradleException(
"Gradle ${minimumGradleVersion.version} or newer is required; current version is ${currentGradleVersion.version}"
)
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {