// 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();