1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.baselineprofile.gradle.utils
18 
19 import com.android.build.api.AndroidPluginVersion
20 import com.android.build.api.dsl.ApplicationExtension
21 import com.android.build.api.dsl.LibraryExtension
22 import com.android.build.api.dsl.TestExtension
23 import com.android.build.api.dsl.TestedExtension
24 import com.android.build.api.variant.AndroidComponentsExtension
25 import com.android.build.api.variant.ApplicationAndroidComponentsExtension
26 import com.android.build.api.variant.ApplicationVariant
27 import com.android.build.api.variant.ApplicationVariantBuilder
28 import com.android.build.api.variant.LibraryAndroidComponentsExtension
29 import com.android.build.api.variant.LibraryVariant
30 import com.android.build.api.variant.LibraryVariantBuilder
31 import com.android.build.api.variant.TestAndroidComponentsExtension
32 import com.android.build.api.variant.TestVariant
33 import com.android.build.api.variant.TestVariantBuilder
34 import com.android.build.api.variant.Variant
35 import com.android.build.api.variant.VariantBuilder
36 import org.gradle.api.GradleException
37 import org.gradle.api.Project
38 import org.gradle.api.Task
39 import org.gradle.api.tasks.TaskProvider
40 
41 /**
42  * Defines callbacks and utility methods to create a plugin that utilizes AGP apis. Callbacks with
43  * the configuration lifecycle of the agp plugins are provided.
44  */
45 internal abstract class AgpPlugin(
46     private val project: Project,
47     private val supportedAgpPlugins: Set<AgpPluginId>,
48     private val minAgpVersionInclusive: AndroidPluginVersion,
49     private val maxAgpVersionExclusive: AndroidPluginVersion,
50 ) {
51 
52     // Properties that can be specified by cmd line using -P<property_name> when invoking gradle.
53     val testMaxAgpVersion by lazy {
54         project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str
55             ->
56             val parts = str.split(".").map { it.toInt() }
57             return@lazy AndroidPluginVersion(parts[0], parts[1], parts[2])
58         } ?: return@lazy null
59     }
60 
61     val suppressWarnings: Boolean by lazy {
62         project.providers.gradleProperty("androidx.baselineprofile.suppresswarnings").isPresent
63     }
64 
65     // Logger
66     protected val logger = BaselineProfilePluginLogger(project.logger)
67 
68     // Defines a list of block to be executed after all the onVariants callback
69     private val afterVariantsBlocks = mutableListOf<() -> (Unit)>()
70 
71     // Callback schedulers for each variant type
72     private val onVariantBlockScheduler = OnVariantBlockScheduler<Variant>("common")
73     private val onAppVariantBlockScheduler =
74         OnVariantBlockScheduler<ApplicationVariant>("application")
75     private val onLibraryVariantBlockScheduler = OnVariantBlockScheduler<LibraryVariant>("library")
76     private val onTestVariantBlockScheduler = OnVariantBlockScheduler<TestVariant>("test")
77 
78     private var checkedAgpVersion = false
79 
80     fun onApply() {
81 
82         val foundPlugins = mutableSetOf<AgpPluginId>()
83 
84         // Try to configure with the supported plugins.
85         for (agpPluginId in supportedAgpPlugins) {
86             project.pluginManager.withPlugin(agpPluginId.value) {
87                 foundPlugins.add(agpPluginId)
88                 configureWithAndroidPlugin()
89             }
90         }
91 
92         // Only used to verify that the android application plugin has been applied.
93         // Note that we don't want to throw any exception if gradle sync is in progress.
94         project.afterEvaluate {
95             if (!isGradleSyncRunning()) {
96                 if (foundPlugins.isEmpty()) {
97                     onAgpPluginNotFound(foundPlugins)
98                 } else {
99                     onAgpPluginFound(foundPlugins)
100                 }
101             }
102         }
103     }
104 
105     private fun configureWithAndroidPlugin() {
106 
107         fun setWarnings() {
108             if (suppressWarnings) {
109                 logger.suppressAllWarnings()
110             } else {
111                 getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
112             }
113         }
114 
115         onBeforeFinalizeDsl()
116 
117         testAndroidComponentExtension()?.let { testComponent ->
118             testComponent.finalizeDsl {
119                 onTestFinalizeDsl(it)
120 
121                 // This can be done only here, since warnings may depend on user configuration
122                 // that is ready only after `finalizeDsl`.
123                 setWarnings()
124                 checkAgpVersion()
125             }
126             testComponent.beforeVariants { onTestBeforeVariants(it) }
127             testComponent.onVariants {
128                 onTestVariantBlockScheduler.onVariant(it)
129                 onTestVariants(it)
130             }
131         }
132 
133         applicationAndroidComponentsExtension()?.let { applicationComponent ->
134             applicationComponent.finalizeDsl {
135                 onApplicationFinalizeDsl(it)
136 
137                 // This can be done only here, since warnings may depend on user configuration
138                 // that is ready only after `finalizeDsl`.
139                 setWarnings()
140                 checkAgpVersion()
141             }
142             applicationComponent.beforeVariants { onApplicationBeforeVariants(it) }
143             applicationComponent.onVariants {
144                 onAppVariantBlockScheduler.onVariant(it)
145                 onApplicationVariants(it)
146             }
147         }
148 
149         libraryAndroidComponentsExtension()?.let { libraryComponent ->
150             libraryComponent.finalizeDsl {
151                 onLibraryFinalizeDsl(it)
152 
153                 // This can be done only here, since warnings may depend on user configuration
154                 // that is ready only after `finalizeDsl`.
155                 setWarnings()
156                 checkAgpVersion()
157             }
158             libraryComponent.beforeVariants { onLibraryBeforeVariants(it) }
159             libraryComponent.onVariants {
160                 onLibraryVariantBlockScheduler.onVariant(it)
161                 onLibraryVariants(it)
162             }
163         }
164 
165         androidComponentsExtension()?.let { commonComponent ->
166             commonComponent.finalizeDsl {
167                 onFinalizeDsl(commonComponent)
168 
169                 // This can be done only here, since warnings may depend on user configuration
170                 // that is ready only after `finalizeDsl`.
171                 getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
172                 checkAgpVersion()
173             }
174             commonComponent.beforeVariants { onBeforeVariants(it) }
175             commonComponent.onVariants {
176                 onVariantBlockScheduler.onVariant(it)
177                 onVariants(it)
178             }
179         }
180 
181         // Runs the after variants callback that is module type dependent
182         val testedExtension = testedExtension()
183         val testExtension = testExtension()
184 
185         val variants =
186             when {
187                 testedExtension != null &&
188                     testedExtension is com.android.build.gradle.AppExtension -> {
189                     testedExtension.applicationVariants
190                 }
191                 testedExtension != null &&
192                     testedExtension is com.android.build.gradle.LibraryExtension -> {
193                     testedExtension.libraryVariants
194                 }
195                 testExtension != null -> {
196                     testExtension.applicationVariants
197                 }
198                 else -> {
199                     if (isGradleSyncRunning()) return
200                     // This cannot happen because of user configuration because the plugin is only
201                     // applied if there is an android gradle plugin.
202                     throw GradleException(
203                         "Module `${project.path}` is not a supported android module."
204                     )
205                 }
206             }
207 
208         var applied = false
209         variants.configureEach {
210             if (applied) return@configureEach
211             applied = true
212 
213             // Execute all the scheduled variant blocks
214             afterVariantsBlocks.forEach { it() }
215             afterVariantsBlocks.clear()
216 
217             // Execute the after variant callback if scheduled.
218             onAfterVariants()
219 
220             // Throw an exception if a scheduled callback was not executed
221             if (afterVariantsBlocks.isNotEmpty()) {
222                 throw IllegalStateException(
223                     "After variants blocks cannot be scheduled in the `onAfterVariants` callback."
224                 )
225             }
226 
227             // Ensure no scheduled callbacks is skipped.
228             onAppVariantBlockScheduler.assertBlockMapEmpty()
229             onTestVariantBlockScheduler.assertBlockMapEmpty()
230             onLibraryVariantBlockScheduler.assertBlockMapEmpty()
231             onVariantBlockScheduler.assertBlockMapEmpty()
232         }
233     }
234 
235     // Utility methods
236 
237     protected fun <T : Task> addArtifactToConfiguration(
238         configurationName: String,
239         taskProvider: TaskProvider<T>,
240         artifactType: String
241     ) {
242         project.artifacts { artifactHandler ->
243             artifactHandler.add(configurationName, taskProvider) { artifact ->
244                 artifact.type = artifactType
245                 artifact.builtBy(taskProvider)
246             }
247         }
248     }
249 
250     protected fun isGradleSyncRunning() = project.isGradleSyncRunning()
251 
252     protected open fun getWarnings(): Warnings? = null
253 
254     protected fun afterVariants(block: () -> (Unit)) = afterVariantsBlocks.add(block)
255 
256     @JvmName("onVariant")
257     protected fun onVariant(variantName: String, block: (Variant) -> (Unit)) =
258         onVariantBlockScheduler.executeOrScheduleOnVariantBlock(variantName, block)
259 
260     @JvmName("onApplicationVariant")
261     protected fun onVariant(variantName: String, block: (ApplicationVariant) -> (Unit)) =
262         onAppVariantBlockScheduler.executeOrScheduleOnVariantBlock(variantName, block)
263 
264     @JvmName("onLibraryVariant")
265     protected fun onVariant(variantName: String, block: (LibraryVariant) -> (Unit)) =
266         onLibraryVariantBlockScheduler.executeOrScheduleOnVariantBlock(variantName, block)
267 
268     @JvmName("onTestVariant")
269     protected fun onVariant(variantName: String, block: (TestVariant) -> (Unit)) =
270         onTestVariantBlockScheduler.executeOrScheduleOnVariantBlock(variantName, block)
271 
272     protected fun removeOnVariantCallback(variantName: String) {
273         onVariantBlockScheduler.removeOnVariantCallback(variantName)
274         onAppVariantBlockScheduler.removeOnVariantCallback(variantName)
275         onLibraryVariantBlockScheduler.removeOnVariantCallback(variantName)
276         onTestVariantBlockScheduler.removeOnVariantCallback(variantName)
277     }
278 
279     protected fun agpVersion() = project.agpVersion()
280 
281     private fun checkAgpVersion() {
282 
283         // According to which callbacks are implemented by the user, this function may be called
284         // more than once but we want to check only once.
285         if (checkedAgpVersion) return
286         checkedAgpVersion = true
287 
288         val agpVersion = project.agpVersion()
289         if (agpVersion.previewType == "dev") {
290             return // Skip version check for androidx-studio-integration branch
291         }
292         if (agpVersion < minAgpVersionInclusive) {
293             throw GradleException(
294                 """
295         This version of the Baseline Profile Gradle Plugin requires the Android Gradle Plugin to be
296         at least version $minAgpVersionInclusive. The current version is $agpVersion.
297         Please update your project.
298             """
299                     .trimIndent()
300             )
301         }
302         if (agpVersion >= (testMaxAgpVersion ?: maxAgpVersionExclusive)) {
303             logger.warn(
304                 property = { maxAgpVersion },
305                 propertyName = "maxAgpVersion",
306                 message =
307                     """
308         This version of the Baseline Profile Gradle Plugin was tested with versions below Android
309         Gradle Plugin version $maxAgpVersionExclusive and it may not work as intended.
310         Current version is $agpVersion.
311                 """
312                         .trimIndent()
313             )
314         }
315     }
316 
317     protected fun supportsFeature(feature: AgpFeature) = agpVersion() >= feature.version
318 
319     protected fun isTestModule() = testAndroidComponentExtension() != null
320 
321     protected fun isLibraryModule() = libraryAndroidComponentsExtension() != null
322 
323     protected fun isApplicationModule() = applicationAndroidComponentsExtension() != null
324 
325     // Plugin application callbacks
326 
327     protected open fun onAgpPluginNotFound(pluginIds: Set<AgpPluginId>) {}
328 
329     protected open fun onAgpPluginFound(pluginIds: Set<AgpPluginId>) {}
330 
331     // Test callbacks
332 
333     protected open fun onTestFinalizeDsl(extension: TestExtension) {}
334 
335     protected open fun onTestBeforeVariants(variantBuilder: TestVariantBuilder) {}
336 
337     protected open fun onTestVariants(variant: TestVariant) {}
338 
339     // Application callbacks
340 
341     protected open fun onApplicationFinalizeDsl(extension: ApplicationExtension) {}
342 
343     protected open fun onApplicationBeforeVariants(variantBuilder: ApplicationVariantBuilder) {}
344 
345     protected open fun onApplicationVariants(variant: ApplicationVariant) {}
346 
347     // Library callbacks
348 
349     protected open fun onLibraryFinalizeDsl(extension: LibraryExtension) {}
350 
351     protected open fun onLibraryBeforeVariants(variantBuilder: LibraryVariantBuilder) {}
352 
353     protected open fun onLibraryVariants(variant: LibraryVariant) {}
354 
355     // Shared callbacks
356 
357     protected open fun onBeforeFinalizeDsl() {}
358 
359     protected open fun onFinalizeDsl(extension: AndroidComponentsExtension<*, *, *>) {}
360 
361     protected open fun onBeforeVariants(variantBuilder: VariantBuilder) {}
362 
363     protected open fun onVariants(variant: Variant) {}
364 
365     protected open fun onAfterVariants() {}
366 
367     // Quick access to extension methods
368 
369     private fun testAndroidComponentExtension(): TestAndroidComponentsExtension? =
370         project.extensions.findByType(TestAndroidComponentsExtension::class.java)
371 
372     private fun applicationAndroidComponentsExtension(): ApplicationAndroidComponentsExtension? =
373         project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)
374 
375     private fun libraryAndroidComponentsExtension(): LibraryAndroidComponentsExtension? =
376         project.extensions.findByType(LibraryAndroidComponentsExtension::class.java)
377 
378     private fun androidComponentsExtension(): AndroidComponentsExtension<*, *, *>? =
379         project.extensions.findByType(AndroidComponentsExtension::class.java)
380 
381     private fun testedExtension(): TestedExtension? =
382         project.extensions.findByType(TestedExtension::class.java)
383 
384     private fun testExtension(): com.android.build.gradle.TestExtension? =
385         project.extensions.findByType(com.android.build.gradle.TestExtension::class.java)
386 }
387 
<lambda>null388 private val gradleSyncProps by lazy {
389     listOf(
390         "android.injected.build.model.v2",
391         "android.injected.build.model.only",
392         "android.injected.build.model.only.advanced",
393     )
394 }
395 
isGradleSyncRunningnull396 internal fun Project.isGradleSyncRunning() =
397     gradleSyncProps.any { property ->
398         providers.gradleProperty(property).map { it.toBoolean() }.orElse(false).get()
399     }
400 
401 /** Enumerates the supported android plugins. */
402 internal enum class AgpPluginId(val value: String) {
403     ID_ANDROID_APPLICATION_PLUGIN("com.android.application"),
404     ID_ANDROID_LIBRARY_PLUGIN("com.android.library"),
405     ID_ANDROID_TEST_PLUGIN("com.android.test")
406 }
407 
408 /**
409  * This class is basically an help to manage executing callbacks on a variant. Because of how agp
410  * variants are published, there is no way to directly access it. This class stores a callback and
411  * executes it when the variant is published in the agp onVariants callback.
412  */
413 private class OnVariantBlockScheduler<T : Variant>(private val variantTypeName: String) {
414 
415     // Stores the current published variants
416     val publishedVariants = mutableMapOf<String, T>()
417 
418     // Defines a list of block to be executed for a certain variant when it gets published
419     val onVariantBlocks = mutableMapOf<String, MutableList<(T) -> (Unit)>>()
420 
executeOrScheduleOnVariantBlocknull421     fun executeOrScheduleOnVariantBlock(variantName: String, block: (T) -> (Unit)) {
422         if (variantName in publishedVariants) {
423             publishedVariants[variantName]?.let { block(it) }
424         } else {
425             onVariantBlocks.computeIfAbsent(variantName) { mutableListOf() } += block
426         }
427     }
428 
removeOnVariantCallbacknull429     fun removeOnVariantCallback(variantName: String) {
430         onVariantBlocks.remove(variantName)
431     }
432 
onVariantnull433     fun onVariant(variant: T) {
434 
435         // This error cannot be thrown because of a user configuration but only an error when
436         // extending AgpPlugin.
437         if (variant.name in publishedVariants)
438             throw IllegalStateException(
439                 """
440             A variant was published more than once. This can only happen if the AgpPlugin base
441             class is used and an additional onVariants callback is directly registered with the
442             base components.
443         """
444                     .trimIndent()
445             )
446 
447         // Stores the published variant
448         publishedVariants[variant.name] = variant
449 
450         // Executes all the callbacks previously scheduled for this variant.
451         onVariantBlocks.remove(variant.name)?.apply {
452             forEach { b -> b(variant) }
453             clear()
454         }
455     }
456 
assertBlockMapEmptynull457     fun assertBlockMapEmpty() {
458         if (onVariantBlocks.isEmpty()) return
459         val variantNames = "[`${onVariantBlocks.toList().joinToString("`, `") { it.first }}`]"
460         throw IllegalStateException(
461             "Callbacks for $variantTypeName variants $variantNames were not executed."
462         )
463     }
464 }
465