How to resolve the runtimeClasspath of another sub-project

Hi!

I created a zip task to package a web application with a webserver. As part of this process I want to exclude all jar files from the exploded war file if they are already present in the webserver’s classpath.

I have the following in the app’s build.gradle.kts file (the buildZip task is registered in a convention plugin, as it is shared by several subprojects and contains the common part of the code):

tasks.buildZip {

    val webserverClasspath = project(":webserver").configurations.runtimeClasspath.get().files.map { it.name }

    into("$baseDirName/webapps/myapp") {
        from(zipTree(tasks.war.get().outputs.files.asPath)) {
            exclude { webserverClasspath.contains(it.name) }
        }
    }
}

This seems to work fine, but now I am getting the following warning that this will no longer work in Gradle 9.0.

Resolution of the configuration :webserver:runtimeClasspath was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. This will fail with an error in Gradle 9.0. For more information, please refer to  in the Gradle documentation.

I took a look at the documentation as instructed in the message, but I still can’t figure out how to make this work.

(As a side note, it seems that the from line isn’t entirely correct either, because I had to include dependsOn(tasks.war) in the task definition to make sure that the war task gets executed first.)

Can somebody point me in the right direction how to implement this properly?

Thanks in advance!

This seems to work fine

“seems to” is the correct term.
Well, it might somehow work eventually under some conditions.
But it is a very bad idea.
Reaching into the model of another project is almost as bad as doing cross-project configuration and should be avoided as hell, as it can lead to quite some problems and also might not work like expected or also fail to work in the future.
The actual deprecation warning you get just happens to be one of these problems.

If you need the runtime classpath of the other project, create a dependency scope configuration, declare a dependency on the webserver project, create a resolvable configuration that extends the dependency scope configuration and requests the typical attributes for JVM dependencies. If you then resolve that configuration, you should get the runtime dependencies.

But also excluding those by file-name is quite fragile actually so think twice if you really want to do it that way.

Maybe a better way would be to actually declare those dependencies as providedCompile right away in the buildZip containing project, so that you don’t have to exclude anything, as you only have them as compile dependencies anyway.

Or actually if you add the dependency to webserver as providedCompile this should also already be enough to not have the packaged in the war as the providedCompile should exclude transitively.


Also, any get() (or similar) you do at configuration time is a quite bad idea, because you break the laziness of configuration and might miss configuration changes done later in the build execution and also loose implicit task dependencies.

As a side note, it seems that the from line isn’t entirely correct either, because I had to include dependsOn(tasks.war) in the task definition to make sure that the war task gets executed first.

Yes, definitely, there are multiple problems in that.
For example, the zip is the only output, so resolving it to a path just to give it to zipTree is just visual clutter and also not too clean as it is a path, not a file.
Just doing zipTree(tasks.war.get().archiveFile) would also work and even preserve the task dependency as archiveFile is a proper output property that contains the task dependency.
But you still have the evil get(), so zipTree(tasks.war.flatMap { it.archiveFile }) would be a bit better.

But even that is quite non-sense, because you build the war, just to again unzip it to temporary location, just to then again add it to a new zip file, that is a big waste of time.

What you actually want is to replace the whole from by with(tasks.war.get()) which reuses the CopySpec part of the War task for this new zip task so that it just copies the same files without packing and unpacking a zip needlessly.

