0.3.3-0-pre.3
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
192
Android/gradle/sasedev-rust-android.gradle
Normal file
192
Android/gradle/sasedev-rust-android.gradle
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user