1515 */
1616package com.skydoves.compose.stability.gradle
1717
18+ import com.android.build.api.variant.AndroidComponentsExtension
1819import org.gradle.api.Project
1920import org.gradle.api.provider.Provider
2021import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
@@ -23,6 +24,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_MAIN_
2324import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer
2425import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
2526import 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