How to Replace RuleSource?

I am trying to update an older custom plugin to Gradle 7.4, with the goal to simplify it at the same time.
The plugin was written with several classes derived from RuleSource.
On the page Rule based model configuration I found this comment:
“Rule based configuration will be deprecated. New plugins should not use this concept.”

I am not very familiar with Gradle, even less with writing plugins myself. So I was wondering what I should use as a replacement for rules. Can anybody give some general directions what type of construct should be used with 7.4 instead of rules?
The preferred language would be Kotlin.

Here is an example of an existing rule that I want to replace with a future-proof solution based on 7.4 technology:

import org.gradle.api.Task
import org.gradle.api.plugins.Convention
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.model.ModelMap
import org.gradle.model.Mutate
import org.gradle.model.Path
import org.gradle.model.RuleSource
import org.gradle.testing.jacoco.tasks.JacocoReport

public class JacocoTestReportRule extends RuleSource {

    @Mutate
    void configureJacocoTestReportTasks(@Path("tasks") ModelMap<Task> tasks, ExtensionContainer extensions) {
        JavaPluginConvention javaPluginConvention = ((Convention) extensions).getPlugin(JavaPluginConvention.class)

        tasks.jacocoTestReport.configure {
            reports {
                xml.enabled false
                csv.enabled false
                html.destination new File("${task.project.buildDir.path}/jacocoHtml")
            }
        }

        tasks.jacocoTestReport.dependsOn tasks.test

        if (tasks.get("integrationTest")) {

            tasks.create("jacocoIntegrationTestReport", JacocoReport.class) {
                description = 'Generates code coverage report for the integrationTest task.'
                group = 'verification'
            }

            tasks.jacocoIntegrationTestReport.configure {
                sourceSets javaPluginConvention.sourceSets.main
                executionData tasks.integrationTest

                reports {
                    xml.enabled = false
                    csv.enabled = false
                    html.destination new File("${task.project.buildDir.path}/jacocoHtml")
                }
            }

            tasks.jacocoIntegrationTestReport.dependsOn tasks.integrationTest
        }
    }
}

I might add that I am not sure whether there was a compelling reason to use rules to implement the above in the first place. Maybe there would have been a simpler way to begin with. The developer who wrote the custom plugin also started pretty much with zero knowledge (and has left the team since).

Talking to myself a bit, but this seems to be an option:
buildSrc

Correct, this configuration didn’t really need or benefit from being implemented as a rule to begin with. They intended to resolve ordering dependencies in configuration through a dependency injection style method. However, this configuration doesn’t require anything that isn’t readily available initially when a plugin is applied.

There are other improvements that could be made (like configuration avoidance APIs now), but the code would function directly in a plugin’s apply(Project) method with just getting the tasks and extensions from the Project.

1 Like

Thanks for the feedback!
Here is what I have come up with in my custom plugin written in Kotlin. It seems to work OK:

class CiPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.setupJacocoPlugin()
    }

    private fun Project.setupJacocoPlugin() {
        project.tasks.withType(JacocoReport::class.java) { jacocoReportTask ->
            jacocoReportTask.reports.xml.required.set(false) // xml.enabled false
            jacocoReportTask.reports.csv.required.set(false) // csv.enabled false
            jacocoReportTask.reports.html.outputLocation.set(File("${jacocoReportTask.project.buildDir.path}/jacocoHtml"))
            jacocoReportTask.dependsOn(tasks.getByName("test"))
        }
    }
}

(Does not correspond exactly to my original question, turns out that this was dead code anyway,)

That looks a lot better for what it was doing. I would make a couple of tweaks to use the current recommended APIs in Gradle (typed directly in this post, so untested exactly as written).


Use the configureEach which is part of the task configuration avoidance API for the configuration of the task.

project.tasks.withType(JacocoReport::class.java) { jacocoReportTask ->

becomes

project.tasks.withType(JacocoReport::class.java).configureEach { jacocoReportTask ->

For the report output location, use the project layout that provides a DirectoryProperty over creating a File from the full path String.

jacocoReportTask.reports.html.outputLocation.set(File("${jacocoReportTask.project.buildDir.path}/jacocoHtml"))

becomes

jacocoReportTask.reports.html.outputLocation.set(project.layout.buildDirectory.dir("jacocoHtml")))
2 Likes

Thank you so much for your helpful feedback! I highly appreciate it as I am trying to build a high-quality plugin for our team.

For completeness’ sake, the function now looks like this:

    private fun Project.setupJacocoPlugin() {
        project.plugins.apply(JacocoPlugin::class.java)
        project.tasks.withType(JacocoReport::class.java).configureEach { jacocoTestReportTask ->
            jacocoTestReportTask.reports.xml.required.set(false) // xml.enabled false
            jacocoTestReportTask.reports.csv.required.set(false) // csv.enabled false
            jacocoTestReportTask.reports.html.outputLocation.set(project.layout.buildDirectory.dir("jacocoHtml"))
            jacocoTestReportTask.dependsOn(tasks.getByName("test"))
        }
    }

Note that the line with project.plugins.apply(JacocoPlugin::class.java) is missing in my previous solution. I had edited it out by mistake.