How to write a custom DSL like the one used for dependencies


(Raffaele Sgarro) #1

I’d like to put this code at the top of my build.gradle

services {
  app1 {
    main = 'org.foo.Bar'
    public = true
  }
  app2 {
    main = 'org.baz.Woo'
  }
}

and then let my plugin define tasks like

  • app1Build
  • app1PackageAfterBuild
  • app2Build
  • app2PackageAfterBuild

I have my ServicePlugin but can’t find a way to feed it through a gradle-style DSL. I want to accomplish the same thing that Gradle does for DependencyHandler but need a clue as to where to watch for the relevant source code. Any help?


(Stefan Oehme) #2

Have a look at the example in the user guide which explains working with multiple domain objects.


(Raffaele Sgarro) #3

Following your answer I used Project.container() and registered a callback on container.whenObejctAdded. Unfortunately that callback is called before the newly added object is configured against its configuration closure. How can I be called after the application of the configuration closure instead?


(Stefan Oehme) #4

Use Project.afterEvaluate to be called after the user’s buildscript is executed.


(Raffaele Sgarro) #5

No, it wouldn’t work. I need to be called after the object has been added
and configured but before user script is evaluated because the callback
creates tasks that the user may customize without being forced to use
.afterEvaluate himself.


(Stefan Oehme) #6

You create the task when the object is added and then configure the task further in the afterEvaluate hook.


(Raffaele Sgarro) #7

I already do this for most tasks, but the problematic one is special
because the configuration is needed to build the task name itself, which
cannot be deferred. As a (hopefully) workaround I’m adding this task on
Project.afterEvaluate, but I really dislike it because this task is not
available in the user script (unless the user itself registers on after
evaluation).

But it seems there’s no solution for this in the current Gradle
implementation. If only there were a callback firing on after object
created and configured… I looked for it but could not find a way to hook
into.


(Stefan Oehme) #8

I don’t know what you mean by “created and configured”, can you elaborate? Generally DSL objects can be mutated at any point, so there is no point in time where they are “fully configured”.


(Raffaele Sgarro) #9

After the first configuration closure is applied. I mean, the configuration
closure passed after the name when the named object is added (if any).


(Stefan Oehme) #10

None of the values in there are final, they could be changed later in the build script. The only thing you can safely rely on for task naming is the domain object’s name.


(Raffaele Sgarro) #11

Ok, I’ll show the code

apply plugin: 'foo'

services {
  foo {
    id 'foo-with-hyphens-illegal-in-groovy-identifiers'
    set 'someValue'
    set somePredefinedVaue()
  }
}

This creates the tasks:

  • fooThis (depends on the name)
  • fooThat (depends on the name, too)
  • foo-with-hyphens-illegal-in-groovy-identifiers (depends on a value set by the configuration callback)

I don’t like that the third task must be created in Project.onAfterEvaluate because the build script then must wait in turn. To me there is a bug in NamedDomainObjectContainer.whenObjectAdded because it fires before the configuration callback is applied, but I have not found a way to replace it, yet.


(Stefan Oehme) #12

That value can change later, it is not safe to use for task naming:

services {
  foo {
    id 'foo-with-hyphens-illegal-in-groovy-identifiers'
    set 'someValue'
    set somePredefinedVaue()
  }
}

services.foo.id = "your-task-now-has-the-wrong-name"

Can you provide a more concrete use case why the task needs to have this complicated name?


(Raffaele Sgarro) #13

The use case provided is concrete. It is production code, and the compilcated task name serves to illustrate the point: the actual task name is shorter but contains hyphens, that are not valid in Groovy identifiers and as such need to be quoted in build scripts.

Note that I’m not writing the build from scratch: it’s legacy code already integrated with many (I can’t even count) tools, so the more stable the build interface stays, the better. Anyway I already found a workaround, but posted back here in the hope of finding a nicer way.

I fully understand your point that task names should depend on nothing but immutable values (what I do is a temporary hack to limit the moving parts in the refactoring of the build), but still think that the configuration callback should be applied before calling whenObjectAdded: It wouldn’t break anything and is very easy to implement. But you certainly have the right to say no :slight_smile:


(Stefan Oehme) #14

It would absolutely be a breaking change. If anything we’d have to add a new callback for the new semantics. It could be useful for properties that can be set once and only once, basically forcing the user to set them on creation time. Feel free to propose an API.