Skip to content

Commit a88b257

Browse files
authored
Introduce separate task variants for Android projects (#101)
* Add support for Android variants * Fix Gradle Build Cache compatibility * Simplify task list providers * Reformat * Document variant-specific tasks * Add missing filter * Fix optional parameters * Always use projectName property * Fix crash with the missing stability folder * only add runtime dependency once * update api * reformat StabilityCheckTask
1 parent 315609c commit a88b257

7 files changed

Lines changed: 227 additions & 66 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,11 @@ Think of it like this:
541541

542542
> **Note**: Keep in mind that, all these Gradle tasks should be done **after compile your project**.
543543
544+
### Android
545+
546+
For Android projects, variant-specific tasks will be created, such as `debugStabilityDump`.
547+
You can use those to only compile one variant of your module.
548+
544549
### Step 1: Create a Stability Baseline
545550

546551
First, you need to generate a baseline—a snapshot of your current composables' stability.

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ androidx-compose-runtime-annotation = { module = "androidx.compose.runtime:runti
2828
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
2929
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
3030
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
31+
android-gradleApi = { module = "com.android.tools.build:gradle-api", version.ref = "androidGradlePlugin" }
3132
junit = { module = "junit:junit", version.ref = "junit" }
3233
kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
3334
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }

stability-gradle/api/stability-gradle.api

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ public abstract class com/skydoves/compose/stability/gradle/StabilityCheckTask :
2828
public abstract fun getIgnoredPackages ()Lorg/gradle/api/provider/ListProperty;
2929
public abstract fun getProjectName ()Lorg/gradle/api/provider/Property;
3030
public abstract fun getQuietCheck ()Lorg/gradle/api/provider/Property;
31-
public abstract fun getStabilityDir ()Lorg/gradle/api/file/DirectoryProperty;
32-
public abstract fun getStabilityInputFile ()Lorg/gradle/api/file/RegularFileProperty;
31+
public abstract fun getStabilityFileSuffix ()Lorg/gradle/api/provider/Property;
32+
public abstract fun getStabilityInputFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
33+
public abstract fun getStabilityReferenceFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
3334
}
3435

3536
public abstract class com/skydoves/compose/stability/gradle/StabilityDumpTask : org/gradle/api/DefaultTask {
@@ -39,6 +40,7 @@ public abstract class com/skydoves/compose/stability/gradle/StabilityDumpTask :
3940
public abstract fun getIgnoredPackages ()Lorg/gradle/api/provider/ListProperty;
4041
public abstract fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
4142
public abstract fun getProjectName ()Lorg/gradle/api/provider/Property;
43+
public abstract fun getStabilityFileSuffix ()Lorg/gradle/api/provider/Property;
4244
public abstract fun getStabilityInputFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
4345
}
4446

