Using an Extra Property to define a Java dependency version in Plugin

I am using a “conventions” plugin to add spring boot amongst other things to projects.

The plugin defines the version of spring boot to include, however I’d like to be able to override this version via an extra property (or some other mechanism) from the project including the plugin.

I tried using lazy evaluation of the property in the dependencies{} block, but it does not seem to work – it always uses the spring boot version defined in the plugin.

Any suggestions?

Example;

project build.gradle

plugins {
    id 'org.myorg.spring-boot-conventions' version '1.0.+'
}

springBoot.version = '2.9.999'
...

plugin code (as a “precompiled script plugin”)

...
springBoot {
    buildInfo()
    ext {
      version = "2.6.3"
    }
}

// load spring-boot-dependencies using version specified. note the lazy evaluation closure when referencing springBoot.version
dependencies {
  implementation platform("org.springframework.boot:spring-boot-dependencies:${-> springBoot.version}")
}

no matter what I define springBoot.version in in the project, it always uses version 2.6.3 defined in the plugin. Any pointers?

As far as I am aware (though I could be wrong), this version cannot be changed dynamically from within the Gradle script itself due to the way that things are evaluated.

Not the exact solution you are seeking, but here is what I did.

NOTE: this is for a Spring Boot library as opposed to a Spring Boot application, so I am not doing id("org.springframework.boot") in the plugins, and instead using the SpringBootPlugin.BOM_COORDINATES.

buildSrc/build.gradle.kts

plugins {
    `kotlin-dsl`
}

val springBootVersion = (project.findProperty("springBootVersion") ?: "2.5.7") as String

dependencies {
    implementation("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    implementation("io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE")
}

In the convention plugin script (kotlin DSL):

plugins {
    `java-library`
    id("io.spring.dependency-management")
}

dependencyManagement {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    }
}

From there the Spring Boot version can be changed from the command line (or gradle.properties):

./gradlew -PspringBootVersion=2.6.3 clean test
./gradlew -PspringBootVersion=2.5.7 clean test

Thanks EarthCitizen! It’s an interesting approach, although the reason I want to [somehow] define the version in the project’s build.gradle is to make it transparent and accessible to developers. 99% of projects will use the plugin version, but for those that need to pin it to a specific version, I’d like to be able to provide in build.gradle as that’s part of version control &c – versus updating the arguments that get passed to the gradle build.

That said I could use a gradle.properties file in the project, although haven’t included one so far…

I am thinking about delaying application of the plugin (apply false), setting the extra property, and then applying the plugin – although I think that’s a bit awkward too.

In no case I would use the io.spring.dependency-management plugin but simply use the built-in BOM support by Gradle. For me this plugin is just a relict from when Gradle could not handle BOMs itself.

Besides that, with the plugins block and an in-script configuration it simply cannot work.
The plugins block is evaluated first separtely and isolated, so nothin in the build script can give information to the applied plugin.

One solution as you suggested yourself is to use apply false and then use apply after the property is set.
But of course this is feeling awkward and looking ugly.
And if a consumer uses Kotlin DSL, he will miss the generated type-safe accessors, as those are only generated for plugins actually applied using the plugins block.
So indeed if you really need this writing the version to gradle.properties is probably the best way.

@Vampire

Out of curiosity, how does one set the BOM properties with the built-in Gradle DSL for BOMs?

You mean like overwriting versions defined in the BOM?
Iirc you don’t, you just make an own constraint for that, or a forced version.

Correct. Overriding versions defined in the BOM.

That’s unfortunate. Hopefully the ability to do that will be added. I would certainly prefer to use native Gradle functionality. The Spring dependency management plugin allows this. Having this ability, I think, could entirely make that plugin unnecessary, though, I have not made a side-by-side comparison of all features.

I don’t think they add another way to do what is already possible.
They don’t even add a way to set properties for normal POMs and there you sometimes don’t have a proper way to do it.
But here I don’t see where this should be necessary if you can just add an own constraint or just declare a newer version.
You don’t even need to force.

