Skip to content

Commit 8c74037

Browse files
danysantiagoDagger Team
authored andcommitted
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 #4423 RELNOTES=Fix a Gradle project isolation issue in the Hilt Gradle Plugin. PiperOrigin-RevId: 739201752
1 parent 515601c commit 8c74037

File tree

4 files changed

+184
-46
lines changed

4 files changed

+184
-46
lines changed

java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltExtension.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ interface HiltExtension {
6262
* for more information.
6363
*/
6464
var disableCrossCompilationRootValidation: Boolean
65+
66+
/**
67+
* If set to `true`, the Hilt Gradle Plugin will not validated that both the Hilt runtime
68+
* dependency and compiler dependency are applied to the project. The default value is `false`.
69+
*/
70+
var disableDependencyCheck: Boolean
6571
}
6672

6773
internal open class HiltExtensionImpl : HiltExtension {
@@ -72,4 +78,5 @@ internal open class HiltExtensionImpl : HiltExtension {
7278
override var enableTransformForLocalTests: Boolean = false
7379
override var enableAggregatingTask: Boolean = true
7480
override var disableCrossCompilationRootValidation: Boolean = false
81+
override var disableDependencyCheck: Boolean = false
7582
}

java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@ import com.android.build.api.instrumentation.FramesComputationMode
2121
import com.android.build.api.instrumentation.InstrumentationScope
2222
import com.android.build.api.variant.AndroidComponentsExtension
2323
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
24-
import com.android.build.api.variant.Component
25-
import com.android.build.api.variant.HasAndroidTest
26-
import com.android.build.api.variant.HasUnitTest
24+
import com.android.build.api.variant.ApplicationVariant
2725
import com.android.build.api.variant.LibraryAndroidComponentsExtension
26+
import com.android.build.api.variant.LibraryVariant
2827
import com.android.build.api.variant.TestAndroidComponentsExtension
29-
import com.android.build.gradle.AppExtension
3028
import com.android.build.gradle.BaseExtension
31-
import com.android.build.gradle.LibraryExtension
32-
import com.android.build.gradle.TestExtension
3329
import com.android.build.gradle.tasks.JdkImageInput
3430
import dagger.hilt.android.plugin.task.AggregateDepsTask
31+
import dagger.hilt.android.plugin.task.DependencyCheckTask
3532
import dagger.hilt.android.plugin.transform.AggregatedPackagesTransform
3633
import dagger.hilt.android.plugin.transform.AndroidEntryPointClassVisitor
3734
import dagger.hilt.android.plugin.transform.CopyTransform
@@ -81,7 +78,6 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor
8178
// plugin to a non-android project.
8279
"The Hilt Android Gradle plugin can only be applied to an Android project."
8380
}
84-
verifyDependencies(it)
8581
}
8682
}
8783

@@ -101,6 +97,7 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor
10197
configureBytecodeTransformASM(androidExtension)
10298
configureAggregatingTask(project, hiltExtension)
10399
configureProcessorFlags(project, hiltExtension, androidExtension)
100+
configureDependencyValidation(project, hiltExtension, androidExtension)
104101
}
105102

106103
// Configures Gradle dependency transforms.
@@ -410,33 +407,43 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor
410407
}
411408
}
412409

