How to correctly use Provider / Properties to chain tasks

In a multi-module code base

We have a code generation task that creates generated source code and some additional resource files and attaches them to the main sources of a specific module.

In another module I want to consume the additional resource files and consume them in some way.

I have looked at:

And tried to use this however I’m having trouble on the consumer side as the variant is the source project’s main resources which is simply a “dir”:

// consumer side build.gradle.kts

// Configuration to extract the resource files from the source project
val generated by configurations.dependencyScope("generated") {
    isTransitive = false
}

val generatedResources by configurations.resolvable("generatedResources") {
    extendsFrom(generated)
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
        attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
        attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE, objects.named(VerificationType.MAIN_SOURCES))
    }
    isTransitive = false  // We're only interested in this module's generated resources
}

dependencies {
    generated(project(":project_that_generates_code_and_resources"))
}

// Class that filters the Generated Code via JVM tool
@CacheableTask
abstract class FilterTask: JavaExec() {
    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val inputFile: RegularFileProperty

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @get:Input
    abstract val componentName: Property<String>

    init {
        mainClass.set("com.example.CustomFilterClass")
    }

    @TaskAction
    override fun exec() {
        args(mutableListOf(inputFile.asFile.get().path, outputFile.asFile.get().path, componentName.get())
        super.exec()
    }
}

// Actual usage of that task class:
val filterGenerated by tasks.registering(FilterTask::class) {
    dependsOn(":project_that_generates_code_and_resources:generatorTask")

    componentName.set("aComponent")

    classpath = toolClasspath // not shown

    // Need assistance here, is this the best approach?
    inputFile.fileProvider(
        project.provider {
            // to access the generated resources - what's the best way?
        }
    )

    outputFile = layout.buildDirectory.file("filtered-example.something")
}

As shown in the instance filterGenerated what’s the best way of safely getting access to the build/generated-sources/resources output from the fileProvider? These aren’t standard properties files, they’re essentially intermediate binary files created by the code generator.

Three general things.

Don’t use by with configuration.... helpers, configurations are made treated lazily by Gradle,
and by using by there you delazify them and realize them immediately and unconditionally.
Is effectively is like callling get() at configuration time which is almost always a bad idea.

You don’t need isTransitive = false here, as the variant you are requesting does not carry dependency information anyway.

Do not use explicit dependsOn except with a lifecycle task on the left-hand side.
Whenever you think you need an explicit dependsOn, it almost always is a sign of not properly wiring task outputs to task inputs properly.
Because when you do, you get the necessary task dependencies automatically where needed and only if really needed.

In this specific case, the second advice is a bit problematic.
And that is because of bug `mainSourceElements` artifacts miss task dependencies · Issue #27578 · gradle/gradle · GitHub , which reports that the mainSourceElements variant does not properly carry on implicit task dependencies to source-generating tasks, even if they are registered properly like in

project_that_generates_code_and_resources/build.gradle.kts:

plugins {
    `java-library`
}

val generatorTask by tasks.registering {
    val output = layout.buildDirectory.dir("generated/sources/$name/resources")
    outputs.dir(output)
    doLast {
        output.get().file("example.something").asFile.writeText("Hello, world!")
    }
}

sourceSets.main {
    resources.srcDir(generatorTask)
}

If you really want to use the mainSourceElements variant, then you either need to modify that outgoing variant to properly carry the task dependency, then you can remove the explicit dependsOn, or you need to keep the explicit dependsOn you have and for example then do something like

inputFile.fileProvider(
    generatedResources.flatMap {
        it.asFileTree.elements
    }.map {
        it.single { it.asFile.name == "example.something" }.asFile
    }
)

An alternative would be to create a dedicated outgoing variant in the producer project that contains exactly the file you want to share and properly carries the task dependency like

val exampleSomething = configurations.consumable("exampleSomething") {
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, named("exampleSomething"))
    }
    outgoing.artifact(generatorTask.map { it.outputs.files.singleFile })
}

and then on the consuming side

val generatedResources = configurations.resolvable("generatedResources") {
    extendsFrom(generated)
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, named("exampleSomething"))
    }
}

val filterGenerated by tasks.registering(FilterTask::class) {
    componentName = "aComponent"

    inputFile.fileProvider(
        generatedResources.flatMap {
            it.asFileTree.elements
        }.map {
            it.single().asFile
        }
    )

    outputFile = layout.buildDirectory.file("filtered-example.something")
}

@Vampire thank you so much! I will try this and come back.

Hi @Vampire,

Thank you very much - that worked perfectly.
Much appreciated.