From bded27c9446a4a067f8b0b2fbac937b0113f740e Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 20 Sep 2026 21:38:21 +0200 Subject: [PATCH] 0.3.1-0-pre.2.fix.1 --- Cargo.toml | 4 +- README.md | 4 +- crates/apps/game-snake-poc-tauri/README.md | 4 +- crates/apps/game-snake-poc-tauri/USAGE.md | 8 +- crates/apps/game-snake-poc-tauri/build.rs | 37 ++- crates/apps/game-snake-poc-tauri/package.json | 5 +- .../scripts/prepare-android-gradle.mjs | 219 ++++++++++++++++++ crates/apps/game-snake-poc-tauri/src/tauri.rs | 28 ++- deltas/0.3.1/0-pre.2.fix.1.md | 181 +++++++++++++++ .../002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md | 8 +- history/0.3.1/0-pre.2.md | 74 ++++++ scripts/audit_distribution_layout.py | 19 +- 12 files changed, 569 insertions(+), 22 deletions(-) create mode 100644 crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs create mode 100644 deltas/0.3.1/0-pre.2.fix.1.md create mode 100644 history/0.3.1/0-pre.2.md diff --git a/Cargo.toml b/Cargo.toml index 26ea9fc..4fdaee0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 71 +# version: 72 [workspace] resolver = "3" @@ -21,7 +21,7 @@ members = [ ] [workspace.package] -version = "0.3.1-0-pre.2" +version = "0.3.1-0-pre.2.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 0c0c2ad..4a3dae3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # 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 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. diff --git a/crates/apps/game-snake-poc-tauri/README.md b/crates/apps/game-snake-poc-tauri/README.md index 93a5fef..72971dc 100644 --- a/crates/apps/game-snake-poc-tauri/README.md +++ b/crates/apps/game-snake-poc-tauri/README.md @@ -1,5 +1,5 @@ - + # 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. +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. diff --git a/crates/apps/game-snake-poc-tauri/USAGE.md b/crates/apps/game-snake-poc-tauri/USAGE.md index 796edc2..6deab4d 100644 --- a/crates/apps/game-snake-poc-tauri/USAGE.md +++ b/crates/apps/game-snake-poc-tauri/USAGE.md @@ -1,5 +1,5 @@ - + # 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/`. -`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/` 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`. @@ -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) ``` -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 diff --git a/crates/apps/game-snake-poc-tauri/build.rs b/crates/apps/game-snake-poc-tauri/build.rs index 4458b3a..af8f7b3 100644 --- a/crates/apps/game-snake-poc-tauri/build.rs +++ b/crates/apps/game-snake-poc-tauri/build.rs @@ -1,7 +1,42 @@ // 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() { tauri_build::build(); + patch_generated_android_gradle(); return; } diff --git a/crates/apps/game-snake-poc-tauri/package.json b/crates/apps/game-snake-poc-tauri/package.json index 85e5282..91e0e59 100644 --- a/crates/apps/game-snake-poc-tauri/package.json +++ b/crates/apps/game-snake-poc-tauri/package.json @@ -6,8 +6,9 @@ "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: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", - "build": "npm run wasm:build && tsc && vite build" + "dev": "npm run android:prepare && npm run wasm:dev && vite", + "build": "npm run android:prepare && npm run wasm:build && tsc && vite build", + "android:prepare": "node scripts/prepare-android-gradle.mjs" }, "dependencies": { "@fltsci/tauri-plugin-tracing": "^0.3", diff --git a/crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs b/crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs new file mode 100644 index 0000000..26b8edb --- /dev/null +++ b/crates/apps/game-snake-poc-tauri/scripts/prepare-android-gradle.mjs @@ -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(); diff --git a/crates/apps/game-snake-poc-tauri/src/tauri.rs b/crates/apps/game-snake-poc-tauri/src/tauri.rs index dcf4659..c697e59 100644 --- a/crates/apps/game-snake-poc-tauri/src/tauri.rs +++ b/crates/apps/game-snake-poc-tauri/src/tauri.rs @@ -1,5 +1,5 @@ // 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. @@ -24,9 +24,7 @@ fn configure_commands(builder: tauri::Builder) -> tauri::Builder std::result::Result<(), String> { +fn run_runtime() -> std::result::Result<(), String> { let logging_guard = game_logging_lib::init_console_tracing(); let _logging_guard = match logging_guard { 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; +} diff --git a/deltas/0.3.1/0-pre.2.fix.1.md b/deltas/0.3.1/0-pre.2.fix.1.md new file mode 100644 index 0000000..7272e00 --- /dev/null +++ b/deltas/0.3.1/0-pre.2.fix.1.md @@ -0,0 +1,181 @@ + + + +# 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. diff --git a/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md b/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md index eb4353b..dd9d1c6 100644 --- a/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md +++ b/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md @@ -1,5 +1,5 @@ - + # 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. +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 Réutiliser sélectivement le host Web validé : @@ -152,7 +154,7 @@ node --version npm --version wasm-bindgen --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 devices -l 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`. -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/`. 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/` 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 diff --git a/history/0.3.1/0-pre.2.md b/history/0.3.1/0-pre.2.md new file mode 100644 index 0000000..9dbf672 --- /dev/null +++ b/history/0.3.1/0-pre.2.md @@ -0,0 +1,74 @@ + + + +# 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. diff --git a/scripts/audit_distribution_layout.py b/scripts/audit_distribution_layout.py index a424520..e2862bf 100644 --- a/scripts/audit_distribution_layout.py +++ b/scripts/audit_distribution_layout.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_distribution_layout.py -# version: 8 +# version: 9 """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/USAGE.md", "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/vite.config.ts", "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" 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" + 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" 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" - 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): 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") tauri = tauri_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") logging = logging_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"), (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, '"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 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"), (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"),