I have a multiproject Gradle build consisting of multiple Java application subprojects and a base Java library subproject that each application references as a dependency. It should be possible to produce a Jar file for each application containing all of it's class files and those of its dependencies (aka fatJar) which is executed during the build
lifecycle.
Ideally I would just override the default Jar task so that it behaves like a "fatJar" by also including the dependencies files from the runtimeClasspath like this:
jar {
manifest.attributes 'Main-Class': application.mainClass
archiveBaseName = "${rootProject.name}-${project.name}"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it) }
}
But this throws a Gradle error:
Task ':app1:jar' uses this output of task ':app-base:jar' without declaring an explicit or implicit dependency.
Possible solutions:
1. Declare task ':app-base:jar' as an input of ':controlling-sim:jar'.
2. Declare an explicit dependency on ':app-base:jar' from ':controlling-sim:jar' using Task#dependsOn.
3. Declare an explicit dependency on ':app-base:jar' from ':controlling-sim:jar' using Task#mustRunAfter.
I don't understand why this exists even with the error explanation and guidance. It doesn't make sense having to declare an explicit dependency between tasks of different projects.
An alternative is to register a new Jar-type task called fatJar
but trying to make this execute during the build
lifecycle causes it to throw the same error as above, if I do:
buildSrc/src/main/groovy/application-conventions.gradle
tasks.assemble.dependsOn fatJar
Without the above dependency, the fatJar task works fine but it requires the user to run the fatJar
task manually.
Full project structure:
buildSrc/src/main/groovy/base-conventions.gradle
plugins {
id 'java-library'
}
repositories {
mavenLocal()
mavenCentral()
}
buildSrc/src/main/groovy/application-conventions.gradle
plugins {
id 'java'
id 'application'
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation project(':app-base')
}
jar {
manifest.attributes 'Main-Class': application.mainClass
archiveBaseName = "${rootProject.name}-${project.name}"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it) }
}
// Alternative...
/*tasks.register('fatJar', Jar) {
manifest {
attributes 'Main-Class': application.mainClass
}
group = 'build'
archiveBaseName = "${rootProject.name}-${project.name}"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it) }
}*/
app-base/build.gradle
plugins {
id 'base-conventions'
}
dependencies {
api 'com.xyz:lib-java:1.0'
implementation 'ch.qos.logback:logback-classic:1.3.14'
}
app1/build.gradle
plugins {
id 'application-conventions'
}
application {
mainClass = 'com.xyz.app1.Main'
}
Explicitly adding a dependency between the tasks seems to work but doesn't seem like the correct approach:
jar {
dependsOn project(":app-base").tasks.jar
}
Adding a dependency between the assemble task and my task:
tasks.assemble.dependsOn fatJar
Gradle is warning that the jar task now uses files that may be generated by something else without having a dependency on them. The "Possible solutions" message about how to fix it is not so helpful in this case though.
As an example, if this is a multi-project and a subproject is declared as a dependency for the runtimeOnly configuration (e.g. runtimeOnly project('something')
, then the standard jar task would not pick it up as it by default only knows cares about the output of this task, thus skipping the subproject. Another example would be a runtime dependency that has not been downloaded yet - if the task doesn't have a dependency on it, it will also be skipped.
To fix it, add a dependency to the configuration in use like this:
jar {
manifest.attributes 'Main-Class': application.mainClass
archiveBaseName = "${rootProject.name}-${project.name}"
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn configurations.runtimeClasspath // <-- This
from configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it) }
}
And if you choose to create a new fatJar task entirely, you will also need a from sourceSets.main.output
to include the project classes. (This implicitly adds a dependency to the compile task.)