AgpPlugin.kt

/*
 * 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.utils

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.TestExtension
import com.android.build.api.dsl.TestedExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
import com.android.build.api.variant.ApplicationVariantBuilder
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.LibraryVariant
import com.android.build.api.variant.LibraryVariantBuilder
import com.android.build.api.variant.TestAndroidComponentsExtension
import com.android.build.api.variant.TestVariant
import com.android.build.api.variant.TestVariantBuilder
import com.android.build.api.variant.Variant
import com.android.build.api.variant.VariantBuilder
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider

/**
 * Defines callbacks and utility methods to create a plugin that utilizes AGP apis.
 * Callbacks with the configuration lifecycle of the agp plugins are provided.
 */
internal abstract class AgpPlugin(
    private val project: Project,
    private val supportedAgpPlugins: Set<AgpPluginId>,
    private val minAgpVersion: AndroidPluginVersion,
    private val maxAgpVersion: AndroidPluginVersion,
) {

    companion object {
        private val gradleSyncProps by lazy {
            listOf(
                "android.injected.build.model.v2",
                "android.injected.build.model.only",
                "android.injected.build.model.only.advanced",
            )
        }
    }

    private val afterVariantBlocks = mutableListOf<() -> (Unit)>()

    fun onApply() {

        val foundPlugins = mutableSetOf<AgpPluginId>()

        // Try to configure with the supported plugins.
        for (agpPluginId in supportedAgpPlugins) {
            project.pluginManager.withPlugin(agpPluginId.value) {
                foundPlugins.add(agpPluginId)
                configureWithAndroidPlugin()
            }
        }

        // 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 (!isGradleSyncRunning()) {
                if (foundPlugins.isEmpty()) {
                    onAgpPluginNotFound(foundPlugins)
                } else {
                    onAgpPluginFound(foundPlugins)
                }
            }
        }
    }

    private fun configureWithAndroidPlugin() {

        checkAgpVersion(min = minAgpVersion, max = maxAgpVersion)

        onBeforeFinalizeDsl()

        testAndroidComponentExtension()?.let { testComponent ->
            testComponent.finalizeDsl { onTestFinalizeDsl(it) }
            testComponent.beforeVariants { onTestBeforeVariants(it) }
            testComponent.onVariants { onTestVariants(it) }
        }

        applicationAndroidComponentsExtension()?.let { applicationComponent ->
            applicationComponent.finalizeDsl { onApplicationFinalizeDsl(it) }
            applicationComponent.beforeVariants { onApplicationBeforeVariants(it) }
            applicationComponent.onVariants { onApplicationVariants(it) }
        }

        libraryAndroidComponentsExtension()?.let { libraryComponent ->
            libraryComponent.finalizeDsl { onLibraryFinalizeDsl(it) }
            libraryComponent.beforeVariants { onLibraryBeforeVariants(it) }
            libraryComponent.onVariants { onLibraryVariants(it) }
        }

        androidComponentsExtension()?.let { commonComponent ->
            commonComponent.finalizeDsl { onFinalizeDsl(commonComponent) }
            commonComponent.beforeVariants { onBeforeVariants(it) }
            commonComponent.onVariants { onVariants(it) }
        }

        // Runs the after variants callback that is module type dependent
        val testedExtension = testedExtension()
        val testExtension = testExtension()

        val variants = when {
            testedExtension != null &&
                testedExtension is com.android.build.gradle.AppExtension -> {
                testedExtension.applicationVariants
            }

            testedExtension != null &&
                testedExtension is com.android.build.gradle.LibraryExtension -> {
                testedExtension.libraryVariants
            }

            testExtension != null -> {
                testExtension.applicationVariants
            }

            else -> {
                if (isGradleSyncRunning()) return
                // This cannot happen because of user configuration because the plugin is only
                // applied if there is an android gradle plugin.
                throw GradleException("Module `${project.path}` is not a supported android module.")
            }
        }

        var applied = false
        variants.all {
            if (applied) return@all
            applied = true

            // Execute all the scheduled variant blocks
            afterVariantBlocks.forEach { it() }

            // Execute the after variant callback if scheduled.
            onAfterVariants()
        }
    }

    // Utility methods

    protected fun <T : Task> addArtifactToConfiguration(
        configurationName: String,
        taskProvider: TaskProvider<T>,
        artifactType: String
    ) {
        project.artifacts { artifactHandler ->
            artifactHandler.add(configurationName, taskProvider) { artifact ->
                artifact.type = artifactType
                artifact.builtBy(taskProvider)
            }
        }
    }

    protected fun afterVariants(block: () -> (Unit)) {
        afterVariantBlocks.add(block)
    }

    private fun checkAgpVersion(min: AndroidPluginVersion, max: AndroidPluginVersion) {
        val agpVersion = project.agpVersion()
        if (agpVersion < min || agpVersion > max) {
            throw GradleException(
                """
        This version of the Baseline Profile Gradle Plugin only works with Android Gradle plugin
        between versions $MIN_AGP_VERSION_REQUIRED and $MAX_AGP_VERSION_REQUIRED. Current version
        is $agpVersion."
            """.trimIndent()
            )
        }
    }

    internal fun isGradleSyncRunning() = gradleSyncProps.any {
        it in project.properties && project.properties[it].toString().toBoolean()
    }

    protected fun isTestModule() = testAndroidComponentExtension() != null
    protected fun isLibraryModule() = libraryAndroidComponentsExtension() != null
    protected fun isApplicationModule() = applicationAndroidComponentsExtension() != null

    // Plugin application callbacks

    protected open fun onAgpPluginNotFound(pluginIds: Set<AgpPluginId>) {}

    protected open fun onAgpPluginFound(pluginIds: Set<AgpPluginId>) {}

    // Test callbacks

    protected open fun onTestFinalizeDsl(extension: TestExtension) {}

    protected open fun onTestBeforeVariants(variantBuilder: TestVariantBuilder) {}

    protected open fun onTestVariants(variant: TestVariant) {}

    // Application callbacks

    protected open fun onApplicationFinalizeDsl(extension: ApplicationExtension) {}

    protected open fun onApplicationBeforeVariants(variantBuilder: ApplicationVariantBuilder) {}

    protected open fun onApplicationVariants(variant: ApplicationVariant) {}

    // Library callbacks

    protected open fun onLibraryFinalizeDsl(extension: LibraryExtension) {}

    protected open fun onLibraryBeforeVariants(variantBuilder: LibraryVariantBuilder) {}

    protected open fun onLibraryVariants(variant: LibraryVariant) {}

    // Shared callbacks

    protected open fun onBeforeFinalizeDsl() {}

    protected open fun onFinalizeDsl(extension: AndroidComponentsExtension<*, *, *>) {}

    protected open fun onBeforeVariants(variantBuilder: VariantBuilder) {}

    protected open fun onVariants(variant: Variant) {}

    protected open fun onAfterVariants() {}

    // Quick access to extension methods

    private fun testAndroidComponentExtension(): TestAndroidComponentsExtension? =
        project
            .extensions
            .findByType(TestAndroidComponentsExtension::class.java)

    private fun applicationAndroidComponentsExtension(): ApplicationAndroidComponentsExtension? =
        project
            .extensions
            .findByType(ApplicationAndroidComponentsExtension::class.java)

    private fun libraryAndroidComponentsExtension(): LibraryAndroidComponentsExtension? =
        project
            .extensions
            .findByType(LibraryAndroidComponentsExtension::class.java)

    private fun androidComponentsExtension(): AndroidComponentsExtension<*, *, *>? =
        project
            .extensions
            .findByType(AndroidComponentsExtension::class.java)

    private fun testedExtension(): TestedExtension? =
        project
            .extensions
            .findByType(TestedExtension::class.java)

    private fun testExtension(): com.android.build.gradle.TestExtension? =
        project
            .extensions
            .findByType(com.android.build.gradle.TestExtension::class.java)
}

/**
 * Enumerates the supported android plugins.
 */
internal enum class AgpPluginId(val value: String) {
    ID_ANDROID_APPLICATION_PLUGIN("com.android.application"),
    ID_ANDROID_LIBRARY_PLUGIN("com.android.library"),
    ID_ANDROID_TEST_PLUGIN("com.android.test")
}