Skip to content

ci(agp-matrix): Generate compatibility matrix dynamically #873

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
57 changes: 22 additions & 35 deletions .github/workflows/test-matrix-agp-gradle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,33 @@ concurrency:
cancel-in-progress: true

jobs:
generate-matrix:
name: Generate Compat Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Generate Compat Matrix
id: generate
run: |
matrix=$(kotlin scripts/generate-compat-matrix.main.kts)

echo "matrix<<EOF" >> "$GITHUB_OUTPUT"
echo "$matrix" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"

echo "$matrix" | jq

publish-dry-run:
needs: generate-matrix
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
agp: [ "7.2.1" ]
gradle: [ "7.4" ]
java: [ "11" ]
groovy: [ "1.2" ]
include:
- agp: "7.3.0"
gradle: "7.5"
java: "11"
groovy: "1.2"
- agp: "7.4.0"
gradle: "7.6"
java: "11"
groovy: "1.2"
- agp: "8.0.0"
gradle: "8.0.2"
java: "17"
groovy: "1.2"
- agp: "8.6.1"
gradle: "8.7"
java: "17"
groovy: "1.2"
- agp: "8.8.1"
gradle: "8.11.1"
java: "17"
groovy: "1.7.1"
- agp: "8.9.0-rc02"
gradle: "8.11.1"
java: "17"
groovy: "1.7.1"
- agp: "8.10.0-alpha05"
gradle: "8.11.1"
java: "17"
groovy: "1.7.1"
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}

name: Test Matrix - AGP ${{ matrix.agp }} - Gradle ${{ matrix.gradle }}
name: Test Matrix - AGP ${{ matrix.agp }} - Gradle ${{ matrix.gradle }} - Java ${{ matrix.java }} - Groovy ${{ matrix.groovy }}
env:
VERSION_AGP: ${{ matrix.agp }}
VERSION_GROOVY: ${{ matrix.groovy }}
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.kotlin) apply false
alias(libs.plugins.kotlinAndroid) apply false
alias(libs.plugins.kapt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.androidApplication) version BuildPluginsVersion.AGP apply false
alias(libs.plugins.androidLibrary) version BuildPluginsVersion.AGP apply false
alias(libs.plugins.spotless)
Expand Down
5 changes: 2 additions & 3 deletions examples/android-instrumentation-sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.androidApplication) version BuildPluginsVersion.AGP
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kapt)
alias(libs.plugins.ksp)
id("io.sentry.android.gradle")
}

Expand Down Expand Up @@ -86,13 +86,12 @@ dependencies {

implementation(libs.sample.room.runtime)
implementation(libs.sample.room.ktx)
implementation(libs.sample.room.rxjava)

implementation(libs.sample.timber.timber)
implementation(project(":examples:android-room-lib"))
implementation(libs.sample.fragment.fragmentKtx)

kapt(libs.sample.room.compiler)
ksp(libs.sample.room.compiler)
}

sentry {
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ android.useAndroidX=true

# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# For AGP matrix tests, we can't infer the correct Gradle version for pre-releases
android.overrideVersionCheck=true
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "1.8.20-1.0.11" }
dokka = { id = "org.jetbrains.dokka", version = "1.8.10" }
spotless = { id = "com.diffplug.spotless", version = "7.0.0" }
groovyGradlePlugin = { id = "dev.gradleplugins.groovy-gradle-plugin", version = "1.2" }
Expand Down Expand Up @@ -77,7 +78,6 @@ sample-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-c
sample-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "sampleRoom" }
sample-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "sampleRoom" }
sample-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "sampleRoom" }
sample-room-rxjava = { group = "androidx.room", name = "room-rxjava2", version.ref = "sampleRoom" }

sample-retrofit-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "sampleRetrofit" }
sample-retrofit-retrofitGson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "sampleRetrofit" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ org.gradle.daemon=false
org.gradle.parallel=true

android.useAndroidX=true
# For AGP matrix tests, we can't infer the correct Gradle version for pre-releases
android.overrideVersionCheck=true
165 changes: 165 additions & 0 deletions scripts/generate-compat-matrix.main.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env kotlin
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
@file:DependsOn("com.squareup.moshi:moshi:1.15.2")
@file:DependsOn("com.squareup.moshi:moshi-kotlin:1.15.2")
@file:DependsOn("com.squareup.okhttp3:okhttp:4.12.0")
@file:DependsOn("com.google.guava:guava:33.4.0-jre")
@file:DependsOn("io.github.z4kn4fein:semver-jvm:3.0.0")
@file:DependsOn("org.jsoup:jsoup:1.17.2")

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.core.main
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import io.github.z4kn4fein.semver.Version
import io.github.z4kn4fein.semver.toVersion
import org.w3c.dom.Element
import javax.xml.parsers.DocumentBuilderFactory
import org.jsoup.Jsoup
import java.net.URL

/** Represents a matrix consumed by GitHub actions */
class Matrix(val include: List<Map<String, String>>)

