• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 The Dagger Authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package dagger.hilt.android.plugin
18 
19 import com.android.build.api.component.Component
20 import com.android.build.api.extension.AndroidComponentsExtension
21 import com.android.build.api.instrumentation.FramesComputationMode
22 import com.android.build.api.instrumentation.InstrumentationScope
23 import com.android.build.gradle.AppExtension
24 import com.android.build.gradle.BaseExtension
25 import com.android.build.gradle.LibraryExtension
26 import com.android.build.gradle.TestExtension
27 import com.android.build.gradle.TestedExtension
28 import com.android.build.gradle.api.AndroidBasePlugin
29 import com.android.build.gradle.api.BaseVariant
30 import com.android.build.gradle.api.TestVariant
31 import com.android.build.gradle.api.UnitTestVariant
32 import dagger.hilt.android.plugin.util.CopyTransform
33 import dagger.hilt.android.plugin.util.SimpleAGPVersion
34 import java.io.File
35 import javax.inject.Inject
36 import org.gradle.api.Plugin
37 import org.gradle.api.Project
38 import org.gradle.api.artifacts.component.ProjectComponentIdentifier
39 import org.gradle.api.attributes.Attribute
40 import org.gradle.api.provider.ProviderFactory
41 
42 /**
43  * A Gradle plugin that checks if the project is an Android project and if so, registers a
44  * bytecode transformation.
45  *
46  * The plugin also passes an annotation processor option to disable superclass validation for
47  * classes annotated with `@AndroidEntryPoint` since the registered transform by this plugin will
48  * update the superclass.
49  */
50 class HiltGradlePlugin @Inject constructor(
51   val providers: ProviderFactory
52 ) : Plugin<Project> {
53   override fun apply(project: Project) {
54     var configured = false
55     project.plugins.withType(AndroidBasePlugin::class.java) {
56       configured = true
57       configureHilt(project)
58     }
59     project.afterEvaluate {
60       check(configured) {
61         // Check if configuration was applied, if not inform the developer they have applied the
62         // plugin to a non-android project.
63         "The Hilt Android Gradle plugin can only be applied to an Android project."
64       }
65       verifyDependencies(it)
66     }
67   }
68 
69   private fun configureHilt(project: Project) {
70     val hiltExtension = project.extensions.create(
71       HiltExtension::class.java, "hilt", HiltExtensionImpl::class.java
72     )
73     configureCompileClasspath(project, hiltExtension)
74     if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(4, 2)) {
75       // Configures bytecode transform using older APIs pre AGP 4.2
76       configureTransform(project, hiltExtension)
77     } else {
78       // Configures bytecode transform using AGP 4.2 ASM pipeline.
79       configureTransformASM(project, hiltExtension)
80     }
81     configureProcessorFlags(project)
82   }
83 
84   private fun configureCompileClasspath(project: Project, hiltExtension: HiltExtension) {
85     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
86       ?: throw error("Android BaseExtension not found.")
87     when (androidExtension) {
88       is AppExtension -> {
89         // For an app project we configure the app variant and both androidTest and test variants,
90         // Hilt components are generated in all of them.
91         androidExtension.applicationVariants.all {
92           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
93         }
94         androidExtension.testVariants.all {
95           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
96         }
97         androidExtension.unitTestVariants.all {
98           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
99         }
100       }
101       is LibraryExtension -> {
102         // For a library project, only the androidTest and test variant are configured since
103         // Hilt components are not generated in a library.
104         androidExtension.testVariants.all {
105           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
106         }
107         androidExtension.unitTestVariants.all {
108           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
109         }
110       }
111       is TestExtension -> {
112         androidExtension.applicationVariants.all {
113           configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
114         }
115       }
116       else -> error(
117         "Hilt plugin is unable to configure the compile classpath for project with extension " +
118           "'$androidExtension'"
119       )
120     }
121 
122     project.dependencies.apply {
123       registerTransform(CopyTransform::class.java) { spec ->
124         // Java/Kotlin library projects offer an artifact of type 'jar'.
125         spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "jar")
126         // Android library projects (with or without Kotlin) offer an artifact of type
127         // 'processed-jar', which AGP can offer as a jar.
128         spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "processed-jar")
129         spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
130       }
131     }
132   }
133 
134   @Suppress("UnstableApiUsage")
135   private fun configureVariantCompileClasspath(
136     project: Project,
137     hiltExtension: HiltExtension,
138     androidExtension: BaseExtension,
139     variant: BaseVariant
140   ) {
141     if (!hiltExtension.enableExperimentalClasspathAggregation) {
142       // Option is not enabled, don't configure compile classpath. Note that the option can't be
143       // checked earlier (before iterating over the variants) since it would have been too early for
144       // the value to be populated from the build file.
145       return
146     }
147 
148     if (androidExtension.lintOptions.isCheckReleaseBuilds &&
149       SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(7, 0)
150     ) {
151       // Sadly we have to ask users to disable lint when enableExperimentalClasspathAggregation is
152       // set to true and they are not in AGP 7.0+ since Lint will cause issues during the
153       // configuration phase. See b/158753935 and b/160392650
154       error(
155         "Invalid Hilt plugin configuration: When 'enableExperimentalClasspathAggregation' is " +
156           "enabled 'android.lintOptions.checkReleaseBuilds' has to be set to false unless " +
157           "com.android.tools.build:gradle:7.0.0+ is used."
158       )
159     }
160 
161     if (
162       listOf(
163           "android.injected.build.model.only", // Sent by AS 1.0 only
164           "android.injected.build.model.only.advanced", // Sent by AS 1.1+
165           "android.injected.build.model.only.versioned", // Sent by AS 2.4+
166           "android.injected.build.model.feature.full.dependencies", // Sent by AS 2.4+
167           "android.injected.build.model.v2", // Sent by AS 4.2+
168         ).any { providers.gradleProperty(it).forUseAtConfigurationTime().isPresent }
169     ) {
170       // Do not configure compile classpath when AndroidStudio is building the model (syncing)
171       // otherwise it will cause a freeze.
172       return
173     }
174 
175     val runtimeConfiguration = if (variant is TestVariant) {
176       // For Android test variants, the tested runtime classpath is used since the test app has
177       // tested dependencies removed.
178       variant.testedVariant.runtimeConfiguration
179     } else {
180       variant.runtimeConfiguration
181     }
182     val artifactView = runtimeConfiguration.incoming.artifactView { view ->
183       view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
184       view.componentFilter { identifier ->
185         // Filter out the project's classes from the aggregated view since this can cause
186         // issues with Kotlin internal members visibility. b/178230629
187         if (identifier is ProjectComponentIdentifier) {
188           identifier.projectName != project.name
189         } else {
190           true
191         }
192       }
193     }
194 
195     // CompileOnly config names don't follow the usual convention:
196     // <Variant Name>   -> <Config Name>
197     // debug            -> debugCompileOnly
198     // debugAndroidTest -> androidTestDebugCompileOnly
199     // debugUnitTest    -> testDebugCompileOnly
200     // release          -> releaseCompileOnly
201     // releaseUnitTest  -> testReleaseCompileOnly
202     val compileOnlyConfigName = when (variant) {
203       is TestVariant ->
204         "androidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}CompileOnly"
205       is UnitTestVariant ->
206         "test${variant.name.substringBeforeLast("UnitTest").capitalize()}CompileOnly"
207       else ->
208         "${variant.name}CompileOnly"
209     }
210     project.dependencies.add(compileOnlyConfigName, artifactView.files)
211   }
212 
213   @Suppress("UnstableApiUsage")
214   private fun configureTransformASM(project: Project, hiltExtension: HiltExtension) {
215     var warnAboutLocalTestsFlag = false
216     fun registerTransform(androidComponent: Component) {
217       if (hiltExtension.enableTransformForLocalTests && !warnAboutLocalTestsFlag) {
218         project.logger.warn(
219           "The Hilt configuration option 'enableTransformForLocalTests' is no longer necessary " +
220             "when com.android.tools.build:gradle:4.2.0+ is used."
221         )
222         warnAboutLocalTestsFlag = true
223       }
224       androidComponent.transformClassesWith(
225         classVisitorFactoryImplClass = AndroidEntryPointClassVisitor.Factory::class.java,
226         scope = InstrumentationScope.PROJECT
227       ) { params ->
228         val classesDir =
229           File(project.buildDir, "intermediates/javac/${androidComponent.name}/classes")
230         params.additionalClassesDir.set(classesDir)
231       }
232       androidComponent.setAsmFramesComputationMode(
233         FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
234       )
235     }
236 
237     val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
238     androidComponents.onVariants { registerTransform(it) }
239     androidComponents.androidTests { registerTransform(it) }
240     androidComponents.unitTests { registerTransform(it) }
241   }
242 
243   private fun configureTransform(project: Project, hiltExtension: HiltExtension) {
244     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
245       ?: throw error("Android BaseExtension not found.")
246     androidExtension.registerTransform(AndroidEntryPointTransform())
247 
248     // Create and configure a task for applying the transform for host-side unit tests. b/37076369
249     val testedExtensions = project.extensions.findByType(TestedExtension::class.java)
250     testedExtensions?.unitTestVariants?.all { unitTestVariant ->
251       HiltTransformTestClassesTask.create(
252         project = project,
253         unitTestVariant = unitTestVariant,
254         extension = hiltExtension
255       )
256     }
257   }
258 
259   private fun configureProcessorFlags(project: Project) {
260     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
261       ?: throw error("Android BaseExtension not found.")
262     // Pass annotation processor flag to disable @AndroidEntryPoint superclass validation.
263     androidExtension.defaultConfig.apply {
264       javaCompileOptions.apply {
265         annotationProcessorOptions.apply {
266           PROCESSOR_OPTIONS.forEach { (key, value) -> argument(key, value) }
267         }
268       }
269     }
270   }
271 
272   private fun verifyDependencies(project: Project) {
273     // If project is already failing, skip verification since dependencies might not be resolved.
274     if (project.state.failure != null) {
275       return
276     }
277     val dependencies = project.configurations.flatMap { configuration ->
278       configuration.dependencies.map { dependency -> dependency.group to dependency.name }
279     }
280     if (!dependencies.contains(LIBRARY_GROUP to "hilt-android")) {
281       error(missingDepError("$LIBRARY_GROUP:hilt-android"))
282     }
283     if (!dependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") &&
284       !dependencies.contains(LIBRARY_GROUP to "hilt-compiler")
285     ) {
286       error(missingDepError("$LIBRARY_GROUP:hilt-compiler"))
287     }
288   }
289 
290   companion object {
291     val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java)
292     const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger"
293 
294     const val LIBRARY_GROUP = "com.google.dagger"
295     val PROCESSOR_OPTIONS = listOf(
296       "dagger.fastInit" to "enabled",
297       "dagger.hilt.android.internal.disableAndroidSuperclassValidation" to "true"
298     )
299     val missingDepError: (String) -> String = { depCoordinate ->
300       "The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
301     }
302   }
303 }
304