Setting project.version using a plugin

I’m looking for advice on how best to set project.version from a plugin.

To set the version when the logic is simple, I’d write something like this directly in the build script:

version = System.getenv("FOO") ?: "bar"

But what about when the logic is more complicated, and involves doing some non-trivial work, such as interacting with my release infrastructure?

I could set project.version in the apply method of a plugin, but I suspect this is bad practice:

class VersionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.version = System.getenv("FOO") ?: "bar"
    }
}

I see a couple of issues with this approach:

  1. It risks being hard to follow: If my plugin lives in a separate project, finding what is setting the version is difficult.
  2. If the calculation of the version takes some appreciable time, the configuration time for every task takes a hit, even when executing tasks that don’t use the version.

If I understand correctly, version isn’t compatible with lazy properties, because project.setVersion just uses the toString() of whatever it is passed. Is there some other way a plugin can expose a property that the version can be set to in such a way that the evaluation is deferred?

toString() is not called on setVersion.
toString() needs to be called whenever you use the version.
So to calculate it lazily, you can set any object as version that calculates the version on first toString() call and caches the result for further calls.

But please strongly consider whether you really want to do that.
Imho any version calculation by release infrastructure, VCS, or whatever dynamic source is bad.
That way you do not get reproducible builds, and you also cannot build outside that infrastructure.
If you e.g. use JGit to get the last tag and calculate the version from it, then you are already lost when you try to build from a secondary Git worktree as JGit cannot handle them. Same for shallow clones where the base for the calculation might not be available, …

Thanks for the speedy response, @Vampire .

I agree with you that reproducible builds are important, but that leads to some awkwardness for version that I’m not sure how best to tackle.

In my case, I’m using the maven-publish plugin, so the version ends up in the names and metadata of the published artifacts. This breaks reproducibility because the Maven repository won’t allow the same artifact to be published twice (in particular, if there’s a transient problem, like the upload of one artifact timing out, the build can’t be rerun; a new version number is needed). In a sense, the maven-publish plugin causes the release infrastructure to be involved in version calculation, because it enables the release infrastructure to reject versions.

My current solution is to pass the version to Gradle from my CI/CD pipeline (which is the only place where the version really matters for me, and where I have easy access to information like commit hash and build number), but the result is quite limited. It would be possible for the pipeline to generate a more meaningful version using shell scripting, but given that the output of the shell script is immediately going to be passed to Gradle which has the full power of the JVM available, I’m tempted to substitute the shell script with a Gradle plugin. Would this be unwise? Perhaps I’m abusing version here, and I should configure the publish task separately?

BTW, thanks for your suggestion about using toString() to defer calculation. It works perfectly:

class LazyVersion {
    private val version: String by lazy {
        System.getenv("FOO") ?: "bar"
    }
    override fun toString() = version
}

open class VersionPluginExtension {
    val version = LazyVersion()
}

class VersionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.create<VersionPluginExtension>("versionPluginExtension")
    }
}

apply<VersionPlugin>()

version = the<VersionPluginExtension>().version

I have a slight concern that future versions of Gradle might call toString() more eagerly, but though this might slow my build, it can’t have any functional impact, so it’s not a big deal.

It could actually be any plugin you apply, not only Gradle itself, that might need the version and thus call toString(). If it is needed, it is needed. :slight_smile:

In a sense, the maven-publish plugin causes the release infrastructure to be involved in version calculation, because it enables the release infrastructure to reject versions.

Well, not really. It is up to the discretion of your repository software whether it allows to overwrite version artefacts or not. Gradle would be perfectly happy with that. :slight_smile:

So yeah, if you need such automatic failure recovery, maybe for you such version calculation is the way to go. I just personally don’t like it. A “publish” does imho not have to be reproducible if the version is already published and the repository does not allow overwriting.