Writing custom plugin which uses convention plus external project properties


(Vadim Kirilchuk) #1

Hi everyone,

I have a custom plugin (gradle-RC1) in which i have: MyPlugin.groovy MyPluginConvention.groovy MyPluginTask.groovy

in my gradle build i have convention block:

myConvention {
    prop1 = "1"
    prop2 = "2"
    prop3 = "3"
}

But i also want to use external project properties (must have higher priority), for example gradle myTask -Pprop1=“100500” must override prop1

In MyPlugin i have next code:

void apply(Project project) {
   MyPluginConvention convention = new MyPluginConvention()
   project.convention.plugins.myConvention = convention
   configureTask(project, convention)
   }
  void configureTask(Project project, MyPluginConvention convention) {
       project.tasks.withType(MyPluginTask).whenTaskAdded { MyPluginTask task ->
       task.conventionMapping.map('prop1')
{ resolveProperty(project, 'prop1', convention.prop1)
}
       task.conventionMapping.map('prop2')
{ resolveProperty(project, 'prop2', convention.prop2)
}
       task.conventionMapping.map('prop3')
{ resolveProperty(project, 'prop3', convention.prop3)
}
    }
}
     private def resolveProperty(Project project, String propertyName, def conventionProperty) {
 return getProjectProperty(project, propertyName) ?: conventionProperty
}
  private def getProjectProperty(Project project, String propertyName) {
        return project.hasProperty(propertyName) ? project.property(propertyName) : null
}

And in MyPluginTask i use explicit getters to evaluate fields lazy. However, resolveProperty() method not works as expected, because when task gets properties the “project.property(‘prop1’)” method returns convention property - not the external project property.

How can i get the right behavior? Also “task.conventionMapping.map” is a little tricky for me. Where can i get some information about how it works, or could you suggest a better way of doing convention mapping?

Currently i’am using next workaround:

myConvention {
    prop1 = loadProp1()
    prop2 = "2"
    prop3 = "3"
}
  String loadProp1() {
    return project.hasProperty('prop1') ? project.prop1 : "1"
}

Thanks.


(Peter Niederwieser) #2

The user guide has some information on writing plugin. Here is a full example that shows how I might code this:

class MyExtension {
    String bar
    String baz
}
  class MyTask extends DefaultTask {
    String bar
    String baz
          @TaskAction
    void run() {
        // inside task class, getter method has to be used explicitly
        println getBar()
        println getBaz()
    }
}
  class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
      def extension = new MyExtension()
       project.extensions.foo = extension
              project.tasks.add("myTask", MyTask)
              // assuming there may be multiple such tasks
      project.tasks.withType(MyTask) {
          conventionMapping.bar = { project.hasProperty("bar") ? project.property("bar") : extension.bar }
          conventionMapping.baz = { project.hasProperty("baz") ? project.property("baz") : extension.baz }
      }
       }
}
  apply plugin: MyPlugin
  foo {
    bar = "one"
    baz = "two"
}

(Vadim Kirilchuk) #3

Thanks, your code snippet helped a lot to clean the code, but i still have some troubles with it:

  1. I had property with name ‘source’, with your code
conventionMapping.source = { resolveProperty(project, 'source', extension.source) }

it throws class cast exception… If i rename ‘source’ to ‘sourcePath’ then it works, but the reason is not very clear for me.

  1. After use of
def extension = new MyExtension()
 project.extensions.foo = extension

i have next message: Dynamic properties have been deprecated (property “foo” on the object “org.gradle.api.reporting.ReportingExtension_Decorated@56ff18” with value “com…MyExtension@185b10b” created). See: http://gradle.org/docs/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html

  1. This code still ignores gradle myTask -Pbar=“SUPERBAR” and will still use script hardcoded bar = “one”

Any suggestions?


(Peter Niederwieser) #4

I double checked my code, and it neither has problem 2) nor 3). I can only imagine that something went wrong when you adapted the code to your needs. As for 1), I can’t tell what the problem is without you showing me the full code, but it looks like the value that your closure produces can’t be assigned to the task property (incompatible types).


(Vadim Kirilchuk) #5

I rewrited my plugin from scratch and it seems that you were right. Works like a charm now. If i reproduce first problem i will provide more information about it.

The problem was in “project.extensions.foo = extension”, i misstyped and wrote “project.extension.foo”(not extensionS). This lead me to mentioned problems.

Thanks a lot!


(Vadim Kirilchuk) #6

Here is the error message with ClassCastException:

Cannot cast object 'com.db.fusion.gradle.MyPlugin$_configureTasks_closure1_closure6@1e4acce' with class 'com.db.fusion.gradle.MyPlugin$_configureTasks_closure1_closure6' to class org.gradle.api.internal.IConventionAware'

Also i tried to do some ‘printlns’ in conventionMapping part:

//println("1source: " + conventionMapping.action)
//println("1class: " + conventionMapping.action.class)
conventionMapping.action
   = { resolveProperty(project, 'action', extension.action) }
println("2source: " + conventionMapping.source)
println("2class: " + conventionMapping.source.class)
conventionMapping.source
   = { resolveProperty(project, 'source', extension.source) }

If i execute task i will see:

2source: task ':myPluginTask'
2class: class com.db.fusion.gradle.MyPluginTask_Decorated

and then classCast error

Cannot cast object 'com.db.fusion.gradle.MyPlugin$_configureTasks_closure1_closure6@1e4acce' with class 'com.db.fusion.gradle.MyPlugin$_configureTasks_closure1_closure6' to class org.gradle.api.internal.IConventionAware'

But if i uncomment printlns in the beggining, then task fails with next message:

No such property: action for class org.gradle.api.internal.ConventionAwareHelper

So, i think that conventionMapping.source is reserved property and cant be used in plugin, am i right?

Thx.


(Peter Niederwieser) #7

It’s a name conflict with a field on the ConventionMapping implementation class. In such a case you can use ‘conventionMapping.map(“source”, { … })’. That notation is less convenient but safer.


(Vadim Kirilchuk) #8

It helped. Great thanks for the help!