Add build plugin to check android badging for changes

This commit is contained in:
Wolf-Martell Montwé 2024-01-16 11:53:47 +01:00
parent 5ebf7e50af
commit e119f1d66d
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
5 changed files with 156 additions and 3 deletions

View file

@ -1,6 +1,7 @@
plugins {
id(ThunderbirdPlugins.App.android)
alias(libs.plugins.dependency.guard)
id("thunderbird.quality.badging")
}
val testCoverageEnabled: Boolean by extra

View file

@ -13,7 +13,8 @@ Gradle Plugins.
The `build-plugin` is used as included build in the root `settings.gradle.kts` and provides all
included `xyz.gradle.kts` as plugins under their `xyz` name to the whole project.
The plugins should try to accomplish single responsibility and leave one-off configuration to the module's `build.gradle.kts`.
The plugins should try to accomplish single responsibility and leave one-off configuration to the
module's `build.gradle.kts`.
## Convention plugins
@ -35,6 +36,10 @@ The plugins should try to accomplish single responsibility and leave one-off con
- `thunderbird.quality.spotless` - [Spotless - Code formatter](https://github.com/diffplug/spotless)
with [Ktlint - Kotlin linter and formatter](https://pinterest.github.io/ktlint/)
- Use `./gradlew spotlessCheck` to check for any issue and `./gradlew spotlessApply` to format your code
- `thunderbird.quality.badging` - [Android Badging Check Plugin](https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt)
- Use `./gradlew generate{VariantName}Badging` to generate badging file
- Use `./gradlew check{VariantName}Badging` to validate allowed badging
- Use `./gradlew update{VariantName}Badging` to update allowed badging
## Add new build plugin
@ -44,8 +49,8 @@ If you need to access dependencies that are not yet defined in `build-plugin/bui
1. Add the dependency to the version catalog `gradle/libs.versions.toml`
2. Then add it to `build-plugin/build.gradle.kts`.
1. In case of a plugin dependency use `implementation(plugin(libs.plugins.YOUR_PLUGIN_DEPENDENCY))`.
1. Otherwise `implementation(libs.YOUR_DEPENDENCY))`.
1. In case of a plugin dependency use `implementation(plugin(libs.plugins.YOUR_PLUGIN_DEPENDENCY))`.
2. Otherwise `implementation(libs.YOUR_DEPENDENCY))`.
When done, add the plugin to `build-plugin/src/main/kotlin/ThunderbirdPlugins.kt`

View file

@ -15,6 +15,9 @@ dependencies {
implementation(plugin(libs.plugins.spotless))
implementation(plugin(libs.plugins.detekt))
implementation(plugin(libs.plugins.dependency.check))
compileOnly(libs.android.tools.common)
compileOnly(libs.assertk)
}
fun plugin(provider: Provider<PluginDependency>) = with(provider.get()) {

View file

@ -0,0 +1,141 @@
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.android.SdkConstants
import com.android.build.api.artifact.SingleArtifact
import org.gradle.configurationcache.extensions.capitalized
/**
* This is a Gradle plugin that adds a task to generate the badging of the APKs and a task to check that the
* generated badging is the same as the golden badging.
*
* This is taken from [nowinandroid](https://github.com/android/nowinandroid) and follows recommendations from
* [Prevent regressions with CI and badging](https://android-developers.googleblog.com/2023/12/increase-your-apps-availability-across-device-types.html).
*/
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
androidComponents {
onVariants { variant ->
val capitalizedVariantName = variant.name.capitalized()
val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
val generateBadging = tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
apk.set(variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE))
aapt2Executable.set(
File(
android.sdkDirectory,
"${SdkConstants.FD_BUILD_TOOLS}/" +
"${android.buildToolsVersion}/" +
SdkConstants.FN_AAPT2,
),
)
badging.set(
project.layout.buildDirectory.file(
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
),
)
}
val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
tasks.register<Copy>(updateBadgingTaskName) {
from(generateBadging.get().badging)
into(project.layout.projectDirectory.dir("badging"))
}
val checkBadgingTaskName = "check${capitalizedVariantName}Badging"
val goldenBadgingPath = project.layout.projectDirectory.file("badging/${variant.name}-badging.txt")
tasks.register<CheckBadgingTask>(checkBadgingTaskName) {
if (goldenBadgingPath.asFile.exists()) {
goldenBadging.set(goldenBadgingPath)
}
generatedBadging.set(
generateBadging.get().badging,
)
this.updateBadgingTaskName.set(updateBadgingTaskName)
output.set(
project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName"),
)
}
}
}
@CacheableTask
abstract class GenerateBadgingTask : DefaultTask() {
@get:OutputFile
abstract val badging: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val apk: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val aapt2Executable: RegularFileProperty
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun taskAction() {
execOperations.exec {
commandLine(
aapt2Executable.get().asFile.absolutePath,
"dump",
"badging",
apk.get().asFile.absolutePath,
)
standardOutput = badging.asFile.get().outputStream()
}
}
}
@CacheableTask
abstract class CheckBadgingTask : DefaultTask() {
// In order for the task to be up-to-date when the inputs have not changed,
// the task must declare an output, even if it's not used. Tasks with no
// output are always run regardless of whether the inputs changed
@get:OutputDirectory
abstract val output: DirectoryProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:Optional
@get:InputFile
abstract val goldenBadging: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
abstract val generatedBadging: RegularFileProperty
@get:Input
abstract val updateBadgingTaskName: Property<String>
override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP
@TaskAction
fun taskAction() {
if (goldenBadging.isPresent.not()) {
logger.error(
"Golden badging file does not exist! " +
"If this is the first time running this task, " +
"run ./gradlew ${updateBadgingTaskName.get()}",
)
return
}
val goldenBadgingContent = goldenBadging.get().asFile.readText()
val generatedBadgingContent = generatedBadging.get().asFile.readText()
if (goldenBadgingContent == generatedBadgingContent) {
logger.info("Generated badging is the same as golden badging!")
return
}
assertThat(generatedBadgingContent).isEqualTo(goldenBadgingContent)
}
}

View file

@ -3,7 +3,9 @@
[versions]
gradle = "8.5"
# AGP and tools should be updated together
androidGradlePlugin = "8.2.1"
androidTools = "31.2.1"
ktlint = "1.0.1"
kotlin = "1.9.22"
@ -58,6 +60,7 @@ dependency-guard = { id = "com.dropbox.dependency-guard", version.ref = "pluginD
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }