I cannot reference a Gradle custom extension using a closure - gradle

I'm writing a custom gradle plugin that uses an extension. Here's what the relevant part look like:
class Aar2Jar implements org.gradle.api.Plugin<Project> {
void apply (Project project) {
println('applying Android Jar')
def ext = project.extensions.create('jarSpec', JarSpecExtension)
if (ext.name.get() == "") {
ext.name.set(project.name)
}
and Here's what the extension looks like:
package test.android.gradle
import org.gradle.api.provider.Property
abstract class JarSpecExtension {
abstract Property<String> getName()
abstract Property<String> getVersion()
abstract Property<String> getInclude()
abstract Property<String> getExclude()
JarSpecExtension() {
name.convention("")
version.convention("1.0.0")
include.convention("*")
exclude.convention("")
}
}
And here's what my build.gradle script that uses this custom plugin looks like:
plugins {
id 'com.android.library'
id 'test.android.jar'
}
jarSpec {
name = 'myJar'
}
The problem is that doesn't work and I get the following error:
* What went wrong:
An exception occurred applying plugin request [id: 'test.android.jar']
> Failed to apply plugin 'test.android.jar'.
> No such property: extensions for class: test.android.gradle.Aar2Jar
However, if I change the build.gradle file to this:
plugins {
id 'com.android.library'
id 'test.android.jar'
}
jarSpec.name = 'myJar'
It works perfect. The question is why can I not refer to the extension using the closure style. I'm using gradle version 7.2.

Related

How do I create nested configuration parameters in a custom gradle plugin?

How do I create a nested parameter structure in a custom gradle plugin?
For starters, I am using Gradle 7.2. I want to make an expressive DSL-like structure for my plugin configuration, with a nested element
fileDiff {
file1 = file('${testFile1.getName()}')
file2 = file('${testFile2.getName()}')
messages {
message1 = 'Hi there'
}
}
While learning how to write Gradle Plugins I have been following the gradle plugin implementation docs, and they are great at showing what to do with the extension but not the "plugin" class.
So I have modeled my extension, FileDiffExtension like so
abstract class FileDiffExtension {
abstract RegularFileProperty getFile1()
abstract RegularFileProperty getFile2()
#Nested
abstract Messages getMessages()
void messages(Action<? super Messages> action) {
action.execute(getMessages())
}
}
And the nested Messages class is modeled as such
abstract class Messages {
abstract Property<String> getMessage1()
}
I think I am good up to this point. Then I need to pull my extension into my plugin and this is where I believe I am running into issues. My Plugin class currently looks like
class FileDiffPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
project.tasks.register('fileDiff', FileDiffTask) {
project.extensions.create('fileDiff', FileDiffExtension)
project.fileDiff.extensions.create("messages", FileDiffExtension.messages)
file1 = project.fileDiff.file1
file2 = project.fileDiff.file2
messages = project.fileDiff.getMessages
}
}
}
I am trying to create a messages extension off of the root level extension fileDiff. Or maybe I am not supposed to set the messages object in the task to the getMessages() abstract method. But I have tried every combination I can think of. The actual task is shown below, but I don't think the problem lies here.
abstract class FileDiffTask extends DefaultTask {
#InputFile
abstract RegularFileProperty getFile1()
#InputFile
abstract RegularFileProperty getFile2()
#Input
abstract Property<Messages> getMessages()
#OutputFile
abstract RegularFileProperty getResultFile()
FileDiffTask() {
resultFile.convention(project.layout.buildDirectory.file('diff-result.txt'))
}
#TaskAction
def diff() {
// Print out the message
println messages.get().message1.toString()
// Now we do some fun file stuff
String diffResult
if (size(file1) == size(file2)) {
diffResult = "Files have the same size at ${file1.get().asFile.getBytes()} bytes}"
} else {
File largestFile = size(file1) > size(file2) ? file1.get().asFile : file2.get().asFile
diffResult = "${largestFile.toString()} is the largest file at ${largestFile.size()} bytes"
}
resultFile.get().asFile.write diffResult
println "File written to $resultFile"
println diffResult
}
private static long size(RegularFileProperty regularFileProperty) {
return regularFileProperty.get().asFile.size()
}
}
To test I am using the gradle test kit, and I'm currently getting the following error.
`
Could not create task ':fileDiff'.
No such property: messages for class: com.robschwartz.plugins.filediff.FileDiffExtension
Possible solutions: messages
`
Hm, according to docs you do not need to do anything with the sub-extension if it's marked as #Nested, it should just work. Have you tried to remove this line entirely?
project.fileDiff.extensions.create("messages", FileDiffExtension.messages)

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"
)
}

Test Custom Gradle plugin after Evaluate

I'm developing a Gradle custom plugin and I'm having issues on how to test it.
The plugin creates an extension to receive configuration and after evaluation (project.afterEvaluate {) creates a tasks with the received configuration, those values are #Input on tasks.
Following the documentation https://docs.gradle.org/current/userguide/custom_plugins.html to create a test for the plugin, I use the following to create the project and apply the plugin
#Before fun setup() {
project = ProjectBuilder.builder().build()
project.pluginManager.apply("my.plugin.name")
and then test that extension got created:
assertTrue(project.extensions.findByName("name") is MyConfigType)
and the task got created:
assertTrue(project.tasks.findByName("mytask") is MyTaskType)
The issue I'm having is that the task is only created afterEvaluate, so this test is failing. As far as I understood, it has to be afterEvaluate so that it can receive the configuration values.
So the only way I could see if I could on the test force this project to be evaluated, but how?
Is there maybe a different way to receive values?
I posted my similar question in the gradle forums and was able to solve the issue:
https://discuss.gradle.org/t/unit-test-plugins-afterevaulate/37437/3
Apparently afterEvaluate is not the best/right place to perform the task creation. If you have a DomainObjectCollection in your extension and want to create a task for each element in the collection, task creation should be done in the all-Callback of the collection:
final MyExtension extension = project.getExtensions().create("extension", MyExtension.class);
extension.configurations.all((c) -> {
// register task here
});
If you have simple properties in the extension that are feed to the task as input, you should use lazy configuration:
public class MyExtension {
public final Property<String> property;
public final NamedDomainObjectContainer<Configuration> configurations;
#Inject
public MyExtension(final ObjectFactory objectFactory) {
property = objectFactory.property(String.class).convention("value");
configurations = objectFactory.domainObjectContainer(Configuration.class);
}
}
public abstract class MyTask extends DefaultTask {
#Input
private final Property<String> property = getProject().getObjects().property(String.class);
public Property<String> getProperty() {
return property;
}
}
And the apply method:
public class MyPlugin implements Plugin<Project> {
#Override
public void apply(final Project aProject) {
final MyExtension extension = aProject.getExtensions().create("extension", MyExtension.class);
aProject.getTasks().register("myTask", MyTask.class).configure((t) -> {
t.getProperty().set(extension.property);
});
}
}
You should call project.evaluationDependsOn(":"):
#Before fun setup() {
project = ProjectBuilder.builder().build()
project.pluginManager.apply("my.plugin.name")
project.evaluationDependsOn(":") // <<--
...
}
It executes your afterEvaluate callback.

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