Skip to content
1 change: 0 additions & 1 deletion .github/workflows/test-matrix-agp-gradle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ jobs:
name: Test Matrix - AGP ${{ matrix.agp }} - Gradle ${{ matrix.gradle }} - Java ${{ matrix.java }} - Kotlin ${{ matrix.kotlin }}
env:
VERSION_AGP: ${{ matrix.agp }}
VERSION_GROOVY: ${{ matrix.groovy }}
VERSION_KOTLIN: ${{ matrix.kotlin }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

Expand Down
15 changes: 11 additions & 4 deletions plugin-build/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.util.Properties
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
Expand All @@ -31,7 +31,7 @@ val fixtureClasspath: Configuration by configurations.creating

dependencies {
compileOnly(libs.gradleApi)
compileOnly(Libs.AGP)
compileOnly(libs.agp)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means we're compiling against a static version of AGP as opposed to dynamic (so we don't hit compilation issues when some AGP APIs are removed, for example, even though we have a proper AGP version guard for those at runtime). Integration tests, however, still run against a dynamic version, so we're testing against newer AGP versions and if our plugin does not handle something yet those would still fail.

compileOnly(libs.proguard)

implementation(libs.asm)
Expand Down Expand Up @@ -91,8 +91,15 @@ tasks.withType<KotlinCompile>().configureEach {

compilerOptions {
jvmTarget.set(JVM_11)
languageVersion.set(KOTLIN_1_8)
apiVersion.set(KOTLIN_1_8)
// Kotlin supports current + 3 previous language versions.
// We want 1.8, but if the compiler no longer supports it, use the oldest it does support.
// e.g. Kotlin 2.1 oldest=1.8, Kotlin 2.3 oldest=2.0
val compilerParts = KotlinCompilerVersion.VERSION.split(".")
val compilerFlat = compilerParts[0].toInt() * 10 + compilerParts[1].toInt()
val oldestFlat = maxOf(compilerFlat - 3, 18) // 18 = Kotlin 1.8
val kotlinLangVersion = KotlinVersion.valueOf("KOTLIN_${oldestFlat / 10}_${oldestFlat % 10}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flat version encoding breaks for minor versions ≥ 10

Low Severity

The flat encoding major * 10 + minor and subsequent decode via oldestFlat / 10 and oldestFlat % 10 is ambiguous when minor version reaches 10 or above. For example, if the computed oldestFlat is 30, it decodes to KOTLIN_3_0 instead of the intended KOTLIN_2_10. This would cause KotlinVersion.valueOf to throw an IllegalArgumentException at configuration time. The earliest this manifests is if Kotlin reaches 2.13 (where oldest = 2.10, flat = 30 → incorrectly decoded as 3.0).

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think kotlin ever reaches 10, they usually bump the major version then

languageVersion.set(kotlinLangVersion)
apiVersion.set(kotlinLangVersion)
}
}

Expand Down
57 changes: 47 additions & 10 deletions scripts/generate-compat-matrix.main.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ class GenerateMatrix : CliktCommand() {
val (currentVersions, latestVersion) = fetchAgpCompatibilityTable(agpVersions)
buildMap<Version, Version> {
for (agpVersion in agpVersions) {
put(
agpVersion,
legacyVersions[agpVersion] ?: currentVersions[agpVersion] ?: latestVersion,
)
val gradleVersion =
legacyVersions[agpVersion]
?: currentVersions[agpVersion]
?: fetchGradleVersionFromReleaseNotes(agpVersion)
?: latestVersion
put(agpVersion, gradleVersion)
}
}
} catch (e: Exception) {
Expand All @@ -86,12 +88,11 @@ class GenerateMatrix : CliktCommand() {
}

// TODO: for now this is manual, but we could try get it from Gradle's github in the future
val gradleToGroovy =
mapOf("7.5".toVersion(strict = false) to "1.2", "8.11".toVersion(strict = false) to "1.7.1")
val gradleToKotlin =
mapOf(
"7.5".toVersion(strict = false) to "1.8.20",
"9.0.0".toVersion(strict = false) to "2.1.0",
"9.5.0-0".toVersion(strict = false) to "2.3.0",
)
// TODO: make it dynamic too
val kotlinVersion = "2.1.0".toVersion()
Expand Down Expand Up @@ -128,10 +129,6 @@ class GenerateMatrix : CliktCommand() {
)
// TODO: if needed we can test against different Java versions
put("java", "17")
val groovy = gradleToGroovy.entries.findLast { finalGradle >= it.key }?.value
if (groovy != null) {
put("groovy", groovy)
}
val kotlin = gradleToKotlin.entries.findLast { finalGradle >= it.key }?.value
if (kotlin != null) {
put("kotlin", kotlin)
Expand Down Expand Up @@ -346,6 +343,46 @@ class GenerateMatrix : CliktCommand() {

return gradleVersions to latest.value
}

/**
* Fetches the minimum required Gradle version from the AGP release notes page. This is used as a
* fallback when the AGP version is not yet listed in the compatibility table.
*
* @param agpVersion the AGP version to look up
* @return the minimum required Gradle version, or null if the release notes page doesn't exist
*/
private fun fetchGradleVersionFromReleaseNotes(agpVersion: Version): Version? {
// Release notes URLs are per-minor version and always use patch 0
// e.g. agp-9-2-0-release-notes covers all 9.2.x versions
val url =
"https://developer.android.com/build/releases/agp-${agpVersion.major}-${agpVersion.minor}-0-release-notes"
val html =
try {
URL(url).readText()
} catch (e: Throwable) {
echo("Warning: Could not fetch release notes from $url")
return null
}
val doc = Jsoup.parse(html)
val tables = doc.select("table")
for (table in tables) {
val headers = table.select("tr").firstOrNull()?.select("th")?.map { it.text() } ?: continue
// The compatibility table has "Minimum version" and "Default version" columns
if (headers.none { it.contains("version", ignoreCase = true) }) continue
for (row in table.select("tr")) {
val cells = row.select("td").map { it.text() }
if (cells.size >= 2 && cells[0].trim().equals("Gradle", ignoreCase = true)) {
return try {
Version.parse(cells[1], strict = false)
} catch (e: Throwable) {
echo("Warning: Could not parse Gradle version '${cells[1]}' from release notes")
null
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
romtsn marked this conversation as resolved.
}
}
return null
}
}

GenerateMatrix().main(args)
Loading