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