Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
uses: ./.github/actions/setup-gradle

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

- name: Check K9 Badging
run: |
Expand All @@ -93,7 +93,7 @@ jobs:
uses: ./.github/actions/setup-gradle

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

- name: Check Thunderbird Badging
run: |
Expand Down
9 changes: 6 additions & 3 deletions app-k9mail/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
id(ThunderbirdPlugins.App.androidCompose)
alias(libs.plugins.dependency.guard)
id("thunderbird.app.version.info")
id("thunderbird.quality.badging")
alias(libs.plugins.tb.app.badging)
alias(libs.plugins.tb.app.versioning)
}

val testCoverageEnabled = hasProperty("testCoverageEnabled")
Expand Down Expand Up @@ -85,10 +85,13 @@ android {
}

buildTypes {
val isCI = project.findProperty("ci") == "true"
release {
signingConfig = signingConfigs.getByType(SigningType.K9_RELEASE)

isMinifyEnabled = true
isMinifyEnabled = !isCI
isShrinkResources = !isCI

proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro",
Expand Down
45 changes: 23 additions & 22 deletions app-thunderbird/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
id(ThunderbirdPlugins.App.androidCompose)
alias(libs.plugins.dependency.guard)
id("thunderbird.app.version.info")
id("thunderbird.quality.badging")
alias(libs.plugins.tb.app.badging)
alias(libs.plugins.tb.app.versioning)
}

val testCoverageEnabled = hasProperty("testCoverageEnabled")
Expand Down Expand Up @@ -88,25 +88,12 @@ android {
}

buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-SNAPSHOT"

enableUnitTestCoverage = testCoverageEnabled
enableAndroidTestCoverage = testCoverageEnabled

isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true

buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "null")
}

val isCI = project.findProperty("ci") == "true"
release {
signingConfig = signingConfigs.getByType(SigningType.TB_RELEASE)

isMinifyEnabled = true
isShrinkResources = true
isMinifyEnabled = !isCI
isShrinkResources = !isCI
isDebuggable = false

proguardFiles(
Expand All @@ -123,8 +110,8 @@ android {
applicationIdSuffix = ".beta"
versionNameSuffix = "b0"

isMinifyEnabled = true
isShrinkResources = true
isMinifyEnabled = !isCI
isShrinkResources = !isCI
isDebuggable = false

matchingFallbacks += listOf("release")
Expand All @@ -143,8 +130,8 @@ android {
applicationIdSuffix = ".daily"
versionNameSuffix = "a1"

isMinifyEnabled = true
isShrinkResources = true
isMinifyEnabled = !isCI
isShrinkResources = !isCI
isDebuggable = false

matchingFallbacks += listOf("release")
Expand All @@ -157,6 +144,20 @@ android {
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1918151
buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "\"nightly\"")
}

debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-SNAPSHOT"

enableUnitTestCoverage = testCoverageEnabled
enableAndroidTestCoverage = testCoverageEnabled

isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true

buildConfigField("String", "GLEAN_RELEASE_CHANNEL", "null")
}
}

flavorDimensions += listOf("app")
Expand Down
2 changes: 0 additions & 2 deletions build-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ plugins {
dependencies {
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))

implementation(plugin(libs.plugins.kotlin.android))
implementation(plugin(libs.plugins.kotlin.jvm))
implementation(plugin(libs.plugins.kotlin.multiplatform))
implementation(plugin(libs.plugins.kotlin.parcelize))
Expand All @@ -26,7 +25,6 @@ dependencies {
// Make custom plugins in ":plugin" available to precompiled convention plugins by classpath
implementation(project(":plugin"))

implementation(libs.diff.utils)
compileOnly(libs.android.tools.common)

// This defines the used Kotlin version for all Plugin dependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.thunderbird.gradle.plugin

import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.getByName

val Project.libs
get(): LibrariesForLibs = extensions.getByName<LibrariesForLibs>("libs")
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package net.thunderbird.gradle.plugin.app.badging

import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.Aapt2
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.tasks.Copy
import org.gradle.kotlin.dsl.register

private val variantsToCheck = listOf("release", "beta", "daily")

/**
* 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 modified 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).
*/
class BadgingPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
}

configureBadging()
}
}

