blob: bebdaa72bdb3e1efbfd7647e1e9783702c68c204 [file] [log] [blame]
/*
* 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.attributes.BaselineProfilePluginVersionAttr
import androidx.baselineprofile.gradle.utils.ATTRIBUTE_BASELINE_PROFILE_PLUGIN_VERSION
import androidx.baselineprofile.gradle.utils.ATTRIBUTE_TARGET_JVM_ENVIRONMENT
import androidx.baselineprofile.gradle.utils.ATTRIBUTE_USAGE_BASELINE_PROFILE
import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
import androidx.baselineprofile.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
import androidx.baselineprofile.gradle.utils.INTERMEDIATES_BASE_FOLDER
import androidx.baselineprofile.gradle.utils.TASK_NAME_SUFFIX
import androidx.baselineprofile.gradle.utils.afterVariants
import androidx.baselineprofile.gradle.utils.agpVersion
import androidx.baselineprofile.gradle.utils.agpVersionString
import androidx.baselineprofile.gradle.utils.camelCase
import androidx.baselineprofile.gradle.utils.checkAgpVersion
import androidx.baselineprofile.gradle.utils.isGradleSyncRunning
import androidx.baselineprofile.gradle.utils.maybeRegister
import com.android.build.api.AndroidPluginVersion
import com.android.build.api.attributes.AgpVersionAttr
import com.android.build.api.attributes.BuildTypeAttr
import com.android.build.api.attributes.ProductFlavorAttr
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.Variant
import org.gradle.api.DefaultTask
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
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.java.TargetJvmEnvironment
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.work.DisableCachingByDefault
private const val GENERATE_TASK_NAME = "generate"
private const val MERGE_TASK_NAME = "merge"
private const val COPY_TASK_NAME = "copy"
/**
* 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> {
companion object {
private const val RELEASE = "release"
private const val PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES =
"android.experimental.art-profile-r8-rewriting"
}
override fun apply(project: Project) {
var foundAppOrLibraryPlugin = false
project.pluginManager.withPlugin("com.android.application") {
foundAppOrLibraryPlugin = true
configureWithAndroidPlugin(project = project, isApplication = true)
}
project.pluginManager.withPlugin("com.android.library") {
foundAppOrLibraryPlugin = true
configureWithAndroidPlugin(project = project, isApplication = false)
}
// Only used to verify that the android application plugin has been applied.
// Note that we don't want to throw any exception if gradle sync is in progress.
project.afterEvaluate {
if (!project.isGradleSyncRunning()) {
if (!foundAppOrLibraryPlugin) {
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()
)
}
project.logger.debug(
"""
[BaselineProfileConsumerPlugin] afterEvaluate check: app or library plugin
was applied""".trimIndent()
)
}
}
}
@Suppress("UnstableApiUsage")
private fun configureWithAndroidPlugin(project: Project, isApplication: Boolean) {
// Checks that the required AGP version is applied to this project.
project.checkAgpVersion()
val baselineProfileExtension =
BaselineProfileConsumerExtension.registerExtension(project)
// Creates the main baseline profile configuration
val mainBaselineProfileConfiguration = createBaselineProfileConfigurationForVariant(
project,
productFlavors = listOf(),
variantName = "",
flavorName = "",
buildTypeName = "",
mainConfiguration = null
)
// Here we select the build types we want to process, i.e. non debuggable build types that
// have not been created by the app target plugin. Variants are used to create
// per-variant configurations, tasks and configured for baseline profiles src sets.
val nonDebuggableBuildTypes = mutableListOf<String>()
// This extension exists only if the module is an application.
project
.extensions
.findByType(ApplicationAndroidComponentsExtension::class.java)
?.finalizeDsl { ext ->
nonDebuggableBuildTypes.addAll(ext.buildTypes
.filter {
// We want to enable baseline profile generation only for non-debuggable
// build types. Additionally we exclude the ones we may have created in the
// app target plugin if this is also applied to this module.
!it.isDebuggable && !it.name.startsWith(
BUILD_TYPE_BASELINE_PROFILE_PREFIX
)
}
.map { it.name }
)
}
// This extension exists only if the module is a library.
project
.extensions
.findByType(LibraryAndroidComponentsExtension::class.java)
?.finalizeDsl { ext ->
nonDebuggableBuildTypes.addAll(ext.buildTypes
.filter {
// Note that library build types don't have a `debuggable` flag so we'll
// just exclude the one named `debug`. Note that we don't need to filter
// for baseline profile build type if this is a library, since the apk
// provider cannot be applied.
it.name != "debug"
}
.map { it.name })
}
// A list of blocks to execute after agp tasks have been created
val afterVariantBlocks = mutableListOf<() -> (Unit)>()
// Iterate baseline profile variants to create per-variant tasks and configurations
project
.extensions
.getByType(AndroidComponentsExtension::class.java)
.apply {
onVariants { variant ->
if (variant.buildType !in nonDebuggableBuildTypes) return@onVariants
// Sets the r8 rewrite baseline profile for the non debuggable variant.
if (baselineProfileExtension.enableR8BaselineProfileRewrite &&
project.agpVersion() >= AndroidPluginVersion(8, 0, 0).beta(2)
) {
// TODO: Note that currently there needs to be at least a baseline profile,
// even if empty. For this reason we always add a src set that points to
// an empty file. This can removed after b/271158087 is fixed.
GenerateDummyBaselineProfileTask.setupForVariant(project, variant)
@Suppress("UnstableApiUsage")
variant.experimentalProperties.put(
PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES,
baselineProfileExtension.enableR8BaselineProfileRewrite
)
}
// Creates the configuration to carry the specific variant artifact
val baselineProfileConfiguration =
createBaselineProfileConfigurationForVariant(
project,
variantName = variant.name,
productFlavors = variant.productFlavors,
flavorName = variant.flavorName ?: "",
buildTypeName = variant.buildType ?: "",
mainConfiguration = mainBaselineProfileConfiguration
)
// 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 = baselineProfileExtension.mergeIntoMain ?: !isApplication
// 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 = project
.tasks
.maybeRegister<MergeBaselineProfileTask>(
MERGE_TASK_NAME, mergeAwareVariantName, TASK_NAME_SUFFIX,
) { task ->
// Sets whether or not baseline profile dependencies have been set.
// If they haven't, the task will fail at execution time.
task.hasDependencies.set(
baselineProfileConfiguration.allDependencies.isNotEmpty()
)
// Sets the name of this variant to print it in error messages.
task.variantName.set(mergeAwareVariantName)
// These are all the configurations this task depends on,
// in order to consume their artifacts. Note that if this task already
// exist (for example if `merge` is `all`) the new artifact will be
// added to the existing list.
task.baselineProfileFileCollection
.from
.add(baselineProfileConfiguration)
// This is the task output for the generated baseline profile. Output
// is always stored in the intermediates
task.baselineProfileDir.set(mergedTaskOutputDir)
// Sets the package filter rules. If this is the first task
task.filterRules.addAll(
baselineProfileExtension.filterRules
.filter {
it.key in listOfNotNull(
"main",
variant.flavorName,
variant.buildType,
variant.name
)
}
.flatMap { it.value.rules }
)
}
// If `saveInSrc` is true, we create an additional task to copy the output
// of the merge task in the src folder.
val lastTaskProvider = if (baselineProfileExtension.saveInSrc) {
val baselineProfileOutputDir =
baselineProfileExtension.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 = project
.tasks
.maybeRegister<MergeBaselineProfileTask>(
COPY_TASK_NAME, mergeAwareVariantName, "baselineProfileIntoSrc",
) { task ->
task.baselineProfileFileCollection
.from
.add(mergeTaskProvider.flatMap { it.baselineProfileDir })
task.baselineProfileDir.set(srcOutputDir)
}
// Applies the source path for this variant
srcOutputDir.asFile.apply {
mkdirs()
variant
.sources
.baselineProfiles?.addStaticSourceDirectory(absolutePath)
}
// Depending on whether the flag `automaticGenerationDuringBuild` is enabled
// we can set either a dependsOn or a mustRunAfter dependency between the
// task that packages the profile and the copy. 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.
afterVariantBlocks.add {
// Determines which AGP task to depend on based on whether this is an
// app or a library.
if (isApplication) {
project.tasks.named(
camelCase("merge", variant.name, "artProfile")
)
} else {
project.tasks.named(
camelCase("prepare", variant.name, "artProfile")
)
}.configure {
// Sets the task dependency according to the configuration flag.
if (baselineProfileExtension.automaticGenerationDuringBuild) {
it.dependsOn(copyTaskProvider)
} else {
it.mustRunAfter(copyTaskProvider)
}
}
}
// In this case the last task is the copy task.
copyTaskProvider
} else {
if (baselineProfileExtension.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.
variant
.sources
.baselineProfiles?.addGeneratedSourceDirectory(
taskProvider = mergeTaskProvider,
wiredWith = MergeBaselineProfileTask::baselineProfileDir
)
} else {
// This is the case of `saveInSrc` and `automaticGenerationDuringBuild`
// both false, that is unsupported. In this case we simply throw an
// error.
if (!project.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.
val variantGenerateTask = maybeCreateGenerateTask<Task>(
project = project,
variantName = mergeAwareVariantName,
childGenerationTaskProvider = 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.name != variant.buildType
) {
maybeCreateGenerateTask<Task>(
project = project,
variantName = variant.buildType!!,
childGenerationTaskProvider = variantGenerateTask
)
}
// TODO: Due to b/265438201 we cannot have a global task
// `generateBaselineProfile` that triggers generation for all the
// variants when there are multiple build types. The temporary workaround
// is to generate baseline profiles only for variants with the `release`
// build type until that bug is fixed, when running the global task
// `generateBaselineProfile`. This can be removed after fix.
if (variant.buildType == RELEASE) {
maybeCreateGenerateTask<MainGenerateBaselineProfileTask>(
project,
"",
variantGenerateTask
)
}
}
}
// After variants have been resolved the AGP tasks have been created, so we can set our
// task dependency if any.
project.afterVariants {
afterVariantBlocks.forEach { it() }
}
}
private inline fun <reified T : Task> maybeCreateGenerateTask(
project: Project,
variantName: String,
childGenerationTaskProvider: TaskProvider<*>? = null
) = project.tasks.maybeRegister<T>(GENERATE_TASK_NAME, variantName, TASK_NAME_SUFFIX) {
it.group = "Baseline Profile"
it.description = "Generates a baseline profile for the specified variants or dimensions."
if (childGenerationTaskProvider != null) it.dependsOn(childGenerationTaskProvider)
}
private fun createBaselineProfileConfigurationForVariant(
project: Project,
variantName: String,
productFlavors: List<Pair<String, String>>,
flavorName: String,
buildTypeName: String,
mainConfiguration: Configuration?
): Configuration {
val buildTypeConfiguration =
if (buildTypeName.isNotBlank() && buildTypeName != variantName) {
project
.configurations
.maybeCreate(camelCase(buildTypeName, CONFIGURATION_NAME_BASELINE_PROFILES))
.apply {
if (mainConfiguration != null) extendsFrom(mainConfiguration)
isCanBeResolved = true
isCanBeConsumed = false
}
} else null
val flavorConfiguration = if (flavorName.isNotBlank() && flavorName != variantName) {
project
.configurations
.maybeCreate(camelCase(flavorName, CONFIGURATION_NAME_BASELINE_PROFILES))
.apply {
if (mainConfiguration != null) extendsFrom(mainConfiguration)
isCanBeResolved = true
isCanBeConsumed = false
}
} else null
return project
.configurations
.maybeCreate(camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES))
.apply {
// The variant specific configuration always extends from build type and flavor
// configurations, when existing.
setExtendsFrom(
listOfNotNull(
mainConfiguration,
flavorConfiguration,
buildTypeConfiguration
)
)
isCanBeResolved = true
isCanBeConsumed = false
attributes {
// Main specialized attribute
it.attribute(
Usage.USAGE_ATTRIBUTE,
project.objects.named(
Usage::class.java, ATTRIBUTE_USAGE_BASELINE_PROFILE
)
)
// Build type
it.attribute(
BuildTypeAttr.ATTRIBUTE,
project.objects.named(
BuildTypeAttr::class.java, buildTypeName
)
)
// Jvm Environment
it.attribute(
TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
project.objects.named(
TargetJvmEnvironment::class.java, ATTRIBUTE_TARGET_JVM_ENVIRONMENT
)
)
// Agp version
it.attribute(
AgpVersionAttr.ATTRIBUTE,
project.objects.named(
AgpVersionAttr::class.java, project.agpVersionString()
)
)
// Baseline Profile Plugin Version
it.attribute(
BaselineProfilePluginVersionAttr.ATTRIBUTE,
project.objects.named(
BaselineProfilePluginVersionAttr::class.java,
ATTRIBUTE_BASELINE_PROFILE_PLUGIN_VERSION
)
)
// Product flavors
productFlavors.forEach { (flavorName, flavorValue) ->
it.attribute(
@Suppress("UnstableApiUsage")
ProductFlavorAttr.of(flavorName),
project.objects.named(
ProductFlavorAttr::class.java, flavorValue
)
)
}
}
}
}
}
@DisableCachingByDefault(because = "Not worth caching.")
abstract class MainGenerateBaselineProfileTask : DefaultTask() {
init {
group = "Baseline Profile"
description = "Generates a baseline profile"
}
@TaskAction
fun exec() {
this.logger.warn(
"""
The task `generateBaselineProfile` cannot currently support
generation for all the variants when there are multiple build
types without improvements planned for a future version of the
Android Gradle Plugin.
Until then, `generateBaselineProfile` will only generate
baseline profiles for the variants of the release build type,
behaving like `generateReleaseBaselineProfile`.
If you intend to generate profiles for multiple build types
you'll need to run separate gradle commands for each build type.
For example: `generateReleaseBaselineProfile` and
`generateAnotherReleaseBaselineProfile`.
Details on https://issuetracker.google.com/issue?id=270433400.
""".trimIndent()
)
}
}
@DisableCachingByDefault(because = "Not worth caching.")
abstract class GenerateDummyBaselineProfileTask : DefaultTask() {
companion object {
fun setupForVariant(
project: Project,
variant: Variant
) {
val taskProvider = project
.tasks
.maybeRegister<GenerateDummyBaselineProfileTask>(
"generate", variant.name, "profileForR8RuleRewrite"
) {
it.outputDir.set(
project
.layout
.buildDirectory
.dir("$INTERMEDIATES_BASE_FOLDER/${variant.name}/empty/")
)
it.variantName.set(variant.name)
}
@Suppress("UnstableApiUsage")
variant.sources.baselineProfiles?.addGeneratedSourceDirectory(
taskProvider, GenerateDummyBaselineProfileTask::outputDir
)
}
}
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
abstract val variantName: Property<String>
@TaskAction
fun exec() {
outputDir
.file("empty-baseline-prof.txt")
.get()
.asFile
.writeText("Lignore/This;")
}
}