PerVariantConsumerExtensionManager.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.consumer

import com.android.build.api.variant.Variant
import org.gradle.api.GradleException
import org.gradle.api.Project

/**
 * The [BaselineProfileConsumerPlugin] supports per variant configuration, according to values
 * expressed in [BaselineProfileVariantConfiguration]. The correct value for a property is
 * determined considering the concept of override or merge.
 * When a property is evaluated considering the override, the variants are evaluated in this order:
 * `variantName`, `buildType` or `productFlavor` and `main`. The first variant configuration to
 * define the property is used to return that property.
 * For lists only, a property can be evaluated also merging all the variant configurations. This
 * is the case for dependencies for example, so that when accessing custom dependencies for variant
 * `freeRelease` the returned list contains the dependencies for `freeRelease`, `free`, `release`
 * and `main` (global ones).
 */
internal class PerVariantConsumerExtensionManager(
    private val extension: BaselineProfileConsumerExtension,
) {

    fun variant(variant: Variant) = VariantConfigurationProxy(
        variant = variant,
        ext = extension
    )

    internal class VariantConfigurationProxy internal constructor(
        private val variant: Variant,
        private val ext: BaselineProfileConsumerExtension,
    ) {

        val filterRules: List<Pair<RuleType, String>>
            get() = getMergedListForVariant(variant) { filters.rules }

        val dependencies: List<Pair<Project, String?>>
            get() = getMergedListForVariant(variant) { dependencies }

        val enableR8BaselineProfileRewrite: Boolean
            get() = getOverriddenValueForVariant(variant) { enableR8BaselineProfileRewrite }

        val saveInSrc: Boolean
            get() = getOverriddenValueForVariant(variant) { saveInSrc }

        val automaticGenerationDuringBuild: Boolean
            get() = getOverriddenValueForVariant(variant) { automaticGenerationDuringBuild }

        val baselineProfileOutputDir: String
            get() = getOverriddenValueForVariant(variant) { baselineProfileOutputDir }

        val mergeIntoMain: Boolean?
            get() = getOverriddenValueForVariantAllowNull(variant) { mergeIntoMain }

        private fun <T> getMergedListForVariant(
            variant: Variant,
            getter: BaselineProfileVariantConfigurationImpl.() -> List<T>
        ): List<T> {
            return listOfNotNull("main", variant.flavorName, variant.buildType, variant.name)
                .mapNotNull { ext.variants.findByName(it) }
                .map { getter.invoke(it) }
                .flatten()
        }

        private fun <T> getOverriddenValueForVariantAllowNull(
            variant: Variant,
            getter: BaselineProfileVariantConfigurationImpl.() -> T
        ): T? {
            // Here we select a setting for the given variant. [BaselineProfileVariantConfiguration]
            // are evaluated in the following order: variant, flavor, build type, `main`.
            // If a property is found it will return it. Note that `main` should have all the
            // defaults set so this method never returns a nullable value and should always return.

            val definedProperties = listOfNotNull(
                variant.name,
                variant.flavorName,
                variant.buildType,
                "main"
            ).mapNotNull {
                val variantConfig = ext.variants.findByName(it) ?: return@mapNotNull null
                return@mapNotNull Pair(it, getter.invoke(variantConfig))
            }.filter { it.second != null }

            // This is a case where the property is defined in both build type and flavor.
            // In this case it should fail because the result is ambiguous.
            val propMap = definedProperties.toMap()
            if (variant.flavorName in propMap &&
                variant.buildType in propMap &&
                propMap[variant.flavorName] != propMap[variant.buildType]
            ) {
                throw GradleException(
                    """
            The per-variant configuration for baseline profiles is ambiguous. This happens when
            that the same property has been defined in both a build type and a flavor.

            For example:

            baselineProfiles {
                variants {
                    free {
                        saveInSrc = true
                    }
                    release {
                        saveInSrc = false
                    }
                }
            }

            In this case for `freeRelease` it's not possible to determine the exact value of the
            property. Please specify either the build type or the flavor.
            """.trimIndent()
                )
            }

            return definedProperties.firstOrNull()?.second
        }

        private fun <T> getOverriddenValueForVariant(
            variant: Variant,
            default: T? = null,
            getter: BaselineProfileVariantConfigurationImpl.() -> T?
        ): T {
            val value = getOverriddenValueForVariantAllowNull(variant, getter)
            if (value != null) return value
            if (default != null) return default

            // This should never happen. It means the extension is missing a default property and
            // n default was specified when accessing this value. This cannot happen because of the
            // user configuration.
            throw GradleException("The required property does not have a default.")
        }
    }
}