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