Android X + Truth + Guava test compile issue
Asked Answered
L

4

12

I have an Android library (called api) gradle module as part of a larger project. I just migrated the whole project to AndroidX. I now have this error when running instrumentation test on the api lib:

 Task :api:checkDebugAndroidTestDuplicateClasses FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':api:checkDebugAndroidTestDuplicateClasses'.
> 1 exception was raised by workers:
  java.lang.RuntimeException: java.lang.RuntimeException: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-25.1-android.jar (com.google.guava:guava:25.1-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)

If I check the runtime class path for the debugAndroidTest variant:

./gradlew api:dependencies --configuration debugAndroidTestRuntimeClasspath | grep --color -E "guava|$"

I get this output. I can see the problem:

------------------------------------------------------------
Project :api
------------------------------------------------------------
debugAndroidTestRuntimeClasspath - Resolved configuration for runtime for variant: debugAndroidTest
+--- project :test_utils
|    +--- project :core
...
|    +--- project :api (*)
|    +--- com.google.android.material:material:1.1.0-alpha03
|    |    +--- androidx.annotation:annotation:1.0.1 -> 1.1.0-alpha01
|    |    +--- androidx.appcompat:appcompat:1.1.0-alpha01
|    |    |    +--- androidx.annotation:annotation:1.0.0 -> 1.1.0-alpha01
|    |    |    +--- androidx.core:core:1.1.0-alpha01 -> 1.1.0-alpha03
|    |    |    |    +--- com.google.guava:listenablefuture:1.0 // <------ GUAVA
|    |    |    |    +--- androidx.annotation:annotation:1.0.1 -> 1.1.0-alpha01
...
+--- com.google.truth:truth:0.42
|    +--- com.google.guava:guava:25.1-android / <------ MORE GUAVA
|    |    +--- com.google.code.findbugs:jsr305:3.0.2
|    |    +--- com.google.errorprone:error_prone_annotations:2.1.3 -> 2.3.1
|    |    +--- com.google.j2objc:j2objc-annotations:1.1
|    |    \--- org.codehaus.mojo:animal-sniffer-annotations:1.14
|    +--- org.checkerframework:checker-compat-qual:2.5.3
|    +--- org.checkerframework:checker-qual:2.5.3
|    +--- junit:junit:4.12 (*)
|    +--- com.googlecode.java-diff-utils:diffutils:1.3.0
|    +--- com.google.auto.value:auto-value-annotations:1.6.2
|    \--- com.google.errorprone:error_prone_annotations:2.3.1
...

AndroidX core is depending on the new "ListableFuture-only" build of guava and Truth is depending on the full Guava 25.

I think I understand the underlying problem with ListenableFuture: https://groups.google.com/forum/#!topic/guava-announce/Km82fZG68Sw

What is the right solution here?

I don't want to exclude Guava from Truth entirely (otherwise Truth won't compile)

androidTestImplementation("com.google.truth:truth:0.42") {
    exclude group: 'com.google.guava', module: 'guava'
}

Can I exclude + force update to Guava 27 by making it a first-level dependency:

androidTestImplementation("com.google.truth:truth:$rootProject.ext.truthVersion") {
    exclude group: 'com.google.guava', module: 'guava'
}
// must add guava as top level dependency to force Truth to use latest version
androidTestImplementation 'com.google.guava:guava:27.0.1-android'

If I do this, should I be using the android or JRE version of guava?

Side question:

Why don't I see the guava dependency when looking at compile classpath? The error is a compile time error, not a runtime error

./gradlew api:dependencies --configuration debugAndroidTestCompileClasspath | grep --color -E "guava|$"

Resulting deps:

