0.3.1-0-pre.2.fix.1

This commit is contained in:
2026-09-20 21:38:21 +02:00
parent 51371c1b24
commit bded27c944
12 changed files with 569 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 71 # version: 72
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -21,7 +21,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.3.1-0-pre.2" version = "0.3.1-0-pre.2.fix.1"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games" repository = "https://git.sasedev.com/Sasedev/games"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md --> <!-- file: README.md -->
<!-- version: 42 --> <!-- version: 43 -->
# games.sasedev # games.sasedev
@@ -26,7 +26,7 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale
Version stable de référence : `0.3.0`. Version stable de référence : `0.3.0`.
Version de développement courante : `0.3.1-0-pre.2`. Version de développement courante : `0.3.1-0-pre.2.fix.1`.
Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme. Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/apps/game-snake-poc-tauri/README.md --> <!-- file: crates/apps/game-snake-poc-tauri/README.md -->
<!-- version: 1 --> <!-- version: 2 -->
# game-snake-poc-tauri # game-snake-poc-tauri
@@ -22,4 +22,6 @@ Le frontend minimal de `0.3.1-0-pre.2` valide uniquement le démarrage Tauri, le
Les hooks `beforeDevCommand` et `beforeBuildCommand` de Tauri possèdent le build WASM et Vite. Aucun orchestrateur Python n'est utilisé. Les bindings WASM, la distribution Vite, les caches, `node_modules/` et le projet Android généré restent hors des sources livrées ou dans des chemins générés ignorés. Les hooks `beforeDevCommand` et `beforeBuildCommand` de Tauri possèdent le build WASM et Vite. Aucun orchestrateur Python n'est utilisé. Les bindings WASM, la distribution Vite, les caches, `node_modules/` et le projet Android généré restent hors des sources livrées ou dans des chemins générés ignorés.
Pour Tauri CLI `2.11.x`, le projet Android généré utilise encore Gradle `8.14.3`. Le hook `scripts/prepare-android-gradle.mjs` applique avant `dev`/`build` la migration Android déjà retenue upstream par Tauri (`Gradle 9.6.1`, AGP `9.3.1`, Kotlin `2.2.10`) afin de rester exécutable avec Java 25. Cette préparation modifie uniquement `gen/android/`, reste idempotente et ne dépend pas du Gradle global installé sur la machine.
Voir [`USAGE.md`](USAGE.md) pour l'initialisation locale et les smokes Android. Voir [`USAGE.md`](USAGE.md) pour l'initialisation locale et les smokes Android.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/apps/game-snake-poc-tauri/USAGE.md --> <!-- file: crates/apps/game-snake-poc-tauri/USAGE.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Utilisation — Snake Tauri Android # Utilisation — Snake Tauri Android
@@ -7,9 +7,9 @@
Le host utilise Cargo/Rust, `cargo tauri`, Node/npm, `wasm-bindgen`, Java et le SDK Android. Le dépôt attend `ANDROID_HOME` vers le SDK Android et des NDK side-by-side sous `$ANDROID_HOME/ndk/<version>`. Le host utilise Cargo/Rust, `cargo tauri`, Node/npm, `wasm-bindgen`, Java et le SDK Android. Le dépôt attend `ANDROID_HOME` vers le SDK Android et des NDK side-by-side sous `$ANDROID_HOME/ndk/<version>`.
`NDK_HOME` n'est pas imposé globalement par le projet. Si une commande Tauri locale le réclame, définir ponctuellement `NDK_HOME` vers une version NDK exacte installée sous `$ANDROID_HOME/ndk/`; ne pas sélectionner implicitement une version lorsque plusieurs NDK coexistent. `NDK_HOME` n'est pas un prérequis utilisateur du projet. Tauri sélectionne la version NDK side-by-side appropriée sous `$ANDROID_HOME/ndk/<version>` et peut injecter lui-même `NDK_HOME` dans ses sous-processus Android. Une variable globale ne doit donc pas être ajoutée uniquement pour satisfaire la documentation générique de Tauri.
`JAVA_HOME` n'est pas imposé tant que la CLI Tauri résout correctement `java` depuis l'environnement. En cas de diagnostic explicite de la CLI, fixer alors `JAVA_HOME` vers le JDK réellement choisi. `JAVA_HOME` n'est pas imposé tant que la CLI Tauri résout correctement `java` depuis l'environnement. L'environnement de validation utilise Java 25 ; Tauri CLI `2.11.x` génère encore un wrapper Gradle `8.14.3`, qui ne peut pas exécuter ce JDK. Le host corrige ce scaffold généré avant les hooks frontend vers Gradle `9.6.1`, AGP `9.3.1` et Kotlin `2.2.10`, combinaison déjà adoptée par la migration Android upstream de Tauri. Le Gradle global installé sur la machine n'est pas utilisé par `gradlew` et n'est pas requis par ce correctif.
Pour les cibles actuellement disponibles, `aarch64-linux-android` permet le smoke sur l'appareil ARM64 et `x86_64-linux-android` celui des AVD x86_64. Les autres ABI ne sont pas requises pour `0-pre.2`. Pour les cibles actuellement disponibles, `aarch64-linux-android` permet le smoke sur l'appareil ARM64 et `x86_64-linux-android` celui des AVD x86_64. Les autres ABI ne sont pas requises pour `0-pre.2`.
@@ -27,7 +27,7 @@ Initialiser une fois le projet Android généré par Tauri :
(cd crates/apps/game-snake-poc-tauri && cargo tauri android init) (cd crates/apps/game-snake-poc-tauri && cargo tauri android init)
``` ```
Le répertoire `gen/` produit par Tauri est généré et ignoré ; il n'appartient pas aux deltas. Le répertoire `gen/` produit par Tauri est généré et ignoré ; il n'appartient pas aux deltas. Le correctif Gradle 9 est appliqué à ce répertoire par le hook Tauri au prochain `android dev` ou `android build`, y compris si `gen/android` existe déjà depuis `0-pre.2`. Il n'est donc pas nécessaire de rejouer `android init` pour appliquer le fix.
## Smoke Android de développement ## Smoke Android de développement

