• 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.attributes.BuildTypeAttr
20 import com.android.build.api.attributes.ProductFlavorAttr
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 dagger.hilt.android.plugin.task.AggregateDepsTask
30 import dagger.hilt.android.plugin.task.HiltTransformTestClassesTask
31 import dagger.hilt.android.plugin.util.AggregatedPackagesTransform
32 import dagger.hilt.android.plugin.util.AndroidComponentsExtensionCompat.Companion.getAndroidComponentsExtension
33 import dagger.hilt.android.plugin.util.ComponentCompat
34 import dagger.hilt.android.plugin.util.CopyTransform
35 import dagger.hilt.android.plugin.util.SimpleAGPVersion
36 import dagger.hilt.android.plugin.util.capitalize
37 import dagger.hilt.android.plugin.util.getSdkPath
38 import java.io.File
39 import javax.inject.Inject
40 import org.gradle.api.JavaVersion
41 import org.gradle.api.Plugin
42 import org.gradle.api.Project
43 import org.gradle.api.artifacts.component.ProjectComponentIdentifier
44 import org.gradle.api.attributes.Attribute
45 import org.gradle.api.attributes.Usage
46 import org.gradle.api.provider.ProviderFactory
47 import org.gradle.api.tasks.compile.JavaCompile
48 import org.gradle.process.CommandLineArgumentProvider
49 
50 /**
51  * A Gradle plugin that checks if the project is an Android project and if so, registers a
52  * bytecode transformation.
53  *
54  * The plugin also passes an annotation processor option to disable superclass validation for
55  * classes annotated with `@AndroidEntryPoint` since the registered transform by this plugin will
56  * update the superclass.
57  */
58 class HiltGradlePlugin @Inject constructor(
59   val providers: ProviderFactory
60 ) : Plugin<Project> {
61   override fun apply(project: Project) {
62     var configured = false
63     project.plugins.withType(AndroidBasePlugin::class.java) {
64       configured = true
65       configureHilt(project)
66     }
67     project.afterEvaluate {
68       check(configured) {
69         // Check if configuration was applied, if not inform the developer they have applied the
70         // plugin to a non-android project.
71         "The Hilt Android Gradle plugin can only be applied to an Android project."
72       }
73       verifyDependencies(it)
74     }
75   }
76 
77   private fun configureHilt(project: Project) {
78     val hiltExtension = project.extensions.create(
79       HiltExtension::class.java, "hilt", HiltExtensionImpl::class.java
80     )
81     configureDependencyTransforms(project)
82     configureCompileClasspath(project, hiltExtension)
83     if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(4, 2)) {
84       // Configures bytecode transform using older APIs pre AGP 4.2
85       configureBytecodeTransform(project, hiltExtension)
86     } else {
87       // Configures bytecode transform using AGP 4.2 ASM pipeline.
88       configureBytecodeTransformASM(project, hiltExtension)
89     }
90     configureAggregatingTask(project, hiltExtension)
91     configureProcessorFlags(project, hiltExtension)
92   }
93 
94   // Configures Gradle dependency transforms.
95   private fun configureDependencyTransforms(project: Project) = project.dependencies.apply {
96     registerTransform(CopyTransform::class.java) { spec ->
97       // Java/Kotlin library projects offer an artifact of type 'jar'.
98       spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "jar")
99       // Android library projects (with or without Kotlin) offer an artifact of type
100       // 'processed-jar', which AGP can offer as a jar.
101       spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "processed-jar")
102       spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
103     }
104     registerTransform(CopyTransform::class.java) { spec ->
105       // File Collection dependencies might be an artifact of type 'directory', e.g. when
106       // adding as a dep the destination directory of the JavaCompile task.
107       spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "directory")
108       spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
109     }
110     registerTransform(AggregatedPackagesTransform::class.java) { spec ->
111       spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
112       spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, AGGREGATED_HILT_ARTIFACT_TYPE_VALUE)
113     }
114   }
115 
116   private fun configureCompileClasspath(project: Project, hiltExtension: HiltExtension) {
117     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
118       ?: throw error("Android BaseExtension not found.")
119     androidExtension.forEachRootVariant { variant ->
120       configureVariantCompileClasspath(project, hiltExtension, androidExtension, variant)
121     }
122   }
123 
124   // Invokes the [block] function for each Android variant that is considered a Hilt root, where
125   // dependencies are aggregated and components are generated.
126   private fun BaseExtension.forEachRootVariant(
127     @Suppress("DEPRECATION") block: (variant: com.android.build.gradle.api.BaseVariant) -> Unit
128   ) {
129     when (this) {
130       is AppExtension -> {
131         // For an app project we configure the app variant and both androidTest and unitTest
132         // variants, Hilt components are generated in all of them.
133         applicationVariants.all { block(it) }
134         testVariants.all { block(it) }
135         unitTestVariants.all { block(it) }
136       }
137       is LibraryExtension -> {
138         // For a library project, only the androidTest and unitTest variant are configured since
139         // Hilt components are not generated in a library.
140         testVariants.all { block(it) }
141         unitTestVariants.all { block(it) }
142       }
143       is TestExtension -> {
144         applicationVariants.all { block(it) }
145       }
146       else -> error("Hilt plugin does not know how to configure '$this'")
147     }
148   }
149 
150   private fun configureVariantCompileClasspath(
151     project: Project,
152     hiltExtension: HiltExtension,
153     androidExtension: BaseExtension,
154     @Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant
155   ) {
156     if (
157       !hiltExtension.enableExperimentalClasspathAggregation || hiltExtension.enableAggregatingTask
158     ) {
159       // Option is not enabled, don't configure compile classpath. Note that the option can't be
160       // checked earlier (before iterating over the variants) since it would have been too early for
161       // the value to be populated from the build file.
162       return
163     }
164 
165     if (
166       androidExtension.lintOptions.isCheckReleaseBuilds &&
167       SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(7, 0)
168     ) {
169       // Sadly we have to ask users to disable lint when enableExperimentalClasspathAggregation is
170       // set to true and they are not in AGP 7.0+ since Lint will cause issues during the
171       // configuration phase. See b/158753935 and b/160392650
172       error(
173         "Invalid Hilt plugin configuration: When 'enableExperimentalClasspathAggregation' is " +
174           "enabled 'android.lintOptions.checkReleaseBuilds' has to be set to false unless " +
175           "com.android.tools.build:gradle:7.0.0+ is used."
176       )
177     }
178 
179     if (
180       listOf(
181         "android.injected.build.model.only", // Sent by AS 1.0 only
182         "android.injected.build.model.only.advanced", // Sent by AS 1.1+
183         "android.injected.build.model.only.versioned", // Sent by AS 2.4+
184         "android.injected.build.model.feature.full.dependencies", // Sent by AS 2.4+
185         "android.injected.build.model.v2", // Sent by AS 4.2+
186       ).any {
187         providers.gradleProperty(it).forUseAtConfigurationTime().isPresent
188       }
189     ) {
190       // Do not configure compile classpath when AndroidStudio is building the model (syncing)
191       // otherwise it will cause a freeze.
192       return
193     }
194 
195     @Suppress("DEPRECATION") // Older variant API is deprecated
196     val runtimeConfiguration = if (variant is com.android.build.gradle.api.TestVariant) {
197       // For Android test variants, the tested runtime classpath is used since the test app has
198       // tested dependencies removed.
199       variant.testedVariant.runtimeConfiguration
200     } else {
201       variant.runtimeConfiguration
202     }
203     val artifactView = runtimeConfiguration.incoming.artifactView { view ->
204       view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
205       view.componentFilter { identifier ->
206         // Filter out the project's classes from the aggregated view since this can cause
207         // issues with Kotlin internal members visibility. b/178230629
208         if (identifier is ProjectComponentIdentifier) {
209           identifier.projectName != project.name
210         } else {
211           true
212         }
213       }
214     }
215 
216     // CompileOnly config names don't follow the usual convention:
217     // <Variant Name>   -> <Config Name>
218     // debug            -> debugCompileOnly
219     // debugAndroidTest -> androidTestDebugCompileOnly
220     // debugUnitTest    -> testDebugCompileOnly
221     // release          -> releaseCompileOnly
222     // releaseUnitTest  -> testReleaseCompileOnly
223     @Suppress("DEPRECATION") // Older variant API is deprecated
224     val compileOnlyConfigName = when (variant) {
225       is com.android.build.gradle.api.TestVariant ->
226         "androidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}CompileOnly"
227       is com.android.build.gradle.api.UnitTestVariant ->
228         "test${variant.name.substringBeforeLast("UnitTest").capitalize()}CompileOnly"
229       else ->
230         "${variant.name}CompileOnly"
231     }
232     project.dependencies.add(compileOnlyConfigName, artifactView.files)
233   }
234 
235   @Suppress("UnstableApiUsage") // ASM Pipeline APIs
236   private fun configureBytecodeTransformASM(project: Project, hiltExtension: HiltExtension) {
237     var warnAboutLocalTestsFlag = false
238     fun registerTransform(androidComponent: ComponentCompat) {
239       if (hiltExtension.enableTransformForLocalTests && !warnAboutLocalTestsFlag) {
240         project.logger.warn(
241           "The Hilt configuration option 'enableTransformForLocalTests' is no longer necessary " +
242             "when com.android.tools.build:gradle:4.2.0+ is used."
243         )
244         warnAboutLocalTestsFlag = true
245       }
246       androidComponent.transformClassesWith(
247         classVisitorFactoryImplClass = AndroidEntryPointClassVisitor.Factory::class.java,
248         scope = InstrumentationScope.PROJECT
249       ) { params ->
250         val classesDir =
251           File(project.buildDir, "intermediates/javac/${androidComponent.name}/classes")
252         params.additionalClassesDir.set(classesDir)
253       }
254       androidComponent.setAsmFramesComputationMode(
255         FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
256       )
257     }
258     getAndroidComponentsExtension(project).onAllVariants { registerTransform(it) }
259   }
260 
261   private fun configureBytecodeTransform(project: Project, hiltExtension: HiltExtension) {
262     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
263       ?: throw error("Android BaseExtension not found.")
264     androidExtension.registerTransform(AndroidEntryPointTransform())
265 
266     // Create and configure a task for applying the transform for host-side unit tests. b/37076369
267     val testedExtensions = project.extensions.findByType(TestedExtension::class.java)
268     testedExtensions?.unitTestVariants?.all { unitTestVariant ->
269       HiltTransformTestClassesTask.create(
270         project = project,
271         unitTestVariant = unitTestVariant,
272         extension = hiltExtension
273       )
274     }
275   }
276 
277   private fun configureAggregatingTask(project: Project, hiltExtension: HiltExtension) {
278     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
279       ?: throw error("Android BaseExtension not found.")
280     androidExtension.forEachRootVariant { variant ->
281       configureVariantAggregatingTask(project, hiltExtension, androidExtension, variant)
282     }
283   }
284 
285   private fun configureVariantAggregatingTask(
286     project: Project,
287     hiltExtension: HiltExtension,
288     androidExtension: BaseExtension,
289     @Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant
290   ) {
291     if (!hiltExtension.enableAggregatingTask) {
292       // Option is not enabled, don't configure aggregating task.
293       return
294     }
295 
296     val hiltCompileConfiguration = project.configurations.create(
297       "hiltCompileOnly${variant.name.capitalize()}"
298     ).apply {
299       // The runtime config of the test APK differs from the tested one.
300       @Suppress("DEPRECATION") // Older variant API is deprecated
301       if (variant is com.android.build.gradle.api.TestVariant) {
302         extendsFrom(variant.testedVariant.runtimeConfiguration)
303       }
304       extendsFrom(variant.runtimeConfiguration)
305       isCanBeConsumed = false
306       isCanBeResolved = true
307       attributes { attrContainer ->
308         attrContainer.attribute(
309           Usage.USAGE_ATTRIBUTE,
310           project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME)
311         )
312         attrContainer.attribute(
313           BuildTypeAttr.ATTRIBUTE,
314           project.objects.named(BuildTypeAttr::class.java, variant.buildType.name)
315         )
316         variant.productFlavors.forEach { flavor ->
317           attrContainer.attribute(
318             Attribute.of(flavor.dimension!!, ProductFlavorAttr::class.java),
319             project.objects.named(ProductFlavorAttr::class.java, flavor.name)
320           )
321         }
322       }
323     }
324     // Add the JavaCompile task classpath and output dir to the config, the task's classpath
325     // will contain:
326     //  * compileOnly dependencies
327     //  * KAPT and Kotlinc generated bytecode
328     //  * R.jar
329     //  * Tested classes if the variant is androidTest
330     project.dependencies.add(
331       hiltCompileConfiguration.name,
332       project.files(variant.javaCompileProvider.map { it.classpath })
333     )
334     project.dependencies.add(
335       hiltCompileConfiguration.name,
336       project.files(variant.javaCompileProvider.map {it.destinationDirectory.get() })
337     )
338 
339     fun getInputClasspath(artifactAttributeValue: String) =
340       hiltCompileConfiguration.incoming.artifactView { view ->
341         view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, artifactAttributeValue)
342       }.files
343 
344     val aggregatingTask = project.tasks.register(
345       "hiltAggregateDeps${variant.name.capitalize()}",
346       AggregateDepsTask::class.java
347     ) {
348       it.compileClasspath.setFrom(getInputClasspath(AGGREGATED_HILT_ARTIFACT_TYPE_VALUE))
349       it.outputDir.set(
350         project.file(project.buildDir.resolve("generated/hilt/component_trees/${variant.name}/"))
351       )
352       @Suppress("DEPRECATION") // Older variant API is deprecated
353       it.testEnvironment.set(
354         variant is com.android.build.gradle.api.TestVariant ||
355           variant is com.android.build.gradle.api.UnitTestVariant
356       )
357       it.crossCompilationRootValidationDisabled.set(
358         hiltExtension.disableCrossCompilationRootValidation
359       )
360     }
361 
362     val componentClasses = project.files(
363       project.buildDir.resolve("intermediates/hilt/component_classes/${variant.name}/")
364     )
365     val componentsJavaCompileTask = project.tasks.register(
366       "hiltJavaCompile${variant.name.capitalize()}",
367       JavaCompile::class.java
368     ) { compileTask ->
369       compileTask.source = aggregatingTask.map { it.outputDir.asFileTree }.get()
370       // Configure the input classpath based on Java 9 compatibility, specifically for Java 9 the
371       // android.jar is now included in the input classpath instead of the bootstrapClasspath.
372       // See: com/android/build/gradle/tasks/JavaCompileUtils.kt
373       val mainBootstrapClasspath =
374         variant.javaCompileProvider.map { it.options.bootstrapClasspath ?: project.files() }.get()
375       if (
376         JavaVersion.current().isJava9Compatible &&
377         androidExtension.compileOptions.targetCompatibility.isJava9Compatible
378       ) {
379         compileTask.classpath =
380           getInputClasspath(DAGGER_ARTIFACT_TYPE_VALUE).plus(mainBootstrapClasspath)
381         //  Copies argument providers from original task, which should contain the JdkImageInput
382         variant.javaCompileProvider.get().let { originalCompileTask ->
383           originalCompileTask.options.compilerArgumentProviders.forEach {
384             compileTask.options.compilerArgumentProviders.add(it)
385           }
386         }
387         compileTask.options.compilerArgs.add("-XDstringConcat=inline")
388       } else {
389         compileTask.classpath = getInputClasspath(DAGGER_ARTIFACT_TYPE_VALUE)
390         compileTask.options.bootstrapClasspath = mainBootstrapClasspath
391       }
392       compileTask.destinationDirectory.set(componentClasses.singleFile)
393       compileTask.options.apply {
394         annotationProcessorPath = project.configurations.create(
395           "hiltAnnotationProcessor${variant.name.capitalize()}"
396         ).also { config ->
397           // TODO: Consider finding the hilt-compiler dep from the user config and using it here.
398           project.dependencies.add(config.name, "com.google.dagger:hilt-compiler:$HILT_VERSION")
399         }
400         generatedSourceOutputDirectory.set(
401           project.file(
402             project.buildDir.resolve("generated/hilt/component_sources/${variant.name}/")
403           )
404         )
405         if (
406           JavaVersion.current().isJava8Compatible &&
407           androidExtension.compileOptions.targetCompatibility.isJava8Compatible
408         ) {
409           compilerArgs.add("-parameters")
410         }
411         compilerArgs.add("-Adagger.fastInit=enabled")
412         compilerArgs.add("-Adagger.hilt.internal.useAggregatingRootProcessor=false")
413         compilerArgs.add("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
414         encoding = androidExtension.compileOptions.encoding
415       }
416       compileTask.sourceCompatibility =
417         androidExtension.compileOptions.sourceCompatibility.toString()
418       compileTask.targetCompatibility =
419         androidExtension.compileOptions.targetCompatibility.toString()
420     }
421     componentClasses.builtBy(componentsJavaCompileTask)
422 
423     variant.registerPostJavacGeneratedBytecode(componentClasses)
424   }
425 
426   private fun getAndroidJar(project: Project, compileSdkVersion: String) =
427     project.files(File(project.getSdkPath(), "platforms/$compileSdkVersion/android.jar"))
428 
429   private fun configureProcessorFlags(project: Project, hiltExtension: HiltExtension) {
430     val androidExtension = project.extensions.findByType(BaseExtension::class.java)
431       ?: throw error("Android BaseExtension not found.")
432     androidExtension.defaultConfig.javaCompileOptions.annotationProcessorOptions.apply {
433       // Pass annotation processor flag to enable Dagger's fast-init, the best mode for Hilt.
434       argument("dagger.fastInit", "enabled")
435       // Pass annotation processor flag to disable @AndroidEntryPoint superclass validation.
436       argument("dagger.hilt.android.internal.disableAndroidSuperclassValidation", "true")
437       // Pass certain annotation processor flags via a CommandLineArgumentProvider so that plugin
438       // options defined in the extension are populated from the user's build file. Checking the
439       // option too early would make it seem like it is never set.
440       compilerArgumentProvider(
441         // Suppress due to https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown
442         @Suppress("ObjectLiteralToLambda")
443         object : CommandLineArgumentProvider {
444           override fun asArguments() = mutableListOf<String>().apply {
445             // Pass annotation processor flag to disable the aggregating processor if aggregating
446             // task is enabled.
447             if (hiltExtension.enableAggregatingTask) {
448               add("-Adagger.hilt.internal.useAggregatingRootProcessor=false")
449             }
450             // Pass annotation processor flag to disable cross compilation root validation.
451             // The plugin option duplicates the processor flag because it is an input of the
452             // aggregating task.
453             if (hiltExtension.disableCrossCompilationRootValidation) {
454               add("-Adagger.hilt.disableCrossCompilationRootValidation=true")
455             }
456           }
457         }
458       )
459     }
460   }
461 
462   private fun verifyDependencies(project: Project) {
463     // If project is already failing, skip verification since dependencies might not be resolved.
464     if (project.state.failure != null) {
465       return
466     }
467     val dependencies = project.configurations.flatMap { configuration ->
468       configuration.dependencies.map { dependency -> dependency.group to dependency.name }
469     }
470     if (!dependencies.contains(LIBRARY_GROUP to "hilt-android")) {
471       error(missingDepError("$LIBRARY_GROUP:hilt-android"))
472     }
473     if (
474       !dependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") &&
475       !dependencies.contains(LIBRARY_GROUP to "hilt-compiler")
476     ) {
477       error(missingDepError("$LIBRARY_GROUP:hilt-compiler"))
478     }
479   }
480 
481   companion object {
482     val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java)
483     const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger"
484     const val AGGREGATED_HILT_ARTIFACT_TYPE_VALUE = "aggregated-jar-for-hilt"
485 
486     const val LIBRARY_GROUP = "com.google.dagger"
487 
488     val missingDepError: (String) -> String = { depCoordinate ->
489       "The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
490     }
491   }
492 }
493