debugAndroidTestCompileClasspath - Resolved configuration for compilation for variant: debugAndroidTest
+--- project :test_utils // <----------- why are test_utils deps not listed here???
...
+--- com.google.truth:truth:0.42
|    +--- com.google.guava:guava:25.1-android <------ GUAVA
|    |    +--- com.google.code.findbugs:jsr305:3.0.2
|    |    +--- org.checkerframework:checker-compat-qual:2.0.0 -> 2.5.3
|    |    +--- com.google.errorprone:error_prone_annotations:2.1.3 -> 2.3.1
|    |    +--- com.google.j2objc:j2objc-annotations:1.1
|    |    \--- org.codehaus.mojo:animal-sniffer-annotations:1.14
|    +--- org.checkerframework:checker-compat-qual:2.5.3
|    +--- org.checkerframework:checker-qual:2.5.3
|    +--- junit:junit:4.12 (*)
|    +--- com.googlecode.java-diff-utils:diffutils:1.3.0
|    +--- com.google.auto.value:auto-value-annotations:1.6.2
|    \--- com.google.errorprone:error_prone_annotations:2.3.1
...
+--- com.google.truth:truth:{strictly 0.42} -> 0.42 (c)
+--- com.google.guava:guava:{strictly 25.1-android} -> 25.1-android (c) // <--------- why is this listed again here at top level?

Update:

After updating to Truth 0.43 I'm now seeing this error:

> Could not resolve all artifacts for configuration ':mymodule:debugAndroidTestRuntimeClasspath'.
   > Could not resolve com.google.guava:listenablefuture:{strictly 1.0}.
     Required by:
         project :mymodule
      > Cannot find a version of 'com.google.guava:listenablefuture' that satisfies the version constraints:
           Dependency path 'example:mymodule:unspecified' --> 'com.google.truth:truth:0.43' --> 'com.google.guava:guava:27.0.1-android' --> 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           Dependency path 'example:mymodule:unspecified' --> 'example:myothermodule:unspecified' --> 'andrexampleoidx.core:core:1.1.0-alpha04' --> 'com.google.guava:listenablefuture:1.0'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
            ...
           Dependency path 'example:mymodule:unspecified' --> 'example:myothermodule:unspecified' --> 'androidx.core:core:1.1.0-alpha04' --> 'androidx.concurrent:concurrent-futures:1.0.0-alpha02' --> 'com.google.guava:listenablefuture:1.0'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           ...

   > Could not resolve com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava.
     Required by:
         project :mymodule > com.google.truth:truth:0.43 > com.google.guava:guava:27.0.1-android
      > Cannot find a version of 'com.google.guava:listenablefuture' that satisfies the version constraints:
           Dependency path 'example:mymodule:unspecified' --> 'com.google.truth:truth:0.43' --> 'com.google.guava:guava:27.0.1-android' --> 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           Dependency path 'example:mymodule:unspecified' --> 'example:myothermodule:unspecified' --> 'androidx.core:core:1.1.0-alpha04' --> 'com.google.guava:listenablefuture:1.0'
           ...
           Dependency path 'example:mymodule:unspecified' --> 'example:myothermodule:unspecified' --> 'androidx.core:core:1.1.0-alpha04' --> 'androidx.concurrent:concurrent-futures:1.0.0-alpha02' --> 'com.google.guava:listenablefuture:1.0'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           ...

   > Could not resolve com.google.guava:listenablefuture:1.0.
     Required by:
         project :mymodule > androidx.core:core:1.1.0-alpha04
         project :mymodule > androidx.concurrent:concurrent-futures:1.0.0-alpha02
      > Cannot find a version of 'com.google.guava:listenablefuture' that satisfies the version constraints:
           Dependency path 'example:mymodule:unspecified' --> 'com.google.truth:truth:0.43' --> 'com.google.guava:guava:27.0.1-android' --> 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           ...
           Dependency path 'example:mymodule:unspecified' --> 'example:myothermodule:unspecified' --> 'androidx.core:core:1.1.0-alpha04' --> 'androidx.concurrent:concurrent-futures:1.0.0-alpha02' --> 'com.google.guava:listenablefuture:1.0'
           Constraint path 'example:mymodule:unspecified' --> 'com.google.guava:listenablefuture:{strictly 1.0}' because of the following reason: debugRuntimeClasspath uses version 1.0
           ...

