Hey. Is there documentation already, or a good exa...
# declarative-gradle
j
Hey. Is there documentation already, or a good example, that shows how to design my own DSL (aka Project Type) with DCL? I would like to understand what the things are I am supposed to use if I want to do this really clean, without anything that only "bridges" something existing into DCL (which most of the implementations in the declarative-gradle repo do IIUC). If I have a
BuildModel
as entry point, what should I use to implement the structure of my DSL inside it? I basically already have the "grammar" and now I wand to "implement" it with DCL in the most idiomatic/minimal way possible. But I can't seem to find the answer to basic questions like: • Do I use
new
or should I inject
ObjectFactory
(or should I never have to instantiate anything myself anymore?) • Do I still need
Property/Provider
? And if yes where (
String
vs
Property<String>
) • When do I need which annotation (such as
@Nested
and
@Adding
) • Is
NamedDomainObjectContainer
something like a standard that should be used? Or can/should I use my own clean data structures? • What's managed by Gradle and what do need to implement "by hand". E.g. would I do
action.execute(element)
in my code or is that already a sign that I am doing something "wrong", because in DCL Gradle can do these things for me. • ... The problem might be that I am thinking too much in traditional Gradle terms. I am coming from the metal model where I design a DSL with traditional Gradle extensions. There I always follow certain patterns to have a "block" in the DSL - like
public void blockName(Action<BlockType> action)
. I have difficulties to understand what should be done different when doing the same with DCL. Sorry if this is a stupid question. 🙂
r
its as stupid as I have the exact same questions at the moment. so at least its 2 of us
😅 2
p
Definition is the „user facing type“. It only supports Properties or nested types, ideally safe ones.
Build model is the „api“ for other features and can hold any data (unsafe or non dcl types).
You should use the latest milestone, it contains many breaking changes.
And also use a apply action class.
I plan to add more features „soon“
You also don't need a „block“ Action, for each nested type Gradle generates apply function.
The DCL will be processed (and finalized) BEFORE your apply action will be called, so can safely call get to access a property, but ideally, you should not, because your tasks use Providers too.
p
We don't yet have a plugin authors guide, this is high on our list. Philip's example is good. To add to what's above: • a safe definition (DSL) is just an interface • a safe build model (inter-plugins API) is just an interface • your build logic resides in the "apply action" Some answers to your questions Jendrik: 1/ don't use new, let Gradle manage all instances 2/ yes, use
Property
& co 3/ Use @Nested on a
val
instead of providing configuring functions. Try to avoid
@Adding
by modeling things as data. 4/ You can use NDOC but if your data can be modeled with simplier primitives, go with the simplicity 5/ I'm not sure what
action
or
element
are in your point so I'll refrain from giving a misplaced answer 🙂
👍 1
j
Thanks for the insights @Philip W @Paul Merlin. Maybe looking at the JVM examples already sent me down the wrong track. I would like to do it as you wrote Paul, without using NDOC to avoid inheriting all kinds register/create/named etc methods. What I struggle with is how to model a list to which I add elements. Like this:
Copy code
// Option 1
myDSL {
    processes {
        register("exampleProcess1") {
           // some details
        }
        register("exampleProcess2") {
           // some details
        }
    }
}

// Option 2
myDSL {
    process("exampleProcess1") {
        // some details
    }
    process("exampleProcess1") {
        // some details
    }
}