Yes, this again has a .get(), but here it indeed is kind of ok, as with cannot handle Provider (yet, see Support CopySpec#with(Provider<CopySpec>) · Issue #10008 · gradle/gradle · GitHub) but needs the plain CopySpec currently. But as this is then evaluated late by the task, this is acceptable until #10008 is fixed.

Thank you for the detailed response. It took me a while to sort everything out, but I think I finally got it working.

Like I mentioned, before posting the question I reviewed this page in the doc that is linked in the error message:

https://docs.gradle.org/8.14.4/userguide/how_to_share_outputs_between_projects.html#variant-aware-sharing

But the samples didn’t work (I got syntax errors in IntelliJ) and anyway they didn’t look anything like what you were suggesting. Then I switched to the current (9.5.1) version of this page:

https://docs.gradle.org/current/userguide/how_to_share_outputs_between_projects.html#variant-aware-sharing

and then everything started to make more sense. So, based on your suggestion and the example in the doc this is what I came up with:

val webserverRuntimeDependencies by configurations.dependencyScope("webserverRuntimeDependencies")

val webserverRuntime by configurations.resolvable("webserverRuntime") {
    extendsFrom(webserverRuntimeDependencies)
}

tasks.buildZip {

    val webserverClasspath = webserverRuntime.files.map { it.name }

    into("$baseDirName/webapps/myapp") {
        with(tasks.war.get())
        exclude { webserverClasspath.contains(it.name) }
    }
    // For debugging
    doLast {
        println(webserverClasspath)
    }
}

You also mentioned to request the “typical attributes for JVM dependencies”, but I am not sure what those are, so I tried the following:

I displayed the attributes of runtimeClasspath:

$ ./gradlew :webserver:outgoingVariants --variant runtimeClasspath

> Task :webserver:outgoingVariants
--------------------------------------------------
Variant runtimeClasspath
--------------------------------------------------
Runtime classpath of source set 'main'.
...
Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.environment     = standard-jvm
    - org.gradle.jvm.version         = 21
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
...

And based on this I added these attributes:

    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
        attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
        attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(TargetJvmEnvironment.STANDARD_JVM))
        attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21)
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
    }

But it didn’t actually make any difference whether I was requesting all these attributes, or no attributes at all. However, the problem with this approach was that it didn’t give me the correct result. (Besides, I don’t understand how does this even supposed to work, I mean, how does Gradle know what to return?)

So, here is the partial output from the above println statement when the task got executed:

[bcprov-jdk18on-1.84.jar, commons-codec-1.18.0.jar, commons-text-1.13.1.jar, commons-lang3-3.20.0.jar, jakarta.xml.bind-api-4.0.2.jar, ..., jakarta.activation-api-2.1.3.jar, ...]

and from the command: ./gradlew :webserver:dependencies --configuration runtimeClasspath

Project ‘:webserver’

runtimeClasspath - Runtime classpath of source set ‘main’.
±-- org.bouncycastle:bcprov-jdk18on:1.84
|    — org.bouncycastle:bcprov-jdk18on:1.84 (c)
±-- commons-codec:commons-codec:1.18.0
±-- org.apache.commons:commons-lang3:3.20.0
±-- org.apache.commons:commons-text:1.13.1 → 1.14.0
|    — org.apache.commons:commons-lang3:3.18.0 → 3.20.0
±-- jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 → 4.0.4
|    — jakarta.activation:jakarta.activation-api:2.1.4

You can see that jakarta.xml.bind:jakarta.xml.bind-api got upgraded to 4.0.4 and with that jakarta.activation:jakarta.activation-api got upgraded too, but these changes are not reflected in the buildZip task, so these jars didn’t get excluded.

So, based on the example in the doc, I added the following to the webserver project:

val webserverClasspath by configurations.runtimeClasspath

configurations {
    consumable("webserverRuntimeClasspath") {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
        }
        outgoing {
            artifacts(provider {
                webserverClasspath.files
            })
        }
    }
}

I also added the same attribute to the above code:

val webserverRuntime by configurations.resolvable("webserverRuntime") {
    extendsFrom(webserverRuntimeDependencies)

    attributes {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
    }
}

This resolved the issue and now I am getting the right versions for the jars in the buildZip task.

I didn’t verify this yet, but the only thing I can think of why this is happening is that I am using the org.gradlex.jvm-dependency-conflict-resolution plugin to do consistent resolution across the subprojects, so the main app is providing versions to webserver. But if this is the case, then apparently the plugin code is not getting executed in the first scenario, only in the second.

In any case, does this solution look OK now?

Btw, I noticed that in 9.6.0 this whole thing will break again, because the syntax will change:

https://docs.gradle.org/9.6.0-rc-2/userguide/how_to_share_outputs_between_projects.html#variant-aware-sharing

But also excluding those by file-name is quite fragile actually so think twice if you really want to do it that way.

