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, +}