If I understand correctly:

  • Truth 0.43 depends on Guava 27
  • Guava 27 depends on 9999.0-empty because it internally contains a copy of ListenableFuture and version 9999.0-empty is well "empty".
  • other libs like AndroidX core depend on listenablefuture:1.0
  • When resolving conflicts (should I use version 1.0 or 9999.0?) Gradle will pick the latest version, in this case 9999.0.
  • So AndroidX core is actually getting ListenableFuture from classpath via Guava, but it's none-the-wiser.
  • However, the strictly keyword is forcing the usage of 1.0
  • this is happening because Android Gradle Plugin forces runtime and compiletime class paths to depend on same version =( read more here

So at the moment I'm stuck as to what to do now.

Llanes answered 11/2, 2019 at 17:25 Comment(0)
N
6

To fix the conflict, merely adding a dependency on Guava 27 should be enough. Doing so should automatically (though the "Version 99 Does Not Exist" hack described in the post you linked) prevent a second copy of ListenableFuture from being pulled in. (Please let us know if not!)

Since you're working on an Android library, it sounds like you want guava-27.0.1-android rather than -jre.

I don't know anything about your side question, sorry. And sorry for the lack of a response for so long.

Norry answered 25/2, 2019 at 17:18 Comment(7)
Additionally: I've just released Truth 0.43, which has its Guava dep updated to 27.0.1-android. You may be able to eliminate the problem just by updating to that version of Truth. Sorry for the trouble.Norry
Latest version of Truth didn't totally solve the problem. In the end we just pulled Truth from integration tests and use Truth on just local (java-only, no-android) tests.Llanes
Wow, yuck. Thanks for all the investigation, and sorry for the trouble. Largely for my reference, here are the commit that added the behavior and a bug reporting a related problem, which links to docs on resolving conflicts. Your link suggests that android.dependency.useConstraints=false may help.Norry
Does it help to force your tests to depend on listenablefuture:1.0? That will result in your having 2 copies of ListenableFuture on your test classpath, but they will work identically, so that's not necessarily a problem. But I think that the Android build system may complain anyway?Norry
(Another thing we could consider here is to release a special separate artifact for Truth that declares its classes -- and, importantly, Guava's classes -- in a separate package. That would prevent it from conflicting with the main Guava and ListenableFuture. (I suppose that users could also do this rewriting themselves.) This special Truth wouldn't interact well with extensions to the "real" Truth, but at least it should work in your case. Something for us to consider, depending on whether the workarounds help and whether this problem comes up often. Sorry again for the trouble.)Norry
Your talking about shading the Guava and ListenableFuture dependencies? I don't know much about this but IIUC this would be a big thing. I'm happy to participate in a discussion on the github repo but I don't want to ask for a whole discussion here in this tiny corner of the internet =)Llanes
Yeah, shading is what I was thinking about. I hope it doesn't come to that, though; I'm mostly just thinking aloud. I will try to talk to the Android build people sometime about alternatives.Norry
S
2

something alike this should prevent the duplicate:

implementation ("com.google.android.material:material:1.1.0-alpha03") {
    exclude group: "com.google.guava", module: "listenablefuture"
}

the part which updates the library seems correct:

androidTestImplementation "com.google.guava:guava:27.0.1-android"
androidTestImplementation ("com.google.truth:truth:0.42") {
    exclude group: "com.google.guava", module: "guava"
}

can only assume without a build.gradle.

Selfdriven answered 25/2, 2019 at 17:16 Comment(0)
N
0

Another thing that should probably "work" is to make your app itself (not just the tests, but the whole app) depend on guava:27.0.1-android. If your build uses Proguard, it should all get removed (aside from ListenableFuture) during the build. If you're not, though, then Guava is a big dependency to be pulling in, especially just to work around a bad interaction between our listenablefuture trick and the subsequent Android Gradle plugin :(

Norry answered 14/3, 2019 at 16:45 Comment(0)
B
0

I had the same (or similar?) problem and it seems on my side it was that I was using both androidx.test.ext:truth and com.google.truth:truth. Settling with the first made the issue go away since internally they depend on different versions of same libraries.

Bullpup answered 10/6, 2021 at 12:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.