How can I use #Delegate in a custom task with Gradle 7? - gradle

I've got a custom task (specifically https://github.com/marklogic-community/ml-gradle/blob/master/src/main/groovy/com/marklogic/gradle/task/MlcpTask.groovy#L26 , though I'm going to provide a bare minimum example below) where I've been using #Delegate on a task property for many versions of Gradle, but it no longer works with Gradle 7. I've tried adding #Input/#Optional everywhere, but no luck.
My use case in the above task is that I want to reuse a Java class that has dozens of properties on it, and I want a user to be able to assign values to those properties via task properties in their build.gradle file.
Here's the easiest way to reproduce the problem I'm having - I add the following to my build.gradle file:
class MyTask extends DefaultTask {
#Delegate
MyBean myBean = new MyBean()
#TaskAction
void myAction() {
println "myBean value: " + myBean.myValue
}
}
class MyBean {
String myValue
}
task myTest(type: MyTask) {
myValue = "something"
}
And when I run "myTest" using Gradle 6.9, it runs fine and prints out "myBean value: something". When I run this on Gradle 7.1.1, I get an error of "Type 'MyTask' property 'myBean' is missing an input or output annotation.".
So I added #Input to myBean. Then I get an error of "Type 'MyTask' property 'myValue' is missing an input or output annotation."
So I add #Input to myValue in the MyBean class, and I get the same error of "Type 'MyTask' property 'myValue' is missing an input or output annotation."
Is there a way for #Delegate to still work in this fashion in Gradle, and if so, how? Or is this possibly a bug in Gradle 7? I'll file one if so. Or, is there a better way to do this - i.e. to reuse an existing Java class in a Gradle task such that the task can delegate property assignments to the Java class?

Related

How to pass #Input String in a task in buildSrc

This custom plugin exists in gradle's buildSrc/:
abstract class MyTask : DefaultTask() {
#get:Input
abstract val buildDir: Property<String>
#TaskAction
fun someTask() {
// do stuff
}
}
class DevelopmentPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.run {
register("myTask", MyTask::class.java) {
inputs.property("buildDir", project.buildDir)
println(inputs.getProperties())
}
}
}
}
and by running the task with e.g. $ ./gradlew myTask fails with:
Could not determine the dependencies of task ':myTask'.
> Cannot query the value of task ':myTask' property 'rootDir' because it has no value available.
Also the prinln outputs {buildDir=null} meaning that the inputs.property("buildDir", project.buildDir) has failed.
How to pass the project.buildDir value from the Plugin in the task?
Using project.buildDir directly from inside the task is not an acceptable answer due to Gradle's incubating build-cache functionality.
Firstly, there is a class type issue which is not visible in Gradle.
buildDir is of type File while the property is String.
So "${project.buildDir}" should be used.
Secondly, since the property is abstract val it can directly be accessed in the closure. Therefore it can be set with:
// instead of:
inputs.property("buildDir", "${project.buildDir}")
// just this:
buildDir.set("${project.buildDir}")

How to provide the value of a #Nested property of a gradle task?

The gradle doc describes the #Nested annotation for custom gradle tasks:
https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:task_input_output_annotations
Unfortunately, there is no complete example of this mechanism in terms of how it is used in a build.gradle file. I created a project to demonstrate a strange exception that happens whenever gradle configures the project:
https://github.com/NicolasRouquette/gradle-nested-property-test
The build.gradle has the following:
task T(type: NestedTest) {
tool = file('x')
metadata = {
a = "1"
}
}
The NestedTest custom task is in the buildSrc folder:
class NestedTest extends DefaultTask {
#InputFile
public File tool
#Nested
#Input
public Metadata metadata
#TaskAction
def run() throws IOException {
// do something...
}
}
The important bit is the #Nested property whose type is really basic:
class Mlang-groovyetadata {
String a
}
When I execute the following: ./gradlew tasks, I get this:
Build file '/opt/local/github.me/gradle-nested-property-test/build.gradle' line: 26
* What went wrong:
A problem occurred evaluating root project 'gradle-nested-property-test'.
> Cannot cast object 'build_6wy0cf8fn1e9nrlxf3vmxnl5z$_run_closure4$_closure5#2bde737' with class 'build_6wy0cf8fn1e9nrlxf3vmxnl5z$_run_closure4$_closure5' to class 'Metadata'
Can anyone explain what is happening and how to make this work?
Nicolas
Looking at the unit tests in Gradle's source code, I found that the syntax for #Nested properties requires invoking the type constructor in the build.gradle file.
That is, the following works:
task T(type: NestedTest) {
tool = file('x')
metadata = new Metadata(
a: "1"
)
}

