/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.baselineprofile.gradle.consumer
import androidx.baselineprofile.gradle.configuration.ConfigurationManager
import androidx.baselineprofile.gradle.consumer.task.MainGenerateBaselineProfileTask
import androidx.baselineprofile.gradle.consumer.task.MergeBaselineProfileTask
import androidx.baselineprofile.gradle.consumer.task.PrintConfigurationForVariantTask
import androidx.baselineprofile.gradle.consumer.task.maybeCreateGenerateTask
import androidx.baselineprofile.gradle.utils.AgpFeature
import androidx.baselineprofile.gradle.utils.AgpPlugin
import androidx.baselineprofile.gradle.utils.AgpPluginId
import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BENCHMARK_PREFIX
import androidx.baselineprofile.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
import androidx.baselineprofile.gradle.utils.INTERMEDIATES_BASE_FOLDER
import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_REQUIRED
import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED
import androidx.baselineprofile.gradle.utils.R8Utils
import androidx.baselineprofile.gradle.utils.RELEASE
import androidx.baselineprofile.gradle.utils.camelCase
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.Variant
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
/**
* This is the consumer plugin for baseline profile generation. In order to generate baseline
* profiles three plugins are needed: one is applied to the app or the library that should consume
* the baseline profile when building (consumer), one is applied to the module that should supply
* the under test app (app target) and the last one is applied to a test module containing the ui
* test that generate the baseline profile on the device (producer).
*/
class BaselineProfileConsumerPlugin : Plugin<Project> {
override fun apply(project: Project) = BaselineProfileConsumerAgpPlugin(project).onApply()
}
private class BaselineProfileConsumerAgpPlugin(private val project: Project) : AgpPlugin(
project = project,
supportedAgpPlugins = setOf(
AgpPluginId.ID_ANDROID_APPLICATION_PLUGIN,
AgpPluginId.ID_ANDROID_LIBRARY_PLUGIN
),
minAgpVersion = MIN_AGP_VERSION_REQUIRED,
maxAgpVersion = MAX_AGP_VERSION_REQUIRED
) {
// List of the non debuggable build types
private val nonDebuggableBuildTypes = mutableListOf<String>()
// Offers quick access to configuration extension, hiding the property override and merge logic
private val perVariantBaselineProfileExtensionManager =
PerVariantConsumerExtensionManager(BaselineProfileConsumerExtension.register(project))
// Manages creation of configurations
private val configurationManager = ConfigurationManager(project)
// Manages r8 properties
private val r8Utils = R8Utils(project)
// Keeps track of the benchmark variants to add src sets to
private val variantToBlockMap: MutableMap<String, (Variant) -> (Unit)> = mutableMapOf()
// Global baseline profile configuration. Note that created here it can be directly consumed
// in the dependencies block.
private val mainBaselineProfileConfiguration = configurationManager.maybeCreate(
nameParts = listOf(CONFIGURATION_NAME_BASELINE_PROFILES),
canBeConsumed = false,
canBeResolved = true,
buildType = null,
productFlavors = null
)
private val Variant.benchmarkVariantName: String
get() {
val parts = listOfNotNull(flavorName, BUILD_TYPE_BENCHMARK_PREFIX, buildType)
.filter { it.isNotBlank() }
return camelCase(*parts.toTypedArray())
}
override fun onAgpPluginNotFound(pluginIds: Set<AgpPluginId>) {
throw IllegalStateException(
"""
The module ${project.name} does not have the `com.android.application` or
`com.android.library` plugin applied. The `androidx.baselineprofile.consumer`
plugin supports only android application and library modules. Please review
your build.gradle to ensure this plugin is applied to the correct module.
""".trimIndent()
)
}
override fun onAgpPluginFound(pluginIds: Set<AgpPluginId>) {
project.logger.debug(
"""
[BaselineProfileConsumerPlugin] afterEvaluate check: app or library plugin was applied
""".trimIndent()
)
}
override fun onApplicationFinalizeDsl(extension: ApplicationExtension) {
// Here we select the build types we want to process if this is an application,
// i.e. non debuggable build types that have not been created by the app target plugin.
// Also exclude the build types starting with baseline profile prefix, in case the app
// target plugin is also applied.
nonDebuggableBuildTypes.addAll(extension.buildTypes
.filter {
!it.isDebuggable &&
!it.name.startsWith(BUILD_TYPE_BASELINE_PROFILE_PREFIX) &&
!it.name.startsWith(BUILD_TYPE_BENCHMARK_PREFIX)
}
.map { it.name }
)
}
override fun onLibraryFinalizeDsl(extension: LibraryExtension) {
// Here we select the build types we want to process if this is a library.
// Libraries don't have a `debuggable` flag. Also we don't need to exclude build types
// prefixed with the baseline profile prefix. Ideally on the `debug` type should be
// excluded.
nonDebuggableBuildTypes.addAll(extension.buildTypes
.filter { it.name != "debug" }
.map { it.name }
)
}
@Suppress("UnstableApiUsage")
override fun onVariants(variant: Variant) {
// Check if some work was already scheduled for this variant. This is only for benchmark
// variants that needs src sets to be set but this information is known only after the
// baseline profile variant has been processed.
variantToBlockMap[variant.name]?.let { block ->
block(variant)
return
}
// Process only the non debuggable build types we previously selected.
if (variant.buildType !in nonDebuggableBuildTypes) return
// This allows quick access to this variant configuration according to the override
// and merge rules implemented in the PerVariantConsumerExtensionManager.
val variantConfiguration = perVariantBaselineProfileExtensionManager.variant(variant)
// For test only: this registers a print task with the configuration of the variant.
PrintConfigurationForVariantTask.registerForVariant(
project = project,
variant = variant,
variantConfig = variantConfiguration
)
// Sets the r8 rewrite baseline profile for the non debuggable variant.
if (variantConfiguration.enableR8BaselineProfileRewrite) {
r8Utils.enableR8RulesRewriteForVariant(variant)
}
// Check if this variant has any direct dependency
val variantDependencies = variantConfiguration.dependencies
// Creates the configuration to carry the specific variant artifact
val baselineProfileConfiguration = createConfigurationForVariant(
variant = variant,
mainConfiguration = mainBaselineProfileConfiguration,
hasDirectConfiguration = variantDependencies.any { it.second != null }
)
// Adds the custom dependencies for baseline profiles. Note that dependencies
// for global, build type, flavor and variant specific are all merged.
variantDependencies.forEach {
val targetProject = it.first
val variantName = it.second
val targetProjectDependency = if (variantName != null) {
val configurationName = camelCase(
variantName,
CONFIGURATION_NAME_BASELINE_PROFILES
)
project.dependencies.project(
mutableMapOf(
"path" to targetProject.path,
"configuration" to configurationName
)
)
} else {
project.dependencyFactory.create(targetProject)
}
baselineProfileConfiguration.dependencies.add(targetProjectDependency)
}
// There are 2 different ways in which the output task can merge the baseline
// profile rules, according to [BaselineProfileConsumerExtension#mergeIntoMain].
// When mergeIntoMain is `true` the first variant will create a task shared across
// all the variants to merge, while the next variants will simply add the additional
// baseline profile artifacts, modifying the existing task.
// When mergeIntoMain is `false` each variants has its own task with a single
// artifact per task, specific for that variant.
// When mergeIntoMain is not specified, it's by default true for libraries and false
// for apps.
val mergeIntoMain = variantConfiguration.mergeIntoMain ?: isLibraryModule()
// TODO: When `mergeIntoMain` is true it lazily triggers the generation of all
// the variants for all the build types. Due to b/265438201, that fails when
// there are multiple build types. As temporary workaround, when `mergeIntoMain`
// is true, calling a generation task for a specific build type will merge
// profiles for all the variants of that build type and output it in the `main`
// folder.
val (mergeAwareVariantName, mergeAwareVariantOutput) = if (mergeIntoMain) {
listOf(variant.buildType ?: "", "main")
} else {
listOf(variant.name, variant.name)
}
// Creates the task to merge the baseline profile artifacts coming from
// different configurations.
val mergedTaskOutputDir = project
.layout
.buildDirectory
.dir("$INTERMEDIATES_BASE_FOLDER/$mergeAwareVariantOutput/merged")
val mergeTaskProvider = MergeBaselineProfileTask.maybeRegisterForMerge(
project = project,
variantName = mergeAwareVariantName,
hasDependencies = baselineProfileConfiguration.allDependencies.isNotEmpty(),
sourceProfilesFileCollection = baselineProfileConfiguration,
outputDir = mergedTaskOutputDir,
filterRules = variantConfiguration.filterRules,
library = isLibraryModule()
)
// If `saveInSrc` is true, we create an additional task to copy the output
// of the merge task in the src folder.
val lastTaskProvider = if (variantConfiguration.saveInSrc) {
val baselineProfileOutputDir = perVariantBaselineProfileExtensionManager
.variant(variant)
.baselineProfileOutputDir
val srcOutputDir = project
.layout
.projectDirectory
.dir("src/$mergeAwareVariantOutput/$baselineProfileOutputDir/")
// This task copies the baseline profile generated from the merge task.
// Note that we're reutilizing the [MergeBaselineProfileTask] because
// if the flag `mergeIntoMain` is true tasks will have the same name
// and we just want to add more file to copy to the same output. This is
// already handled in the MergeBaselineProfileTask.
val copyTaskProvider = MergeBaselineProfileTask.maybeRegisterForCopy(
project = project,
variantName = mergeAwareVariantName,
library = isLibraryModule(),
sourceDir = mergeTaskProvider.flatMap { it.baselineProfileDir },
outputDir = project.provider { srcOutputDir },
)
// Applies the source path for this variant
srcOutputDir.asFile.apply {
mkdirs()
variant
.sources
.baselineProfiles?.addStaticSourceDirectory(absolutePath)
}
// If this is an application, we need to ensure that:
// If `automaticGenerationDuringBuild` is true, building a release build
// should trigger the generation of the profile. This is done through a
// dependsOn rule.
// If `automaticGenerationDuringBuild` is false and the user calls both
// tasks to generate and assemble, assembling the release should wait of the
// generation to be completed. This is done through a `mustRunAfter` rule.
// Depending on whether the flag `automaticGenerationDuringBuild` is enabled
// Note that we cannot use the variant src set api
// `addGeneratedSourceDirectory` since that overwrites the outputDir,
// that would be re-set in the build dir.
// Also this is specific for applications: doing this for a library would
// trigger a circular task dependency since the library would require
// the profile in order to build the aar for the sample app and generate
// the profile.
if (isApplicationModule()) {
// Sets the task dependency according to the configuration flag.
val automaticGeneration = perVariantBaselineProfileExtensionManager
.variant(variant)
.automaticGenerationDuringBuild
// Defines a function to apply the baseline profile source sets to a variant.
val applySourceSetsFunc: (String) -> (Unit) = { variantName ->
project
.tasks
.named(camelCase("merge", variantName, "artProfile"))
.configure { t ->
// TODO: this causes a circular task dependency when the producer points
// to a consumer that does not have the appTarget plugin. (b/272851616)
if (automaticGeneration) {
t.dependsOn(copyTaskProvider)
} else {
t.mustRunAfter(copyTaskProvider)
}
}
}
afterVariants {
// Apply the source sets to the variant.
applySourceSetsFunc(variant.name)
// Apply the source sets to the benchmark variant if supported.
if (supportsFeature(AgpFeature.TEST_MODULE_SUPPORTS_MULTIPLE_BUILD_TYPES)) {
applySourceSetsFunc(variant.benchmarkVariantName)
}
}
}
// In this case the last task is the copy task.
copyTaskProvider
} else {
if (variantConfiguration.automaticGenerationDuringBuild) {
// If the flag `automaticGenerationDuringBuild` is true, we can set the
// merge task to provide generated sources for the variant, using the
// src set variant api. This means that we don't need to manually depend
// on the merge or prepare art profile task.
// Defines a function to apply the baseline profile source sets to a variant.
val applySourceSetsFunc: (Variant) -> (Unit) = { v ->
v.sources.baselineProfiles?.addGeneratedSourceDirectory(
taskProvider = mergeTaskProvider,
wiredWith = MergeBaselineProfileTask::baselineProfileDir
)
}
// Apply the source sets to the variant.
applySourceSetsFunc(variant)
// Apply the source sets to the benchmark variant if supported.
if (supportsFeature(AgpFeature.TEST_MODULE_SUPPORTS_MULTIPLE_BUILD_TYPES)) {
// Note that there is no way to access directly a specific variant and, at this
// point, we're too late in the configuration flow schedule another onVariants
// callback. So we store the variants name to modify and we expect to receive
// it later in this method.
if (variant.benchmarkVariantName in variantToBlockMap) {
// Note that this cannot happen but checking anyway to avoid possible weird
// bugs in future.
throw IllegalStateException(
"""
Another block was already scheduled for `${variant.benchmarkVariantName}`.
""".trimIndent()
)
}
variantToBlockMap[variant.benchmarkVariantName] = { v ->
applySourceSetsFunc(v)
}
}
} else {
// This is the case of `saveInSrc` and `automaticGenerationDuringBuild`
// both false, that is unsupported. In this case we simply throw an
// error.
if (!isGradleSyncRunning()) {
throw GradleException(
"""
The current configuration of flags `saveInSrc` and `automaticGenerationDuringBuild`
is not supported. At least one of these should be set to `true`. Please review your
baseline profile plugin configuration in your build.gradle.
""".trimIndent()
)
}
}
// In this case the last task is the merge task.
mergeTaskProvider
}
// Here we create the final generate task that triggers the whole generation for this
// variant and all the parent tasks. For this one the child task is either copy or merge,
// depending on the configuration.
maybeCreateGenerateTask<Task>(
project = project,
variantName = mergeAwareVariantName,
lastTaskProvider = lastTaskProvider
)
// Create the build type task. For example `generateReleaseBaselineProfile`
// The variant name is equal to the build type name if there are no flavors.
// Note that if `mergeIntoMain` is `true` the build type task already exists.
if (!mergeIntoMain &&
!variant.buildType.isNullOrBlank() &&
variant.buildType != variant.name
) {
maybeCreateGenerateTask<Task>(
project = project,
variantName = variant.buildType!!,
lastTaskProvider = lastTaskProvider
)
}
if (supportsFeature(AgpFeature.TEST_MODULE_SUPPORTS_MULTIPLE_BUILD_TYPES)) {
// Generate a flavor task, such as `generateFreeBaselineProfile`
if (!mergeIntoMain &&
!variant.flavorName.isNullOrBlank() &&
variant.flavorName != variant.name
) {
maybeCreateGenerateTask<Task>(
project = project,
variantName = variant.flavorName!!,
lastTaskProvider = lastTaskProvider
)
}
// Generate the main global tasks `generateBaselineProfile
maybeCreateGenerateTask<Task>(
project = project,
variantName = "",
lastTaskProvider = lastTaskProvider
)
} else {
// Due to b/265438201 we cannot have a global task `generateBaselineProfile` that
// triggers generation for all the variants when there are multiple build types.
// So for version of AGP that don't support that, invoking `generateBaselineProfile`
// will run generation for `release` build type only, that is the same behavior of
// `generateReleaseBaselineProfile`. For this same reason we cannot have a flavor
// task, such as `generateFreeBaselineProfile` because that would run generation for
// all the build types with flavor free, that is not as well supported.
if (variant.buildType == RELEASE) {
maybeCreateGenerateTask<MainGenerateBaselineProfileTask>(
project = project,
variantName = "",
lastTaskProvider = lastTaskProvider
)
}
}
}
private fun createConfigurationForVariant(
variant: Variant,
mainConfiguration: Configuration?,
hasDirectConfiguration: Boolean
): Configuration {
val variantName = variant.name
val productFlavors = variant.productFlavors
val flavorName = variant.flavorName ?: ""
val buildTypeName = variant.buildType ?: ""
val buildTypeConfiguration =
if (buildTypeName.isNotBlank() && buildTypeName != variantName) {
configurationManager.maybeCreate(
nameParts = listOf(buildTypeName, CONFIGURATION_NAME_BASELINE_PROFILES),
canBeResolved = true,
canBeConsumed = false,
buildType = null,
productFlavors = null,
extendFromConfigurations = listOfNotNull(mainConfiguration)
)
} else null
val flavorConfiguration =
if (flavorName.isNotBlank() && flavorName != variantName) {
configurationManager.maybeCreate(
nameParts = listOf(flavorName, CONFIGURATION_NAME_BASELINE_PROFILES),
canBeResolved = true,
canBeConsumed = false,
buildType = null,
productFlavors = null,
extendFromConfigurations = listOfNotNull(mainConfiguration)
)
} else null
// When there is direct configuration for the dependency the matching through attributes
// is bypassed, because most likely the user meant to match a configuration that does not
// have the same tags (for example to a different flavor or build type).
return if (hasDirectConfiguration) {
configurationManager.maybeCreate(
nameParts = listOf(variantName, CONFIGURATION_NAME_BASELINE_PROFILES),
canBeResolved = true,
canBeConsumed = false,
extendFromConfigurations = listOfNotNull(
mainConfiguration,
flavorConfiguration,
buildTypeConfiguration
),
buildType = null,
productFlavors = null
)
} else {
configurationManager.maybeCreate(
nameParts = listOf(variantName, CONFIGURATION_NAME_BASELINE_PROFILES),
canBeResolved = true,
canBeConsumed = false,
extendFromConfigurations = listOfNotNull(
mainConfiguration,
flavorConfiguration,
buildTypeConfiguration
),
buildType = buildTypeName,
productFlavors = productFlavors
)
}
}
}