Add Gradle capability declarations to detect duplicate Guava artifacts
fixes #6666
Problem
Users can accidentally include duplicate Guava artifacts (guava-jdk5, guava-base, sisu-guava, etc.) alongside the main Guava library, causing classpath conflicts and runtime issues that are difficult to debug.
Solution
Declare that Guava provides the capabilities of known duplicate artifacts in module.json, following the existing google-collections pattern. This enables Gradle to detect and report conflicts at build time.
Changes
- Added capability declarations for 4 duplicate Guava artifacts in all 4 variant sections of module.json:
- com.google.guava:guava-base
- com.google.guava:guava-jdk5
- org.sonatype.sisu:sisu-guava
- org.hudsonci.lib.guava:guava
Note: The servicemix capability was removed after testing revealed it caused conflicts between Gradle variants.
Testing
# Build and install locally with module metadata
./mvnw install -pl guava -DskipTests -q
# Verify module metadata contains capability declarations
grep -E "(guava-base|guava-jdk5|sisu-guava|hudsonci)" \
~/.m2/repository/com/google/guava/guava/999.0.0-HEAD-jre-SNAPSHOT/guava-999.0.0-HEAD-jre-SNAPSHOT.module
# Expected: 20 lines (3 artifacts × 4 = 12, hudsonci × 4 = 4, total = 16 capability lines + 4 group lines)
# For Gradle users - test conflict detection after release
# Note: This will only show conflicts once the changes are in a released version
# The SNAPSHOT version may not resolve correctly from mavenLocal()
mkdir test-guava-conflict && cd test-guava-conflict
cat > build.gradle << 'EOF'
plugins { id 'java' }
repositories { mavenCentral() }
dependencies {
implementation 'com.google.guava:guava:NEXT_RELEASE_VERSION'
implementation 'com.google.guava:guava-jdk5:17.0'
}
EOF
gradle dependencies --configuration compileClasspath
# Expected after release: Capability conflict error
# Current behavior (without these changes): Both dependencies resolve without conflict
Breaking Changes
Builds that currently (incorrectly) include both Guava and duplicate artifacts will now fail with a capability conflict error. Users must resolve by excluding the duplicate artifact or using Gradle's capability resolution.
Why this breaking change is necessary:
- Prevents silent runtime failures (NoSuchMethodError, ClassNotFoundException)
- Having duplicate Guava classes leads to unpredictable classloading behavior
- Build-time failure is preferable to production runtime failure
- Follows the established pattern already used for google-collections
- Simple fix: exclude the duplicate or explicitly choose which one to use
- "Fail fast, fail loud, fail at build time - not in production"
Thanks. I took the liberty of pulling some more items from #6666 in https://github.com/google/guava/pull/7991. (Because of the specifics of how our process works, we always submit an internal CL, which generates a new GitHub PR, but it still gets attributed to the original contributor.) If you don't want your name to show up on the additions there, let me know, and I can add them in a separate change.
Huh, somehow this breaks the Gradle integration test:
> Task :androidAndroidConstraintCompileClasspathJava:testClasspath FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':androidAndroidConstraintCompileClasspathJava:testClasspath'.
> Could not resolve all files for configuration ':androidAndroidConstraintCompileClasspathJava:compileClasspath'.
> Could not resolve com.google.collections:google-collections:1.0.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.apache.servicemix.bundles:org.apache.servicemix.bundles.guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
> Could not resolve com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.apache.servicemix.bundles:org.apache.servicemix.bundles.guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
> Could not resolve com.google.guava:guava.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.apache.servicemix.bundles:org.apache.servicemix.bundles.guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
Any theories?
Sorry, I forgot to say:
That's https://github.com/google/guava/blob/master/util/gradle_integration_tests.sh. You might need to set JAVA_HOME to JDK11 or at least some version close to that.
@cpovirk Thanks for pointing me to the Gradle integration test
Any Theories?
What I found: The servicemix capability declaration creates an internal conflict within Guava's module metadata. Since all variants (jreApiElements, androidApiElements, etc.) declare they provide the same capability, Gradle rejects the entire module before it can apply variant selection rules.
I noticed the build config has special capability resolution logic for google-collections but not for servicemix:
https://github.com/google/guava/blob/master/integration-tests/gradle/build.gradle.kts#L115-L126
withCapability("com.google.collections:google-collections") {
candidates
.find {
val idField = it.javaClass.getDeclaredMethod("getId")
(idField.invoke(it) as ModuleComponentIdentifier).module == "guava"
}
?.apply { select(this) }
}
Rather than adding similar logic for servicemix, I've removed its capability declaration while keeping the other 4 capabilities.
The specific failing test now passes:
> Task :androidAndroidConstraintCompileClasspathJava:testClasspath
BUILD SUCCESSFUL in 12s
This seems like a reasonable compromise - we still get duplicate detection for the other artifacts while avoiding the variant conflict issue.
Thanks! Hmm, I wonder what's special about that one? Really, I wonder if that's the one that's behaving as expected: Maybe Gradle should be forcing us to resolve the conflict for all the libraries? Maybe, to make that happen, we need to list the various conflicting libraries as dependencies in the integration tests and probably as extraLegacyDependencies, too? And then it would force us to make the capability-resolution edit that you're suggesting?
But wait, maybe we're seeing the conflict even in the PR's current form?
Execution failed for task ':androidAndroidConstraintCompileClasspathJava:testClasspath'.
> Could not resolve all files for configuration ':androidAndroidConstraintCompileClasspathJava:compileClasspath'.
> Could not resolve com.google.collections:google-collections:1.0.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.hudsonci.lib.guava:guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
> Could not resolve com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.hudsonci.lib.guava:guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
> Could not resolve com.google.guava:guava.
Required by:
project :androidAndroidConstraintCompileClasspathJava
> Module 'com.google.guava:guava' has been rejected:
Cannot select module with conflict on capability 'org.hudsonci.lib.guava:guava:999.0.0-HEAD-android-SNAPSHOT' also provided by [com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(jreApiElements), com.google.guava:guava:999.0.0-HEAD-android-SNAPSHOT(androidApiElements)]
I wonder if the single conflict created by pulling in google-collections turns out to be enough to trigger the whole thing? If so, maybe we need only the capability-resolution edit that you're suggesting?
Agreed - we need resolution strategies. I've added them following the PR #3683 pattern and integration tests now pass on my local machine.
Added resolution strategies to build.gradle.kts for all capability conflicts:
guava-baseguava-jdk5sisu-guavaorg.hudsonci.lib.guava:guava(the one causing your error)listenablefuture
Your Hypothesis:
"I wonder if the single conflict created by pulling in
google-collectionsturns out to be enough to trigger the whole thing?"
Confirmed. The cascade effect works - we only needed resolution strategies, not explicit test dependencies for the 4 artifacts.
"If so, maybe we need only the capability-resolution edit that you're suggesting?"
Yes. Minimal approach appears sufficient.
See integration-tests/gradle/build.gradle.kts:115-171 for the changes.
Testing
cd integration-tests/gradle && ./gradlew testClasspath # BUILD SUCCESSFUL - all 12 tests passed including `:androidAndroidConstraintCompileClasspathJava:testClasspath`
@cpovirk any thoughts/requests on this?
I don't think, thanks. I just keep having things come up, but I do have this thread starred for when I finally think I have a spare moment.