413-
private fun verifyDependencies(project: Project) {
414-
// If project is already failing, skip verification since dependencies might not be resolved.
415-
if (project.state.failure != null) {
416-
return
417-
}
418-
val dependencies =
419-
project.configurations
420-
.filterNot {
421-
// Exclude plugin created config since plugin adds the deps to them.
422-
it.name.startsWith("hiltAnnotationProcessor") || it.name.startsWith("hiltCompileOnly")
410+
private fun configureDependencyValidation(
411+
project: Project,
412+
hiltExtension: HiltExtension,
413+
androidExtension: AndroidComponentsExtension<*, *, *>,
414+
) {
415+
androidExtension.onVariants { variant ->
416+
if (hiltExtension.disableDependencyCheck) {
417+
return@onVariants
418+
}
419+
// Only check applications and libraries, using Hilt in tests is optional
420+
if (variant !is ApplicationVariant && variant !is LibraryVariant) {
421+
return@onVariants
422+
}
423+
424+
fun Configuration.getDependenciesIds() =
425+
incoming.dependencies.filterIsInstance<ExternalDependency>().map { dependency ->
426+
dependency.group to dependency.name
423427
}
424-
.flatMap { configuration ->
425-
configuration.dependencies.filterIsInstance<ExternalDependency>().map { dependency ->
426-
dependency.group to dependency.name
427-
}
428+
429+
variant.sources.java?.addGeneratedSourceDirectory(
430+
project.tasks.register(
431+
"hiltDependencyCheck${variant.name.capitalize()}",
432+
DependencyCheckTask::class.java,
433+
) { checkTask ->
434+
checkTask.runtimeDependencies = variant.compileConfiguration.getDependenciesIds()
435+
checkTask.processorDependencies =
436+
buildList {
437+
add(variant.annotationProcessorConfiguration.name)
438+
addAll(getKaptConfigNames(variant))
439+
addAll(getKspConfigNames(variant))
440+
}
441+
.mapNotNull { configName -> project.configurations.findByName(configName) }
442+
.flatMap { it.getDependenciesIds() }
428443
}
429-
.toSet()
430-
fun getMissingDepMsg(depCoordinate: String): String =
431-
"The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
432-
if (!dependencies.contains(LIBRARY_GROUP to "hilt-android")) {
433-
error(getMissingDepMsg("$LIBRARY_GROUP:hilt-android"))
434-
}
435-
if (
436-
!dependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") &&
437-
!dependencies.contains(LIBRARY_GROUP to "hilt-compiler")
438-
) {
439-
error(getMissingDepMsg("$LIBRARY_GROUP:hilt-compiler"))
444+
) {
445+
return@addGeneratedSourceDirectory it.outputDirectory
446+
}
440447
}
441448
}
442449

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (C) 2025 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.task
18+
19+
import dagger.hilt.android.plugin.HiltGradlePlugin.Companion.LIBRARY_GROUP
20+
import org.gradle.api.DefaultTask
21+
import org.gradle.api.artifacts.Configuration
22+
import org.gradle.api.artifacts.ExternalDependency
23+
import org.gradle.api.file.DirectoryProperty
24+
import org.gradle.api.provider.ListProperty
25+
import org.gradle.api.provider.Property
26+
import org.gradle.api.tasks.Input
27+
import org.gradle.api.tasks.OutputDirectory
28+
import org.gradle.api.tasks.TaskAction
29+
import org.gradle.work.DisableCachingByDefault
30+
31+
/** Check Hilt dependencies are applied since the project has the Hilt Gradle Plugin applied. */
32+
@DisableCachingByDefault(because = "not worth caching")
33+
abstract class DependencyCheckTask : DefaultTask() {
34+
35+
@get:Input
36+
abstract var runtimeDependencies: List<Pair<String?, String>>
37+
38+
@get:Input
39+
abstract var processorDependencies: List<Pair<String?, String>>
40+
41+
@get:OutputDirectory
42+
abstract val outputDirectory: DirectoryProperty
43+
44+
@TaskAction
45+
fun check() {
46+
if (!runtimeDependencies.contains(LIBRARY_GROUP to "hilt-android")) {
47+
error(getMissingDepMsg("$LIBRARY_GROUP:hilt-android"))
48+
}
49+
50+
if (
51+
!processorDependencies.contains(LIBRARY_GROUP to "hilt-android-compiler") &&
52+
!processorDependencies.contains(LIBRARY_GROUP to "hilt-compiler")
53+
) {
54+
error(getMissingDepMsg("$LIBRARY_GROUP:hilt-compiler"))
55+
}
56+
}
57+
58+
private fun getMissingDepMsg(depCoordinate: String): String =
59+
"The Hilt Android Gradle plugin is applied but no $depCoordinate dependency was found."
60+
}

java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/util/Configurations.kt

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,64 @@
1616

1717
package dagger.hilt.android.plugin.util
1818

19+
import com.android.build.api.variant.AndroidTest
20+
import com.android.build.api.variant.TestVariant
21+
import com.android.build.api.variant.Variant
22+
1923
@Suppress("DEPRECATION") // Older variant API is deprecated
20-
internal fun getKaptConfigName(variant: com.android.build.gradle.api.BaseVariant)
21-
= getConfigName(variant, "kapt")
24+
internal fun getKaptConfigName(variant: com.android.build.gradle.api.BaseVariant) =
25+
getConfigName(prefix = "kapt", variant = variant)
2226

2327
@Suppress("DEPRECATION") // Older variant API is deprecated
24-
internal fun getKspConfigName(variant: com.android.build.gradle.api.BaseVariant)
25-
= getConfigName(variant, "ksp")
28+
internal fun getKspConfigName(variant: com.android.build.gradle.api.BaseVariant) =
29+
getConfigName(prefix = "ksp", variant = variant)
2630

2731
@Suppress("DEPRECATION") // Older variant API is deprecated
28-
internal fun getConfigName(
32+
private fun getConfigName(
33+
prefix: String,
34+
mode: VariantNameMode = VariantNameMode.FULL,
2935
variant: com.android.build.gradle.api.BaseVariant,
30-
prefix: String
36+
) =
37+
getConfigName(
38+
prefix = prefix,
39+
mode = mode,
40+
variantFullName = variant.name,
41+
variantFlavorName = variant.flavorName,
42+
isAndroidTest = variant is com.android.build.gradle.api.TestVariant,
43+
isUnitTest = variant is com.android.build.gradle.api.UnitTestVariant,
44+
)
45+
46+
internal fun getKaptConfigNames(variant: Variant) =
47+
VariantNameMode.entries.map { mode ->
48+
getConfigName(prefix = "kapt", mode = mode, variant = variant)
49+
}
50+
51+
internal fun getKspConfigNames(variant: Variant) =
52+
VariantNameMode.entries.map { mode ->
53+
getConfigName(prefix = "ksp", mode = mode, variant = variant)
54+
}
55+
56+
private fun getConfigName(
57+
prefix: String,
58+
mode: VariantNameMode = VariantNameMode.FULL,
59+
variant: Variant,
60+
) =
61+
getConfigName(
62+
prefix = prefix,
63+
mode = mode,
64+
variantFullName = variant.name,
65+
variantFlavorName = variant.flavorName,
66+
isAndroidTest = variant is AndroidTest,
67+
isUnitTest = variant is TestVariant,
68+
)
69+
70+
private fun getConfigName(
71+
prefix: String,
72+
mode: VariantNameMode,
73+
variantFullName: String,
74+
variantFlavorName: String?,
75+
isAndroidTest: Boolean,
76+
isUnitTest: Boolean,
3177
): String {
3278
// Config names don't follow the usual task name conventions:
3379
// <Variant Name> -> <Config Name>
@@ -36,12 +82,30 @@ internal fun getConfigName(
3682
// debugUnitTest -> <prefix>TestDebug
3783
// release -> <prefix>Release
3884
// releaseUnitTest -> <prefix>TestRelease
39-
return when (variant) {
40-
is com.android.build.gradle.api.TestVariant ->
41-
"${prefix}AndroidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}"
42-
is com.android.build.gradle.api.UnitTestVariant ->
43-
"${prefix}Test${variant.name.substringBeforeLast("UnitTest").capitalize()}"
44-
else ->
45-
"${prefix}${variant.name.capitalize()}"
85+
return buildString {
86+
append(prefix)
87+
if (isAndroidTest) {
88+
append("AndroidTest")
89+
} else if (isUnitTest) {
90+
append("Test")
91+
}
92+
append(
93+
when (mode) {
94+
VariantNameMode.BASE -> ""
95+
VariantNameMode.FLAVOR -> checkNotNull(variantFlavorName)
96+
VariantNameMode.FULL ->
97+
when {
98+
isAndroidTest -> variantFullName.substringBeforeLast("AndroidTest")
99+
isUnitTest -> variantFullName.substringBeforeLast("UnitTest")
100+
else -> variantFullName
101+
}
102+
}.capitalize()
103+
)
46104
}
47-
}
105+
}
106+
107+
private enum class VariantNameMode {
108+
BASE,
109+
FLAVOR,
110+
FULL,
111+
}

0 commit comments

Comments
 (0)