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