private fun Project.configureBadging() {
extensions.configure<ApplicationAndroidComponentsExtension> {
onVariants { variant ->
if (variantsToCheck.any { variant.name.contains(it, ignoreCase = true) }) {
val capitalizedVariantName = variant.name.capitalized()
val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
val generateBadging = tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE)
aapt2Executable = this@configure.sdkComponents.aapt2.flatMap(Aapt2::executable)
badging = 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.map(GenerateBadgingTask::badging))
into(project.layout.projectDirectory.dir("badging"))
}

val checkBadgingTaskName = "check${capitalizedVariantName}Badging"
tasks.register<CheckBadgingTask>(checkBadgingTaskName) {
goldenBadging = project.layout.projectDirectory.file("badging/${variant.name}-badging.txt")

generatedBadging.set(generateBadging.flatMap(GenerateBadgingTask::badging))

this.updateBadgingTaskName = updateBadgingTaskName

output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName")
}

tasks.named("build") {
dependsOn(checkBadgingTaskName)
}
}
}
}
}
}

private fun String.capitalized() = replaceFirstChar {
if (it.isLowerCase()) it.titlecase() else it.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package net.thunderbird.gradle.plugin.app.badging

import com.github.difflib.text.DiffRow
import com.github.difflib.text.DiffRowGenerator
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin

@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.RELATIVE)
@get:Optional
@get:InputFile
abstract val goldenBadging: RegularFileProperty

@get:PathSensitive(PathSensitivity.RELATIVE)
@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()) {
printlnColor(
ANSI_YELLOW,
"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) {
printlnColor(ANSI_YELLOW, "Generated badging is the same as golden badging!")
return
}

val diff = performDiff(goldenBadgingContent, generatedBadgingContent)
printDiff(diff)

throw GradleException(
"""
Generated badging is different from golden badging!

If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}
""".trimIndent(),
)
}

private fun performDiff(goldenBadgingContent: String, generatedBadgingContent: String): String {
val generator: DiffRowGenerator = DiffRowGenerator.create()
.showInlineDiffs(true)
.mergeOriginalRevised(true)
.inlineDiffByWord(true)
.oldTag { _ -> "" }
.newTag { _ -> "" }
.build()

return generator.generateDiffRows(
goldenBadgingContent.lines(),
generatedBadgingContent.lines(),
).filter { row -> row.tag != DiffRow.Tag.EQUAL }
.joinToString("\n") { row ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (row.tag) {
DiffRow.Tag.INSERT -> {
"+ ${row.newLine}"
}

DiffRow.Tag.DELETE -> {
"- ${row.oldLine}"
}

DiffRow.Tag.CHANGE -> {
"+ ${row.newLine}"
"- ${row.oldLine}"
}

DiffRow.Tag.EQUAL -> ""
}
}
}

private fun printDiff(diff: String) {
printlnColor("", null)
printlnColor(ANSI_YELLOW, "Badging diff:")

diff.lines().forEach { line ->
val ansiColor = if (line.startsWith("+")) {
ANSI_GREEN
} else if (line.startsWith("-")) {
ANSI_RED
} else {
null
}
printlnColor(line, ansiColor)
}
}

private fun printlnColor(text: String, ansiColor: String?) {
println(
if (ansiColor != null) {
ansiColor + text + ANSI_RESET
} else {
text
},
)
}

private companion object {
const val ANSI_RESET = "\u001B[0m"
const val ANSI_RED = "\u001B[31m"
const val ANSI_GREEN = "\u001B[32m"
const val ANSI_YELLOW = "\u001B[33m"
}
}
Loading
Loading