I’m currently facing an issue when building a rather complex application with a lot of different gradle modules. I was able to break it down into a minimal example with just two gradle modules to reproduce the issue.
Issue
When creating a far jar for the final application module using:
apply plugin: 'java'
apply plugin: 'jacoco'
sourceSets {
main {
output.resourcesDir = "build/classes/java/main"
}
}
task fatJar(type: Jar, group: 'build') {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
dependencies {
implementation project(":web:client")
}
17:31:48: Executing 'fatJar'...
> Task :libgdx:desktop:processResources NO-SOURCE
> Task :web:client:compileJava
> Task :libgdx:desktop:compileJava NO-SOURCE
> Task :libgdx:desktop:classes UP-TO-DATE
> Task :libgdx:desktop:fatJar FAILED
2 actionable tasks: 2 executed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':libgdx:desktop:fatJar'.
> Cannot expand ZIP 'D:\Development\Projects\gradle-test\web\client\build\libs\client.jar' as it does not exist.
it looks like gradle doesn’t execute the :web:client:jar task when using the java plugin with implementation dependencies.
Why is this the case? I would expect gradle to build the dependencies correctly.
This doesn’t happen when using the configurations.compileClasspath for building the far jar. But this has the issue that the files from the resources folders of the dependent modules are not included in the jar.
For now I’ve manually declated fatJar dependsOn [':web:client:jar'] but this feels like a hack.
Is this the expected gradle behavior? Anyone got some more elegant solutions?
Also I’ve noticed that when using a chain of module dependencies, gradle calls :jar for some of them, but I don’t understand when/why.
Maybe you don’t want to hear it, but I have to start with telling you not to build a fat jar.
They are evil, have many pitfalls you have to care about and are an abuse of Java functionality.
Well except if done properly like Spring Boot built fat jars.
See https://fatjar.net for more quotes about it.
I personally strongly recommend you build a proper distribution using the application plugin instead, then you end up with a distributable archive with your dependencies, your code, configured additional files if you want so, and generated start scripts that properly put everthing on the class path as needed.
To fix that particular problem you have, you can do dependsOn(configurations.runtimeClasspath) and all necessary tasks will be depended upon, but of course all explicit dependsOn are a code smell, but well, you also build a fat jar.
With compileClasspath it works for the simple reason that it does not contain the jar for the java-library dependency, but the classes folder as only that is necessary for compilation so the jar-building would be wasted time. And it also only accidentally works in that case as the compilation of the jar contents depend on those classes being compiled already and you tell to include the jar contents.
If you really insist on building a fat jar, I’d recommend to at least use the shadow plugin of John Engelman which takes care at least about some of the pitfalls you will get trapped by when working with fat jars, even if not all.
Thanks for your quick reply!
Yes I know that building far jars isn’t ideal, however in that case it’s required because of other constraints further down the line (signing, update mechanism, cdn architecture, packaging, ect.).
I also tried the shadow plugin a couple of years ago with this project but it failed to work correctly and the latest release doesn’t work when being used with a custom task definition (it generates completely wrong jars, either only resources or only some classes).
Using the custom far-jar approach worked very well over the last years fo us, and your hint to use dependsOn with the runtime classpath did indeed resolve the issue, thanks!