The case where this is useful is when there is a family of dependencies that use the same version. But, much of the time, I don’t think this would be necessary.

Yeah, but for those it is anyway better if they are defined as platform and if not provided by that project as virtual platform.

If you have a BOM that defines A:a:$aversion and A:b:$aversion, then those are still separate unrelated constraints. If you then set aversion to 2 and add a separate dependency on B which depends on A:b:3, then without a platform or virtual platform for A, you will end up with A:a:2 and A:b:3.

I ended up using gradle.properties and something that looks like

ext {
  stooges = [
    larry: [ 
      version: findProperty('stooges.larry.version') ?: '0.1.0'
    ],
    curly: [ 
      version: findProperty('stooges.curly.version') ?: '0.1.0'
    ],
    moe: [ 
      version: findProperty('stooges.moe.version') ?: '0.1.0'
    ]
  ]
}

dependencies {
  implementation "com.stooges:larry-library:${stooges.larry.version}"
  implementation "com.stooges:curly-library:${stooges.curly.version}"
  implementation "com.stooges:moe-library:${stooges.moe.version}"
}

in my plugin. thus you can add stooges.moe.version = 0.2.0 to the project’s gradle.properties to override…

Also to add to the discussion re: spring-boot-dependencies;
@Vampire I do use the platform BOM, although please understand that the official tooling and documentation still uses io.spring.dependency-managementplugin and the following block:

ext {
	set('springCloudVersion', "2021.0.0")
}
dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}

at least for the spring cloud stuff. See https://start.spring.io/#!type=gradle-project&language=java&platformVersion=2.6.3&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=cloud-eureka

If we feel strongly enough to abolish this, I’d bring up with the maintainers of the initializer tool and docs team.

Well, at least they didn’t close the issue, so maybe they’ll add something: https://github.com/gradle/gradle/issues/9160.
Probably not using ext as almost all usages of ext are either wrong or a bad work-around.

In your example for example, if the stooges variable is just for usages within the script, then define it like that using def stooges = .... If it should be available on the project for external usage, then better add a proper extension, then you also have proper type-safe support, especially when someone uses Kotlin DSL.

Basically said, always when you use ext, you should feel dirty. :smiley:

The dependency management has other features that one might want to use, like a very strange exclusion handling that mimics the imho broken-by-design “nearest declaration wins” logic of Maven within Gradle for exclusions and other things. If one really want to use these things the plugin is still fine.

But just for the BOM handling it is imho obsolete in favor of built-in functionality.

@briceburg I have exact same requirement. Just before opened a question gradle - Override external plugin version in precompiled script plugin - Stack Overflow

For me this doesn’t seem like a working solution. As @Vampire mentioned properties are evaluated only after the plugin so there is no way to overwrite them at the time you specify plugin and its version. You set version to 0.1.0. My expectation is that always version 0.1.0 will be taken. At least I tried to reproduce it and no matter what I set my main project gradle.properties file, external plugin version isn’t overwritten.

Based on your answer on SO I created example project.

nebula.ospackage-application-spring-boot plugin internally applies com.bmuschko:gradle-docker-plugin. I am able to overwrite plugin version in plugins{} block or buildscript{}. But If I use buildSrc classpath dependency version of com.bmuschko:gradle-docker-plugin isn’t overwritten.

I am checking versions with gradlew buildEnvironment

main build.gradle

/**
 * Example overwriting delegated plugin version of com.bmuschko:gradle-docker-plugin
 *
 * 1) in plugins{} WORKS
 * 2) buildscript{} WORKS
 * 3) buildSrc DOESN'T WORK
 */

buildscript {
  dependencies {
    // classpath "com.bmuschko:gradle-docker-plugin:3.5.0" // 2) WORKS Overwriting plugin com.bmuschko:gradle-docker-plugin:3.2.1 -> 3.5.0
  }
}

