Explicitly let configuration artifact resolution depend on task

Hi, is it possible to let the artifact resolution of a dependency configuration explicitly depend on the execution of a task? Gradle seems to already do this implicitly when adding a file dependency (e.g. compileOnly(files(tasks.named<MyTask>("mytask").flatMap { it.outputFile })), resolving compileClasspath will run MyTask). Is there a way to add explicit task dependencies, something like configuration.dependsOn(Task)?

My use-case for this is that I am working on a gradle plugin that generates jar+sources-jar+pom.xml into a project-local maven repository. I am doing this instead of using file dependencies because the jar also has transitive dependencies (hence the pom.xml), which I want to let gradle download normally (they’re not in the project-local repository). I also want to keep the dependency tree, which I don’t think is possible with file dependencies. I tried adding a Provider<ExternalModuleDependency> that points to this artifact and also depends on the output of MyTask, but that results in an error:

Could not resolve all dependencies for configuration ‘:compileClasspath’.
Querying the mapped value of […] before task ‘:myTask’ has completed is not supported.

I am assuming this is because gradle tries to read the dependency coordinates before artifact resolution. However, the coordinates themselves are not a result of the task, the task only populates the project-local maven repository.

So, I was thinking this could be solved by adding the external dependency that points to the project-local maven repository by itself, then having the artifact resolution depend on my task. Currently, I am working around this by generating an empty file that is an output of the same task:

compileOnly(files(tasks.named<MyTask>("mytask").flatMap { it.emptyFile })) // implicit dependency on MyTask for artifact resolution
compileOnly(/* coordinates, found in project-local repo */)

I was wondering if there is a better way to do this?

Is there an inherent reason you go that route?

Why don’t you for example have a separate project where you apply that plugin and that project will be a normal java-library project configured by the plugin to produce the washer jar and having the wanted dependencies, then you can just depend on that project.

Or if a separate project is not feasible, maybe just let the plugin create and configure an additional source set which has the wanted just as outcome and the according dependencies defined and then depend on that source set, resp. a feature variant defined from it.

Hi, thanks for your reply.
To give some more context, the gradle plugin is intended as part of a Minecraft mod development toolchain. The basic functionality of it is to:

  1. Download Minecraft
  2. Apply bytecode modifications on the jar (influenced by plugin configuration)
  3. Separately, decompile the untransformed jar and then apply transformations on the decompiled sources (influenced by plugin configuration)

Then, users can just add the dependency on the transformed Minecraft jar like any other dependency, and let gradle resolve transitive dependencies.

Regarding your first point, requiring users to add an extra subproject just to add a dependency seems quite boilerplate-heavy. Unless you’re suggesting that the plugin creates this subproject itself and applies itself to it (not sure if this is possible)? But that feels like even more of a hack than what I’m currently doing.

Regarding the second point, note that the sources jar is actually already decompiler output, so I’m not sure whether compiling against that is a good idea. Additionally, I’d like to keep generation of the sources jar optional, as the decompilation step takes quite some time and sources are only needed during development. As far as I know it’s not possible to put compiled classes into source sets, is it?

I was not familiar with source set feature variants, so I just read How to Create Feature Variants for a Library in Gradle. The way I understood this page, the variant would also need to be published somewhere before it can be consumed, so I don’t understand how they would help in my case.

Regarding the second point, note that the sources jar is actually already decompiler output, so I’m not sure whether compiling against that is a good idea.

I nowhere suggested something like that.

As far as I know it’s not possible to put compiled classes into source sets, is it?

Why not?
Well, there is not really an API, but you can easily replace the according task’s actions by a different action that just places files wherever you need them.

The way I understood this page, the variant would also need to be published somewhere before it can be consumed.

No, they can also be consumed directly in the same build or even within the same project.
You actually most probably want to disable publishing for those feature variants.
So written directly in the build script it could for example look something like this:

val foo by sourceSets.registering
java {
    registerFeature("foo") {
        usingSourceSet(foo.get())
        withSourcesJar()
        disablePublication()
    }
}
val fooApiElements by configurations.existing {
    outgoing.variants.matching { it.name == "classes" }.configureEach {
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named("none"))
        }
    }
}
val fooRuntimeElements by configurations.existing {
    outgoing.variants.matching { it.name == "classes" }.configureEach {
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named("none"))
        }
    }
}
val fooJar by tasks.existing(Jar::class) {
    actions.clear()
    doLast {
        val theManipulatedJarBytes: ByteArray = byteArrayOf()
        archiveFile.get().asFile.writeBytes(theManipulatedJarBytes)
    }
}
val fooSourcesJar by tasks.existing(Jar::class) {
    actions.clear()
    doLast {
        val theManipulatedSourcesJarBytes: ByteArray = byteArrayOf()
        archiveFile.get().asFile.writeBytes(theManipulatedSourcesJarBytes)
    }
}
val fooApi by configurations.existing
dependencies {
    fooApi("commons-io:commons-io:+")
    compileOnly(project(":")) {
        capabilities {
            requireCapability("showcase:showcase-foo")
        }
    }
}

The byte-array writing can of course also be anything else like copying a file that is an output of another task into place and so on. That part is mainly a showcase. The rest should more-or-less work like it is I think.

Hi, thanks for the example. I’ve played with it a bit, but sadly it seems like IntelliJ doesn’t look at the jar written by the fooJar task. So while I can write compile-able code in the main source set that uses classes from the jar injected in fooJar, IntelliJ marks everything red. Presumably IntelliJ just looks at the source set.

I think I’ll just keep my current approach, but thanks anyway for your help.

I guess for IJ you should have an additional task that unpacks the sources somewhere and define that task as source dir for the foo source set, so that IJ know the sources to look at. Of course it assumes it should look at sources, not a jar for a locally produce artifact.

Actually, the initial thought would have been using an artifact transform, but you cannot transform the sources along.