View File

@@ -1,7 +1,42 @@
// file: crates/apps/game-snake-poc-tauri/build.rs // file: crates/apps/game-snake-poc-tauri/build.rs
// version: 1 // version: 2
//! Build-time integration for the Snake Tauri host.
const GENERATED_ANDROID_TAURI_BUILD_GRADLE: &str = "gen/android/app/tauri.build.gradle.kts";
const GRADLE_8_IMPLEMENTATION_DECLARATION: &str = "val implementation by configurations";
const GRADLE_9_IMPLEMENTATION_DECLARATION: &str = "val implementation = configurations.getByName(\"implementation\")";
fn patch_generated_android_gradle() {
let manifest_dir = match std::env::var_os("CARGO_MANIFEST_DIR") {
Some(value) => std::path::PathBuf::from(value),
None => return,
};
let path = manifest_dir.join(GENERATED_ANDROID_TAURI_BUILD_GRADLE);
let content = match std::fs::read_to_string(&path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
std::result::Result::Err(error) => {
println!("cargo:warning=failed to inspect {}: {error}", path.display());
return;
},
};
if content.contains(GRADLE_9_IMPLEMENTATION_DECLARATION) {
return;
}
if !content.contains(GRADLE_8_IMPLEMENTATION_DECLARATION) {
println!("cargo:warning={} does not match the expected Tauri 2.11.x Gradle bridge", path.display());
return;
}
let patched = content.replace(GRADLE_8_IMPLEMENTATION_DECLARATION, GRADLE_9_IMPLEMENTATION_DECLARATION);
if let std::result::Result::Err(error) = std::fs::write(&path, patched) {
println!("cargo:warning=failed to prepare {} for Gradle 9: {error}", path.display());
}
return;
}
fn main() { fn main() {
tauri_build::build(); tauri_build::build();
patch_generated_android_gradle();
return; return;
} }

View File