stability-gradle/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ kotlin {
3030

3131
dependencies {
3232
compileOnly(kotlin("gradle-plugin", version = libs.versions.kotlin.get()))
33+
implementation(libs.android.gradleApi)
3334

3435
testImplementation(kotlin("test"))
3536
testImplementation(kotlin("test-junit"))
@@ -57,4 +58,4 @@ gradlePlugin {
5758
java {
5859
sourceCompatibility = JavaVersion.VERSION_11
5960
targetCompatibility = JavaVersion.VERSION_11
60-
}
61+
}

stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt

Lines changed: 172 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.skydoves.compose.stability.gradle
1717

18+
import com.android.build.api.variant.AndroidComponentsExtension
1819
import org.gradle.api.Project
1920
import org.gradle.api.provider.Provider
2021
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
@@ -23,6 +24,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_MAIN_
2324
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer
2425
import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
2526
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
27+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2628

2729
/**
2830
* Gradle plugin for Compose Stability Analyzer.
@@ -64,6 +66,24 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
6466
// Add runtime to compiler plugin classpath for all compilations
6567
addRuntimeToCompilerClasspath(target)
6668

69+
val androidComponents = target.extensions.findByType(AndroidComponentsExtension::class.java)
70+
if (androidComponents == null) {
71+
registerTasksNonAndroid(target, extension)
72+
} else {
73+
registerTasksAndroid(target, extension, androidComponents)
74+
}
75+
76+
// Add output parameter to the Kotlin tasks to ensure it is compatible with the Build Cache
77+
target.tasks.withType(KotlinCompile::class.java).configureEach {
78+
val stabilityDir = target.layout.buildDirectory.dir("stability").get()
79+
outputs.dir(stabilityDir).optional(true)
80+
}
81+
}
82+
83+
private fun registerTasksNonAndroid(
84+
target: Project,
85+
extension: StabilityAnalyzerExtension,
86+
) {
6787
// Register stability dump task
6888
val stabilityDumpTask = target.tasks.register(
6989
"stabilityDump",
@@ -84,10 +104,10 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
84104
StabilityCheckTask::class.java,
85105
) {
86106
projectName.set(target.name)
87-
stabilityInputFile.set(
107+
stabilityInputFiles.from(
88108
target.layout.buildDirectory.file("stability/stability-info.json"),
89109
)
90-
stabilityDir.set(extension.stabilityValidation.outputDir)
110+
stabilityReferenceFiles.from(extension.stabilityValidation.outputDir)
91111
ignoredPackages.set(extension.stabilityValidation.ignoredPackages)
92112
ignoredClasses.set(extension.stabilityValidation.ignoredClasses)
93113
failOnStabilityChange.set(extension.stabilityValidation.failOnStabilityChange)
@@ -103,7 +123,88 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
103123

104124
// Configure after project evaluation
105125
target.afterEvaluate {
106-
configureTaskDependencies(target, extension, stabilityDumpTask, stabilityCheckTask)
126+
configureTaskDependencies(target, extension, null, stabilityDumpTask, stabilityCheckTask)
127+
addRuntimeDependency(target)
128+
}
129+
}
130+
131+
private fun registerTasksAndroid(
132+
target: Project,
133+
extension: StabilityAnalyzerExtension,
134+
androidComponents: AndroidComponentsExtension<*, *, *>,
135+
) {
136+
val aggregateDumpTask = target.tasks.register("stabilityDump") {
137+
group = "verification"
138+
description = "Dump composable stability information to stability file"
139+
}
140+
val aggregateCheckTask = target.tasks.register("stabilityCheck") {
141+
group = "verification"
142+
description = "Check composable stability against reference file"
143+
}
144+
145+
androidComponents.onVariants { variant ->
146+
val variantNameLowerCase = variant.name.replaceFirstChar { it.lowercaseChar() }
147+
val variantNameUpperCase = variant.name.replaceFirstChar { it.uppercaseChar() }
148+
149+
// Register stability dump task
150+
val stabilityDumpTask = target.tasks.register(
151+
"${variantNameLowerCase}StabilityDump",
152+
StabilityDumpTask::class.java,
153+
) {
154+
projectName.set(target.name)
155+
stabilityInputFiles.setFrom(
156+
target.layout.buildDirectory.file("stability/stability-info.json"),
157+
)
158+
outputDir.set(extension.stabilityValidation.outputDir)
159+
ignoredPackages.set(extension.stabilityValidation.ignoredPackages)
160+
ignoredClasses.set(extension.stabilityValidation.ignoredClasses)
161+
stabilityFileSuffix.set(variant.name)
162+
}
163+
164+
// Register stability check task
165+
val stabilityCheckTask = target.tasks.register(
166+
"${variantNameLowerCase}StabilityCheck",
167+
StabilityCheckTask::class.java,
168+
) {
169+
projectName.set(target.name)
170+
stabilityInputFiles.from(
171+
target.layout.buildDirectory.file("stability/stability-info.json"),
172+
)
173+
stabilityReferenceFiles.from(extension.stabilityValidation.outputDir)
174+
ignoredPackages.set(extension.stabilityValidation.ignoredPackages)
175+
ignoredClasses.set(extension.stabilityValidation.ignoredClasses)
176+
failOnStabilityChange.set(extension.stabilityValidation.failOnStabilityChange)
177+
quietCheck.set(extension.stabilityValidation.quietCheck)
178+
stabilityFileSuffix.set(variant.name)
179+
}
180+
181+
aggregateDumpTask.configure {
182+
dependsOn(stabilityDumpTask)
183+
}
184+
aggregateCheckTask.configure {
185+
dependsOn(stabilityCheckTask)
186+
}
187+
188+
// Make check task depend on stabilityCheck if enabled (only if check task exists)
189+
target.plugins.withId("base") {
190+
target.tasks.named("check") {
191+
dependsOn(stabilityCheckTask)
192+
}
193+
}
194+
195+
// Configure after project evaluation
196+
target.afterEvaluate {
197+
configureTaskDependencies(
198+
target,
199+
extension,
200+
variantNameUpperCase,
201+
stabilityDumpTask,
202+
stabilityCheckTask,
203+
)
204+
}
205+
}
206+
207+
target.afterEvaluate {
107208
addRuntimeDependency(target)
108209
}
109210
}
@@ -235,8 +336,10 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
235336
private fun configureTaskDependencies(
236337
project: Project,
237338
extension: StabilityAnalyzerExtension,
339+
filter: String? = null,
238340
stabilityDumpTask: org.gradle.api.tasks.TaskProvider<StabilityDumpTask>,
239341
stabilityCheckTask: org.gradle.api.tasks.TaskProvider<StabilityCheckTask>,
342+
240343
) {
241344
// Get the includeTests provider for lazy evaluation
242345
val includeTestsProvider = extension.stabilityValidation.includeTests
@@ -245,68 +348,88 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
245348
stabilityDumpTask.configure {
246349
// Use provider to lazily collect Kotlin compile task names
247350
dependsOn(
248-
project.provider {
249-
val includeTests = includeTestsProvider.get()
351+
includeTestsProvider.map { includeTests ->
250352
project.tasks.matching { task ->
251-
val taskName = task.name
252-
val taskNameLower = taskName.lowercase()
253-
254-
// Match only actual Kotlin compilation tasks, excluding infrastructure tasks
255-
val isKotlinCompile = taskName.startsWith("compile") &&
256-
taskName.contains("Kotlin") &&
257-
// Exclude wasm-specific sync/webpack/executable tasks
258-
!taskNameLower.contains("sync") &&
259-
!taskNameLower.contains("webpack") &&
260-
!taskNameLower.contains("executable") &&
261-
!taskNameLower.contains("link") &&
262-
!taskNameLower.contains("assemble")
263-
264-
val isTestTask = taskNameLower.let {
265-
it.contains("test") || it.contains("androidtest") || it.contains("unittest")
266-
}
267-
268-
// Include task if it's a Kotlin compile task and either:
269-
// 1. includeTests is true, OR
270-
// 2. it's not a test task
271-
isKotlinCompile && (includeTests || !isTestTask)
353+
isKotlinTaskApplicable(task.name, includeTests) &&
354+
(filter == null || task.name.contains(filter))
272355
}
273356
},
274357
)
358+
359+
if (filter != null) {
360+
// For now, stability check compiler plugin still creates the same files
361+
// for different variant tasks. That means that even though our variant task
362+
// is not dependent on the other-variant kotlin tasks, it is still implicitly coupled
363+
// with them as it reads the same files. To mitigate for this,
364+
// we tell Gradle that this task must run after any kotlin tasks, even
365+
// if their variant does not match ours
366+
mustRunAfter(
367+
includeTestsProvider.map { includeTests ->
368+
project.tasks.matching { task ->
369+
isKotlinTaskApplicable(task.name, includeTests) &&
370+
!task.name.contains(filter)
371+
}
372+
},
373+
)
374+
}
275375
}
276376

277377
stabilityCheckTask.configure {
278378
// Use provider to lazily collect Kotlin compile task names
279379
dependsOn(
280-
project.provider {
281-
val includeTests = includeTestsProvider.get()
380+
includeTestsProvider.map { includeTests ->
282381
project.tasks.matching { task ->
283-
val taskName = task.name
284-
val taskNameLower = taskName.lowercase()
285-
286-
// Match only actual Kotlin compilation tasks, excluding infrastructure tasks
287-
val isKotlinCompile = taskName.startsWith("compile") &&
288-
taskName.contains("Kotlin") &&
289-
// Exclude wasm-specific sync/webpack/executable tasks
290-
!taskNameLower.contains("sync") &&
291-
!taskNameLower.contains("webpack") &&
292-
!taskNameLower.contains("executable") &&
293-
!taskNameLower.contains("link") &&
294-
!taskNameLower.contains("assemble")
295-
296-
val isTestTask = taskNameLower.let {
297-
it.contains("test") || it.contains("androidtest") || it.contains("unittest")
298-
}
299-
300-
// Include task if it's a Kotlin compile task and either:
301-
// 1. includeTests is true, OR
302-
// 2. it's not a test task
303-
isKotlinCompile && (includeTests || !isTestTask)
382+
isKotlinTaskApplicable(task.name, includeTests) &&
383+
(filter == null || task.name.contains(filter))
304384
}
305385
},
306386
)
387+
388+
if (filter != null) {
389+
// For now, stability check compiler plugin still creates the same files
390+
// for different variant tasks. That means that even though our variant task
391+
// is not dependent on the other-variant kotlin tasks, it is still implicitly coupled
392+
// with them as it reads the same files. To mitigate for this,
393+
// we tell Gradle that this task must run after any kotlin tasks, even
394+
// if their variant does not match ours
395+
mustRunAfter(
396+
includeTestsProvider.map { includeTests ->
397+
project.tasks.matching { task ->
398+
isKotlinTaskApplicable(
399+
task.name,
400+
includeTests,
401+
) &&
402+
!task.name.contains(filter)
403+
}
404+
},
405+
)
406+
}
307407
}
308408
}
309409

410+
private fun isKotlinTaskApplicable(taskName: String, includeTests: Boolean): Boolean {
411+
val taskNameLower = taskName.lowercase()
412+
413+
// Match only actual Kotlin compilation tasks, excluding infrastructure tasks
414+
val isKotlinCompile = taskName.startsWith("compile") &&
415+
taskName.contains("Kotlin") &&
416+
// Exclude wasm-specific sync/webpack/executable tasks
417+
!taskNameLower.contains("sync") &&
418+
!taskNameLower.contains("webpack") &&
419+
!taskNameLower.contains("executable") &&
420+
!taskNameLower.contains("link") &&
421+
!taskNameLower.contains("assemble")
422+
423+
val isTestTask = taskNameLower.let {
424+
it.contains("test") || it.contains("androidtest") || it.contains("unittest")
425+
}
426+
427+
// Include task if it's a Kotlin compile task and either:
428+
// 1. includeTests is true, OR
429+
// 2. it's not a test task
430+
return isKotlinCompile && (includeTests || !isTestTask)
431+
}
432+
310433
/**
311434
* Check if a compilation is a test compilation.
312435
*/

0 commit comments

Comments
 (0)