Skip to content

Commit 6b9d154

Browse files
authored
refactor: build plugin for AGP 9 compatibility (#10425)
2 parents 3f8870e + 44d6d6b commit 6b9d154

22 files changed

Lines changed: 657 additions & 550 deletions

.github/workflows/build-android.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
uses: ./.github/actions/setup-gradle
7070

7171
- name: Build K9 application
72-
run: ./gradlew :app-k9mail:assemble
72+
run: ./gradlew :app-k9mail:assemble -Pci=true
7373

7474
- name: Check K9 Badging
7575
run: |
@@ -93,7 +93,7 @@ jobs:
9393
uses: ./.github/actions/setup-gradle
9494

9595
- name: Build Thunderbird application
96-
run: ./gradlew :app-thunderbird:assemble
96+
run: ./gradlew :app-thunderbird:assemble -Pci=true
9797

9898
- name: Check Thunderbird Badging
9999
run: |

app-k9mail/build.gradle.kts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
plugins {
22
id(ThunderbirdPlugins.App.androidCompose)
33
alias(libs.plugins.dependency.guard)
4-
id("thunderbird.app.version.info")
5-
id("thunderbird.quality.badging")
4+
alias(libs.plugins.tb.app.badging)
5+
alias(libs.plugins.tb.app.versioning)
66
}
77

88
val testCoverageEnabled = hasProperty("testCoverageEnabled")
@@ -85,10 +85,13 @@ android {
8585
}
8686

8787
buildTypes {
88+
val isCI = project.findProperty("ci") == "true"
8889
release {
8990
signingConfig = signingConfigs.getByType(SigningType.K9_RELEASE)
9091

91-
isMinifyEnabled = true
92+
isMinifyEnabled = !isCI
93+
isShrinkResources = !isCI
94+
9295
proguardFiles(
9396
getDefaultProguardFile("proguard-android.txt"),
9497
"proguard-rules.pro",

app-thunderbird/build.gradle.kts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
plugins {
22
id(ThunderbirdPlugins.App.androidCompose)
33
alias(libs.plugins.dependency.guard)
4-
id("thunderbird.app.version.info")
5-
id("thunderbird.quality.badging")
4+
alias(libs.plugins.tb.app.badging)
5+
alias(libs.plugins.tb.app.versioning)
66
}
77

88
val testCoverageEnabled = hasProperty("testCoverageEnabled")
@@ -88,25 +88,12 @@ android {
8888
}
8989

9090
buildTypes {
91-
debug {
92-
applicationIdSuffix = ".debug"
93-
versionNameSuffix = "-SNAPSHOT"
94-
95-
enableUnitTestCoverage = testCoverageEnabled
96-
enableAndroidTestCoverage = testCoverageEnabled
97-
98-
isMinifyEnabled = false
99-
isShrinkResources = false
100-
isDebuggable = true
101-
102-
buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "null")
103-
}
104-
91+
val isCI = project.findProperty("ci") == "true"
10592
release {
10693
signingConfig = signingConfigs.getByType(SigningType.TB_RELEASE)
10794

108-
isMinifyEnabled = true
109-
isShrinkResources = true
95+
isMinifyEnabled = !isCI
96+
isShrinkResources = !isCI
11097
isDebuggable = false
11198

11299
proguardFiles(
@@ -123,8 +110,8 @@ android {
123110
applicationIdSuffix = ".beta"
124111
versionNameSuffix = "b0"
125112

126-
isMinifyEnabled = true
127-
isShrinkResources = true
113+
isMinifyEnabled = !isCI
114+
isShrinkResources = !isCI
128115
isDebuggable = false
129116

130117
matchingFallbacks += listOf("release")
@@ -143,8 +130,8 @@ android {
143130
applicationIdSuffix = ".daily"
144131
versionNameSuffix = "a1"
145132

146-
isMinifyEnabled = true
147-
isShrinkResources = true
133+
isMinifyEnabled = !isCI
134+
isShrinkResources = !isCI
148135
isDebuggable = false
149136

150137
matchingFallbacks += listOf("release")
@@ -157,6 +144,20 @@ android {
157144
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1918151
158145
buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "\"nightly\"")
159146
}
147+
148+
debug {
149+
applicationIdSuffix = ".debug"
150+
versionNameSuffix = "-SNAPSHOT"
151+
152+
enableUnitTestCoverage = testCoverageEnabled
153+
enableAndroidTestCoverage = testCoverageEnabled
154+
155+
isMinifyEnabled = false
156+
isShrinkResources = false
157+
isDebuggable = true
158+
159+
buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "null")
160+
}
160161
}
161162

162163
flavorDimensions += listOf("app")

build-plugin/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ plugins {
55
dependencies {
66
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
77

8-
implementation(plugin(libs.plugins.kotlin.android))
98
implementation(plugin(libs.plugins.kotlin.jvm))
109
implementation(plugin(libs.plugins.kotlin.multiplatform))
1110
implementation(plugin(libs.plugins.kotlin.parcelize))
@@ -26,7 +25,6 @@ dependencies {
2625
// Make custom plugins in ":plugin" available to precompiled convention plugins by classpath
2726
implementation(project(":plugin"))
2827

29-
implementation(libs.diff.utils)
3028
compileOnly(libs.android.tools.common)
3129

3230
// This defines the used Kotlin version for all Plugin dependencies
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package net.thunderbird.gradle.plugin
2+
3+
import org.gradle.accessors.dm.LibrariesForLibs
4+
import org.gradle.api.Project
5+
import org.gradle.kotlin.dsl.getByName
6+
7+
val Project.libs
8+
get(): LibrariesForLibs = extensions.getByName<LibrariesForLibs>("libs")
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package net.thunderbird.gradle.plugin.app.badging
2+
3+
import com.android.build.api.artifact.SingleArtifact
4+
import com.android.build.api.variant.Aapt2
5+
import org.gradle.api.Plugin
6+
import org.gradle.api.Project
7+
import org.gradle.kotlin.dsl.assign
8+
import org.gradle.kotlin.dsl.configure
9+
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
10+
import org.gradle.api.tasks.Copy
11+
import org.gradle.kotlin.dsl.register
12+
13+
private val variantsToCheck = listOf("release", "beta", "daily")
14+
15+
/**
16+
* This is a Gradle plugin that adds a task to generate the badging of the APKs and a task to check that the
17+
* generated badging is the same as the golden badging.
18+
*
19+
* This is modified from [nowinandroid](https://github.com/android/nowinandroid) and follows recommendations from
20+
* [Prevent regressions with CI and badging](https://android-developers.googleblog.com/2023/12/increase-your-apps-availability-across-device-types.html).
21+
*/
22+
class BadgingPlugin : Plugin<Project> {
23+
override fun apply(target: Project) {
24+
with(target) {
25+
with(pluginManager) {
26+
apply("com.android.application")
27+
}
28+
29+
configureBadging()
30+
}
31+
}
32+
33+
private fun Project.configureBadging() {
34+
extensions.configure<ApplicationAndroidComponentsExtension> {
35+
onVariants { variant ->
36+
if (variantsToCheck.any { variant.name.contains(it, ignoreCase = true) }) {
37+
val capitalizedVariantName = variant.name.capitalized()
38+
val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
39+
val generateBadging = tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
40+
apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE)
41+
aapt2Executable = this@configure.sdkComponents.aapt2.flatMap(Aapt2::executable)
42+
badging = project.layout.buildDirectory.file(
43+
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
44+
)
45+
}
46+
47+
val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
48+
tasks.register<Copy>(updateBadgingTaskName) {
49+
from(generateBadging.map(GenerateBadgingTask::badging))
50+
into(project.layout.projectDirectory.dir("badging"))
51+
}
52+
53+
val checkBadgingTaskName = "check${capitalizedVariantName}Badging"
54+
tasks.register<CheckBadgingTask>(checkBadgingTaskName) {
55+
goldenBadging = project.layout.projectDirectory.file("badging/${variant.name}-badging.txt")
56+
57+
generatedBadging.set(generateBadging.flatMap(GenerateBadgingTask::badging))
58+
59+
this.updateBadgingTaskName = updateBadgingTaskName
60+
61+
output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName")
62+
}
63+
64+
tasks.named("build") {
65+
dependsOn(checkBadgingTaskName)
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
private fun String.capitalized() = replaceFirstChar {
74+
if (it.isLowerCase()) it.titlecase() else it.toString()
75+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package net.thunderbird.gradle.plugin.app.badging
2+
3+
import com.github.difflib.text.DiffRow
4+
import com.github.difflib.text.DiffRowGenerator
5+
import org.gradle.api.DefaultTask
6+
import org.gradle.api.GradleException
7+
import org.gradle.api.file.DirectoryProperty
8+
import org.gradle.api.file.RegularFileProperty
9+
import org.gradle.api.provider.Property
10+
import org.gradle.api.tasks.CacheableTask
11+
import org.gradle.api.tasks.Input
12+
import org.gradle.api.tasks.InputFile
13+
import org.gradle.api.tasks.Optional
14+
import org.gradle.api.tasks.OutputDirectory
15+
import org.gradle.api.tasks.PathSensitive
16+
import org.gradle.api.tasks.PathSensitivity
17+
import org.gradle.api.tasks.TaskAction
18+
import org.gradle.language.base.plugins.LifecycleBasePlugin
19+
20+
@CacheableTask
21+
abstract class CheckBadgingTask : DefaultTask() {
22+
23+
// In order for the task to be up-to-date when the inputs have not changed,
24+
// the task must declare an output, even if it's not used. Tasks with no
25+
// output are always run regardless of whether the inputs changed
26+
@get:OutputDirectory
27+
abstract val output: DirectoryProperty
28+
29+
@get:PathSensitive(PathSensitivity.RELATIVE)
30+
@get:Optional
31+
@get:InputFile
32+
abstract val goldenBadging: RegularFileProperty
33+
34+
@get:PathSensitive(PathSensitivity.RELATIVE)
35+
@get:InputFile
36+
abstract val generatedBadging: RegularFileProperty
37+
38+
@get:Input
39+
abstract val updateBadgingTaskName: Property<String>
40+
41+
override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP
42+
43+
@TaskAction
44+
fun taskAction() {
45+
if (goldenBadging.isPresent.not()) {
46+
printlnColor(
47+
ANSI_YELLOW,
48+
"Golden badging file does not exist!" +
49+
" If this is the first time running this task," +
50+
" run ./gradlew ${updateBadgingTaskName.get()}",
51+
)
52+
return
53+
}
54+
55+
val goldenBadgingContent = goldenBadging.get().asFile.readText()
56+
val generatedBadgingContent = generatedBadging.get().asFile.readText()
57+
if (goldenBadgingContent == generatedBadgingContent) {
58+
printlnColor(ANSI_YELLOW, "Generated badging is the same as golden badging!")
59+
return
60+
}
61+
62+
val diff = performDiff(goldenBadgingContent, generatedBadgingContent)
63+
printDiff(diff)
64+
65+
throw GradleException(
66+
"""
67+
Generated badging is different from golden badging!
68+
69+
If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}
70+
""".trimIndent(),
71+
)
72+
}
73+
74+
private fun performDiff(goldenBadgingContent: String, generatedBadgingContent: String): String {
75+
val generator: DiffRowGenerator = DiffRowGenerator.create()
76+
.showInlineDiffs(true)
77+
.mergeOriginalRevised(true)
78+
.inlineDiffByWord(true)
79+
.oldTag { _ -> "" }
80+
.newTag { _ -> "" }
81+
.build()
82+
83+
return generator.generateDiffRows(
84+
goldenBadgingContent.lines(),
85+
generatedBadgingContent.lines(),
86+
).filter { row -> row.tag != DiffRow.Tag.EQUAL }
87+
.joinToString("\n") { row ->
88+
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
89+
when (row.tag) {
90+
DiffRow.Tag.INSERT -> {
91+
"+ ${row.newLine}"
92+
}
93+
94+
DiffRow.Tag.DELETE -> {
95+
"- ${row.oldLine}"
96+
}
97+
98+
DiffRow.Tag.CHANGE -> {
99+
"+ ${row.newLine}"
100+
"- ${row.oldLine}"
101+
}
102+
103+
DiffRow.Tag.EQUAL -> ""
104+
}
105+
}
106+
}
107+
108+
private fun printDiff(diff: String) {
109+
printlnColor("", null)
110+
printlnColor(ANSI_YELLOW, "Badging diff:")
111+
112+
diff.lines().forEach { line ->
113+
val ansiColor = if (line.startsWith("+")) {
114+
ANSI_GREEN
115+
} else if (line.startsWith("-")) {
116+
ANSI_RED
117+
} else {
118+
null
119+
}
120+
printlnColor(line, ansiColor)
121+
}
122+
}
123+
124+
private fun printlnColor(text: String, ansiColor: String?) {
125+
println(
126+
if (ansiColor != null) {
127+
ansiColor + text + ANSI_RESET
128+
} else {
129+
text
130+
},
131+
)
132+
}
133+
134+
private companion object {
135+
const val ANSI_RESET = "\u001B[0m"
136+
const val ANSI_RED = "\u001B[31m"
137+
const val ANSI_GREEN = "\u001B[32m"
138+
const val ANSI_YELLOW = "\u001B[33m"
139+
}
140+
}

0 commit comments

Comments
 (0)