This message was deleted.
# plugin-development
s
This message was deleted.
c
For featureEnabled: Either afterEvaluate or you have to create the tasks and use the flag to enable/disable them
e
1. One option is to create all the tasks and then enable or disable them based on
featureEnabled
2. Make
configFile
a property. The Kotlin DSL syntax will have to change to
configFile.set("...")
but this will allow you to configure task inputs before the extension is configured
c
About the configFile you can map the property and read the file content, so the tasks can have the inputs you need. (Considering it's a property as @ephemient suggested)
m
You could make everything a method and create the task from your method.
Copy code
myPlugin {
  enableFeature(featureEnabled: Boolean, configFile: File)
}
c
True, method is a good "hack" to create tasks
e
as long as your users call the function exactly once
m
Credit goes to @Thomas Broyer for suggesting it in that other question I had about convention plugin
e
it's an option but not one I think scales well to being configurable by other plugins
👀 1
c
another option is to use NamedDomainObjectContainer as the type in the extension, with whenObjectAdded callbacks - when a “feature” is added to that container the callback is invoked.
j
Looks like that the task being itself which decides if it has to be enabled or disabled is the more intuitive and less problematic way to solve this, right?
e
there's still potential confusion in that users will still see the disabled tasks (e.g. on the cli, as "skipped" in Gradle build output) but it's the less problematic solution IMO
n
Thank you all for the insights. Yeah so seems like that the enabled/disabled task approach is the 'preferred'. @ephemient for the
configFile
, that's already a Property, I used the Groovy dsl in the example for simplicity. The problem is that the configFile is a YAML file in my case, that contains paths which I'd like to use as
@InputFile
/`@InputFiles` for some of my tasks. I could set
configFile
itself as
@InputFile
for the task, but I haven't found an elegant way to parse the content of the provided file + pass the user provided paths as
@InputFile
for the task I'm creating.
e
ah that's something that I think Gradle can't do a good job of capturing - a dependency both on the contents of the config file and also the contents of a file determined by its contents.
depends on how safe you want to be. you could create yet another task that reads the config file, outputs the collection of files it references, and is untracked; then use that as the other tasks' input files
c
I don't like disabling tasks, because if the task is in the graph, even if it's disabled, Gradle will run all its dependencies
👍 1
n
you could create yet another task that reads the config file, outputs the collection of files it references, and is untracked; then use that as the other tasks' input files
Thanks for the hint @ephemient. I'm still trying to find a way to make it work correctly. Not sure how the first task should outputs the collection of files it references. The problem is that this list of file will be known only at execution time and won't be accounted for the UP-TO-DATE checks of the second task
e
the first task will declare an OutputDirectory and copy the referenced files to it, which will then be a known location that Gradle can use for subsequent tasks' inputs
if you decide that's too overkill, that's fine, but then all of your other tasks will have to read files outside of what Gradle can track
n
and copy the referenced files to it
But then I'll end up copying over and over the files for every build no?
e
it will, but subsequent tasks should still be skipped if it matches a previous cached run
n
Yeah but still really suboptimal no? Then the
project.afterEvaluate
is probably better?
e
doing it in afterEvaluate is going to end up reading all the files anyway (when gradle evaluates whether those tasks can use cache or not)
c
…with the added bonus of unpredictable ordering of afterEvaluate callbacks 🤯
➕ 1
😞 2
fixed a similar problem using this pattern (could use different DomainObjectCollection types, and of course the generic type can be whatever works for your case - a String, complex object, etc).
Copy code
// extension class
abstract class ProjectsExtension @Inject constructor(private val objectFactory: ObjectFactory) {

    @get:Input
    abstract val buildFiles: DomainObjectSet<File>
}

// plugin code
val projectsExt = extensions.create("projects", ProjectsExtension::class)
projectsExt.buildFiles.whenObjectAdded {
    // do magic here
}
e
might not matter (since you're using
File
anyway) but
whenObjectAdded
is not lazy (like
all
)
c
understood. the goal there isn’t the laziness - it’s responding to the extension being configured (in this example, a file being added), allowing the callback to then do <whatever> (have used it to register specific tasks, etc). There doesn’t appear to be a loss of laziness, as this responds to setting items on the extension and acts on them (so long as those actions use lazy configuration for tasks, etc).
n
Thanks for the heads up @Chris Lee. It sounds like this could potentially do the trick (it's a bit of a bummer that we have to rely on a
DomainObjectSet
to do this). The annoying part here is that: 1. My
configFile
is actually a
RegularFileProperty
. Changing it to a
DomainObjectSet<File>
would be a breaking change 2. I don't have multiple files, but just one. So modelling it with a
DomainObjectSet
would not reflect reality. I.e. would give the false sense to the user that they can provide multiple
configFile
which in reality they can't
c
understood. You can change to use DomainObjectSet<RegularFileProperty> if need be. yea, for scalar values it’s unfortunate to need to use DomainObjectSet. Alternately, a function on your extension that does the setup may be cleaner.
j
For the paths from the config file as input. I would probably do it like this: • Parse file at config time, but try to be cheap. Maybe you don’t need to parse the whole yaml but just find a string in it with a regex. Or just parse the first lines until you reach the info you need. I would use
project.providers.fileContents(configFile).asText.get()
(like I did here). Then when you use configuration cache, the file will be tracked as “input to configuration” and only parsed again if something changed. Then you can do all of the lazily and you do not need to worry about order. Something like this:
Copy code
tasks.register<MyType>("myTaks") {
    someInputFile.set(myPluginExtension.configFile.map {
        parsePathFromFile(it)
    })
}


fun parsePathFromFile(it: Provider<RegularFile>) =
    project.layout.projectDirectory.file(
        providers.fileContents(it.get()).asText.get().substring(0, 10)) // Instead of 'substring' do the real parsing
If you have more than one file path to read, I think I would still parse each path individually from the content of the config file. So you do not have to deal with some state yourself. Reading the actual file from disc will only be done one time by Gradle I think when you go through the
providers
API.
c
the `providers`are eager, resolving their contents each time
get()
is called - assuming that it’ only called once, you’d be good, but have the risk of multiple executions. Have added a memoize capability to providers for expensive operations (parsing, API calls) to enforce at-most-once execution.
Copy code
fun <T> Provider<T>.memoize(): Provider<T> {
    return MemoizingProvider(this as ProviderInternal<T>)
}

internal class MemoizingProvider<T>(private val provider: ProviderInternal<T>) :
    AbstractMinimalProvider<T>() {

    private var memoizedValue: ValueSupplier.Value<out T>? = null

    override fun getType(): Class<T>? {
        return provider.type
    }

    override fun calculateOwnValue(consumer: ValueSupplier.ValueConsumer): ValueSupplier.Value<out T> {
        if (memoizedValue == null) {
            memoizedValue = provider.calculateValue(consumer)
        }
        return memoizedValue as ValueSupplier.Value<out T>
    }

    override fun toString(): String {
        return "memoized($provider)"
    }
}
n
Thanks for the hints @Jendrik Johannes you made my day! (I owe you a beer in Hamburg!)
j
Sure. Happy it helped. Ping me if you are ever around again. 😉