Private method visibility confusion

I am confused with Groovy method visibility in the context of my Gradle build.
For some tests in my project, I have to first start a server.
For this I created a custom task class that extends Gradle's Test like so:
class TestWithServer extends Test {
TestWithServer() {
super()
beforeTest {
startServer()
}
}
private void startServer() {
println('placeholder')
}
}
But if I try to run such a task, I get an error:
Could not find method startServer() for arguments [] on task ':testWithServer' of type TestWithServer.
I found that when I change the visibility of startServer() to the default (public), the task runs fine.
How come I can't use the private method from within its own class?
It is not the same class, because Gradle adds some magic to the task types. Just add println this.class into the beforeTest closure to see the name of the actual class (something like TestWithServer_Decorated). This additional magic also explains why the error message contains the task name and how the class knows about being a task (type) at all. Since the decorated class is a subclass of your class you can use the protected modifier to encapsulate your method.

Groovy override default call method

I have the following groovy class as part of my gradle plugin:
class MyClass {
final Expando someOptions
MyClass() {
someOptions = new Expando()
}
def call(Closure configure) {
configure.delegate = someOptions
configure.resolveStrategy = Closure.DELEGATE_ONLY
configure()
}
}
Now I want to user to have the ability to configure this class by adding extra properties to it, but those properties should be stored in someOptions.
I tried doing this in the class:
def call(final Closure configure) {
configure.delegate = someOptions
configure.resolveStrategy = Closure.DELEGATE_ONLY
configure()
}
The user of the plugin can do:
myClass {
hello='world'
}
However, gradle does not seem to understand that the hello property does not exist on the myClass instance but rather on someOptions within the class. Whenever I use the above, I get errors about hello not existing in the MyClass instance.
How do I do this? Is it possible?
FWIW, it works in the groovy console, but not in gradle.
Any classes you define in your plugin are not directly used in Gradle, but wrapped in proxy classes by Gradle. As an example,
Gradle will create a proxy class for the actual class implementation and adds (among other things) also a property setter method. The method has the name of the property and has a single argument of the same type as the property. It is different from the setProperty and getProperty methods already added by Groovy. For example if we have a task with a property with the name message of type String then Gradle will add the method message(String) to the proxy class. (Source)
This is the reason, why you can omit the assignment sign in Gradle scrips:
task myTask {
myProperty true // uses Gradle generated method
myProperty = true // uses Groovy generated setter
}
Gradle also adds a method similar to yours to allow the configuration of any object in the DSL:
myExtension {
// this works thanks to Gradle
}
Without this proxy method, it would be necessary to use the method with(Closure) from the Groovy language for any block:
myExtension.with {
// this works thanks to Groovy
}
It seems like this proxy method overrides the call(Closure) method of your example.
To solve this, you could use the Delegate annotation in Groovy on someOptions. This would make all its properties available to the MyClass instance. You could also register someOptions as convention on MyClass.
EDIT
You can see that your method is never called by comparing the stacktrace of your current example and a second stacktrace, after you changed the name of the call method and called it explicitly (you need to use another property to get the same exception).

How to reference field in Gradle plugin class?

I'm using a custom Gradle plugin with a class defined like:
class CustomPlugin implements Plugin<Project> {
public static final String CONSTANT = 'value'
}
I've tried all combinations of apply plugin: 'platform-java8-fix' and import netflix.nebula.platformjava8fix.PlatformJava8FixPlugin but none of them allow me to reference CustomPlugin.CONSTANT.
I would like to do something like:
dependencies {
compile "group:module:${CustomPlugin.CONSTANT}"
}
How can this be done?
UPDATE: From within build.gradle, I'm able to access CustomPlugin.CONSTANT. I would like to access it from another file, dependencies.gradle, and have build.gradle do:
apply from: "${rootDir}/dependencies.gradle"
But when I try to perform the import, I get the error:
startup failed:
script 'dependencies.gradle': 1: unable to resolve class com.example.CustomPlugin

Resources