I am having trouble following the guidelines and best practices related to cross-project artifact sharing combined with needing more explicit dependencies.
I have a root project with sub-projects setup and am sharing artifacts (jar) from one sub-project to a jar task in the root project level. Achieving this via strategy described in Gradle doc here has always worked. The producer updates a consumable configuration with the task that makes the jar.
The consumer maps that configuration into its own resolvable configuration.
Then the consumer creates its own jar, referencing in a from its configuration that points to the producer.
This has all worked well, until recent upgrade to Gradle 8.
Now I get the error about the dependency between the root jar task, that uses the root consumable configuration, thereby needing the jar task of the sub-project to be more explicit.
Reason: Task ':jarApp' uses this output of task
':webApp:warStatic' without declaring an explicit
or implicit dependency. This can lead to incorrect
results being produced, depending on what order
the tasks are executed.
I was expecting the configuration style reference to also imply a valid task depdendency. Apparently it does not.
What is the proper way to get a cross-project artifact scenario like described in the documentation example to also satisfy Gradle 8’s rule about more explicit task dependencies?
If Gradle 8 complains, it was most likely already incorrect in previous versions and just worked by luck. Gradle 8 just added recognition and warning of such problems.
If you did the setup correctly, the task dependency should indeed be there implicitly, as any explicit task dependency (unless a lifecycle task is on the right-hand side) is a code smell.
Can you share more details about your setup, especially how you set up the producer?
Maybe you can share an MCVE?
You are right, it never really worked right .
I just got lucky that the artifacts got built before I needed them to.
If they exist they are included, if not there is no dependency so they aren’t built nor included.
Basically my setup is as follows…
Producer sub-project
// create the war artifact
val warStaticTask = tasks.register<War>("warStatic") {
from(file("src/main/webapp/static"))
}
// add war artifact task to consumable configuration
var artConfig = configurations.create("published")
artConfig.isCanBeConsumed = true
artConfig.isCanBeResolved = false
project.artifacts {
add(artConfig.name, warStaticTask)
}
I also tried adding warStaticTask.get(), didn’t seem to make any difference.
Consumer root project
// Gather the published artifacts in a resolvable configuration
val allPublishedArtifacts: Configuration by configurations.creating
// wait until all the projects have been evaluated and set up their own
// published configurations, so we can collect them here
gradle.projectsEvaluated {
dependencies {
allprojects.forEach { p ->
val projectPublishedArtifacts = p.configurations.findByName("published")
if (projectPublishedArtifacts != null)
allPublishedArtifacts(project(mapOf("path" to p.path, "configuration" to "published")))
}
}
}
// create uber-jar of all sub-project artifacts
val jarAppTask = tasks.register<Jar>("jarApp") {
from(allPublishedArtifacts)
}
Result
when I run jarApp task, if the static war is already built, then it gets included in the uber-jar. If it does not exist then it does not and I get the message
file or directory '***/webApp/build/libs/static.war', not found
So I guess the artifact reference is there, since it does get included if it exists, but isn’t a proper task reference, so it is never setup as a dependency.
If I just put the mapping directly in the root project like this it seems to work
dependencies {
allPublishedArtifacts(project(mapOf("path" to ":webApp", "configuration" to PUBLISHED_ARTIFACTS)))
}
But I was trying to not hardcode all the sub-project references. If I remove the projectsEvaluated handler then the configuration is null for every sub-project (presumably because it hasn’t been configured yet).
But if I leave the projectsEvaluated, then it does find the configurations, but mapping them into the local resolvable configuration seems to break the dependency linking.
By using that “dynamic logic”, you actually totally destroy the proper publishing you tried to use by again going directly to the project model of the other projects. No wonder that this does not work properly.
Declare a dependency on all the projects and then use a lenient artifact view to only get the ones actually existing and not fail due to dependencies on non-existing artifacts.
No, that’s explicitly listing which you didn’t want.
Depend on all exisiting ones, whether they provide the artifact or not and then use a lenient artifact view to get the ones that actually exist.
Ok, so in the (root project) consumer I’m trying to follow what you suggest.
Is this a bit closer?
// Gather the published artifacts configuration of all sub-projects into the
// resolvable collection configurations here in the root project.
// We then reference that below when building the aggregate erapp.jar
// https://docs.gradle.org/current/userguide/cross_project_publications.html
val artifactConfigurationNames = mutableListOf(
"publishedArtifacts",
"publishedClasses",
"publishedDependencyArtifacts",
"publishedJar",
"publishedSourceArtifacts",
"publishedMainJar"
)
// create the collecting resolvable configuration here in the root project
artifactConfigurationNames.forEach { acn ->
val collectingName = acn + "All"
val cfg = configurations.create(collectingName)
cfg.isCanBeConsumed = false
cfg.isCanBeResolved = true
}
// for each publish config, collect contents from each subproject
// regardless of if the subproject has the configuration or not
subprojects.forEach { p ->
artifactConfigurationNames.forEach { acn ->
val collectingName = acn + "All"
val collectingConfig = configurations[collectingName]
dependencies {
collectingConfig(project(mapOf("path" to p.name, "configuration" to acn)))
}
}
}
// create lenient views of the collecting configurations that will
// not fail if a project doesn't have them
val publishedArtifactsAllView =
configurations["publishedArtifactsAll"]
.incoming
.artifactView { isLenient = true }
val publishedDependencyArtifactsAllView =
configurations["publishedDependencyArtifactsAll"]
.incoming
.artifactView { isLenient = true }
val publishedSourceArtifactsAllView =
configurations["publishedSourceArtifactsAll"]
.incoming
.artifactView { isLenient = true }
then the jar task in root project that uses them
val jarAppTask = tasks.register<Jar>("jarApp") {
from(publishedArtifactsAllView.files)
}
On the right track?
Any other tips or suggestions?
I’m on mobile right now, but yes, that looks better I think, if it works. I was not sure actually, whether it works with the “simple” way or only with the full-fledged attributes way.