plugins {
  id 'java'
  // id "com.bmuschko.docker-spring-boot-application" version "3.5.0" apply false // 1) WORKS Overwriting plugin com.bmuschko:gradle-docker-plugin:3.2.1 -> 3.5.0
  id "org.springframework.boot" version "2.6.1"
  id "nebula.ospackage-application-spring-boot" version "9.1.1"
}

buildSrc/build.gradle

repositories {
  mavenCentral()
}

dependencies {
  implementation "com.bmuschko:gradle-docker-plugin:3.2.4" // 3) DOESN'T WORK It doesn't overwrite default plugin version 3.2.1 in main project.
}

Or is it actually overwriting the version just that buildEnvironment task can’t pick that up? Should I verify version of applied plugin in some other way for buildSrc?

Btw. I meant runtimeOnly, not implementation.
Fixed on SO.
implementation doesn’t make sense if you don’t actually have code that uses it in buildSrc.

It could well be that the buildEnvironment task is not picking this up.
Afair the special handling of buildSrc makes it not taking part in conflict resolution but just overwrite versions by puttting them to a class loader higher in the hierarchy.
To manually verify, easiest is to get the version from classes in that plugin if it provides the version somehow.
If not, then maybe get the Class instance of a class in it, get the class loader and then look at the URLs in that class loader to see which JAR the class was loaded from.

1 Like

Guess what, buildSrc approach actually works! :fireworks:

gradle-docker-plugin version is really upgraded when checking the URL in classloader I can see class is taken from the artifact with version 3.2.4.

Lessons learned: Classloader is your friend :slight_smile:

1 Like

Now I was testing with upgrading the version of external plugin and it worked as explained. But most of colleagues require to downgrade plugin in some special cases. Now this gets tricky.

If I downgrade version in buildSrc everything works as expected. Dependency is just overwritten.

If I downgrade version in buildscript{} block with classpath dependency I have to explicitly set the strict flag otherwise Gradle picks latest version of the plugin, but still works.

Now the problem is with the new plugins{} block. There I don’t have an option to se the flag like strict or similar… So plugins{} block can’t be used to downgrade external plugins:

buildscript {
  dependencies {
    // 2) UPGRADE WORKS Overwriting plugin com.bmuschko:gradle-docker-plugin:3.2.1 -> 3.5.0
    // classpath "com.bmuschko:gradle-docker-plugin:3.5.0"

    // 2) DOWNGRADE) WORKS Have to use strict flag as its resolving dependencies 1.0.11.RELEASE -> 1.0.10.RELEASE
    //classpath("io.spring.gradle:dependency-management-plugin") {
    //  version {
    //    strictly "1.0.10.RELEASE"
    //  }
    //}
  }
}

plugins {
  id 'java'

  // 1) UPGRADE) WORKS Overwriting plugin com.bmuschko:gradle-docker-plugin:3.2.1 -> 3.5.0
  // id "com.bmuschko.docker-spring-boot-application" version "3.5.0" apply false

  // 1) DOWNGRADE) DOESN'T WORK, there is no way of forcing downgrade with plugins extension actual plugin implementation will be 1.0.11.RELEASE
  // id "io.spring.dependency-management" version "1.0.10.RELEASE" apply false
  
  id "org.springframework.boot" version "2.6.1"
  id "nebula.ospackage-application-spring-boot" version "9.1.1"
}

Yes, you explained it perfectly well.
That’s the situation and that’s it.
No magic trick for the plugins block.
buildSrc dependencies do not take part in conflict resolution but are simply in a higher class loader so they win
implicit build script dependencies (plugins block) and explicit build script dependencies take part in usual conflict resolution process and thus just defining a version can upgrade, but not downgrade, using a strict version can also downgrade.
You could maybe add some dependency resolution rules or similar, but it will not get nicer.

Well, you can make it nicer using the shorthand notation for strict versions:

classpath("io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE!!") {
2 Likes