How do I access nested configuration values from my custom gradle extension? - gradle

I'm writing a custom gradle plugin that needs to accept an arbitrary number of nested parameters from the buildscript. Something like:
myPlugin{
configObjects = [
{
name="objectA",
value=5,
},
{
name="objectB",
value=9,
}
]
}
...where the number of items in configObjects, and the the values inside them is defined in whatever buildscript is importing the plugin.
So in my plugin code, I create an extension...
val config = extensions.create("myPlugin", myPluginTaskConfiguration::class.java, project)
tasks {
register<myPluginTask>("myPlugin") {
configObjects= config.configObjects
}
}
and a class defining the structure of the data received through the extension:
open class myPluginTaskConfiguration(project: Project) {
#Input
#Option(option="configObjects", description = "list of configObjects")
var configObjects:List<ConfigObject>?=null
}
Gradle allows me to specify the outer type, but apparently not the inner members. Running my plugin task I get the following error:
class build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5 cannot be cast to class com.myplugin.ConfigObject (build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5 is in unnamed module of loader org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler$ScriptClassLoader #224ed88; com.myplugin.ConfigObject is in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader #72fe231e)
It's not clear to me what the type of the objects in the configObjects block is (well, apparently they're of type build_f42r2ugava4a351q5usw8u65g$_run_closure1$_closure5, but I don't think that's something I can use at author-time)
How can I take the list of items from my groovy buildscript, and convert them into typed objects in my plugin (preferably in a way that allows the IDE to provide suggestions/hints to users editing the buildscript)?

#Input and #Option are for tasks. From the looks of it, you are using them for extensions.
There is no need to need to pass in a project instance in the constructor of a Task. All Tasks have a reference to the Project they belong to https://docs.gradle.org/current/javadoc/org/gradle/api/Task.html#getProject--
With that said, full working example in Kotlin would be:
open class MyPluginTaskConfiguration #Inject constructor(objects: ObjectFactory) {
val configObjects: ListProperty<Map<*, *>> = objects.listProperty()
}
open class MyPluginTask : DefaultTask() {
#Input
#Option(option="configObjects", description = "list of configObjects")
val configObjects: ListProperty<Map<*, *>> = project.objects.listProperty()
#TaskAction
fun printMessage() {
configObjects.get().forEach {
println("$it")
}
}
}
val config = extensions.create("myPlugin", MyPluginTaskConfiguration::class.java)
configure<MyPluginTaskConfiguration> {
configObjects.set(listOf(
mapOf<String, Any>(
"name" to "objectA",
"value" to 5
),
mapOf<String, Any>(
"name" to "objectB",
"value" to 9
)
))
}
tasks.register("myPlugin", MyPluginTask::class) {
configObjects.set(config.configObjects)
}
Executing the above produces:
./gradlew myPlugin
> Task :myPlugin
{name=objectA, value=5}
{name=objectB, value=9}
Refer to below doc for more details:
https://docs.gradle.org/current/userguide/lazy_configuration.html

Related

populate NamedDomainObjectContainer from file

Suppose I am currently authoring a Gradle plugin, and I have this extension:
abstract class MyExtension {
abstract val inputFiles: ConfigurableFileCollection
val names: Provider<List<String>> by lazy {
// Read input files to get a list of names
inputFiles.elements.map { ... }
}
abstract val instances: NamedDomainObjectContainer<MyType>
}
What I want to do in my plugin is to make sure that every name provided by names is registered in instances if not already registered. That is, something like this:
names.get().forEach {
if (!instances.names.contains(it)) {
instances.register(it) {
// Additional configuration goes here
}
}
}
I have heard that project.afterEvaluate() is an option, however I have also heard that it is a bad option that shouldn't be used. What would be a way to have this functionality?
You can use another DomainObjectContainer, which has various functionality to allow it to stay up-to-date with other objects being configured.
For instance you could have the extension class:
abstract class MyExtension {
abstract val inputFiles: ConfigurableFileCollection
abstract val names: DomainObjectSet<String>
abstract val instances: NamedDomainObjectContainer<MyType>
}
Then have in your plugin code:
val ext = project.extensions.create("MyExtension", MyExtension::class.java)
ext.names.all { eachName ->
ext.instances.register(eachName) {
// Additional config
}
}
ext.names.addAllLater(ext.inputFiles.elements.map { ... })
The lambda passed to all will be executed for each new name added to names, including all the files, so all the collections will stay current with each other.