@@ -6,8 +6,9 @@
"scripts": { "scripts": {
"wasm:dev": "cargo build -p game-snake-poc-wasm --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/debug/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm", "wasm:dev": "cargo build -p game-snake-poc-wasm --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/debug/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm",
"wasm:build": "cargo build -p game-snake-poc-wasm --release --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/release/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm", "wasm:build": "cargo build -p game-snake-poc-wasm --release --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/release/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm",
"dev": "npm run wasm:dev && vite", "dev": "npm run android:prepare && npm run wasm:dev && vite",
"build": "npm run wasm:build && tsc && vite build" "build": "npm run android:prepare && npm run wasm:build && tsc && vite build",
"android:prepare": "node scripts/prepare-android-gradle.mjs"
}, },
"dependencies": { "dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3", "@fltsci/tauri-plugin-tracing": "^0.3",

View File

@@ -0,0 +1,219 @@
// file: crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs
// version: 1
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import process from "node:process";
const GRADLE_VERSION = "9.6.1";
const AGP_VERSION = "9.3.1";
const KOTLIN_VERSION = "2.2.10";
function fail(message) {
process.stderr.write(`Snake Tauri Android Gradle preparation failed: ${message}\n`);
process.exitCode = 1;
throw new Error(message);
}
function replaceRequired(content, previous, next, label) {
if (content.includes(next)) {
return content;
}
if (!content.includes(previous)) {
return fail(`${label}: expected neither the Tauri 2.11.x form nor the prepared form`);
}
return content.replace(previous, next);
}
function removeLine(content, line) {
const withLf = `${line}\n`;
if (content.includes(withLf)) {
return content.replace(withLf, "");
}
if (content.endsWith(line)) {
return content.slice(0, -line.length);
}
return content;
}
function appendProperty(content, property) {
if (content.includes(`${property}\n`) || content.endsWith(property)) {
return content;
}
const separator = content.endsWith("\n") ? "" : "\n";
return `${content}${separator}${property}\n`;
}
function updateFile(path, transform) {
if (!existsSync(path)) {
return fail(`missing generated Android file: ${path}`);
}
const before = readFileSync(path, "utf8");
const after = transform(before);
if (after !== before) {
writeFileSync(path, after, "utf8");
}
}
function patchWrapper(androidRoot) {
const path = resolve(androidRoot, "gradle/wrapper/gradle-wrapper.properties");
updateFile(path, content => replaceRequired(
content,
"gradle-8.14.3-bin.zip",
`gradle-${GRADLE_VERSION}-bin.zip`,
"Gradle wrapper",
));
}
function patchRootBuild(androidRoot) {
const path = resolve(androidRoot, "build.gradle.kts");
updateFile(path, content => {
let result = replaceRequired(
content,
'classpath("com.android.tools.build:gradle:8.11.0")',
`classpath("com.android.tools.build:gradle:${AGP_VERSION}")`,
"Android Gradle Plugin",
);
result = replaceRequired(
result,
'classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")',
`classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${KOTLIN_VERSION}")`,
"Kotlin Gradle Plugin",
);
return result;
});
}
function patchBuildSrcManifest(androidRoot) {
const path = resolve(androidRoot, "buildSrc/build.gradle.kts");
updateFile(path, content => replaceRequired(
content,
'implementation("com.android.tools.build:gradle:8.11.0")',
`implementation("com.android.tools.build:gradle:${AGP_VERSION}")`,
"buildSrc Android Gradle Plugin",
));
}
function patchBuildTask(androidRoot) {
const path = resolve(androidRoot, "buildSrc/src/main/kotlin/BuildTask.kt");
updateFile(path, content => {
let result = content;
if (!result.includes("import javax.inject.Inject")) {
result = replaceRequired(
result,
"import org.gradle.api.tasks.TaskAction\n",
"import org.gradle.api.tasks.TaskAction\nimport javax.inject.Inject\nimport org.gradle.process.ExecOperations\n",
"BuildTask imports",
);
}
result = replaceRequired(
result,
"open class BuildTask : DefaultTask() {\n @Input\n var rootDirRel: String? = null\n",
"abstract class BuildTask : DefaultTask() {\n @get:Inject\n abstract val execOperations: ExecOperations\n\n @Input\n var rootDirRel: String? = null\n @Input\n var projectDir: String? = null\n",
"BuildTask ExecOperations declaration",
);
result = replaceRequired(
result,
" project.exec {\n workingDir(File(project.projectDir, rootDirRel))\n",
" execOperations.exec {\n workingDir(File(projectDir, rootDirRel))\n",
"BuildTask process execution",
);
result = result.replaceAll("project.logger.isEnabled", "logger.isEnabled");
return result;
});
}
function patchRustPlugin(androidRoot) {
const path = resolve(androidRoot, "buildSrc/src/main/kotlin/RustPlugin.kt");
updateFile(path, content => {
let result = replaceRequired(
content,
" val targetBuildTask = project.tasks.maybeCreate(\n \"rustBuild$targetArchCapitalized$profileCapitalized\",\n BuildTask::class.java\n ).apply {\n",
" val targetBuildTask = project.tasks.register(\n \"rustBuild$targetArchCapitalized$profileCapitalized\",\n BuildTask::class.java\n ) {\n",
"RustPlugin task registration",
);
if (!result.includes(" projectDir = project.projectDir.path\n")) {
result = replaceRequired(
result,
" rootDirRel = config.rootDirRel\n target = targetName\n",
" rootDirRel = config.rootDirRel\n projectDir = project.projectDir.path\n target = targetName\n",
"RustPlugin project directory",
);
}
return result;
});
}
function patchAppBuild(androidRoot) {
const path = resolve(androidRoot, "app/build.gradle.kts");
updateFile(path, content => {
let result = content;
if (!result.includes("import org.jetbrains.kotlin.gradle.dsl.JvmTarget")) {
result = `import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n${result}`;
}
result = replaceRequired(
result,
" isMinifyEnabled = true\n proguardFiles(\n *fileTree(\".\") { include(\"**/*.pro\") }\n .plus(getDefaultProguardFile(\"proguard-android-optimize.txt\"))\n .toList().toTypedArray()\n )\n",
" optimization {\n enable = true\n }\n proguardFiles(\n *fileTree(\".\") {\n include(\"**/*.pro\")\n exclude(\"build/**\")\n }.files.toTypedArray()\n )\n",
"Android release optimization DSL",
);
result = replaceRequired(
result,
" kotlinOptions {\n jvmTarget = \"1.8\"\n }\n buildFeatures {\n",
" compileOptions {\n sourceCompatibility = JavaVersion.VERSION_1_8\n targetCompatibility = JavaVersion.VERSION_1_8\n }\n buildFeatures {\n",
"Android compile options",
);
if (!result.includes("kotlin {\n compilerOptions")) {
result = replaceRequired(
result,
"}\n\nrust {\n",
"}\n\nkotlin {\n compilerOptions {\n jvmTarget = JvmTarget.JVM_1_8\n }\n}\n\nrust {\n",
"Kotlin compiler options",
);
}
result = result.replace('apply(from = "tauri.build.gradle.kts")', 'apply(from = file("tauri.build.gradle.kts"))');
return result;
});
}
function patchGradleProperties(androidRoot) {
const path = resolve(androidRoot, "gradle.properties");
updateFile(path, content => {
let result = content;
result = removeLine(result, "android.useAndroidX=true");
result = removeLine(result, "android.nonTransitiveRClass=true");
result = removeLine(result, "android.nonFinalResIds=false");
result = appendProperty(result, "org.gradle.configuration-cache=true");
result = appendProperty(result, "android.proguard.failOnMissingFiles=false");
result = appendProperty(result, "android.builtInKotlin=false");
result = appendProperty(result, "android.newDsl=false");
return result;
});
}
function resolveAndroidRoot() {
const marker = "--android-project";
const markerIndex = process.argv.indexOf(marker);
if (markerIndex >= 0) {
const value = process.argv[markerIndex + 1];
if (value === undefined || value.startsWith("--")) {
return fail(`${marker} requires a path`);
}
return resolve(process.cwd(), value);
}
return resolve(process.cwd(), "gen/android");
}
function main() {
const androidRoot = resolveAndroidRoot();
patchWrapper(androidRoot);
patchRootBuild(androidRoot);
patchBuildSrcManifest(androidRoot);
patchBuildTask(androidRoot);
patchRustPlugin(androidRoot);
patchAppBuild(androidRoot);
patchGradleProperties(androidRoot);
process.stdout.write(`Prepared Tauri Android Gradle ${GRADLE_VERSION} / AGP ${AGP_VERSION} / Kotlin ${KOTLIN_VERSION}.\n`);
}
main();

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-snake-poc-tauri/src/tauri.rs // file: crates/apps/game-snake-poc-tauri/src/tauri.rs
// version: 1 // version: 2
//! Tauri runtime assembly and Web/Rust commands for the Snake Android POC. //! Tauri runtime assembly and Web/Rust commands for the Snake Android POC.
@@ -24,9 +24,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
return builder.invoke_handler(tauri::generate_handler![get_runtime_descriptor, frontend_ready]); return builder.invoke_handler(tauri::generate_handler![get_runtime_descriptor, frontend_ready]);
} }
/// Runs the Snake Tauri application. fn run_runtime() -> std::result::Result<(), String> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() -> std::result::Result<(), String> {
let logging_guard = game_logging_lib::init_console_tracing(); let logging_guard = game_logging_lib::init_console_tracing();
let _logging_guard = match logging_guard { let _logging_guard = match logging_guard {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -63,3 +61,25 @@ pub fn run() -> std::result::Result<(), String> {
}, },
}; };
} }
/// Runs the Snake Tauri application on a desktop host.
#[cfg(not(mobile))]
pub fn run() -> std::result::Result<(), String> {
return run_runtime();
}
/// Runs the Snake Tauri application from Tauri's mobile entry point.
#[cfg(mobile)]
#[tauri::mobile_entry_point]
pub fn run() {
let result = run_runtime();
if let std::result::Result::Err(error) = result {
tracing::error!(
target: "games::tauri::snake",
action = "mobile_entrypoint_error",
error = error.as_str(),
"Snake Tauri mobile entry point terminated after a runtime error"
);
}
return;
}

