Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement Hilt dependency validation as a task #4651

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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<ExternalDependency>().map { dependency ->
dependency.group to dependency.name
}
.flatMap { configuration ->
configuration.dependencies.filterIsInstance<ExternalDependency>().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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pair<String?, String>>

@get:Input
abstract var processorDependencies: List<Pair<String?, String>>

@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."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
// <Variant Name> -> <Config Name>
Expand All @@ -36,12 +82,30 @@ internal fun getConfigName(
// debugUnitTest -> <prefix>TestDebug
// release -> <prefix>Release
// releaseUnitTest -> <prefix>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()
)
}
}
}

private enum class VariantNameMode {
BASE,
FLAVOR,
FULL,
}
Loading