blob: a82826ed8121a55f9fa0f9a745208879f9abcda9 [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* 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 dagger.hilt.android.plugin
import com.android.build.api.component.Component
import com.android.build.api.extension.AndroidComponentsExtension
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.TestExtension
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.AndroidBasePlugin
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant
import dagger.hilt.android.plugin.util.CopyTransform
import dagger.hilt.android.plugin.util.SimpleAGPVersion
import java.io.File
import javax.inject.Inject
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.attributes.Attribute
import org.gradle.api.provider.ProviderFactory
/**
* A Gradle plugin that checks if the project is an Android project and if so, registers a
* bytecode transformation.
*
* The plugin also passes an annotation processor option to disable superclass validation for
* classes annotated with `@AndroidEntryPoint` since the registered transform by this plugin will
* update the superclass.
*/
class HiltGradlePlugin @Inject constructor(
val providers: ProviderFactory
) : Plugin<Project> {
override fun apply(project: Project) {
var configured = false
project.plugins.withType(AndroidBasePlugin::class.java) {
configured = true
configureHilt(project)
}
project.afterEvaluate {
check(configured) {
// Check if configuration was applied, if not inform the developer they have applied the
// plugin to a non-android project.
"The Hilt Android Gradle plugin can only be applied to an Android project."
}
verifyDependencies(it)
}
}
private fun configureHilt(project: Project) {
val hiltExtension = project.extensions.create(
HiltExtension::class.java, "hilt", HiltExtensionImpl::class.java
)
configureCompileClasspath(project, hiltExtension)
if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(4, 2)) {
// Configures bytecode transform using older APIs pre AGP 4.2
configureTransform(project, hiltExtension)
} else {
// Configures bytecode transform using AGP 4.2 ASM pipeline.
configureTransformASM(project, hiltExtension)
}
configureProcessorFlags(project)
}
private fun configureCompileClasspath(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.extensions.findByType(BaseExtension::class.java)
?: throw error("Android BaseExtension not found.")
when (androidExtension) {
is AppExtension -> {
// For an app project we configure the app variant and both androidTest and test variants,
// Hilt components are generated in all of them.
androidExtension.applicationVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.testVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.unitTestVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
is LibraryExtension -> {
// For a library project, only the androidTest and test variant are configured since
// Hilt components are not generated in a library.
androidExtension.testVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.unitTestVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
is TestExtension -> {
androidExtension.applicationVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
else -> error(
"Hilt plugin is unable to configure the compile classpath for project with extension " +
"'$androidExtension'"
)
}
project.dependencies.apply {
registerTransform(CopyTransform::class.java) { spec ->
// Java/Kotlin library projects offer an artifact of type 'jar'.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "jar")
// Android library projects (with or without Kotlin) offer an artifact of type
// 'processed-jar', which AGP can offer as a jar.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "processed-jar")
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
}
}
}
@Suppress("UnstableApiUsage")
private fun configureVariantCompileClasspath(
project: Project,
hiltExtension: HiltExtension,
androidExtension: BaseExtension,
variant: BaseVariant
) {
if (!hiltExtension.enableExperimentalClasspathAggregation) {
// Option is not enabled, don't configure compile classpath. Note that the option can't be
// checked earlier (before iterating over the variants) since it would have been too early for
// the value to be populated from the build file.
return
}
if (androidExtension.lintOptions.isCheckReleaseBuilds &&
SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(7, 0)
) {
// Sadly we have to ask users to disable lint when enableExperimentalClasspathAggregation is
// set to true and they are not in AGP 7.0+ since Lint will cause issues during the
// configuration phase. See b/158753935 and b/160392650
error(
"Invalid Hilt plugin configuration: When 'enableExperimentalClasspathAggregation' is " +
"enabled 'android.lintOptions.checkReleaseBuilds' has to be set to false unless " +
"com.android.tools.build:gradle:7.0.0+ is used."
)
}
if (
listOf(
"android.injected.build.model.only", // Sent by AS 1.0 only
"android.injected.build.model.only.advanced", // Sent by AS 1.1+
"android.injected.build.model.only.versioned", // Sent by AS 2.4+
"android.injected.build.model.feature.full.dependencies", // Sent by AS 2.4+
"android.injected.build.model.v2", // Sent by AS 4.2+
).any { providers.gradleProperty(it).forUseAtConfigurationTime().isPresent }
) {
// Do not configure compile classpath when AndroidStudio is building the model (syncing)
// otherwise it will cause a freeze.
return
}
val runtimeConfiguration = if (variant is TestVariant) {
// For Android test variants, the tested runtime classpath is used since the test app has
// tested dependencies removed.
variant.testedVariant.runtimeConfiguration
} else {
variant.runtimeConfiguration
}
val artifactView = runtimeConfiguration.incoming.artifactView { view ->
view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
view.componentFilter { identifier ->
// Filter out the project's classes from the aggregated view since this can cause
// issues with Kotlin internal members visibility. b/178230629
if (identifier is ProjectComponentIdentifier) {
identifier.projectName != project.name
} else {
true
}
}
}
// CompileOnly config names don't follow the usual convention:
// <Variant Name> -> <Config Name>
// debug -> debugCompileOnly
// debugAndroidTest -> androidTestDebugCompileOnly
// debugUnitTest -> testDebugCompileOnly
// release -> releaseCompileOnly
// releaseUnitTest -> testReleaseCompileOnly
val compileOnlyConfigName = when (variant) {
is TestVariant ->
"androidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}CompileOnly"
is UnitTestVariant ->
"test${variant.name.substringBeforeLast("UnitTest").capitalize()}CompileOnly"
else ->
"${variant.name}CompileOnly"
}
project.dependencies.add(compileOnlyConfigName, artifactView.files)
}
@Suppress("UnstableApiUsage")
private fun configureTransformASM(project: Project, hiltExtension: HiltExtension) {
var warnAboutLocalTestsFlag = false
fun registerTransform(androidComponent: Component) {
if (hiltExtension.enableTransformForLocalTests && !warnAboutLocalTestsFlag) {
project.logger.warn(
"The Hilt configuration option 'enableTransformForLocalTests' is no longer necessary " +
"when com.android.tools.build:gradle:4.2.0+ is used."
)
warnAboutLocalTestsFlag = true
}
androidComponent.transformClassesWith(
classVisitorFactoryImplClass = AndroidEntryPointClassVisitor.Factory::class.java,
scope = InstrumentationScope.PROJECT
) { params ->
val classesDir =
File(project.buildDir, "intermediates/javac/${androidComponent.name}/classes")
params.additionalClassesDir.set(classesDir)
}
androidComponent.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { registerTransform(it) }
androidComponents.androidTests { registerTransform(it) }
androidComponents.unitTests { registerTransform(it) }
}
private fun configureTransform(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.extensions.findByType(BaseExtension::class.java)
?: throw error("Android BaseExtension not found.")
androidExtension.registerTransform(AndroidEntryPointTransform())
// Create and configure a task for applying the transform for host-side unit tests. b/37076369
val testedExtensions = project.extensions.findByType(TestedExtension::class.java)
testedExtensions?.unitTestVariants?.all { unitTestVariant ->
HiltTransformTestClassesTask.create(
project = project,
unitTestVariant = unitTestVariant,
extension = hiltExtension
)
}
}
private fun configureProcessorFlags(project: Project) {
val androidExtension = project.extensions.findByType(BaseExtension::class.java)
?: throw error("Android BaseExtension not found.")
// Pass annotation processor flag to disable @AndroidEntryPoint superclass validation.
androidExtension.defaultConfig.apply {
javaCompileOptions.apply {
annotationProcessorOptions.apply {
PROCESSOR_OPTIONS.forEach { (key, value) -> argument(key, value) }
}
}
}
}
private fun verifyDependencies(project: Project) {
// If project is already failing, skip verification since dependencies might not be resolved.
if (project.state.failure != null) {
return
}
val dependencies = project.configurations.flatMap { configuration ->
configuration.dependencies.map { dependency -> dependency.group to dependency.name }
}
if (!dependencies.contains(LIBRARY_GROUP to "hilt-android")) {
error(missingDepError("$LIBRARY_GROUP:hilt-android"))
}
if (!dependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") &&
!dependencies.contains(LIBRARY_GROUP to "hilt-compiler")
) {
error(missingDepError("$LIBRARY_GROUP:hilt-compiler"))
}
}
companion object {
val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java)
const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger"
const val LIBRARY_GROUP = "com.google.dagger"
val PROCESSOR_OPTIONS = listOf(
"dagger.fastInit" to "enabled",
"dagger.hilt.android.internal.disableAndroidSuperclassValidation" to "true"
)
val missingDepError: (String) -> String = { depCoordinate ->
"The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
}
}
}