How do I expose a variable from a plugin to the project?

I have a plugin that, when applied, does a bit of work to compute a result that I want to expose to the user of the plugin in the form of a variable. What is the correct way to expose plugin data structures to the user, ideally in a read-only fashion (though read-write is fine if that is easier/more appropriate).

I have found the documentation for how to supply plugins with data: Developing Custom Gradle Plugins

There are also extra properties but those appear to be intended as a sort of global state for the project, not really for plugins: Project - Gradle DSL Version 8.4

Finally there is the Covention Object that sounds like exactly what I need but I have been unable to find any good documentation on how to actually use it: Convention (Gradle API 8.4)

The convention properties added to the project by the plugins. A plugin can add properties and methods to a project through the project’s Convention object. The properties of this scope may be readable or writable, depending on the convention objects.

For reference (in case anyone needs more details about my use case) I want to expose the versionInfo object to the user after it is filled in on this line: https://github.com/Zoltu/Gradle.Plugin.Versioning/blob/master/src/main/kotlin/com/zoltu/gradle/plugin/GitVersioning.kt#L23

If the result is the result from running a task, you can expose it as a read-only property (getter without setter) of that task. That would be my recommended approach. In the case of Git plugin, consider that some projects may span more repos - you should be able to easily add custom tasks for them and get the version.

If you really want it calculated when the plugin is applied, a better choice is a property of a plugin extension.

The project ext properties has the advantage (and disadvantage) of being in the default namespace. It is convenient at first, but then may become confusing later.

I wouldn’t recommend using the convention objects for this.

In this case, I am not exposing the result of a task, I’m exposing the result of some logic that was executed when the plugin was applied (I want the data available as early as possible in the build pipeline, before everything else as it may be leveraged by other tasks).

When you mention plugin extension, I’m assuming you are talking about this? https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtensionAware.html If so, I’ll look into using that. I believe I have used it before in other plugins, but always as a mechanism for getting configuration data from the user, not providing data to the user.

That is what input/output task dependencies are for really.

In any case, if you do it in the plugin apply() method, do take care to calculate and use the value lazily or you may end up with unnecessary overhead in the project configuration phase.

Keep in mind that the project configuration runs every time you type gradle - even if it is to for gradle clean or gradle help

What are input/output tasks?

My plugin sets the version attribute on the project and I want to expose the components of the set version to the user. My concern is that if I do it as a task, then users won’t be able to reference the set version or the exposed components in their build.gradle directly if I set it in a task since my understanding is that the build.gradle executes in its entirety before any tasks are executed. So if the user, in the root of their project, does something like:

if (versionInfo.minor == 0 && versionInfo.patch == 0) {
	// TODO: major version bump setup
} else {
	// TODO: minor version bump setup
}

I want that to work.

We went down the road of creating a version plugin with extensions and tasks, etc. In the end it was a bit of a disaster and eventually we threw it away and created a class for our versions. So, now our build files look something like:

buildscript {
    dependencies {
        // usually gets pulled in transitively from our other high-level plugins
        classpath 'module:containing-version-class:1.2.3'
    }
}
version = new package.Version( required, args )

As long as your class implements toString you should be good (as long as the returned value is consistent). You could possibly combine a version class with a plugin, but we never found a need for it. Our Version class takes the input constructor arguments and combines it with environmental information to generate versions that differ (ie. developer machines vs CI).
Your version class can then expose any extra properties and they can be accessed in the build script via version.xyz.

Understood. If that is really what you need, there is no other way - just I would suggest that you code your version object so that minor and patch are lazy and the version information is fetched only when needed.

Here is example:

import groovy.transform.Memoized
import groovy.transform.TupleConstructor

@TupleConstructor
class GitInfo  {
    final def gitUrl

    @Memoized Map getVersion() {
        println 'parsing git commit...'
        return [major: 0, minor: 1, patch: 0]
    }
}

project.extensions.create('gitRepo1', GitInfo, 'git@gitlab.acme.com:acme/foobar.git')

task compile {
    doLast { println "compiling...  (fast)" }
}

task build(dependsOn: 'compile')

task deploy {
    finalizedBy 'updateCmdb'
    doLast { println "deploying $gitRepo1.version (slow - uses Git version)" }
}

task updateCmdb {
    onlyIf { gitRepo1.version.minor==0 && gitRepo1.version.patch==0 }
    doLast { println "CMDB updated (even slower)" }
}

Running gradle build is fast and does not touch git.

Running gradle deploy gets info from git (slower) and optionally runs the updateCmdb task if a new minor version is detected.