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: crates/apps/game-snake-poc-tauri/README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# 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.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/apps/game-snake-poc-tauri/USAGE.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# 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>`.
`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`.
@@ -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

View File

@@ -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;
}

View File

@@ -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",

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
// 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::Wry>) -> tauri::Builder<tau
return builder.invoke_handler(tauri::generate_handler![get_runtime_descriptor, frontend_ready]);
}
/// Runs the Snake Tauri application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() -> 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;
}