View File

@@ -0,0 +1,181 @@
<!-- file: deltas/0.3.1/0-pre.2.fix.1.md -->
<!-- version: 1 -->
# Delta 0.3.1-0-pre.2.fix.1
## Base requise
`0.3.1-0-pre.2`.
Le delta `0-pre.2.md` reste immuable. La gate utilisateur du 2026-09-20 est consignée dans `history/0.3.1/0-pre.2.md`.
## Objectif
Corriger uniquement les défauts qui empêchent la gate de `0-pre.2` :
- warning `missing_docs` du build script, bloquant sous Clippy `-D warnings` ;
- résultat ignoré par le wrapper `tauri::mobile_entry_point` Android ;
- incompatibilité du wrapper Gradle `8.14.3` généré par Tauri CLI `2.11.3` avec Java 25.
Le scope fonctionnel de `0-pre.2` ne change pas. Canvas, contrôles, assets et lifecycle restent réservés aux tranches suivantes.
## Version technique
Le correctif touche du code Rust et le chemin de build Tauri/Android. La version workspace passe donc à :
```text
0.3.1-0-pre.2.fix.1
```
La version frontend locale de `package.json` reste `0.1.0` conformément à `VER-TAURI-003` et `VER-TAURI-004`.
## Correction Rust
`build.rs` possède désormais une rustdoc de crate et reste conforme aux règles Rust du workspace.
Le point d'entrée est séparé en deux niveaux :
- `run_runtime() -> Result<(), String>` possède l'exécution réelle ;
- le host non mobile conserve `run() -> Result<(), String>` ;
- le host mobile expose `#[tauri::mobile_entry_point] pub fn run()` et traite explicitement l'erreur éventuelle au lieu de laisser le macro ignorer un `Result` `must_use`.
Le comportement fonctionnel Tauri n'est pas modifié.
## Correction Gradle 9
Le log utilisateur confirme que Tauri sélectionne lui-même le NDK installé sous `$ANDROID_HOME/ndk/30.0.14904198` puis injecte `NDK_HOME` dans son sous-processus. `NDK_HOME` n'est donc pas ajouté comme prérequis utilisateur.
Le défaut réel est :
```text
Tauri CLI 2.11.3 -> gen/android/gradlew -> Gradle 8.14.3 -> Java 25
Unsupported class file major version 69
```
Le Gradle global de la machine n'est pas consommé par ce chemin ; modifier uniquement le `PATH` ne corrige donc pas le wrapper Tauri.
Le fix ajoute `crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs`. Avant chaque hook `dev`/`build`, ce script adapte idempotemment le projet généré `gen/android` vers la combinaison déjà adoptée par la migration Android upstream de Tauri :
```text
Gradle 9.6.1
Android Gradle Plugin 9.3.1
Kotlin Gradle Plugin 2.2.10
```
Il adapte aussi les ruptures Gradle 9 pertinentes du scaffold Tauri `2.11.x` :
- `Project.exec` devient `ExecOperations` dans `BuildTask.kt` ;
- les tâches Rust ciblées utilisent `tasks.register` et reçoivent explicitement `projectDir` ;
- le DSL Android/Kotlin de l'application est aligné sur le template Gradle 9 upstream ;
- les propriétés de compatibilité AGP 9 sont ajoutées ;
- les anciennes propriétés Android devenues des defaults sont retirées.
Le script est local à la crate Tauri et ne modifie ni le Gradle global ni le cache Cargo.
## Bridge généré par tauri-build
`tauri-build 2.6.3` régénère `gen/android/app/tauri.build.gradle.kts` pendant la compilation Rust, donc un patch effectué uniquement avant Cargo serait écrasé.
`build.rs` applique par conséquent, immédiatement après `tauri_build::build()`, la migration :
```text
val implementation by configurations
```
vers :
```text
val implementation = configurations.getByName("implementation")
```
lorsque le fichier Android généré existe. Le build script reste un no-op pour ce point lors des checks hôte sans `gen/android`.
## Hooks Tauri
`package.json` conserve Tauri comme propriétaire du pipeline :
```text
cargo tauri android dev
-> beforeDevCommand
-> npm run dev
-> android:prepare
-> WASM
-> Vite
```
et de même pour `beforeBuildCommand`.
`npm run dev` et `npm run build` ne deviennent pas des gates manuelles.
## Documentation et garde-fous
- `USAGE.md` retire `NDK_HOME` des prérequis utilisateur et explique la migration Gradle 9 ;
- le plan vivant enregistre le défaut et le fix sans ouvrir `0-pre.3` ;
- `audit_distribution_layout.py` exige désormais le hook Gradle, ses versions épinglées, la migration `ExecOperations` et le bridge `tauri-build` compatible Gradle 9 ;
- le test synthétique du script Node a été exécuté deux fois sur le même fixture pour vérifier son idempotence.
## Fichiers ajoutés
```text
crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs
history/0.3.1/0-pre.2.md
deltas/0.3.1/0-pre.2.fix.1.md
```
## Fichiers modifiés
```text
Cargo.toml
README.md
crates/apps/game-snake-poc-tauri/README.md
crates/apps/game-snake-poc-tauri/USAGE.md
crates/apps/game-snake-poc-tauri/build.rs
crates/apps/game-snake-poc-tauri/package.json
crates/apps/game-snake-poc-tauri/src/tauri.rs
docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md
scripts/audit_distribution_layout.py
```
## Suppressions
Aucune.
## Validation générateur
Les contrôles statiques applicables sont exécutés sur l'état livré. Les commandes Cargo/npm/Tauri/Gradle restent à exécuter côté utilisateur conformément à `CMD-GEN-007` et `CMD-BUILD-005`.
## Gate utilisateur
Depuis la racine :
```bash
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 fmt --all
cargo fmt --all -- --check
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p game-snake-poc-wasm --all-targets --all-features
```
Le build Web direct était déjà propre dans la gate `0-pre.2` et aucune de ses sources n'est modifiée par ce fix ; il n'est pas nécessaire de répéter sa gate pour ce correctif borné.
Le projet Android généré existe déjà chez l'utilisateur. Ne pas rejouer `cargo tauri android init`. Relancer directement :
```bash
(cd crates/apps/game-snake-poc-tauri && cargo tauri android dev)
```
Le hook doit afficher avant le build Android :
```text
Prepared Tauri Android Gradle 9.6.1 / AGP 9.3.1 / Kotlin 2.2.10.
```
Puis le wrapper doit utiliser Gradle 9.6.1 au lieu de 8.14.3. Le smoke reste celui de `0-pre.2` : démarrage sur l'appareil/AVD, bridge Rust, chargement WASM, provenance `android / tauri-webview / wasm / unknown / unknown` et tracing sans erreur bloquante.
## Suite
Si cette gate est propre, `0-pre.2` est considéré réparé et la progression reprend sur `0.3.1-0-pre.3` conformément au plan vivant.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md --> <!-- file: docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md -->
<!-- version: 2 --> <!-- version: 3 -->
# Plan v0.3.1 — second host Snake Tauri Android # Plan v0.3.1 — second host Snake Tauri Android
@@ -77,6 +77,8 @@ La tranche vise un premier démarrage Tauri Android minimal, pas encore toute la
Gate utilisateur : audits, Cargo workspace strict, test ciblé de l'adapter WASM, non-régression du build Web direct, `cargo tauri android init` puis `cargo tauri android dev` sur un AVD ou appareil disponible. `cargo tauri android build` reste reporté aux phases finales sauf besoin diagnostique. Gate utilisateur : audits, Cargo workspace strict, test ciblé de l'adapter WASM, non-régression du build Web direct, `cargo tauri android init` puis `cargo tauri android dev` sur un AVD ou appareil disponible. `cargo tauri android build` reste reporté aux phases finales sauf besoin diagnostique.
Le premier smoke `0-pre.2` a révélé deux défauts bornés : `build.rs` échouait sous Clippy strict à cause de la rustdoc manquante, et Java 25 ne pouvait pas exécuter le wrapper Gradle `8.14.3` généré par Tauri CLI `2.11.3`. `0-pre.2.fix.1` conserve Tauri 2.x mais prépare idempotemment le projet généré vers la combinaison Android upstream `Gradle 9.6.1 / AGP 9.3.1 / Kotlin 2.2.10`, tout en adaptant les API Gradle 9 retirées du `buildSrc`. Le Gradle global de la machine ne fait pas partie du contrat reproductible.
### `0-pre.3` — shell Snake, Canvas, input et assets ### `0-pre.3` — shell Snake, Canvas, input et assets
Réutiliser sélectivement le host Web validé : Réutiliser sélectivement le host Web validé :
@@ -152,7 +154,7 @@ node --version
npm --version npm --version
wasm-bindgen --version wasm-bindgen --version
java -version java -version
printf 'JAVA_HOME=%s\nANDROID_HOME=%s\nNDK_HOME=%s\n' "$JAVA_HOME" "$ANDROID_HOME" "$NDK_HOME" printf 'JAVA_HOME=%s\nANDROID_HOME=%s\n' "$JAVA_HOME" "$ANDROID_HOME"
adb version adb version
adb devices -l adb devices -l
emulator -list-avds emulator -list-avds
@@ -160,7 +162,7 @@ emulator -list-avds
L'absence d'un outil ne doit pas être masquée. Elle devient un prérequis concret de `0-pre.2` ou motive un fix borné de `0-pre.1`. L'absence d'un outil ne doit pas être masquée. Elle devient un prérequis concret de `0-pre.2` ou motive un fix borné de `0-pre.1`.
La gate `0-pre.1` validée le 2026-09-20 a confirmé Rust `1.94.1`, Tauri CLI `2.11.3`, Node `24.21.0`, npm `11.19.0`, wasm-bindgen `0.2.128`, OpenJDK `25.0.4.1`, `ANDROID_HOME=/home/sinus/DEV/AndroidSDK`, un appareil ARM64 réel et trois AVD disponibles. `JAVA_HOME` et `NDK_HOME` sont vides dans cet environnement mais ne sont pas traités comme prérequis globaux du dépôt : Java est résolu depuis le `PATH`, et les NDK side-by-side restent sous `$ANDROID_HOME/ndk/<version>`. Si la CLI Tauri exige explicitement `NDK_HOME` pour une opération donnée, la validation utilisera une variable ponctuelle visant une version NDK exacte plutôt qu'un réglage global ambigu. La gate `0-pre.1` validée le 2026-09-20 a confirmé Rust `1.94.1`, Tauri CLI `2.11.3`, Node `24.21.0`, npm `11.19.0`, wasm-bindgen `0.2.128`, OpenJDK `25.0.4.1`, `ANDROID_HOME=/home/sinus/DEV/AndroidSDK`, un appareil ARM64 réel et trois AVD disponibles. `JAVA_HOME` est vide mais Java est résolu depuis le `PATH`. `NDK_HOME` n'est pas un prérequis à inventorier : Tauri a démontré pendant le smoke `0-pre.2` qu'il sélectionne le NDK side-by-side sous `$ANDROID_HOME/ndk/<version>` et injecte lui-même cette variable dans ses sous-processus. Le défaut observé concernait Java 25 face au wrapper Gradle 8.14.3, pas la résolution du NDK.
## Critères de fermeture de 0.3.1 ## Critères de fermeture de 0.3.1

