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