/** Generates a matrix of different build tools we use. */
class GenerateMatrix : CliktCommand() {

// TODO: introduce params if needed
// private val gradleProperties: File by option("--gradle-properties").file().required()
// private val versionsToml: File by option("--versions-toml").file().required()

@OptIn(ExperimentalStdlibApi::class)
override fun run() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()

/**
* we test against AGP:
* - Latest stable
* - Pre-release alpha
* - Pre-release beta/rc
* - Previous latest major
*/
@Suppress("SwallowedException", "TooGenericExceptionCaught")
val agpVersions = try {
fetchAgpVersions()
} catch (e: Exception) {
print(e.printStackTrace())
echo("Error parsing AGP versions")
throw ProgramResult(1)
}

val agpToGradle = try {
fetchAgpCompatibilityTable(agpVersions)
} catch (e: Exception) {
print(e.printStackTrace())
echo("Error parsing AGP compatibility table")
throw ProgramResult(1)
}
// 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 baseIncludes = buildList {
for (entry in agpToGradle.entries) {
add(
buildMap {
put("agp", entry.key.toString())
// Gradle does not use .patch if it's 0 ¯\_(ツ)_/¯
val gradle = entry.value
val (gradleMajor, gradleMinor, gradlePatch) = gradle
put("gradle", if (gradlePatch == 0) "${gradleMajor}.${gradleMinor}" else gradle.toString())
// TODO: if needed we can test against different Java versions
put("java", "17")
val groovy = gradleToGroovy.entries.findLast { gradle >= it.key }?.value
if (groovy != null) {
put("groovy", groovy)
}
}
)
}
}

val allIncludes = baseIncludes + extraIncludes(baseIncludes)

val json = moshi.adapter<Matrix>().toJson(Matrix(allIncludes))

// Example output: {"include":[{"agp":"8.11.0-alpha08","gradle":"8.11.1","java":"17","groovy":"1.7.1"},{"agp":"8.10.0-rc04","gradle":"8.11.1","java":"17","groovy":"1.7.1"},{"agp":"8.9.2","gradle":"8.11.1","java":"17","groovy":"1.7.1"},{"agp":"7.4.2","gradle":"7.5","java":"17","groovy":"1.2"}]}
echo(json)
}

/**
* Add extra configuration
*/
@Suppress("UNUSED_PARAMETER")
private fun extraIncludes(baseIncludes: List<Map<String, String>>): List<Map<String, String>> {
return emptyList()
// return baseIncludes.map { matrix ->
// matrix.toMutableMap().apply {
// put("kotlin", "x.y.z")
// }
// }
}

private fun fetchAgpVersions(): List<Version> {
val semvers = mutableListOf<Version>()
val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val document = documentBuilder.parse("https://dl.google.com/dl/android/maven2/com/android/tools/build/group-index.xml")
document.documentElement.normalize()

val root = document.documentElement
val gradleElements = root.getElementsByTagName("gradle")
for (i in 0 until gradleElements.length) {
val element = gradleElements.item(i) as Element
val versions = element.getAttribute("versions")
versions.split(",").forEach { version ->
val semver = Version.parse(version, strict = false)
semvers += semver
}
}

// AGP usually has two pre-releases at the same time
val latestPreRelease = semvers.last { it.isPreRelease }
val secondToLatestPreRelease = semvers.last { it.isPreRelease && it.minor == (latestPreRelease.minor - 1) }
val latest = semvers.last { it.isStable }
val previousMajorLatest = semvers.last { it.isStable && it.major == (latest.major - 1)}
return listOf(latestPreRelease, secondToLatestPreRelease, latest, previousMajorLatest)
}

private fun fetchAgpCompatibilityTable(agpVersions: List<Version>): Map<Version, Version> {
val gradleVersions = mutableMapOf<Version, Version>()
val html = URL("https://developer.android.com/build/releases/gradle-plugin#updating-gradle").readText()
val doc = Jsoup.parse(html)
val table = doc.selectFirst("table") ?: error("No table found")
val rows = table.select("tr")
val headers = rows.first()?.select("th")?.map { it.text() } ?: listOf()

// the table is in format
// AGP version (without .patch) - Gradle version
val agpToGradle = LinkedHashMap<Version, Version>()
for (row in rows) {
val cells = row.select("td").map { it.text() }
if (cells.size > 0) {
val agp = Version.parse(cells[0], strict = false)
val gradle = Version.parse(cells[1], strict = false)
agpToGradle[agp] = gradle
}
}

val latest = agpToGradle.entries.first()
for (agpVersion in agpVersions) {
// the compat table does not contain the .patch part, so we compare major and minor
val entry = agpToGradle.entries.find { it.key.major == agpVersion.major && it.key.minor == agpVersion.minor }
if (entry != null) {
gradleVersions[agpVersion] = entry.value
} else {
// it's a pre-release so we use the latest compat entry
gradleVersions[agpVersion] = latest.value
}
}

return gradleVersions
}
}

GenerateMatrix().main(args)
Loading