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