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 55a7d5551df..6395a3f9558 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 @@ -30,6 +30,8 @@ import com.android.build.api.variant.ScopedArtifacts import com.android.build.api.variant.TestAndroidComponentsExtension import com.android.build.gradle.api.AndroidBasePlugin import com.android.build.gradle.tasks.JdkImageInput +import dagger.hilt.android.plugin.rules.HiltArtifactCompatibilityRule +import dagger.hilt.android.plugin.rules.HiltArtifactDisambiguationRule import dagger.hilt.android.plugin.task.AggregateDepsTask import dagger.hilt.android.plugin.task.HiltSyncTask import dagger.hilt.android.plugin.transform.AggregatedPackagesTransform @@ -102,6 +104,7 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor val hiltExtension = project.extensions.create(HiltExtension::class.java, "hilt", HiltExtensionImpl::class.java) HiltPluginEnvironment(project, hiltExtension).apply { + configureCompatibilityRules() configureDependencyTransforms() configureCompileClasspath() configureBytecodeTransformASM() @@ -113,20 +116,36 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor // Configures Gradle dependency transforms. private fun HiltPluginEnvironment.configureDependencyTransforms() = project.dependencies.apply { + /** + * NOTE: 'CopyTransform' is intentionally used for directories to create 'Path Asymmetry.' + * + * In AGP 9.0+, if we use a 'Pure Rules' approach (making both android-classes and directories + * compatible via AttributeCompatibilityRule), Gradle discovers two competing transformation + * chains for every JAR artifact: + * 1. jar -> IdentityTransform -> android-classes-jar -> [Rule] hilt-all-classes + * 2. jar -> UnzipTransform -> directory -> [Rule] hilt-all-classes + * + * Both chains have the same length, thus giving Multiple transformation chains error. And + * disambiguation rules don't help choosing one over the other. + * + * The solution is to make the 'directory' chain one step more complex than the + * 'android-classes' chain. + * + * By using a 'CopyTransform' for directories and EXCLUDING 'directory' from the + * CompatibilityRules, we ensure that the 'Unzip' path is one step more complex than the + * android-classes path. This asymmetry allows Gradle to prioritize the Identity/Rule path for + * JARs and avoids the ambiguity error, while still providing a dedicated gateway for local + * project directories. The chains would look like: + * 1. jar -> IdentityTransform -> android-classes-jar -> [Rule] hilt-all-classes (preferred) + * 2. jar -> UnzipTransform -> directory -> CopyTransform -> hilt-all-classes + */ registerTransform(CopyTransform::class.java) { spec -> - // AGP has transforms from jar to android-classes for Java/Kotlin/Android libraries. - spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "android-classes") - spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE) - } - registerTransform(CopyTransform::class.java) { spec -> - // File Collection dependencies might be an artifact of type 'directory', e.g. when - // adding as a dep the destination directory of the JavaCompile task. spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "directory") - spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE) + spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, HILT_ALL_CLASSES_ARTIFACT_TYPE_VALUE) } registerTransform(AggregatedPackagesTransform::class.java) { spec -> - spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE) - spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, AGGREGATED_HILT_ARTIFACT_TYPE_VALUE) + spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, HILT_ALL_CLASSES_ARTIFACT_TYPE_VALUE) + spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, HILT_METADATA_CLASSES_ARTIFACT_TYPE_VALUE) } } @@ -149,7 +168,7 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor // runtime classpath has the tested dependencies removed in these cases. val artifactView = (testedVariant ?: variant).runtimeConfiguration.incoming.artifactView { view -> - view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE) + view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, HILT_ALL_CLASSES_ARTIFACT_TYPE_VALUE) view.componentFilter { identifier -> // Filter out the project's classes from the aggregated view since this can cause // issues with Kotlin internal members visibility. b/178230629 @@ -208,7 +227,10 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor testedVariant = testedVariant, classpath = project.files( - getHiltTransformedDependencies(configurations, AGGREGATED_HILT_ARTIFACT_TYPE_VALUE) + getHiltTransformedDependencies( + configurations, + HILT_METADATA_CLASSES_ARTIFACT_TYPE_VALUE, + ) ), ) @@ -219,7 +241,7 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor sources = project.files(aggregatingTask.map { it.outputDir }), classpath = project.files( - getHiltTransformedDependencies(configurations, DAGGER_ARTIFACT_TYPE_VALUE) + getHiltTransformedDependencies(configurations, HILT_ALL_CLASSES_ARTIFACT_TYPE_VALUE) ), ) @@ -398,6 +420,15 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor } } + private fun HiltPluginEnvironment.configureCompatibilityRules() { + project.dependencies.attributesSchema { schema -> + schema.attribute(ARTIFACT_TYPE_ATTRIBUTE) { attribute -> + attribute.compatibilityRules.add(HiltArtifactCompatibilityRule::class.java) + attribute.disambiguationRules.add(HiltArtifactDisambiguationRule::class.java) + } + } + } + private fun verifyDependencies(project: Project) { // If project is already failing, skip verification since dependencies might not be resolved. if (project.state.failure != null) { @@ -430,8 +461,10 @@ class HiltGradlePlugin @Inject constructor(private val providers: ProviderFactor companion object { private val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java) - const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger" - const val AGGREGATED_HILT_ARTIFACT_TYPE_VALUE = "aggregated-jar-for-hilt" + /** Includes all classes from `android-classes` and `directory` artifacts. */ + const val HILT_ALL_CLASSES_ARTIFACT_TYPE_VALUE = "hilt-all-classes" + /** A filtered view of hilt-all-classes that only includes the Hilt metadata. */ + const val HILT_METADATA_CLASSES_ARTIFACT_TYPE_VALUE = "hilt-metadata-classes" const val LIBRARY_GROUP = "com.google.dagger" diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactCompatibilityRule.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactCompatibilityRule.kt new file mode 100644 index 00000000000..0cc60294d8f --- /dev/null +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactCompatibilityRule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 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.rules + +import org.gradle.api.attributes.AttributeCompatibilityRule +import org.gradle.api.attributes.CompatibilityCheckDetails + +/** + * A compatibility rule for Hilt artifact types. This rule is used to allow the "hilt-all-classes" + * artifact type to be compatible with "android-classes" and "android-classes-jar". + */ +abstract class HiltArtifactCompatibilityRule : AttributeCompatibilityRule { + override fun execute(details: CompatibilityCheckDetails) { + if ( + details.consumerValue == "hilt-all-classes" && details.producerValue == "android-classes-jar" + ) { + details.compatible() + } + } +} diff --git a/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactDisambiguationRule.kt b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactDisambiguationRule.kt new file mode 100644 index 00000000000..00ad363073d --- /dev/null +++ b/java/dagger/hilt/android/plugin/main/src/main/kotlin/dagger/hilt/android/plugin/rules/HiltArtifactDisambiguationRule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 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.rules + +import org.gradle.api.attributes.AttributeDisambiguationRule +import org.gradle.api.attributes.MultipleCandidatesDetails + +/** + * A disambiguation rule for Hilt artifact types. This rule is used to disambiguate between + * "android-classes" when the consumer value is "hilt-all-classes". + */ +abstract class HiltArtifactDisambiguationRule : AttributeDisambiguationRule { + override fun execute(details: MultipleCandidatesDetails) { + if ( + details.consumerValue == "hilt-all-classes" && + details.candidateValues.contains("android-classes-jar") + ) { + details.closestMatch("android-classes-jar") + } + } +}