74
history/0.3.1/0-pre.2.md Normal file
View File

@@ -0,0 +1,74 @@
<!-- file: history/0.3.1/0-pre.2.md -->
<!-- version: 1 -->
# Historique 0.3.1-0-pre.2
## Statut
Gate utilisateur exécutée le 2026-09-20 : validation partielle, `0.3.1-0-pre.2.fix.1` requis avant toute progression vers `0-pre.3`.
## Résultats propres
Les contrôles suivants passent sur `0-pre.2` :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
games.sasedev workspace audit: clean
Markdown table audit: clean
Distribution layout audit: clean
cargo fmt --all: clean
cargo fmt --all -- --check: clean
cargo check --workspace: terminé avec un warning build.rs
cargo test -p game-snake-poc-wasm --all-targets --all-features: 5 passed
cargo build -p game-snake-poc-wasm --target wasm32-unknown-unknown: clean
wasm-bindgen: clean
Web/game-snake-poc npm install + npm run build: clean
crates/apps/game-snake-poc-tauri npm install: clean
cargo tauri android init: succès
```
Le build Web direct reste donc non régressé et l'initialisation Android Tauri produit correctement le projet Android local.
## Défaut Rust
`cargo clippy --workspace --all-targets --all-features -- -D warnings` échoue sur :
```text
crates/apps/game-snake-poc-tauri/build.rs
missing documentation for the crate
```
Pendant la compilation Android, le macro `tauri::mobile_entry_point` révèle également un `unused_must_use` parce que le point d'entrée annoté retourne `Result<(), String>`. Le fix sépare le wrapper mobile `()` de l'exécution interne retournant `Result` afin de conserver la propagation d'erreur sur Desktop sans laisser le point d'entrée Android ignorer implicitement le résultat.
## Défaut Gradle / Java
La CLI Tauri `2.11.3` sélectionne correctement le NDK side-by-side installé :
```text
Using installed NDK: .../AndroidSDK/ndk/30.0.14904198
```
Elle injecte ensuite elle-même `NDK_HOME` dans le sous-processus Android. L'absence de `NDK_HOME` dans l'environnement utilisateur n'est donc pas la cause du défaut.
`cargo tauri android dev` échoue plus tard lorsque le wrapper généré télécharge Gradle `8.14.3` puis l'exécute avec OpenJDK `25.0.4.1` :
```text
Unsupported class file major version 69
```
Le Gradle global présent dans le `PATH` n'est pas utilisé, puisque Tauri lance `gen/android/gradlew`.
## Direction du fix
Le correctif suit la migration Android déjà retenue upstream par Tauri :
```text
Gradle 9.6.1
Android Gradle Plugin 9.3.1
Kotlin Gradle Plugin 2.2.10
```
Le scaffold `gen/android` reste généré et ignoré. Un hook Node idempotent adapte le projet généré avant les hooks WASM/Vite, y compris les usages Gradle 9 supprimés dans `buildSrc`. Le build script Rust adapte après `tauri_build::build()` le bridge Gradle généré par `tauri-build 2.6.3`.
Aucun gameplay, Canvas, input, asset ou lifecycle de `0-pre.3` n'est ouvert par ce fix.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# file: scripts/audit_distribution_layout.py # file: scripts/audit_distribution_layout.py
# version: 8 # version: 9
"""Audit the static distribution layout expected by supported POCs.""" """Audit the static distribution layout expected by supported POCs."""
@@ -34,6 +34,7 @@ REQUIRED_PATHS = (
"crates/apps/game-snake-poc-tauri/README.md", "crates/apps/game-snake-poc-tauri/README.md",
"crates/apps/game-snake-poc-tauri/USAGE.md", "crates/apps/game-snake-poc-tauri/USAGE.md",
"crates/apps/game-snake-poc-tauri/package.json", "crates/apps/game-snake-poc-tauri/package.json",
"crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs",
"crates/apps/game-snake-poc-tauri/tsconfig.json", "crates/apps/game-snake-poc-tauri/tsconfig.json",
"crates/apps/game-snake-poc-tauri/vite.config.ts", "crates/apps/game-snake-poc-tauri/vite.config.ts",
"crates/apps/game-snake-poc-tauri/tauri.conf.json", "crates/apps/game-snake-poc-tauri/tauri.conf.json",
@@ -155,10 +156,12 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]:
package_path = root / "crates/apps/game-snake-poc-tauri/package.json" package_path = root / "crates/apps/game-snake-poc-tauri/package.json"
tauri_path = root / "crates/apps/game-snake-poc-tauri/tauri.conf.json" tauri_path = root / "crates/apps/game-snake-poc-tauri/tauri.conf.json"
tauri_rust_path = root / "crates/apps/game-snake-poc-tauri/src/tauri.rs" tauri_rust_path = root / "crates/apps/game-snake-poc-tauri/src/tauri.rs"
build_script_path = root / "crates/apps/game-snake-poc-tauri/build.rs"
gradle_prepare_path = root / "crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs"
frontend_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/main.ts" frontend_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/main.ts"
logging_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/logging.ts" logging_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/logging.ts"
wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs" wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs"
paths = (cargo_path, package_path, tauri_path, tauri_rust_path, frontend_path, logging_path, wasm_runtime_path) paths = (cargo_path, package_path, tauri_path, tauri_rust_path, build_script_path, gradle_prepare_path, frontend_path, logging_path, wasm_runtime_path)
if not all(path.exists() for path in paths): if not all(path.exists() for path in paths):
return violations return violations
@@ -166,6 +169,8 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]:
package = package_path.read_text(encoding="utf-8") package = package_path.read_text(encoding="utf-8")
tauri = tauri_path.read_text(encoding="utf-8") tauri = tauri_path.read_text(encoding="utf-8")
tauri_rust = tauri_rust_path.read_text(encoding="utf-8") tauri_rust = tauri_rust_path.read_text(encoding="utf-8")
build_script = build_script_path.read_text(encoding="utf-8")
gradle_prepare = gradle_prepare_path.read_text(encoding="utf-8")
frontend = frontend_path.read_text(encoding="utf-8") frontend = frontend_path.read_text(encoding="utf-8")
logging = logging_path.read_text(encoding="utf-8") logging = logging_path.read_text(encoding="utf-8")
wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8") wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8")
@@ -174,9 +179,17 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]:
(cargo, 'tauri-plugin-tracing.workspace = true', "DIST-LAYOUT-022: Snake Tauri host must enable the tracing plugin"), (cargo, 'tauri-plugin-tracing.workspace = true', "DIST-LAYOUT-022: Snake Tauri host must enable the tracing plugin"),
(package, '"wasm:dev": "cargo build -p game-snake-poc-wasm', "DIST-LAYOUT-023: Snake Tauri dev hook must build the shared Snake WASM adapter"), (package, '"wasm:dev": "cargo build -p game-snake-poc-wasm', "DIST-LAYOUT-023: Snake Tauri dev hook must build the shared Snake WASM adapter"),
(package, 'wasm-bindgen ../../../../builds/sasedev-games/', "DIST-LAYOUT-024: Snake Tauri bindings must be generated outside the repository"), (package, 'wasm-bindgen ../../../../builds/sasedev-games/', "DIST-LAYOUT-024: Snake Tauri bindings must be generated outside the repository"),
(package, '"android:prepare": "node scripts/prepare-android-gradle.mjs"', "DIST-LAYOUT-036: Snake Tauri host must prepare the generated Android Gradle project through its native Node hook"),
(package, '"dev": "npm run android:prepare && npm run wasm:dev && vite"', "DIST-LAYOUT-037: Snake Tauri dev hook must prepare Gradle before WASM/Vite"),
(package, '"build": "npm run android:prepare && npm run wasm:build && tsc && vite build"', "DIST-LAYOUT-038: Snake Tauri build hook must prepare Gradle before WASM/Vite"),
(gradle_prepare, 'const GRADLE_VERSION = "9.6.1";', "DIST-LAYOUT-039: Snake Tauri Android preparation must pin the reviewed Gradle 9 version"),
(gradle_prepare, 'const AGP_VERSION = "9.3.1";', "DIST-LAYOUT-040: Snake Tauri Android preparation must pin the reviewed AGP version"),
(gradle_prepare, 'const KOTLIN_VERSION = "2.2.10";', "DIST-LAYOUT-041: Snake Tauri Android preparation must pin the reviewed Kotlin plugin version"),
(gradle_prepare, 'org.gradle.process.ExecOperations', "DIST-LAYOUT-042: Snake Tauri Gradle 9 preparation must migrate removed Project.exec usage"),
(build_script, 'val implementation = configurations.getByName', "DIST-LAYOUT-043: Snake Tauri build script must repair the tauri-build Gradle bridge for Gradle 9"),
(tauri, '"script": "npm run dev"', "DIST-LAYOUT-025: Tauri must own the frontend development hook"), (tauri, '"script": "npm run dev"', "DIST-LAYOUT-025: Tauri must own the frontend development hook"),
(tauri, '"script": "npm run build"', "DIST-LAYOUT-026: Tauri must own the frontend production hook"), (tauri, '"script": "npm run build"', "DIST-LAYOUT-026: Tauri must own the frontend production hook"),
(tauri_rust, '#[cfg_attr(mobile, tauri::mobile_entry_point)]', "DIST-LAYOUT-027: Snake Tauri run entry point must support Tauri mobile"), (tauri_rust, '#[tauri::mobile_entry_point]', "DIST-LAYOUT-027: Snake Tauri run entry point must support Tauri mobile"),
(tauri_rust, 'tauri_plugin_tracing::Builder::new()', "DIST-LAYOUT-028: Snake Tauri runtime must install the tracing plugin"), (tauri_rust, 'tauri_plugin_tracing::Builder::new()', "DIST-LAYOUT-028: Snake Tauri runtime must install the tracing plugin"),
(frontend, 'configure_runtime_provenance(', "DIST-LAYOUT-029: Snake Tauri frontend must configure provenance through the shared adapter"), (frontend, 'configure_runtime_provenance(', "DIST-LAYOUT-029: Snake Tauri frontend must configure provenance through the shared adapter"),
(logging, '@fltsci/tauri-plugin-tracing', "DIST-LAYOUT-030: Snake Tauri frontend logs must use the tracing bridge"), (logging, '@fltsci/tauri-plugin-tracing', "DIST-LAYOUT-030: Snake Tauri frontend logs must use the tracing bridge"),