// Option 3
myDSL {
    process {
        name = exampleProcess1
        // some details
    }
    process {
        name = exampleProcess2
        // some details
    }
}
How do I model such a
@Nested
list for which Gradle instantiates the objects for me? I probably just miss something obvious. But that kind of was my point 5. In traditional Gradle, it's common to implement such a register method "by hand" and then inside you would have to create an object yourself and add it to some list you maintain yourself. How can I model it that multiple
process
/
register
blocks are allowed which then add to a list (property)?
p
NDOC is special: Gradle (DCL) generates a "register" method based on the name of the type:
Copy code
myDSL {
  processes { // @get:Nested val processes: NDOC<Process>
    process("myName") {} // <- generated by Gradle
  }
}
All you need to do is writing
@get:Nested val processes: NDOC<Process>
in the
Definition
And you get an instantiated object that you can use inside your ApplyAction class later.
So as a software developer, you would see option 2.
j
Ah. Nice. Thanks. That's one bit I was missing.
The outer block seems redundant if I only have one possible construct in the inner block, but that's how it is, right? Or can I somehow manage to have one list with different "types" of elements?
Copy code
processes {
    process("myName") {}
    // can I somehow have more here? Like:
    specialProcess("otherName) { }
  }
Or is there an annotation (?) to tell that a NDOC can hold multiple subtypes of something. And then I get a method for each type?
p
That's called PolymorphicDomainObjectContainer and not yet supported in DCL.
At the moment, you also do need a "useless" wrapper class: https://github.com/gradle/gradle/issues/36486
j
Yes I also had that thought. Good that you created an issue already!
p
Yeah, I just create different containers for each subtype. Not ideal, but it works.
j
That's called PolymorphicDomainObjectContainer and not yet supported in DCL.
Is there a discussion (issue?) on that as well? Would be interested to follow. I think a feature like that would be the right solution for what I have in mind.
p
I didn't find one (and I also didn't create one, because I know this limitation. There is https://github.com/gradle/gradle/issues/37003 about the error message, but the (outdated) roadmap contains this feature marked as later: https://declarative.gradle.org/docs/ROADMAP/#incubating-project-types-and-dcl
j
👍 thanks for the link. I am involved in redesigning the DSL for a custom build system, which has grown "confusing" over time. I think the limitations of using DCL with only Interfaces and NDOC would make it much cleaner in the new implementation to not "mess things up again". I will probably try to prototype it with DCL if I do not hit any total blockers. Despite the ugliness with "redundant" container statements. When I have something, I can probably share some of it as data point for what features that are missing for such use cases.
p
Another option for polymorphic collections is to simply use a
ListProperty<T>
and expose polymorphic "Factory functions" to get something like:
Copy code
myDsl {
  processes = listOf(
    process("myName", 23, "foo"),
    specialProcess("otherName", 42)
  )
}
There is currently no configuration block possible for elements so it doesn't work for all the cases supported by NDOC but it has the merits of simplicity.
DCL also supports appending to lists with
+=
j
expose polymorphic "Factory functions"
But such functions, I would need to implement with ObjectFactory right? I can't just define them in interfaces, correct?
One more question relating NDOC and Lists of elements. I am trying to stick to defining everything in interfaces as discussed above. So I have a List of elements, which do not have a name. There is no way to make it look like this, right?
Copy code
populations {
    population {
        source = DIRECTORY
        from = "work/acomponent/src"
    }
    population {
        source = OUTPUT
        from = "GENERATE"
    }
    population {
        source = INPUT
        from = "SHARED_SOURCES"
    }
}
Right now, I only have it working like this:
Copy code
populations {
    population("") { // "" is not used/needed
        source = DIRECTORY
        from = "work/acomponent/src"
    }
    population("") {
        source = OUTPUT
        from = "GENERATE"
    }
    population("") {
        source = INPUT
        from = "SHARED_SOURCES"
    }
}
p
If you use this code, there will only be 1 instance with an empty name (not sure if it is supported at all). If you want to use a nameless list, don't use NDOC but a list with factory functions that do require ObjectFactory
j
Thanks for the info. Can you point me at an example for how I would define such factory methods? I don't really get how I get this working best. Right now my "Container" looks like this.
Copy code
public interface Process extends Named {

    @Nested
    NamedDomainObjectContainer<Population> getPopulations();

    ListProperty<String> getTools();

    MapProperty<String, String> getOptions();
}
What would I have to change in this interface and which annotations do I need to use? How do I get access to ObjectFactory? I tried a few different things but didn't get it working. Probably thinking in the wrong classic Gradle direction again.
j
Facing the issue @Jendrik Johannes commented, it would be nice if we can get some sugar for the default container instead of doing that manually.
Copy code
mapVersions {
    mapVersion { // without qualifier, matches all versions

    }
    mapVersion("kotlin") { // Extend SemverDefinition, matches only versions with "kotlin" qualifier, priority over the previous one if matches
        metadata = gradleProperty("kotlinVersion")
        // or metadata = environmentVariable("KOTLIN_VERSION")
        // or metadata = "1.5.0" // hardcoded value
        // or metadata = property("kotlinVersion") // gradle property > environment variable

        conditions {
            condition(Condition.MetadataIsPresent) // condition enum
            condition(Condition.RequestedTagPrefixMatches())
        }

        snapshotRules {
            rule("kotlin-dev") { // if matches, the version is considered a snapshot
                contains("kotlin-dev") // multiple ways to create this rule, from simple string match to regex
                contains("another-string")
                matches {
                    pattern("(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)-dev-(0|[1-9]\\d*)")
                    pattern("another-regex")
                }
            }
        }
    }
}
p
j
I have to ask this again, as I still could not figure out how I can make this work. I would like to achieve this:
Copy code
outerType {
    population { // adds first instance
        source = DIRECTORY
        filter { includes = listOf("src/*.xyz") }
    }
    population { // adds second instance
        source = OUTPUT
        filter { includes = listOf("**/*.o") }
    }
}
Because a "Population" in this example is a complex object where users should be able to configure different details in a readable structure with named properties and subblocks, I cannot put it into a "flat" factory method like
population(.., .., ..)
. I want a
{ ... }
block, with nested
{ ... }
blocks. I tried these in
OuterType
(see also previous messages): •
NamedDomainObjectContainer<Population> getPopulations()
- requires each entry to have a name defined by the user, which I don't want/need. It also adds the additional
populations { ... }
, which I could accept, but which is rather redundant in my case. •
Population getPopulations()
gives me the syntax I want, but the block always configures the same instance. I tried doing something with
@Adding
, but that does not seem to have an effect here. • I tried turning the type into an abstract class, injecting object factory and defining a method "old style" like this:
public void population(Action<Population> action) { return objects.newInstance("...") }
but that only gives me an error I cannot make sense of. Some more pointers here would be very much appreciated. 🙏
p
If each "population" has a name, then use a NDOC with a custom factory name annotation and
population("name") {}
. If each "population" doesn't have a name you are looking at a simple
List<Population>
and a factory function for your complex object. DSL might not be as nice but it would be explicit:
Copy code
populations = listOf(
  population(DIRECTORY, 23)
  population(OUTPUT, 23)
)
But these can't have further configuration blocks. We don't have support for "lists of complex objects requiring nested configuration blocks" yet. Good candidate for a feature request.