Maybe a better way would be to actually declare those dependencies as providedCompile right away in the buildZip containing project, so that you don’t have to exclude anything, as you only have them as compile dependencies anyway.

Yes, you’re right, but the problem is that sometimes we need the whole application and its dependencies in the war file that can be deployed on a standalone app server.

Thanks.

So, based on your suggestion and the example in the doc this is what I came up with:

This misses the attributes, which means you might maybe get the right jars, but maybe others or resolution errors maybe also in the future.
Declare all the JVM-attributes on the resolvable configuration to avoid that.

Also, to build the webserverClasspath you iterate over the webserverRuntime configuration at configuration time. This is not the best idea as there could be some configuration coming later that influences the resolution, and if it would contain something that is built by the current build and not just external dependencies, this would also not work properly.

And of course the file-name based matching remains questionable. :slight_smile:

And based on this I added these attributes:

Looks fine

But it didn’t actually make any difference whether I was requesting all these attributes, or no attributes at all.

That might accidentally currently eventually be the case.
But with them you say “give me the right jars”, without you say “give me some jar but maybe the wrong one or even fail resolution eventually”.
Heavily depends on the actual resolved dependencies and how they are published.

You can see that jakarta.xml.bind:jakarta.xml.bind-api got upgraded to 4.0.4 and with that jakarta.activation:jakarta.activation-api got upgraded too, but these changes are not reflected in the buildZip task, so these jars didn’t get excluded.

Hard to say without any context.
Something is upgrading for example commons-text to 1.14.0 in the webserver project, maybe you set some constraints there or whatever, that are not propagated to the consumer.
Impossible to say without having the build at hand or at least a build --scan URL.

I am using the org.gradlex.jvm-dependency-conflict-resolution plugin to do consistent resolution across the subprojects

Yes, that could quite well be the cause,
you would probably there also have to use it something, no idea.
But you consumable configuration could also work yeah, besides that it might still be better to just use compileOnly instead of doing file-name-based exclusions.

Btw, I noticed that in 9.6.0 this whole thing will break again, because the syntax will change:

Not sure what syntax-change you mean, but in a minor update there should be no breaking changes.

Yes, you’re right, but the problem is that sometimes we need the whole application and its dependencies in the war file that can be deployed on a standalone app server.

You can have those libs in question in a separate dependency bucket configuration, then you can make compileOnly extend from that configuration and for the wars where you need them packaged, have a resolvable configuration that extends from the dependency bucket and sets the attributes, and then package that configuration to those wars additionally. :man_shrugging:

Just to summarize, this is what I have currently:

producer (webserver project):

plugins {
    id("java-library")
}

val webserverClasspath by configurations.runtimeClasspath

configurations {
    consumable("webserverRuntimeClasspath") {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
        }
        outgoing {
            artifacts(provider {
                webserverClasspath.files
            })
        }
    }
}

consumer (app project):

plugins {
    id("application")
}

val webserverRuntimeDependencies by configurations.dependencyScope("webserverRuntimeDependencies")

val webserverRuntime by configurations.resolvable("webserverRuntime") {
    extendsFrom(webserverRuntimeDependencies)

    attributes {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
    }
}

dependencies {
    webserverRuntimeDependencies(project(":webserver"))
}

tasks.buildZip {

    val webserverClasspath = webserverRuntime.files

    into("$baseDirName/webapps/myapp") {
        with(tasks.war.get())
        exclude { file -> webserverClasspath.map { it.name }.contains(file.name) }
    }
}

Also, to build the webserverClasspath you iterate over the webserverRuntime configuration at configuration time.

Does the above fixes this issue?

if it would contain something that is built by the current build and not just external dependencies, this would also not work properly.

Yes, that’s a good point, but in this case I was only concerned about the external dependencies.

Not sure what syntax-change you mean, but in a minor update there should be no breaking changes.

For example:

9.5.1:

val instrumentedRuntimeDependencies by configurations.dependencyScope("instrumentedRuntimeDependencies")

9.6.0-rc-2:

val instrumentedRuntimeDependencies = configurations.dependencyScope("instrumentedRuntimeDependencies").get()

9.5.1:

