blob: f2dd881af626b08af38e436c9f16fd94790b47e2 [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.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.tasks.JdkImageInput
import dagger.hilt.android.plugin.task.AggregateDepsTask
import dagger.hilt.android.plugin.task.HiltTransformTestClassesTask
import dagger.hilt.android.plugin.util.AggregatedPackagesTransform
import dagger.hilt.android.plugin.util.ComponentCompat
import dagger.hilt.android.plugin.util.CopyTransform
import dagger.hilt.android.plugin.util.SimpleAGPVersion
import dagger.hilt.android.plugin.util.capitalize
import dagger.hilt.android.plugin.util.getAndroidComponentsExtension
import dagger.hilt.android.plugin.util.getKaptConfigName
import dagger.hilt.android.plugin.util.getSdkPath
import dagger.hilt.processor.internal.optionvalues.GradleProjectType
import java.io.File
import javax.inject.Inject
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.attributes.Attribute
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.process.CommandLineArgumentProvider
import org.gradle.util.GradleVersion
import org.objectweb.asm.Opcodes
/**
* 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
)
configureDependencyTransforms(project)
configureCompileClasspath(project, hiltExtension)
if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(4, 2)) {
// Configures bytecode transform using older APIs pre AGP 4.2
configureBytecodeTransform(project, hiltExtension)
} else {
// Configures bytecode transform using AGP 4.2 ASM pipeline.
configureBytecodeTransformASM(project, hiltExtension)
}
configureAggregatingTask(project, hiltExtension)
configureProcessorFlags(project, hiltExtension)
}
// Configures Gradle dependency transforms.
private fun configureDependencyTransforms(project: Project) = 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
// 'android-classes', which AGP can offer as a jar.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "android-classes")
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
}
registerTransform(CopyTransform::class.java) { spec ->
// File Collection dependencies might be an artifact of type 'directory', e.g. when
// adding as a dep the destination directory of the JavaCompile task.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "directory")
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
}
registerTransform(AggregatedPackagesTransform::class.java) { spec ->
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, AGGREGATED_HILT_ARTIFACT_TYPE_VALUE)
}
}
private fun configureCompileClasspath(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.baseExtension() ?: error("Android BaseExtension not found.")
androidExtension.forEachRootVariant { variant ->
configureVariantCompileClasspath(project, hiltExtension, androidExtension, variant)
}
}
// Invokes the [block] function for each Android variant that is considered a Hilt root, where
// dependencies are aggregated and components are generated.
private fun BaseExtension.forEachRootVariant(
@Suppress("DEPRECATION") block: (variant: com.android.build.gradle.api.BaseVariant) -> Unit
) {
when (this) {
is AppExtension -> {
// For an app project we configure the app variant and both androidTest and unitTest
// variants, Hilt components are generated in all of them.
applicationVariants.all { block(it) }
testVariants.all { block(it) }
unitTestVariants.all { block(it) }
}
is LibraryExtension -> {
// For a library project, only the androidTest and unitTest variant are configured since
// Hilt components are not generated in a library.
testVariants.all { block(it) }
unitTestVariants.all { block(it) }
}
is TestExtension -> {
applicationVariants.all { block(it) }
}
else -> error("Hilt plugin does not know how to configure '$this'")
}
}
private fun configureVariantCompileClasspath(
project: Project,
hiltExtension: HiltExtension,
androidExtension: BaseExtension,
@Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant
) {
if (
!hiltExtension.enableExperimentalClasspathAggregation || hiltExtension.enableAggregatingTask
) {
// 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 {
// forUseAtConfigurationTime() is deprecated in 7.4 and later:
// https://docs.gradle.org/current/userguide/upgrading_version_7.html#changes_7.4
if (GradleVersion.version(project.gradle.gradleVersion) < GradleVersion.version("7.4.0")) {
@Suppress("DEPRECATION")
providers.gradleProperty(it).forUseAtConfigurationTime().isPresent
} else {
providers.gradleProperty(it).isPresent
}
}
) {
// Do not configure compile classpath when AndroidStudio is building the model (syncing)
// otherwise it will cause a freeze.
return
}
@Suppress("DEPRECATION") // Older variant API is deprecated
val runtimeConfiguration = if (variant is com.android.build.gradle.api.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
@Suppress("DEPRECATION") // Older variant API is deprecated
val compileOnlyConfigName = when (variant) {
is com.android.build.gradle.api.TestVariant ->
"androidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}CompileOnly"
is com.android.build.gradle.api.UnitTestVariant ->
"test${variant.name.substringBeforeLast("UnitTest").capitalize()}CompileOnly"
else ->
"${variant.name}CompileOnly"
}
project.dependencies.add(compileOnlyConfigName, artifactView.files)
}
@Suppress("UnstableApiUsage") // ASM Pipeline APIs
private fun configureBytecodeTransformASM(project: Project, hiltExtension: HiltExtension) {
var warnAboutLocalTestsFlag = false
fun registerTransform(androidComponent: ComponentCompat) {
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
)
}
getAndroidComponentsExtension(project).onAllVariants { registerTransform(it) }
}
private fun configureBytecodeTransform(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.baseExtension() ?: error("Android BaseExtension not found.")
androidExtension::class.java.getMethod(
"registerTransform",
Class.forName("com.android.build.api.transform.Transform"),
Array<Any>::class.java
).invoke(androidExtension, AndroidEntryPointTransform(), emptyArray<Any>())
// Create and configure a task for applying the transform for host-side unit tests. b/37076369
project.testedExtension()?.unitTestVariants?.all { unitTestVariant ->
HiltTransformTestClassesTask.create(
project = project,
unitTestVariant = unitTestVariant,
extension = hiltExtension
)
}
}
private fun configureAggregatingTask(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.baseExtension() ?: error("Android BaseExtension not found.")
androidExtension.forEachRootVariant { variant ->
configureVariantAggregatingTask(project, hiltExtension, androidExtension, variant)
}
}
private fun configureVariantAggregatingTask(
project: Project,
hiltExtension: HiltExtension,
androidExtension: BaseExtension,
@Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant
) {
if (!hiltExtension.enableAggregatingTask) {
// Option is not enabled, don't configure aggregating task.
return
}
val hiltCompileConfiguration = project.configurations.create(
"hiltCompileOnly${variant.name.capitalize()}"
).apply {
isCanBeConsumed = false
isCanBeResolved = true
}
// Add the JavaCompile task classpath and output dir to the config, the task's classpath
// will contain:
// * compileOnly dependencies
// * KAPT and Kotlinc generated bytecode
// * R.jar
// * Tested classes if the variant is androidTest
project.dependencies.add(
hiltCompileConfiguration.name,
project.files(variant.javaCompileProvider.map { it.classpath })
)
project.dependencies.add(
hiltCompileConfiguration.name,
project.files(variant.javaCompileProvider.map {it.destinationDirectory.get() })
)
fun getInputClasspath(artifactAttributeValue: String) =
mutableListOf<Configuration>().apply {
@Suppress("DEPRECATION") // Older variant API is deprecated
if (variant is com.android.build.gradle.api.TestVariant) {
add(variant.testedVariant.runtimeConfiguration)
}
add(variant.runtimeConfiguration)
add(hiltCompileConfiguration)
}.map { configuration ->
configuration.incoming.artifactView { view ->
view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, artifactAttributeValue)
}.files
}.let {
project.files(*it.toTypedArray())
}
val aggregatingTask = project.tasks.register(
"hiltAggregateDeps${variant.name.capitalize()}",
AggregateDepsTask::class.java
) {
it.compileClasspath.setFrom(getInputClasspath(AGGREGATED_HILT_ARTIFACT_TYPE_VALUE))
it.outputDir.set(
project.file(project.buildDir.resolve("generated/hilt/component_trees/${variant.name}/"))
)
@Suppress("DEPRECATION") // Older variant API is deprecated
it.testEnvironment.set(
variant is com.android.build.gradle.api.TestVariant ||
variant is com.android.build.gradle.api.UnitTestVariant ||
androidExtension is com.android.build.gradle.TestExtension
)
it.crossCompilationRootValidationDisabled.set(
hiltExtension.disableCrossCompilationRootValidation
)
if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION >= SimpleAGPVersion(7, 1)) {
it.asmApiVersion.set(Opcodes.ASM9)
}
}
val componentClasses = project.files(
project.buildDir.resolve("intermediates/hilt/component_classes/${variant.name}/")
)
val componentsJavaCompileTask = project.tasks.register(
"hiltJavaCompile${variant.name.capitalize()}",
JavaCompile::class.java
) { compileTask ->
compileTask.source = aggregatingTask.map { it.outputDir.asFileTree }.get()
// Configure the input classpath based on Java 9 compatibility, specifically for Java 9 the
// android.jar is now included in the input classpath instead of the bootstrapClasspath.
// See: com/android/build/gradle/tasks/JavaCompileUtils.kt
val mainBootstrapClasspath =
variant.javaCompileProvider.map { it.options.bootstrapClasspath ?: project.files() }.get()
if (
JavaVersion.current().isJava9Compatible &&
androidExtension.compileOptions.targetCompatibility.isJava9Compatible
) {
compileTask.classpath =
getInputClasspath(DAGGER_ARTIFACT_TYPE_VALUE).plus(mainBootstrapClasspath)
// Copies argument providers from original task, which should contain the JdkImageInput
variant.javaCompileProvider.get().let { originalCompileTask ->
originalCompileTask.options.compilerArgumentProviders
.filter {
it is HiltCommandLineArgumentProvider || it is JdkImageInput
}
.forEach {
compileTask.options.compilerArgumentProviders.add(it)
}
}
compileTask.options.compilerArgs.add("-XDstringConcat=inline")
} else {
compileTask.classpath = getInputClasspath(DAGGER_ARTIFACT_TYPE_VALUE)
compileTask.options.bootstrapClasspath = mainBootstrapClasspath
}
compileTask.destinationDirectory.set(componentClasses.singleFile)
compileTask.options.apply {
annotationProcessorPath = project.configurations.create(
"hiltAnnotationProcessor${variant.name.capitalize()}"
).also { config ->
config.isCanBeConsumed = false
config.isCanBeResolved = true
// Add user annotation processor configuration, so that SPI plugins and other processors
// are discoverable.
val apConfigurations: List<Configuration> = mutableListOf<Configuration>().apply {
add(variant.annotationProcessorConfiguration)
project.configurations.findByName(getKaptConfigName(variant))?.let { add(it) }
}
config.extendsFrom(*apConfigurations.toTypedArray())
// Add hilt-compiler even though it might be in the AP configurations already.
project.dependencies.add(config.name, "com.google.dagger:hilt-compiler:$HILT_VERSION")
}
generatedSourceOutputDirectory.set(
project.file(
project.buildDir.resolve("generated/hilt/component_sources/${variant.name}/")
)
)
if (
JavaVersion.current().isJava8Compatible &&
androidExtension.compileOptions.targetCompatibility.isJava8Compatible
) {
compilerArgs.add("-parameters")
}
compilerArgs.add("-Adagger.fastInit=enabled")
compilerArgs.add("-Adagger.hilt.internal.useAggregatingRootProcessor=false")
compilerArgs.add("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
encoding = androidExtension.compileOptions.encoding
}
compileTask.sourceCompatibility =
androidExtension.compileOptions.sourceCompatibility.toString()
compileTask.targetCompatibility =
androidExtension.compileOptions.targetCompatibility.toString()
}
componentClasses.builtBy(componentsJavaCompileTask)
variant.registerPostJavacGeneratedBytecode(componentClasses)
}
private fun getAndroidJar(project: Project, compileSdkVersion: String) =
project.files(File(project.getSdkPath(), "platforms/$compileSdkVersion/android.jar"))
private fun configureProcessorFlags(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.baseExtension() ?: error("Android BaseExtension not found.")
val projectType = when (androidExtension) {
is AppExtension -> GradleProjectType.APP
is LibraryExtension -> GradleProjectType.LIBRARY
is TestExtension -> GradleProjectType.TEST
else -> error("Hilt plugin does not know how to configure '$this'")
}
// Pass annotation processor flags via a CommandLineArgumentProvider so that plugin
// options defined in the extension are populated from the user's build file. Checking the
// option too early would make it seem like it is never set.
androidExtension.defaultConfig.javaCompileOptions.annotationProcessorOptions
.compilerArgumentProvider(HiltCommandLineArgumentProvider(hiltExtension, projectType))
}
private class HiltCommandLineArgumentProvider(
private val hiltExtension: HiltExtension,
private val projectType: GradleProjectType
): CommandLineArgumentProvider {
override fun asArguments() = mutableListOf<Pair<String, String>>().apply {
// Pass annotation processor flag to enable Dagger's fast-init, the best mode for Hilt.
add("dagger.fastInit" to "enabled")
// Pass annotation processor flag to disable @AndroidEntryPoint superclass validation.
add("dagger.hilt.android.internal.disableAndroidSuperclassValidation" to "true")
add("dagger.hilt.android.internal.projectType" to projectType.toString())
// Pass annotation processor flag to disable the aggregating processor if aggregating
// task is enabled.
if (hiltExtension.enableAggregatingTask) {
add("dagger.hilt.internal.useAggregatingRootProcessor" to "false")
}
// Pass annotation processor flag to disable cross compilation root validation.
// The plugin option duplicates the processor flag because it is an input of the
// aggregating task.
if (hiltExtension.disableCrossCompilationRootValidation) {
add("dagger.hilt.disableCrossCompilationRootValidation" to "true")
}
}.map { (key, value) -> "-A$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"))
}
}
private fun Project.baseExtension(): BaseExtension?
= extensions.findByType(BaseExtension::class.java)
private fun Project.testedExtension(): TestedExtension?
= extensions.findByType(TestedExtension::class.java)
companion object {
val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java)
const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger"
const val AGGREGATED_HILT_ARTIFACT_TYPE_VALUE = "aggregated-jar-for-hilt"
const val LIBRARY_GROUP = "com.google.dagger"
val missingDepError: (String) -> String = { depCoordinate ->
"The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
}
}
}