From 9fd82e44bdc8a96f1c11dfdb9b642b1b614c0048 Mon Sep 17 00:00:00 2001 From: Daniel Santiago Date: Fri, 21 Mar 2025 09:11:12 -0700 Subject: [PATCH] Reimplement Hilt dependency validation as a task The Hilt Gradle Plugin validates that if applied Hilt's runtime and processor dependencies are also applied such that aggregated metadata is properly generated and not missed at the root. The validation was done during configuration time by inspecting dependencies but such strategy is not compatible with project isolation. This commit changes the validation strategy to be done in a task that will be wired as a dependency to other common build tasks by being a 'source generating' task that doesn't actually generate any new sources. There is no Android Gradle Plugin API to hook into the compile or assemble tasks which is why `addGeneratedSourceDirectory()` is used. Also add an Hilt Gradle Plugin option to disable the validation for project authors who which to disable it because it might not work well for their setup. Fixes https://github.com/google/dagger/issues/4423 RELNOTES=Fix a Gradle project isolation issue in the Hilt Gradle Plugin. PiperOrigin-RevId: 739201752 --- .../hilt/android/plugin/HiltExtension.kt | 7 ++ .../hilt/android/plugin/HiltGradlePlugin.kt | 73 ++++++++------- .../plugin/task/DependencyCheckTask.kt | 60 ++++++++++++ .../android/plugin/util/Configurations.kt | 92 ++++++++++++++++--- 4 files changed, 186 insertions(+), 46 deletions(-) create mode 100644 java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/task/DependencyCheckTask.kt diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltExtension.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltExtension.kt index 4c78d1b778c..ea5c2b160e3 100644 --- a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltExtension.kt +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltExtension.kt @@ -62,6 +62,12 @@ interface HiltExtension { * for more information. */ var disableCrossCompilationRootValidation: Boolean + + /** + * If set to `true`, the Hilt Gradle Plugin will not validated that both the Hilt runtime + * dependency and compiler dependency are applied to the project. The default value is `false`. + */ + var disableDependencyCheck: Boolean } internal open class HiltExtensionImpl : HiltExtension { @@ -72,4 +78,5 @@ internal open class HiltExtensionImpl : HiltExtension { override var enableTransformForLocalTests: Boolean = false override var enableAggregatingTask: Boolean = true override var disableCrossCompilationRootValidation: Boolean = false + override var disableDependencyCheck: Boolean = false } diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt index 5c6f5ee680d..59cf0cba7bf 100644 --- a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt @@ -21,17 +21,14 @@ import com.android.build.api.instrumentation.FramesComputationMode import com.android.build.api.instrumentation.InstrumentationScope import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.api.variant.Component -import com.android.build.api.variant.HasAndroidTest -import com.android.build.api.variant.HasUnitTest +import com.android.build.api.variant.ApplicationVariant import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.LibraryVariant import com.android.build.api.variant.TestAndroidComponentsExtension -import com.android.build.gradle.AppExtension import com.android.build.gradle.BaseExtension -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.TestExtension import com.android.build.gradle.tasks.JdkImageInput import dagger.hilt.android.plugin.task.AggregateDepsTask +import dagger.hilt.android.plugin.task.DependencyCheckTask import dagger.hilt.android.plugin.transform.AggregatedPackagesTransform import dagger.hilt.android.plugin.transform.AndroidEntryPointClassVisitor import dagger.hilt.android.plugin.transform.CopyTransform @@ -41,7 +38,9 @@ import dagger.hilt.android.plugin.util.addKspTaskProcessorOptions import dagger.hilt.android.plugin.util.capitalize import dagger.hilt.android.plugin.util.forEachRootVariant import dagger.hilt.android.plugin.util.getKaptConfigName +import dagger.hilt.android.plugin.util.getKaptConfigNames import dagger.hilt.android.plugin.util.getKspConfigName +import dagger.hilt.android.plugin.util.getKspConfigNames import dagger.hilt.android.plugin.util.isKspTask import dagger.hilt.android.plugin.util.onAllVariants import dagger.hilt.processor.internal.optionvalues.GradleProjectType @@ -81,7 +80,6 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor // plugin to a non-android project. "The Hilt Android Gradle plugin can only be applied to an Android project." } - verifyDependencies(it) } } @@ -101,6 +99,7 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor configureBytecodeTransformASM(androidExtension) configureAggregatingTask(project, hiltExtension) configureProcessorFlags(project, hiltExtension, androidExtension) + configureDependencyValidation(project, hiltExtension, androidExtension) } // Configures Gradle dependency transforms. @@ -410,33 +409,43 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor } } - private fun verifyDependencies(project: Project) { - // If project is already failing, skip verification since dependencies might not be resolved. - if (project.state.failure != null) { - return - } - val dependencies = - project.configurations - .filterNot { - // Exclude plugin created config since plugin adds the deps to them. - it.name.startsWith("hiltAnnotationProcessor") || it.name.startsWith("hiltCompileOnly") + private fun configureDependencyValidation( + project: Project, + hiltExtension: HiltExtension, + androidExtension: AndroidComponentsExtension<*, *, *>, + ) { + androidExtension.onVariants { variant -> + if (hiltExtension.disableDependencyCheck) { + return@onVariants + } + // Only check applications and libraries, using Hilt in tests is optional + if (variant !is ApplicationVariant && variant !is LibraryVariant) { + return@onVariants + } + + fun Configuration.getDependenciesIds() = + incoming.dependencies.filterIsInstance().map { dependency -> + dependency.group to dependency.name } - .flatMap { configuration -> - configuration.dependencies.filterIsInstance().map { dependency -> - dependency.group to dependency.name - } + + variant.sources.java?.addGeneratedSourceDirectory( + project.tasks.register( + "hiltDependencyCheck${variant.name.capitalize()}", + DependencyCheckTask::class.java, + ) { checkTask -> + checkTask.runtimeDependencies = variant.compileConfiguration.getDependenciesIds() + checkTask.processorDependencies = + buildList { + add(variant.annotationProcessorConfiguration.name) + addAll(getKaptConfigNames(variant)) + addAll(getKspConfigNames(variant)) + } + .mapNotNull { configName -> project.configurations.findByName(configName) } + .flatMap { it.getDependenciesIds() } } - .toSet() - fun getMissingDepMsg(depCoordinate: String): String = - "The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found." - if (!dependencies.contains(LIBRARY_GROUP to "hilt-android")) { - error(getMissingDepMsg("$LIBRARY_GROUP:hilt-android")) - } - if ( - !dependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") && - !dependencies.contains(LIBRARY_GROUP to "hilt-compiler") - ) { - error(getMissingDepMsg("$LIBRARY_GROUP:hilt-compiler")) + ) { + return@addGeneratedSourceDirectory it.outputDirectory + } } } diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/task/DependencyCheckTask.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/task/DependencyCheckTask.kt new file mode 100644 index 00000000000..381ccdbcb50 --- /dev/null +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/task/DependencyCheckTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Dagger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dagger.hilt.android.plugin.task + +import dagger.hilt.android.plugin.HiltGradlePlugin.Companion.LIBRARY_GROUP +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ExternalDependency +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Check Hilt dependencies are applied since the project has the Hilt Gradle Plugin applied. */ +@DisableCachingByDefault(because = "not worth caching") +abstract class DependencyCheckTask : DefaultTask() { + + @get:Input + abstract var runtimeDependencies: List> + + @get:Input + abstract var processorDependencies: List> + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun check() { + if (!runtimeDependencies.contains(LIBRARY_GROUP to "hilt-android")) { + error(getMissingDepMsg("$LIBRARY_GROUP:hilt-android")) + } + + if ( + !processorDependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") && + !processorDependencies.contains(LIBRARY_GROUP to "hilt-compiler") + ) { + error(getMissingDepMsg("$LIBRARY_GROUP:hilt-compiler")) + } + } + + private fun getMissingDepMsg(depCoordinate: String): String = + "The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found." +} diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/util/Configurations.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/util/Configurations.kt index 1f4a792c9ca..8186058e057 100644 --- a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/util/Configurations.kt +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/util/Configurations.kt @@ -16,18 +16,64 @@ package dagger.hilt.android.plugin.util +import com.android.build.api.variant.AndroidTest +import com.android.build.api.variant.TestVariant +import com.android.build.api.variant.Variant + @Suppress("DEPRECATION") // Older variant API is deprecated -internal fun getKaptConfigName(variant: com.android.build.gradle.api.BaseVariant) - = getConfigName(variant, "kapt") +internal fun getKaptConfigName(variant: com.android.build.gradle.api.BaseVariant) = + getConfigName(prefix = "kapt", variant = variant) @Suppress("DEPRECATION") // Older variant API is deprecated -internal fun getKspConfigName(variant: com.android.build.gradle.api.BaseVariant) - = getConfigName(variant, "ksp") +internal fun getKspConfigName(variant: com.android.build.gradle.api.BaseVariant) = + getConfigName(prefix = "ksp", variant = variant) @Suppress("DEPRECATION") // Older variant API is deprecated -internal fun getConfigName( +private fun getConfigName( + prefix: String, + mode: VariantNameMode = VariantNameMode.FULL, variant: com.android.build.gradle.api.BaseVariant, - prefix: String +) = + getConfigName( + prefix = prefix, + mode = mode, + variantFullName = variant.name, + variantFlavorName = variant.flavorName, + isAndroidTest = variant is com.android.build.gradle.api.TestVariant, + isUnitTest = variant is com.android.build.gradle.api.UnitTestVariant, + ) + +internal fun getKaptConfigNames(variant: Variant) = + VariantNameMode.entries.map { mode -> + getConfigName(prefix = "kapt", mode = mode, variant = variant) + } + +internal fun getKspConfigNames(variant: Variant) = + VariantNameMode.entries.map { mode -> + getConfigName(prefix = "ksp", mode = mode, variant = variant) + } + +private fun getConfigName( + prefix: String, + mode: VariantNameMode = VariantNameMode.FULL, + variant: Variant, +) = + getConfigName( + prefix = prefix, + mode = mode, + variantFullName = variant.name, + variantFlavorName = variant.flavorName, + isAndroidTest = variant is AndroidTest, + isUnitTest = variant is TestVariant, + ) + +private fun getConfigName( + prefix: String, + mode: VariantNameMode, + variantFullName: String, + variantFlavorName: String?, + isAndroidTest: Boolean, + isUnitTest: Boolean, ): String { // Config names don't follow the usual task name conventions: // -> @@ -36,12 +82,30 @@ internal fun getConfigName( // debugUnitTest -> TestDebug // release -> Release // releaseUnitTest -> TestRelease - return when (variant) { - is com.android.build.gradle.api.TestVariant -> - "${prefix}AndroidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}" - is com.android.build.gradle.api.UnitTestVariant -> - "${prefix}Test${variant.name.substringBeforeLast("UnitTest").capitalize()}" - else -> - "${prefix}${variant.name.capitalize()}" + return buildString { + append(prefix) + if (isAndroidTest) { + append("AndroidTest") + } else if (isUnitTest) { + append("Test") + } + append( + when (mode) { + VariantNameMode.BASE -> "" + VariantNameMode.FLAVOR -> checkNotNull(variantFlavorName) + VariantNameMode.FULL -> + when { + isAndroidTest -> variantFullName.substringBeforeLast("AndroidTest") + isUnitTest -> variantFullName.substringBeforeLast("UnitTest") + else -> variantFullName + } + }.capitalize() + ) } -} \ No newline at end of file +} + +private enum class VariantNameMode { + BASE, + FLAVOR, + FULL, +}