Skip to content

Gradle Reporting API#

Gradle has a reporting system that tasks creating reports can expose, letting users configure formats and output locations. For plugin developers that would like closer integration with Gradle systems, to integrate the Reporting API into your own Gradle plugin, two things are needed: a ReportContainer class and a Report object.

We’ll be working with the VerifyPluginProjectConfigurationTask, a task from the IntelliJ Platform Gradle Plugin that does the simple job of validating a plugin project’s configuration.

Tasks that produce reports must implement the Reporting interface with a type argument that extends ReportContainer:

abstract class VerifyPluginProjectConfigurationTask : DefaultTask(), Reporting<VerifyPluginConfigurationReports> {
    // skipped lines
}

VerifyPluginConfigurationReports is an interface that extends ReportContainer<SingleFileReport>. SingleFileReport is a useful wrapper around Report that represents that our output will be a single file. If your report was a group of files you’d want to put in a directory, of which a DirectoryReport would be used instead.

interface VerifyPluginConfigurationReports: ReportContainer<SingleFileReport> {
    @get:Nested
    val txt: SingleFileReport
}

This interface defines the public contract of reports available to users. Having multiple report formats, for example, text, html and/or markdown, can be defined like so:

interface VerifyPluginConfigurationReports: ReportContainer<SingleFileReport> {
    @get:Nested
    val txt: SingleFileReport

    @get:Nested
    val html: SingleFileReport

    @get:Nested
    val markdown: SingleFileReport      
}

If you need a custom report with additional properties beyond say SingleFileReport, you can define your own report type (e.g., an interface extending SingleFileReport) and still provide it from your ReportContainer.

Next, we need an implementation which will define how exactly our reports will be created:

class VerifyPluginConfigurationReportsImpl : VerifyPluginConfigurationReports {

}

Providing an implementation of our report container requires that we override over 40 different methods. To avoid doing this, we can delegate access to these methods to another ReportContainer class with the help of DelegatingReportContainer.

Since DelegatingReportContainer, DefaultReportContainer and DefaultSingleFileReport are internal classes, keep their usages inside your implementation so your plugin’s API only exposes public Gradle types like ReportContainer and SingleFileReport.

class VerifyPluginConfigurationReportsImpl @Inject constructor(
    owner: Describable, 
    objectFactory: ObjectFactory,
) : DelegatingReportContainer<SingleFileReport>(
        DefaultReportContainer.create(objectFactory,SingleFileReport::class.java) { factory -> 
            listOf(factory.instantiateReport(DefaultSingleFileReport::class.java, "txt", owner)) 
        }
     ), 
    VerifyPluginConfigurationReports {

}

DefaultSingleFileReport requires a name and a Describable object. And the DefaultReportContainer contains a create function that allows us to create a new report container by supplying an ObjectFactory instance, the type of reports that this container holds and a generator that creates those reports using ReportGenerator:

public interface ReportGenerator<T extends Report> { 
    Collection<T> generateReports(ReportFactory<T> factory);
}

Here, generateReports demands that we return a collection of our report types, so using the ReportFactory we can instantiate our report by passing in the required constructor arguments: name of the report and a Describable object.

Next, we must provide an implementation for our txt property by referencing the name of our report:

class VerifyPluginConfigurationReportsImpl @Inject constructor(
    owner: Describable,
    objectFactory: ObjectFactory,
) : DelegatingReportContainer<SingleFileReport>(
    DefaultReportContainer.create(objectFactory,SingleFileReport::class.java) { factory ->
        listOf(factory.instantiateReport(DefaultSingleFileReport::class.java, "txt", owner))
    }
),
    VerifyPluginConfigurationReports {

    override val txt: SingleFileReport get() = getByName("txt")

}

After defining our report container and report objects, back to our task to start working with them:

abstract class VerifyPluginProjectConfigurationTask : DefaultTask(), Reporting<VerifyPluginConfigurationReports> { 
    // skipped lines

    @get:Inject 
    abstract val objectFactory: ObjectFactory 

    private val verifyPluginProjectReports : VerifyPluginConfigurationReports = objectFactory.newInstance(
        VerifyPluginConfigurationReportsImpl::class.java, 
        Describables.quoted("Task", identityPath)
    ) 
    // skipped lines
}

We will not be able to create an instance of VerifyPluginConfigurationReportsImpl because it’s a final class, so let’s make it open:

open class VerifyPluginConfigurationReportsImpl @Inject constructor(
    owner: Describable, 
    objectFactory: ObjectFactory,
) /*skipped lines*/ {
    // skipped lines
}

Override these functions to rely on our reports property verifyPluginProjectReports:

abstract class VerifyPluginProjectConfigurationTask : DefaultTask(), Reporting<VerifyPluginConfigurationReports> {
    // skipped lines

    @Nested 
    override fun getReports(): VerifyPluginConfigurationReports = verifyPluginProjectReports

    override fun reports(closure: Closure<*>): VerifyPluginConfigurationReports { 
        return reports(ClosureBackedAction(closure)) 
    }

    override fun reports(configureAction: Action<in VerifyPluginConfigurationReports>): VerifyPluginConfigurationReports { 
        configureAction.execute(verifyPluginProjectReports)
        return verifyPluginProjectReports 
    } 
    // skipped lines
}

We annotate getReports() with @Nested so Gradle inspects the returned report container’s properties (like txt, html, markdown) and their annotations.

When configuring our task, we can go ahead to use our function to set up our reports:

reports { 
    txt.required.set(true)
    txt.outputLocation.convention(project.layout.buildDirectory.file("reports/verifyPluginConfiguration/report.txt"))
}

Finally, writing to our reports can be done in this fashion:

if (verifyPluginProjectReports.txt.required.get()) { 
    verifyPluginProjectReports.txt.outputLocation.get().asFile.writeText(it)
}

References#

  • https://docs.gradle.org/current/dsl/org.gradle.api.reporting.ReportContainer.html
  • https://docs.gradle.org/current/dsl/org.gradle.api.reporting.Reporting.html
  • https://docs.gradle.org/current/dsl/org.gradle.api.reporting.Report.html
  • https://docs.gradle.org/current/dsl/org.gradle.api.reporting.SingleFileReport.html
  • https://docs.gradle.org/current/dsl/org.gradle.api.reporting.DirectoryReport.html
  • https://github.com/gradle/gradle/issues/7063

Tested with: Gradle 9.0.0