Hello. I have a plugin which adds an extension, up...
# plugin-development
j
Hello. I have a plugin which adds an extension, upon which various settings can be configured by the user. Based on those settings I need to generate 1 or more tasks. The tasks only exist if the settings say they should. What I am having trouble doing is figuring out exactly where this code for building the tasks should be. I tried, on my custom extension objects, to do it in configureEach: except this seems to run before the user's build.gradle.kts file is processed, so the options the user sets aren't yet available. I did get it working by doing my work in project afterEvalute. But this is not ideal. Maybe I'm misunderstanding how this is supposed to be done.
m
Add a function to your extension:
Copy code
myExtension {
  configureStuff { // registering task is happening here
    myOption = "foobar"
  }
}
j
That was long done.
Wait. You mean in the configureStuff() method?
m
Yes
j
Hmm.
That could work. It's not really deferred though.... so the user could technically call it multiple times.
m
From your extension it's something like so:
Copy code
open class MyExtension {
  fun configureStuff(action: Action<Options>) {
     options = TODO()
     action.execute(options)
     // add your tasks here based on options
  }
}
the user could technically call it multiple times.
Right. You can add a runtime check but that's basically it
The other option is to register all the tasks possible and only enable some of them based on user input
There is no silver bullet
j
The other option is to register all the tasks possible and only enable some of them based on user input
That was my first approach, but it got ugly when I started having a long list of tasks that come and go, but need to have inputs connected ot outputs dynamically.
m
I feel you, I've been there but if you need your task graph to be modified by the user I don't think there's another way. At least the function thing is very explicity when things are happening compared to
afterEvaluate {}
and the likes
👆 1
t
The only way creating tasks can be "reactive" is to use a "container" (objects.domainObjectContainer and the likes) and create the tasks whenObjectAdded (or configureEach); this is how creating a source set creates the compile and process resources tasks for instance, or how creating a test suite creates a source set and a test task. If you cannot do this, then you have to make the build more "imperative", as suggested above (this is the approach taken by Spotless for instance) …either that or create all the tasks and have them skipped or no-op if the thing they rely on hasn't been configured.
j
That is basically what I'm doing: but processing the user input doesn't seem to run before the configure of the container
Or configure of the items in the container. Now not actually sure.
I have an item called a Build, which I put in a NamedDomainObjectContainer on my extension. I set up configureEach on the container. But that seems to happen before the user calls to the items inside the container run.
v
Yes, that will not work.
whenObjectAdded
is eager, thus bad anyway. And
configureEach
executes in order of registration. So if your plugin registers a
configureEach
action and after that the user registers his task with a configure action, your plugin's action is executed first and then the user's action. If the user is settings properties that you wire into your task, that will work. But if you need the configured values at configuration time to decide what task to add or its name or similar, then it will not work.
Doing it reactive in a function call like @Thomas Broyer said is also what I usually recommend. And if the method must not be called multiple times then a check that it is not called multiple times.
t
I have an item called a Build, which I put in a NamedDomainObjectContainer on my extension. I set up configureEach on the container. But that seems to happen before the user calls to the items inside the container run.
Then you're not fully reactive, the "user calls to the items inside the container" need to be imperative then.
(fwiw, another example of "imperative" configuration can be found in Gradle itself with
java.withJavadocJar()
and
java.withSourcesJar()
)
j
By "user calls to the items insidr the container", I mean by using DSL such as:
Copy code
cics {
    builds {
        named("aquOnline") {
            maps {
                remote = remotes.named("aix")
                options {
Each named "build" object there exists because I generate them from sourceSets.
every source set gets an associated "build" object. The user can then apply additional configuration to them.
Configuration which determines which set of tasks to generate.
There being a maps(Action) method on the Build object. That's what I mean by the "user calls to the items inside the container". In that DSL he calls build.maps(Action).
I could, as you suggested (Thomas), generate the tasks at the end of maps(Action).
But, the Build object actually has other methods as well.... and I'd need to regen the set of tasks at the end of each.
v
As I said, configuration actions are running in registration order. First your plugin is applied and registers its
configureEach
action. Then the build script is executed and registers its
named
... action. So your plugin's action does not see the changes from the build script and there is no clean way to make it if you need the values at configuration time except for having a function instead that the user calls.
You cannot regenerate the set of tasks at the end of each as you cannot unregister tasks cleanly
j
Yeah.....
v
So as Thomas suggested, have all those wrapped in one more nesting level, do the task registering at the end of that enclosing level and throw an exception if the user calls it a second time
j
Like a big wrapping configure method.
v
Unless it is ok to be called multiple times of course
Like a big wrapping configure method.
Exactly
j
Copy code
cics {
  build {
    named("foo") {
      configure {
        maps {}
        otherthing {}
And then at the end of configure()
v
Well, maybe not called
configure
as that might conflict with Gradle built-in function, but conceptually, yes, exactly that.
j
Well one obvious problem here is this code will never run if the user doens't configure it then. Hehe.
There is also a default mode, if the user doesn't specify anything. Heh.
v
No, there isn't 🙂
1
j
Hehe
v
There is no good way to apply defaults in that case as you never know when user configuration finished. Even if you use
afterEvaluate
, the user could use
afterEvaluate
too and thus do configuration after your action. One of the joys of using
afterEvaluate
and actually it's main effect, introducing ordering problems, timing problems, and race conditions.
Best you could do is to have some verification at some place that verifies the user did do the configuration if really necessary.
☝️ 1
m
Apollo used to have a default mode using
afterEvaluate {}
to save one nesting level and have an "easy" API. Took us several years to get rid of it. The "easy" API was counterproductive because as soon as you needed some custom config, you needed to use the "explicit" API anyways. Go straight for the "explicit" API and save yourself a lot of trouble, not to mention consistent documentation, etc...
v
Or have a separate / additional plugin the user can choose to apply that configures the defaults instead of the user calling a function to do so, that basically calls that function the user would call with the default values.
1
j
In terms of domain object collects, do parent and children configure in a specific order?
v
parent and children ... what?
You don't mean parent and children projects, do you?
j
Well l imagine I nested two collections, but put the task generation on the outside one.
v
I think I know what you mean, but can you show an example?
j
Copy code
cics {
  build {
    named("foo") {
      children {
          named("bar") {
            maps {}
            otherthing {}
Would configures of bar run before configures of foo?
I'm not suggesting that. But trying to understand the model. Because there might be a trick in here somehow.
v
You don't know. As long as you use lazy api, the configuration actions are run when the respective element needs to be realized. So assuming
foo
and
bar
are both on
build
, and you realize
bar
but not
foo
, then you miss the configuration of
bar
as the configuration of
foo
never executed which would register the configuration action for
bar
.
j
But there's no concern for any sort of hierarchy.
v
And if you register a
configureEach
for
build
in your plugin, then this would still run before that configure action, so the
bar
configure action would not be registered yet either.
If the nesting level is what you are concerned about, you could also do something like
Copy code
cics {
  build {
    named("foo") {
      maps {}
      otherthing {}
      doTheTaskCreationNow()
But then the user could forget to call that function and you also have to make sure he does not do additional configuration changes after that function was called.
With the nesting, you are sure that when the outer function is called, the configuration is finished as it is done in the supplied action and by preventing the method to be called again you ensure the configuration is not tried to be changed again
j
All of these answers kinda suck. =(
v
Of course the user could always do bad things if he really wants like
Copy code
cics {
  build {
    named("foo") {
      var bar
      configure {
        maps {}
        bar = maps
        otherthing {}
      }
      // do something bad with bar
but you do not try make it evil-user-proof, just convenient and reliable and reproducable.
j
I'm writing a plugin for Cobol. I'd like the user to just be able to drop a source set, or even just use main and put in a /cobol folder. But not configure anything. But cobol is a world full of third party preprocessors. And these are best handled at build time by tasks that depend on each other (so we get the benefits of up-to-date, etc).
So teh user has to be able to register these preprocessors. And those would generate tasks. But they need to specify the order that they run in.
But they can also just not use any preprocessors.
So anything from zero-configuration to adding custom processors in custom orders. The other option is to just not use Tasks, but make my entire own model of it all.
And stuff it all into a single Task that does the work internally calling the preprocessors. But then the user doesn't get the benefits of up-to-date, or the abiltiy to execute a single one of the tasks in isolation for debugging.
I have it all working cleanly using afterEvaluate. Heh.
(for now)
v
I have it all working cleanly using afterEvaluate
That is impossible, it just seems so. As I said, main benefit of
afterEvaluate
is added ordering problems, timing problems, and race conditions.
t
Looks like a similar level of complexity as Android… https://developer.android.com/build/extend-agp#access-modify-artifacts
v
What is the problem with not needing preprocessors as default? • register compile task that uses sources as input • if user calls the method, register additional tasks in the respective order and wired together and reconfigure the input of the compile task to use the output of the last preprocessor task
👍 1
j
Hmm
That could work....
I could also use the order of the method calls in the build() itself to define the chaining couldn't I.
e
or always register the tasks and use
onlyIf
or
@SkipWhenEmpty
etc. to skip them if there's nothing to do
v
I could also use the order of the method calls in the build() itself to define the chaining couldn't I.
Probably
@ephemient you're late to the party. 🙂 Thomas already suggested that, but Jerome does not want a gazillion unused tasks if the user does not need them 🙂
e
oh I didn't see it
j
Copy code
cobol {
  builds {
    named("main") {
      vcpre {}
      vsqlpre {}
      db2 {}
      kixclt {}
e
not even to handle the default case?
j
So, I could have each call to one of those methods generate the appropriate task and rewrite the existing compile task.
Appending in that order.
So they could call kixclt() before db2() to have the tasks ordered that way instead. That's a good idea.
And if they just don't call anything all that exists is that default compile task.
j
I think this is ideal.... and it looks like it would work..... Thanks guys!
👌 1
j
Minor issue here. How might I cast a TaskProvider<SpecificType> to a TaskProvider<Task> type?
I need some method signature for my 'plugin' preprocessor classes to have a method that a) creates their specific task and b) returns that newly created task to the caller, while preserve the ability for the caller code to further call configure(). But configure() is only on TaskProvider or NamedDomainObjectProvider which I don't really seem to have access to.
Guess I could wrap it in some further Provider implementation and route configure() through.
v
You cannot really cast a
TaskProvider<SpecificType>
to a
TaskProvider<Task>
. At runtime it is anyway just
TaskProvider
due to type-erasure. But a
TaskProvider<SpecificType>
is a
TaskProvider<? extends Task>
so maybe you want to return that. Or you have
<T extends Task>
as type parameter and return a
TaskProvider<T>
. ...
👍 1
Or I completely misunderstood and you should maybe knit an MCVE 🙂
j
TaskProvider<? extends MyTask> I think does what I need.
👌 1
p
BTW I also wrote a Cobol gradle plugin (https://github.com/hfhbd/kobol) and I just used the java pattern: Always register the Cobol sourceset in the SourceSetContainer and also always register all tasks for this sourceSet. Yes, you create many tasks directly, but I put all of the into a task group named "cobol" to not pollute the task overview in IntelliJ.
j
Interesting.
What compiler are you using?
Oh this is your own compiler?
p
Yes it is.
j
Hmm. The one I'm using requires special code to invoke a special runtime.
For VSAM, CICS, and all sorts of stuff.
Big commercial stuff.
I want to know more about all this. Hehe. What's this translate the Cobol to? Just a plain java class with an entry point?
p
The focus is Kotlin, and it is a normal function by default or a main function if the main plugin is applied