Multiple dependency versions with Gradle 5 - java

I need to support 2 different versions of the same library (to support a legacy version), es4hadoop for Apache Spark.
Version 6.2.2(https://mvnrepository.com/artifact/org.elasticsearch/elasticsearch-spark-13_2.10/6.2.2)
Version 6.3.2 (https://mvnrepository.com/artifact/org.elasticsearch/elasticsearch-spark-13_2.10/6.3.2)
Both versions have same dependencies (scala-lang and Spark).
Not sure at all about the naming of that, but I would like something like:
implementation(group: 'org.elasticsearch', name: 'elasticsearch-spark-13_' + scalaVersion, version:'6.2.2') {
exclude group: "org.scala-lang"
}
implementation(group: 'org.elasticsearch', name: 'elasticsearch-spark-13_' + scalaVersion, version:'6.3.2') {
exclude group: "org.scala-lang"
relocate org.elasticsearch org.elasticsearch6 // ???
}
so I can use both new and old elasticsearch library, in the same project / JVM.
I know already it's possible to relocate lib with the shadowJar plugin, but is it possible to relocate a specific version?

Put one of the elasticsearch-spark dependencies into a subproject aa2 and relocate it. Then the other subproject aa1 can depend on aa2's shadow configuration.
// aa2/build.gradle
dependencies {
implementation 'org.elasticsearch:elasticsearch-spark-13_2.10:6.2.2'
}
shadowJar {
relocate "org.elasticsearch", "org.elasticsearch_v6_2_2"
}
// aa1/build.gradle
dependencies {
implementation 'org.elasticsearch:elasticsearch-spark-13_2.10:6.3.2'
implementation project(path: ':aa2', configuration: 'shadow')
}
You can now declare the same class in this way:
package com.github.chehsunliu.stackoverflow.q56332118;
import org.elasticsearch.spark.cfg.SparkSettings;
public class App {
public static void main(String[] args) {
System.out.println(SparkSettings.class);
System.out.println(org.elasticsearch_v6_2_2.spark.cfg.SparkSettings.class);
}
}
However, you should pay more attention to their transitive dependencies. You might also need to relocate them to make the direct dependencies work normally. Sometimes I will decompress the output JAR file and investigate these .class files to ensure the resolution correctness.
References
Minimal project: https://github.com/chehsunliu/stackoverflow/tree/main/gradle/q56332118
Decompiler https://java-decompiler.github.io/

Related

How to let dependencies in modules have higher precedence over build.gradle in the top level repository

My project repository tree looks like the following:
Project
module1
module1.gradle
module2
module2.gradle
...
build.gradle
Then in the build.gradle file there are some dependencies and some forced resolutionStrategies like
configurations.all {
resolutionStrategy {
cacheChangingModulesFor 0, 'seconds'
force 'org.apache.hadoop:hadoop-common:2.5.0-cdh5.3.3',
}
}
dependencies {
compile(group: 'org.apache.hadoop', name: 'hadoop-common', version: '2.6.0-cdh5.16.1')
}
However, module1.gradle defines a later version of hadoop-common like
compile group: 'org.apache.hadoop', name: 'hadoop-common', version: '2.5.0-cdh5.3.3'
I want to make this upgrade to my module1, module2..moduleX to use the '2.6.0-cdh5.16.1'. There are some projects that don't need the upgrade and should keep using the older version so I need to keep the forced dependencies in the top level build.gradle file. In fact, I don't want to touch any modules that I don't own.
Is there a way to specify at the module level to ALWAYS use whatever it is defined in the current module instead of inheriting from the top level build.gradle?
The force(..) statement at the top level is intended to override whatever version is specified in the modules.
You can re-override that force(..) statement by placing another force(..) statement inside the module build. This second force statement is evaluated after the top-level one, so it will re-override the module version.
You can place in the build.gradle file for your module:
configurations.all {
resolutionStrategy {
force 'org.apache.hadoop:hadoop-common:2.6.0-cdh5.16.1'
}
}
Note though that overriding and re-overriding versions can cause confusion. In example, people editing the top-level build will expect their force to be rightly applied to all modules- which isn't the case if you re-override that in the module.

Versionless gradle dependencies

When I do this
dependencies {
file('libs/something.jar')
}
I have something.jar in my distribution.
But, when I to push the dependency to remote repository (ivy or maven) and want to use it from there, gradle always adds a version postfix to the jar.
dependencies {
compile group: 'org.company', name: 'something', version: '1.0'
}
results in something-1.0.jar.
Even this
dependencies {
compile group: 'org.company', name: 'something'
}
results in something-.jar (note the dash).
Can I somehow prevent the repository dependency to have a version?
(My motivation is that the jar is a 3rd party jar, I don't want to have it in git repo but I also don't want its name to change.)
As you've said, you're ok with giving the artifact a version in the repository. You just don't want a version in the jar in your application. You could do
configurations {
something { transitive = false }
}
dependencies {
something 'org.company:something:1.0'
something 'org.company:something-else:1.0'
compile files(tasks['dummyTask'])
compile 'org.foo:some-normal-dep:1.1'
}
task copySomething(type:Copy) {
from configurations.something
into "$buildDir/something"
rename '(.+)-.+?\\.jar', '$1.jar'
}
task dummyTask {
dependsOn copySomething
inputs.dir "$buildDir/something"
outputs.files fileTree("$buildDir/something")
}
The main enabler for this is that Project.files(...) can accept a Task

Switching dependencies in Gradle depending on platform

I am trying to get Gradle to select different dependencies in my multiproject build based on whether I am building for desktop or for Android. I have a common subproject (a library) I am trying to reuse. However, I cannot get Gradle to correctly switch dependency configurations.
My main settings.gradle simply includes all the dependencies:
// /settings.gradle
rootProject.name = 'myProject'
include 'androidUI'
include 'reusableLibrary'
include 'desktopUI'
Now both androidUI and desktopUI specify reusableLibrary as a dependency:
// /androidUI/build.gradle and /desktopUI/build.gradle
apply plugin: 'java'
dependencies {
compile project(path: ':reusableLibrary', configuration: 'desktop')
}
reusableLibrary itself specifies two configurations, because its dependencies are different whether it's building on desktop or Android:
// /reusableLibrary/build.gradle
apply plugin: 'java'
configurations {
desktop {
extendsFrom compile
}
android {
extendsFrom compile
}
}
dependencies {
// Just examples, the real list is longer.
// The point is that h2database is only included on desktop,
// and ormlite is only included on Android.
android 'com.j256.ormlite:ormlite-jdbc:5.0'
desktop 'com.h2database:h2:1.4.192'
}
This looks fine to me. But when I compile either desktopUI or androidUI, I can see that although the dependencies of reusableLibrary are being included on the classpath in the manner I desire, the actual JAR provided by reusableLibrary itself is not included. This of course causes the build to fail. I suspect I'm not setting up reusableLibrary correctly; I'm not clear on what the configurations {} blocks do.
Why aren't the compiled items in reusableLibrary being included on the classpaths of the UI projects? And what is the canonical way to include platform-specific dependencies in this manner?
The original configuration is pretty close to right. The key is to understand this dependency graph from the Gradle Java plugin's documentation:
This is a visualization of the Java plugin's various dependency configurations, which is Gradle-ese for "list of dependencies." When you add compile lines to a dependencies {...} block, you're adding Dependency elements to the compile dependency list.
The default dependency configuration is special; it is the one included by a compile project("path") line unless a different one is chosen with the configuration: argument. This means that when you build the library, the runtime dependency list (which includes the compiled jar from the library itself) is added to the classpath of the client project.
The original configuration creates two new nodes, desktop and android in this graph, and couples them both to compile by using extendsFrom. They are not otherwise connected to the graph! Now the problem with the original configuration is apparent: by switching the upstream project to either of these, it is missing the compiled code from runtime. This explains the classpath omission.
The solution is a bit more subtle than just aiming desktop and android at runtime. In order to ensure that everything is correctly decoupled when we add tests, we need one extra layer of dependency configurations to keep testCompile from indirectly depending on runtime. Additionally, the library's source code itself may need things on its classpath just to typecheck; we can use compileOnly for this. The end solution looks like this:
configurations {
desktopCompile
androidCompile
compileOnly.extendsFrom desktopCompile
testCompile.extendsFrom desktopCompile // Assuming tests run on the desktop
desktop {
extendsFrom desktopCompile
extendsFrom runtime
}
android {
extendsFrom androidCompile
extendsFrom runtime
}
}
dependencies {
androidCompile "some.android:dependency"
desktopCompile "other.desktop:dependency"
}

How do I include a single dependency in my JAR with Gradle?

I'm starting with Gradle and I was wondering how do I include a single dependency (TeamSpeak API in my case) into my JAR so that it could be available at the runtime.
Here is a part of my build.gradle :
apply plugin: 'java'
compileJava {
sourceCompatibility = '1.8'
options.encoding = 'UTF-8'
}
jar {
manifest {
attributes 'Class-Path': '.......'
}
from {
* What should I put here ? *
}
}
dependencies {
compile group: 'org.hibernate', name: 'hibernate-core', version: '4.3.7.Final'
compile group: 'org.spigotmc', name: 'spigot', version: '1.8-R0.1-RELEASE'
// Many other dependencies, all available at runtime...
// This one isn't. So I need to include it into my JAR :
compile group: 'com.github.theholywaffle', name: 'teamspeak3-api', version: '+'
}
Thanks for your help :)
The easiest way is to start with a separate configuration for the dependencies you want to include. I know you only asked about a single jar but this solution will work if you add more dependencies to your new configuration. Maven has a well known name for this sort of thing called provided, so that is what we will use.
configurations {
provided
// Make compile extend from our provided configuration so that things added to bundled end up on the compile classpath
compile.extendsFrom(provided)
}
dependencies {
provided group: 'org.spigotmc', name: 'spigot', version: '1.8-R0.1-RELEASE'
}
jar {
// Include all of the jars from the bundled configuration in our jar
from configurations.provided.asFileTree.files.collect { zipTree(it) }
}
Using provided as the name of the configuration is also important because when the jar gets published, any dependencies you have in the providedconfiguration will show up as provided in the POM.xml that gets published with the JAR. Maven dependency resolvers will not pull down provided dependencies and users of your jar will not end up with duplicate copies of classes on the classpath. See Maven Dependency Scopes

How can I force Gradle to set the same version for two dependencies?

I use the following two dependencies:
compile 'com.google.guava:guava:14.0.1'
compile 'com.google.guava:guava-gwt:14.0.1'
Both must be the same version to work correctly. Since my other dependencies use a higher version, Gradle uses different versions for each dependency.
I found this by running gradle dependencies:
compile - Compile classpath for source set 'main'.
+--- com.google.guava:guava:14.0.1 -> 17.0
+--- com.google.guava:guava-gwt:14.0.1
| +--- com.google.code.findbugs:jsr305:1.3.9
| \--- com.google.guava:guava:14.0.1 -> 17.0
How can I force Gradle to set the same version for these two dependencies?
Add this section to just above your dependencies block.
Groovy/Gradle:
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:14.0.1'
force 'com.google.guava:guava-gwt:14.0.1'
}
}
Kotlin Script:
configurations.all {
resolutionStrategy {
force("com.google.guava:guava:14.0.1")
force("com.google.guava:guava-gwt:14.0.1")
}
}
configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.google.guava') {
details.useVersion "14.0.1"
}
}
}
dependencies {
compile 'com.google.guava:guava'
compile 'com.google.guava:guava-gwt'
}
I had a similar situation where one of the dependencies used spring-web 4.2.4 which was broken. You have to force specific library version you want. As mentioned in another comment, it might cause compatibility issues but sometimes is necessary.
Least intrusive way of forcing a library version I found was instead of using
compile "org.springframework:spring-web:4.2.3.RELEASE"
specifying dependency configuration as forced:
compile("org.springframework:spring-web:4.2.3.RELEASE"){
force = true
}
I used it when I needed to downgrade Spring version temporarily (until next release).
One of your dependencies is forcing the guava version to update. Use gradle dependencies to locate which library is evicting your version.
The problem you have is that if you force it to use 14.0.1 another library may not work properly. Can you not just use the 17.0 version as your dependency?
Rather than maintain individual version numbers in the build.gradle I use a dependencies.gradle file which will have a mapping of version numbers and pull that into the build.gradle. That way I only need to maintain the single guava version. So your example will be:
dependencies.gradle
ext {
ver = [
guava: '14.0.1'
]
}
and then in the build.gradle file you can have:
apply from: "dependencies.gradle"
dependencies {
compile group: 'com.google.guava', module: 'guava', version: ver.guava
compile group: 'com.google.guava', module: 'guava-gwt', version: ver.guava
}
then when I want to move to 17.0 I only need to change the dependencies.gradle.
I am also a definite fan of setting transitive dependencies to false with
configurations.compile { transitive = false }
this way you do not have some dependencies evicted at compile time, although you may have a problem at run time if the evicting library is not fully backward compatible. Lets face it if you are writing the code you should know what libraries you use and you should be explicit about your dependencies. It protects you from one of your dependencies upgrading and messing you up.
Another option is to use dependency constraint: https://docs.gradle.org/current/userguide/dependency_constraints.html
dependencies {
implementation 'org.apache.httpcomponents:httpclient'
constraints {
implementation('org.apache.httpcomponents:httpclient:4.5.3') {
because 'previous versions have a bug impacting this application'
}
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}
I would suggest against setting transitive = false, as this approach would force yo to resolve the dependency tree yourself, manually.
You could either force the desired guava version via configurations.all, or add the dependency explicitly and set it forced = true.
Examples here: http://www.devsbedevin.net/android-understanding-gradle-dependencies-and-resolving-conflicts/
This is for Kotlin DSL (build.gradle.kts) tested with Gradle 7.1:
dependencies {
implementation("org.jsoup", "jsoup") {
version {
strictly("1.14.3")
}
}
}
Another way:
dependencies {
implementation("org.jsoup:jsoup") {
version {
strictly("1.14.+") // You can also use dynamic versions
}
}
}
Another way as suggested by other answers:
configurations.all {
resolutionStrategy {
force("org.jsoup:jsoup:1.14.3")
force("com.google.guava:guava-gwt:14.0.1")
}
}
Alternatively you can use dependencySets (or mavenBom when BOM POM is available) support in spring-dependency-management Gradle plugin. Note that this plugin is also automatically applied with spring-boot Gradle plugin. For more details see here.
plugins {
id 'io.spring.dependency-management' version '1.0.1.RELEASE'
}
dependencyManagement {
dependencies {
dependencySet(group: 'com.google.guava', version: '14.0.1') {
entry 'guava'
entry 'guava-gwt'
}
}
}
dependencies {
compile 'com.google.guava:guava'
compile 'com.google.guava:guava-gwt'
}
If it's OK to just use the newer version for both dependencies, the simplest way to fix your problem is to update your dependencies:
compile 'com.google.guava:guava:17.0'
compile 'com.google.guava:guava-gwt:17.0'
That will make sure both of them are on 17.0. It's simpler than trying to force both of them on the older version and as an added bonus you get a newer version, which (probably) comes with bug fixes and new features.
To be fair #Klunk mentions this in his answer, by asking "Can you not just use the 17.0 version as your dependency?", but it's just in passing and easy to miss, so I thought it made sense to post as a separate answer.
You can "force" versions for the libraries/dependencies in gradle like this:
configurations.all {
resolutionStrategy {
force("org.jetbrains.kotlin:kotlin-stdlib:1.4.32",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32",
"org.jetbrains.kotlin:kotlin-reflect:1.4.32"
)
}
}
You can get more information on this here: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html
app/build.gradle
dependencies {
compile('com.google.guava:guava') {
version {
strictly '14.0.1'
}
}
compile('com.google.guava:guava-gwt') {
version {
strictly '14.0.1'
}
}
}
Version Gradlew
Gradle 6.7

Categories