Imagine I have a convention plugin that defines a generic buildImage
task based on a Dockerfile
that is assumed to be in the main project directory (in my use case, I’m using the upstream gradle-docker-plugin
).
Now imagine I want to improve my convention plugin, and define a standard way to generate a Dockerfile by defining another task generateDockerfile
. But I still want to allow client projects to use their own in-project Dockerfile if they want to (as an opt-in feature).
So I create an extension so that users of this plugin can use a custom DSL to configure whether they want the plugin to automatically generate their Dockerfile (by passing some configuration) or rather use their own in-project Dockerfile. The latter would look like something similar to:
// Final project's build script
docker {
dockerfile(file('Dockerfile')) // I want to use my own Dockerfile
}
Therefore, here’s the plugin code (simplified):
// Docker convention plugin
// Extension for configuring
interface DockerExtension {
RegularFileProperty getDockerfile()
// other properties to configure the generated dockerfile
}
extensions.create("docker", DockerExtension)
// Build image task
tasks.register("buildImage", DockerBuildImage) {
...
}
// Generate Dockerfile task
tasks.register("generateDockerfile", Dockerfile) {
onlyIf { !docker.dockerfile.isPresent() }
inputs.property('dockerfile', docker.dockerfile).optional(true)
// Instructions to generate Dockerfile
// ...
}
So the question is: what is the best/most idiomatic way I can configure the buildImage
task so that its inputs depend CONDITIONALLY on the outputs of generateDockerfile
task, if dockerfile
property is not present (no dependency otherwise).
Here’s the options I’m considering. I’d like to know if I’m missing something and if there is a best-practice way to solve this.
Option 1: dependsOn
tasks.named('buildImage') {
inputs.property('dockerfile', docker.dockerfile).optional(true)
if (!docker.dockerfile.isPresent()) {
dependsOn 'generateDockerfile'
}
}
Option 2: inputs/outputs wire-up
tasks.named('buildImage') {
inputs.property('dockerfile', docker.dockerfile).optional(true)
if (!docker.dockerfile.isPresent()) {
inputs.files(tasks.getByName('generateDockerfile').outputs.files)
}
}
Option 3: separate the plugins
This option consists of breaking down the plugin into two plugins: a ‘base’ one with the buildImage
logic, and a specialized one for those clients that want to opt-in to the unified Dockerfile facility. In this case, I don’t need conditional logic in the latter because I can assume that the final project does want the standardised Dockerfile so I can wire-up the two tasks unconditionally. I would not need the extension’s property either.
When picking a solution, think about the possibility of scaling it too. E.g. different ways of generating the unified Dockerfile. With the fist two options, representing the property gets a bit more cumbersome, maybe I’d need a nested object in my DSL, then I’d need a task for each case (e.g. generateFooDockerfile
, generateBarDockerfile
, etc). In the last option, I would need a “special” plugin for each of these cases, each having their own simple extension to configure the various arguments.
Concerns
I have tried all the above solutions and they seem to work, but I am concerned about whether eager-vs-lazy configuration matters here. I.e. I understand the condition if (!docker.dockerfile.isPresent())
checks the property eagerly. Is this a problem or a bad-practice? If so, why, and what’s its lazy counterpart?
Or should I not be concerned at all because tasks.register()
and tasks.named()
both act lazily anyway?
Please notice this is a Gradle DSL question, the docker use case is purely incidental.