tasks.register<JavaExec>("runWithInstrumentation") {
    // Use the resolved instrumented classpath
    classpath = instrumentedRuntime
    mainClass.set("com.example.Main")
}

9.6.0-rc-2:

tasks.register<JavaExec>("runWithInstrumentation") {
    // Use the resolved instrumented classpath
    classpath = instrumentedRuntime.get()
    mainClass.set("com.example.Main")
}

And of course the file-name based matching remains questionable. :slight_smile:

And of course I don’t disagree with you.:slight_smile: But for one, at the time I thought it was a good idea that the exclusion is done programmatically, and also I didn’t know of another other way to implement this. But I’m always happy to listen to suggestions.:slight_smile:

You can have those libs in question in a separate dependency bucket configuration, then you can make compileOnly extend from that configuration and for the wars where you need them packaged, have a resolvable configuration that extends from the dependency bucket and sets the attributes, and then package that configuration to those wars additionally. :man_shrugging:

This actually made me laugh (at myself). You make it sound so easy, but of course I have no idea how to do any of this. I only know so much Gradle to realize that I don’t know much.:man_shrugging: But sounds like this will be a better and less complex implementation (once I figure it out), so I will look into it.

Thanks.

val webserverClasspath by configurations.runtimeClasspath

You should not use by here.
Gradle treats configurations lazily and with using by here you effectively do val webserverClasspath = configurations.runtimeClasspath.get()which you should never do in configuration phase.

Does the above fixes this issue?

No.
webserverRuntime.files gives you a Set<File>, so even though you are not iterating, you still already resolve it at configuration time.
If you store webserverRuntime.elements and then use that, it should be better I think.

9.5.1:

val instrumentedRuntimeDependencies by configurations.dependencyScope("instrumentedRuntimeDependencies")

9.6.0-rc-2:

