I’m writing a custom task that needs to be configured with a collection of domain objects, and I’m struggling with how to make this work with a DSL, lazy properties and incremental builds. Here’s what I’d like to be able to write in my build:
My task has multiple sources (similar to how copy specs can have multiple from declarations), and each source has multiple outputs.
Here’s what I’ve got so far (full source here, for reference):
abstract class Svg2IcoTask @Inject constructor(objects: ObjectFactory) : DefaultTask() {
private val sources = objects.domainObjectSet(Source::class.java)
@get:OutputFile
abstract val destination: RegularFileProperty
interface OutputDimensionsHandler {
fun output(width: Int, height: Int)
}
class Source(val sourcePath: File) : OutputDimensionsHandler {
val outputDimensions = mutableListOf<OutputDimension>()
override fun output(width: Int, height: Int) {
outputDimensions.add(OutputDimension(width, height))
}
}
data class OutputDimension(val width: Int, val height: Int)
fun source(sourcePath: File, action: Action<OutputDimensionsHandler>) {
sources.add(Source(sourcePath).apply {
action.execute(this)
})
}
}
This supports the syntax I want to configure my plugin, but:
bypasses Gradle’s incremental build functionality (in fact, it causes the task to be skipped despite changes to sources),
the sources are evaluated eagerly, and
it forces the sourcePath argument to be a File and nothing else, rather than benefiting from the range of types the destination property supports.
As I understand it, the solution is managed properties, but I don’t understand how to use them for collections, nor how that fits in with the mini DSL I’m defining.
in fact, it causes the task to be skipped despite changes to sources
Sure, you define an input and an output for the task.
These are looked at for up-to-dateness.
If they didn’t change, the task is up-to-date.
You need to define your source image files and your configured dimensions as inputs too.
You can for example widen the access to sources for example to protected, annotate it with @Nested and then add input annotations like @InputFile to the properties of Source, same for outputDimension.
the sources are evaluated eagerly
Not sure what you mean here
it forces the sourcePath argument to be a File and nothing else, rather than benefiting from the range of types the destination property supports.
This probably depends on your flexibility with the dsl.
You could for example make Source have a RegularFilePropertysourcePath instead and then do
Thanks for the super-quick response, @Vampire! Your suggested on widening the scope of sources to protected and annotating with @Nested was exactly what I needed. In the end, I also decided to change the DSL slightly, to make it more directly reliant on Gradle types.
For reference, here’s what I ended up with:
abstract class Svg2IcoTask @Inject constructor(private val objectFactory: ObjectFactory) : DefaultTask() {
@get:Nested
protected abstract val sources: ListProperty<Source>
@get:OutputFile
abstract val destination: RegularFileProperty
abstract class Source @Inject constructor(private val objectFactory: ObjectFactory) {
@get:InputFile
abstract val sourcePath: RegularFileProperty
@get:Nested
val outputDimensions: ListProperty<OutputDimension> = objectFactory.listProperty(OutputDimension::class.java)
.convention(listOf(64, 48, 32, 24, 16).map { dimension ->
objectFactory.newInstance(OutputDimension::class.java).apply {
width.set(dimension)
height.set(dimension)
}
})
fun output(action: Action<OutputDimension>) {
outputDimensions.add(objectFactory.newInstance(OutputDimension::class.java).apply {
action.execute(this)
})
}
}
abstract class OutputDimension {
@get:Input
abstract val width: Property<Int>
@get:Input
abstract val height: Property<Int>
}
fun source(action: Action<Source>) {
sources.add(objectFactory.newInstance(Source::class.java).apply {
action.execute(this)
})
}
}