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