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

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)

Related

How to incorporate command-line arguments in a custom Gradle Task?

I'm working on building a Gradle plugin that contains a single task which will combine two input files (the filename of one is constructed from command-line input) into one output file. I have followed this DZone article to set up my task and plugin classes. When I run the task, I see the log statement containing the command-line input, but the the plugin code to configure the task gets a null value for the input. How can I fix this? Example code below.
class FooPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
project.tasks.register("foo", FooTask) {
if (it.hasProperty("bar") {
it.baseFile.convention(project.layout.projectDirectory.file("my${bar}.txt")
}
}
}
}
#CompileSTatic
abstract class FooTask extends DefaultTask {
#Input
String bar
#InputFile
abstract RegularFileProperty getBarFile()
#Option(option = "bar", description = "Does bar-ish things")
void setBar(bar) {
println "Setting bar variable from command-line argument $bar"
this.bar = bar
}
#TaskAction
void doFoo() {
// task logic using the bar variable
}
}

How do I implement a gradle plugin which depends on an extension being configured?

For context, I'm attempting to write a plugin which will generate source code (Java classes), and it exposes multiple tasks (for different purposes). These tasks rely on information given by the user via extensions. Without this user defined information, these tasks wouldn't know what to do, and so they shouldn't be run at all if the extension hasn't been defined in build.gradle by the user.
One key thing to note is that, since I'm generating source code, I'm making the JavaCompile task depend on each of these tasks. However, I only want to define this relationship if the extension has been configured in build.gradle. How do I achieve this?
The following is a simple example for one task.
The MyPlugin class which is the implementation class:
public class MyPlugin implements Plugin<Project> {
// define extensions
MyPluginConfig myPluginConfig = project.getExtensions().create("myPluginConfig", MyPluginConfig.class);
((ExtensionAware) myPluginConfig).getExtensions().create("fileConfig", FileConfig.class);
// register task
TaskProvider<MyPluginTask> taskProvider = project.getTasks().register("myPluginTask", MyPluginTask.class, task -> {
task.getFilesConfigs().set(myPluginConfig.getFileConfigs());
});
// ensure JavaCompile task depends on this plugin's task
project.getTasks().withType(JavaCompile.class).configureEach(task -> task.dependsOn(taskProvider));
}
MyPluginConfig is an extension class which just contains a list of FileConfig:
public abstract class MyPluginConfig {
public abstract ListProperty<FileConfig> getFileConfigs();
}
FileConfig is an extension class (only used within MyPluginConfig) which contains the actual information the task may use to generate Java classes:
public abstract class FileConfig {
public abstract Property<String> getX();
public abstract Property<String> getY();
public abstract Property<String> getZ();
}
And finally, MyPluginTask uses the information from these extensions to generate the Java classes:
public abstract class MyPluginTask {
#Input
public abstract ListProperty<FileConfig> getFileConfigs();
#TaskAction
public void execute() {
System.out.println("Executing task...");
}
}
In build.gradle, a user can now use this plugin like so:
plugins {
id 'com.example.my-plugin' version '0.0.1'
}
/*
myPluginConfig {
fileConfigs = [
fileConfig {
x = 'x'
y = 'y'
z = 'z'
},
fileConfig {
x = '2x'
y = '2y'
z = '2z'
},
]
}
*/
Notice that I actually commented out the myPluginConfig configuration above. As such, I don't want my task (i.e, MyPluginTask#execute) to run at all. With the way I've implemented it so far, it will run because of the dependency I created between it and the JavaCompile task.
An easy way to work around this would be to check if MyPluginTask#getFileConfigs returns an empty list. However, this doesn't feel "clean"? It seems a bit fragile since the business logic within the task could change in the future. Is there an official or proper way to do what I'm trying to achieve?

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 use Groovy CodeVisitorSupport visitor class when parsing a gradle script?

I would like to parse an android app's build.gradle file in Java and I am trying to use Groovy CodeVistorSupport for that as follows:
public class parseBuildGradle extends CodeVisitorSupport{
#Override
public void visitMethodCallExpression(MethodCallExpression call)
{
//My code
}
}
In order to use this class, I assume I should somehow get the compilation unit or ast and then call the class. However, I am not sure what APIs I should be using and unfortunately I could not find any related documentation. I am wondering if anyone can help me with that.
Below is a possible answer to my question:
SourceUnit unit = SourceUnit.create("gradle", gradleFileToString);
unit.parse();
unit.completePhase();
unit.convert();
visitScriptCode(unit, new parseBuildGradle());
private void visitScriptCode(SourceUnit source, GroovyCodeVisitor transformer) {
source.getAST().getStatementBlock().visit(transformer);
for (Object method : source.getAST().getMethods()) {
MethodNode methodNode = (MethodNode) method;
methodNode.getCode().visit(transformer);
}
}

Gradle Custom Task Configuration

I'm working on a Gradle plugin that has a Custom Task with two member variables that I would like to initialize using an extension class. I am extending the plugin class as follows:
class CustomPlugin implements Plugin<Project> {
#Override void apply(Project project) {
def extension = project.extensions.create("Custom", CustomExtension)
project.task("doTask", type: CustomTask, {
group = "Awesome"
description = "Runs a custom routine"
filePath = extension.filePath
name = extension.name
})
}
}
Is this the proper way to initialize a Task that extends DefaultTask? I'm trying to understand if initialization is done in CustomPlugin or if within the CustomTask one would use:
#TaskAction void removeUnusedResources() {
String filePath = project.Custom.lintXmlFilePath
String name = project.Custom.ignoreResFiles
// Proceed with the task action
}
Only the second approach seems to work for me. One common pattern I am noticing in other plugins is that tasks tend to be defined within the plugin using project.task("taskName") << { // task actions here} instead of creating a separate class that extends DefaultTask. What is the right convention and where can I find more information?

Resources