In Gradle, how do you perform validation of lazily evaluated properties (on extensions)?

Is there a way to validate a property value when the property is evaluated? I can't do it in the getter because that returns the Property object - I want the validation to run only when the actual value is calculated (i.e. I want to be lazy evaluation friendly).
They show extensions using the Property object here:
https://docs.gradle.org/current/userguide/lazy_configuration.html#connecting_properties_together
However, they don't explain how to do property validation when the value is calculated. Here is the snipet of code from the Gradle documentation provided example:
// A project extension
class MessageExtension {
// A configurable greeting
final Property<String> greeting
#javax.inject.Inject
MessageExtension(ObjectFactory objects) {
greeting = objects.property(String)
}
}
If I wanted to make sure the value of greeting was not equal to test, then how would I do that when it is evaluated?
For most use cases, it should be sufficient to just validate the property value once you resolve it in your task or in other internal parts of your plugin. Only a few extensions are actually designed to be consumed by other plugins or the build script.
Gradle does not provide some validation that can be attached to a property, however you can build this functionality on your own like in the example below:
class MessageExtension {
private final Property<String> _greeting
final Provider<String> greeting
#javax.inject.Inject
MessageExtension(ObjectFactory objects) {
_greeting = objects.property(String)
greeting = _greeting.map { value ->
if (value.equals('test'))
throw new RuntimeException('Invalid greeting')
return value
}
}
def setGreeting(String value) {
_greeting.set(value)
}
def setGreeting(Provider<String> value) {
_greeting.set(value)
}
}
project.extensions.create('message', MessageExtension)
message {
greeting = 'test'
}
println message.greeting.get()
I turned the Property into a backing field for a Provider that runs the validation when resolved. If you do not want to throw an exception, but just return an empty Provider, you may replace the map with a flatMap.

Gradle properties not visible inside extension container closure

