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