val instrumentedRuntimeDependencies = configurations.dependencyScope("instrumentedRuntim

Well, the delegate stuff got deprecated, yes, but it would still work until Gradle 10.
But, you should never use by there anyway, same as above with by configurations.runtimeClasspath.
configurations.dependencyScope gives you a provider and with using by you immediately get() it which you shouldn’t do.
So the “replacement” you posted is not good either, as you immediately get() it at configuration time instead of treating it lazy.
You should practically never call .get() or related at configuration phase as you always destroy the laziness with that and often introduce the same race conditions we have with the “good old” afterEvaluate.

classpath = instrumentedRuntime.get()

Same here, do not use get() at configuration time.
What you want here is classpath = files(instrumentedRuntime).

You make it sound so easy, but of course I have no idea how to do any of this.

It’s really just what I said more or less literally.
For example like this:

val myMaybeDeps = configurations.dependencyScope("myMaybeDeps")
configurations.compileOnly {
    extendsFrom(myMaybeDeps)
}
val myMaybeDepsClasspath = configurations.resolvable("myMaybeDepsClasspath") {
    extendsFrom(myMaybeDeps)
    attributes {
        attribute(all the JVM ecosystem attributes)
    }
}
dependencies {
    myMaybeDeps("what:ever:1.0.0")
}
tasks.register<War>("myWarWithDeps") {
    from(myMaybeDepsClasspath) {
        into("WEB-INF/lib")
    }
}

Thank you so much for taking your time and helping me with this. While I see that I will need to review some of the basic concepts, i.e. what code is executed at the configuration phase vs the execution phase and try to implement things accordingly, it looks like the documentation should also be updated to reflect these best practices.

You should not use by here.

You should practically never call .get() or related at configuration phase as you always destroy the laziness with that and often introduce the same race conditions we have with the “good old” afterEvaluate.

The examples I posted above are from the Gradle doc. In 9.5.1 the example used by and in 9.6.0 this was replaced by = and .get() . But you’re saying that both are wrong.:man_shrugging:

And actually by gave me an error in IntelliJ with Gradle 9.6.0 (which I was using by accident). This is how I discovered that the sample code on the page has been updated.

Also, if using .get() is such a bad idea then maybe it should be removed from Gradle completely. Or at least Gradle should give a warning that this should avoided, and let the user set the org.gradle.do.not.bother.me.about.best.practices = true property to suppress these warnings.

classpath = instrumentedRuntime.get()

Same here, do not use get() at configuration time.
What you want here is classpath = files(instrumentedRuntime).

This example is also straight from the doc. How does a poor soul, like myself, supposed to learn, if the examples shown in the doc are incorrect?:man_shrugging: I hope that @PersonResponsibleForWritingTheDocsPersonResponsibleForWritingTheDocs is reading this.:grin:

webserverRuntime.files gives you a Set<File>, so even though you are not iterating, you still already resolve it at configuration time.
If you store webserverRuntime.elements and then use that, it should be better I think.

Actually my configuration is a little more complex, because I have 3 different subprojects that need this, so I moved the code into a CopySpec in a convention plugin. Since configurations.webserverRuntime gave me an error, I had to use configurations.named(“webserverRuntime”) instead, so the whole expression is configurations.named(“webserverRuntime”).get().files. I understand from the above that get() should be avoided, but I couldn’t get it working any other way. For example `configurations.named(“webserverRuntime”).elements doesn’t work here.

This is how the buildZip task looks like now:

tasks.register<Zip>("buildZip") {
    ...
    with(commonCopySpec())
    ...
}

fun commonCopySpec(): CopySpec {
    return copySpec {
        ...
        val baseName = buildArchive.archiveBaseName.get()

        val webserverRuntimeJars = configurations.named("webserverRuntime").get().files

        into("$baseName/webapps/myapp") {
            with(tasks.war.get())
            exclude { file -> webserverRuntimeJars.map { it.name }.contains(file.name) }
        }
        ...
    }
}

It’s really just what I said more or less literally.

Thanks for the example, it really saved me hours of frustration. I may have to try and implement this approach, given the seemingly never-ending problems and complexities with my original idea.

For example like this:

val myMaybeDeps = configurations.dependencyScope("myMaybeDeps")
configurations.compileOnly {
    extendsFrom(myMaybeDeps)
}
val myMaybeDepsClasspath = configurations.resolvable("myMaybeDepsClasspath") {
    extendsFrom(myMaybeDeps)
    attributes {
        attribute(all the JVM ecosystem attributes)
    }
}
dependencies {
    myMaybeDeps("what:ever:1.0.0")
}
tasks.register<War>("myWarWithDeps") {
    from(myMaybeDepsClasspath) {
        into("WEB-INF/lib")
    }
}

I tried this, but unfortunately it didn’t seem to work, because the jar file I wanted to exclude is also a transitive dependency in the project, so even if I make it compileOnly it is still pulled into the runtimeClasspath. Also, this configuration doesn’t use the consistentResolution feature of the org.gradlex.jvm-dependency-conflict-resolution plugin, so when I copied the jar into WEB-INF/lib I ended up with two different versions of the same jar. (The same issue that happened to me earlier, before I created the consumable configuration in the webserver project.)

it looks like the documentation should also be updated to reflect these best practices.

Quite possible.

It could also be that this example is from a time when configurations were not treated lazy and thus it did not make much difference in that case.

Or from a time when extendsFrom did not accept a Provider but needed the plain Configuration.

Nowadays both is not valid anymore.
It is best to simply treat all Provider as if they were treated lazily, then you are also safe already when they are at some point in the future.

And actually by gave me an error in IntelliJ with Gradle 9.6.0 (which I was using by accident). This is how I discovered that the sample code on the page has been updated.

Shouldn’t give you any error, just a deprecation warning unless you upgraded warnings to errors.

Also, if using .get() is such a bad idea then maybe it should be removed from Gradle completely.

That’s non-sense.
You need to call it to get the value.
What you should not do is, to call it at configuration phase as the outcome could still change and you would get the wrong value by prematurely evaluating it, or in case of tasks and configurations, you could realize the object unnecessarily and thus waste precious build time each time you run the build.

There are even cases where you need to call .get() at configuration phase and even some where it is ok, but both are quite rare edge cases.

Or at least Gradle should give a warning that this should avoided,

Like so many other constructs that are obsolete and you shouldn’t use any longer, yes.
But there is no such “safe mode” yet, but there is a feature request about it.

This example is also straight from the doc. How does a poor soul, like myself, supposed to learn, if the examples shown in the doc are incorrect?

You cannot, except by being told by experts, that’s why I hint at such things if I see them, not to tease you, but to teach you. :slight_smile:
Gradle’s docs are imho pretty great, but just like any software has bugs, any documentation has errors,
and almost all documentation is outdated the moment it is written.
As said above, those snippets are probably from times where it did not make much difference and maybe written by people either not aware or not agreeing to treat all Providers lazy. :man_shrugging:

I hope that @PersonResponsibleForWritingTheDocsPersonResponsibleForWritingTheDocs is reading this.:grin:

Very unlikely.
But you are free to open a bug report about it. :slight_smile:

configurations.named(“webserverRuntime”).elements doesn’t work here.

Yeah, of course, sorry, I missed that you had val webserverRuntime by configurations.resolvable("webserverRuntime") there, wich of course again is the bad implicit get() again, so just the same as with configurations.named(“webserverRuntime”).

So yeah, don’t .get() it.
Just do the get() in the exclude { ... }.
Or if the repeated evaluation is an issue, maybe something like this will work:

val webserverRuntimeFileNames = objects.property<List<String>>()
webserverRuntimeFileNames
    .value(
        configurations
            .named("webserverRuntime")
            .flatMap { it.elements }
            .map { it.map { it.asFile.name } }
    )
    .finalizeValueOnRead()

and then .get() that inside the exclude { ... }.

I tried this, but unfortunately it didn’t seem to work, because the jar file I wanted to exclude is also a transitive dependency in the project

Well, with the standard war plugin there is a providedCompile and providedRuntime configuration, maybe you can look at how it is done there. :man_shrugging:

It could also be that this example is from a time when configurations were not treated lazy and thus it did not make much difference in that case.

When I was talking about the doc above, I was always referring to this page specifically, not the doc in general:

https://docs.gradle.org/current/userguide/how_to_share_outputs_between_projects.html#variant-aware-sharing

I didn’t go through all the different versions, but it looks like the by syntax was introduced in 9.0/9.1 (on this page), and in the (now current) 9.6.0 version it was changed to use .get() instead. So, the page is being updated, and the examples are syntactically correct, but as you said, it is not the best idea to do it that way.

Shouldn’t give you any error, just a deprecation warning unless you upgraded warnings to errors.

You’re right, I checked again, and they were warnings, not errors, but at that time they looked scary enough to make me downgrade back to 9.5.1

That’s non-sense.
You need to call it to get the value.

I know, sorry, what I meant is that it would be nice if there would be some way to enforce, or at least encourage, the use of Providers and Properties where it is appropriate.

But there is no such “safe mode” yet, but there is a feature request about it.

I am glad to hear that there is already a feature request about this. But surely there are more things to implement than time to actually do them.

that’s why I hint at such things if I see them, not to tease you, but to teach you. :slight_smile:

I understand, and actually I was going to comment on it that you’re a good teacher, and I appreciate that.

Gradle’s docs are imho pretty great, but just like any software has bugs, any documentation has errors,
and almost all documentation is outdated the moment it is written.

I agree with all this, but I think the problem here is a little different and maybe tricky to solve. In my case the warning message said:

The 'val name by provider' property delegate syntax has been deprecated. This is scheduled to be removed in Gradle 10. Use 'val value = provider.get()' instead.

The example on the 9.5.1 page used val name by provider and the 9.6.0 page used val value = provider.get() So, everything is in sync and looks good, The problem is when the expert (you) looks at it and says that well, you really shouldn’t be using either one of these here. Not sure what’s the best way to improve this, either by only showing the way an expert would do it, or, as I’ve seen on other pages, show multiple ways of doing the same thing, and point out why one way is better than the other. Or have the “expert” built into Gradle, so it can point out bad practices on the fly.:slight_smile:

val webserverRuntimeFileNames = objects.property<List<String>>()
webserverRuntimeFileNames
    .value(
        configurations
            .named("webserverRuntime")
            .flatMap { it.elements }
            .map { it.map { it.asFile.name } }
    )
    .finalizeValueOnRead()

Thanks for this suggestion, it worked nicely with two small changes.

The first line gave me this error:

val webserverRuntimeFileNames = objects.property<List<String>>()
> Creating a property of type 'Property<List<..>>' is unsupported. Use 'ListProperty<..>' instead.

So, I changed it to:

val webserverRuntimeFileNames = objects.listProperty(String::class.java)

And for the second one IntelliJ gave me this warning:

Implicit parameter 'it' of enclosing lambda is shadowed

So, to make it happy I changed it to this:

webserverRuntimeFileNames
    .value(
        configurations
            .named("webserverRuntime")
            .flatMap { it.elements }
            .map { element -> element.map { it.asFile.name } }
    )
    .finalizeValueOnRead()

Thanks.

I didn’t go through all the different versions, but it looks like the by syntax was introduced in 9.0/9.1 (on this page), and in the (now current) 9.6.0 version it was changed to use .get() instead. So, the page is being updated, and the examples are syntactically correct, but as you said, it is not the best idea to do it that way.

Well, as I said, this is from a time where it was necessary to unqualify anyway.
In 9.1.0 where that snippet was added extendsFrom did only accept Configuration, not Provider<Configuration> and with the removal of the property delegates was simply migrated.

But since 9.4.0 extendsFrom accepts a Provider<Configruation>, so since then the example is outdated and should be changed to neither use by nor get().

In my case the warning message said:

And the warning was completely right.
The replacement the warning suggests is semantically the same code just with different syntax.
Just that since 9.4.0 both variants are bad practice as the premature evaluation is no longer necessary.

So, everything is in sync and looks good, The problem is when the expert (you) looks at it and says that well, you really shouldn’t be using either one of these here.

As I said, docs are most often outdated the moment you write them.
Things change and docs are not always properly updated like in that case.
That’s simply a documentation bug.

Thanks for this suggestion, it worked nicely with two small changes.

:ok_hand:

Hopefully this is the last one.:slight_smile: Does this look OK now?

Thanks.

producer (webserver project):

plugins {
    id("org.gradle.java-library")
}

configurations {
    // Create a custom consumable configuration named 'webserverClasspath'
    // This allows this project to supply its runtimeClasspath to other projects
    consumable("webserverClasspath") {
        // Assign attributes so that consuming projects can match on these
        attributes {
            // The unique attribute allows targeted selection
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
        }
        // Add all jar files from the classpath to the 'webserverClasspath' configuration
        outgoing { artifacts(provider { runtimeClasspath.get() }) }
    }
}

consumer (app project):

plugins {
    id("org.gradle.war")
}

// This configuration is used to declare dependencies only.
// It is neither resolvable nor consumable.
val webserverRuntimeDependencies = configurations.dependencyScope("webserverRuntimeDependencies")

// This resolvable configuration is used to resolve the runtimeClasspath of the 'webserver' project.
// It extends from the dependency-declaring configuration above.
val webserverRuntime = configurations.resolvable("webserverRuntime") {
    // Wire the dependency declarations
    extendsFrom(webserverRuntimeDependencies)

    // These attributes must be compatible with the producer (the 'webserver' project)
    attributes {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("webserver-classpath"))
    }
}

dependencies {
    ...
    // Declare a project dependency on the producer's output
    webserverRuntimeDependencies(project(":webserver"))
}

tasks.register<Zip>("buildZip") {
    ...
    with(commonCopySpec)
}

tasks.register<Tar>("buildTar") {
    ...
    with(commonCopySpec)
}

val commonCopySpec: CopySpec = copySpec {
    val baseName = project.name

    val webserverRuntimeFiles = objects.listProperty(String::class.java)
    webserverRuntimeFiles
        .value(
            configurations
                .named("webserverRuntime")
                .flatMap { it.elements }
                .map { file -> file.map { it.asFile.name } }
        )
        .finalizeValueOnRead()

    into("$baseName/webapps/myapp") {
        with(tasks.war.get())
        // Use the resolved classpath
        exclude { webserverRuntimeFiles.get().contains(it.name) }
    }
}

From a cursory look on mobile, I’d say yes

Awesome! I’ll mark it as a solution then. Thank you so much for your help!