I'm trying to write this custom plugin for Gradle but I'm stuck in properly passing parameters to the plugin.
inside the plugin I'm creating an extension like following:
#Override void apply(final Project p) {
p.extensions.create('myPlugin', MyPluginData.class)
then inside MyPluginData I'm handling def propertyMissing(String name, value) to receive the customer parameters I expect.
And finally inside the client application build.gradle I'm trying to configure the data:
println("From root value is " + SOME_VALUE)
myPlugin {
println("From plugin value is " + SOME_VALUE)
println("But from plugin 'findProperty' value is " + findProperty("SOME_VALUE"))
clientDataSet = {
data_1 = SOME_VALUE
data_2 = findProperty("SOME_VALUE")
data_3 = "this is a string"
SOME_VALUE is defined on my project gradle.properties, and I got the following log during build:
From root value is correct value from properties
From plugin value is null
But from plugin 'findProperty' value is correct value from properties
and then of course, while receiving data_1 SOME_VALUE is null, data_2 have the correct value and data 3 is the hard-coded string I passed.
My question:
What am I doing wrong or which configuration is missing on my plugin, so that the client application can directly reference properties from their gradle.properties files?
Edit: as requested on the comments
MyPluginData is simply extends HashMap<String, MyPluginDataSet> and MyPluginDataSet is just a few strings.
So inside propertyMissing I'm simply adding the property name to the map, and creating the MyPluginDataSet with the strings, (that later is used to generate custom tasks).
The missing property function:
def propertyMissing(String name, value) {
// Create the new data set and add to the map
def data = new MyPluginDataSet()
put(name, data)
// setup and execute the client closure to configure the data
def closure = value as Closure
closure.delegate = data
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure.run()
}
By making MyPluginData inherit from Map<>, I think you somehow "break" the property resolution process ( see ExtensionAware) and Gradle will not try to search for "SOME_VALUE" property in the different scopes (so it will not find this property from gradle properties extension)
Maybe you can try to simplify you MyPluginData class by storing an internal map instead of inheriting from Map ? something like that:
class MyPluginData {
Map<String, MyPluginDataSet> internalMap = new HashMap<>()
def propertyMissing(String name, value) {
println "Entering propertyMissing for name = $name"
// Create the new data set and add to the map
def data = new MyPluginDataSet()
internalMap.put(name, data)
// setup and execute the client closure to configure the data
def closure = value as Closure
closure.delegate = data
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure.run()
}
}

why can quotes be left out in names of gradle tasks

I don't understand why we don't need to add quotes to the name of gradle task when we declare it
like:
task hello (type : DefaultTask) {
}
I've tried in a groovy project and found that it's illegal, how gradle makes it works.
And I don't understand the expression above neither, why we can add (type : DefaultTask), how can we analyze it with groovy grammar?
As an example in a GroovyConsole runnable form, you can define a bit of code thusly:
// Set the base class for our DSL
#BaseScript(MyDSL)
import groovy.transform.BaseScript
// Something to deal with people
class Person {
String name
Closure method
String toString() { "$name" }
Person(String name, Closure cl) {
this.name = name
this.method = cl
this.method.delegate = this
}
def greet(String greeting) {
println "$greeting $name"
}
}
// and our base DSL class
abstract class MyDSL extends Script {
def methodMissing(String name, args) {
return new Person(name, args[0])
}
def person(Person p) {
p.method(p)
}
}
// Then our actual script
person tim {
greet 'Hello'
}
So when the script at the bottom is executed, it prints Hello tim to stdout
But David's answer is the correct one, this is just for example
See also here in the documentation for Groovy
A Gradle build script is a Groovy DSL application. By careful use of "methodMissing" and "propertyMissing" methods, all magic is possible.
I don't remember the exact mechanism around "task ". I think this was asked in the Gradle forum (probably more than once).
Here is the code that make the magic possible and legal.
// DSL class - Gradle Task
class Task {
def name;
}
// DSL class - Gradle Project
class Project {
List<Task> tasks = [];
def methodMissing(String name, def args) {
if(name == "task"){
Task t = new Task(name:args[0])
tasks << t
}
}
def propertyMissing(String name) {
name
}
}
// gradle build script
def buildScript = {
task myTask
println tasks[0].name
}
buildScript.delegate = new Project()
// calling the script will print out "myTask"
buildScript()

Gradle: Using string interpolation in model DSL

How can I substitute a project property in a string in a model DSL? I tried the following:
apply plugin: 'com.android.model.native'
model {
android {
...
sources {
main {
jni {
source {
srcDirs "src"
include "*.cpp"
}
exportedHeaders {
srcDir "${project.rootDir}/include"
}
}
}
}
}
}
But I got this error:
Error:Attempt to read a write only view of model of type 'org.gradle.model.ModelMap<org.gradle.language.base.FunctionalSourceSet>' given to rule 'android { ... } # android/build.gradle line 6, column 5'
It works if I assign the property value to a variable outside of the model block and substitute that variable instead:
def fooDir = project.rootDir
...
srcDir "${fooDir}/include"
But that's a bit inconvenient.
It's a weird error, but it means you are trying to access a property that has not been declared as an input for the rule you are creating. To declare something as an input you have to use the "special syntax" for the model dsl. For example, to access the project.buildDir, you must use:
$.buildDir
Unfortunately, the project.rootDir is not bound to a model path yet. Caching the value in a variable accessible to your model rules (as you have done) seems to be a decent workaround for now.

Resources