Gradle plugin supporting multiple version of Gradle

Is there any way to easily support multiple versions of Gradle from a Gradle plugin?

e.g., say I want to use the task configuration avoidance APIs (register*) for all newer Gradle versions that support it, but want to support older versions without it by using the legacy API (create*). Is there any way to either generate different plugin jars (preferably differentiated by version of Gradle requesting plugin from portal), or somehow have one jar support multiple versions of Gradle (almost like a multi-release jar)? Can plugins even specify the ranges of Gradle with which they’ll work, just as documentation, even if not as a programmatic selector?

If any mechanism like this exists, can it also work for versions of plugins for other frameworks / libraries?

@Schalk_Cronje has done some work in this area

I’m not sure it has the exact features that you want but I know that Grolifant has utilities that work across multiple versions of Gradle. You could possibly raise a feature request or submit a pull request there for additional utilities

On the testing side there is the Gradle Test plugin where you can verify your plugin against multiple versions of Gradle

Yes, @Lance has mentioned the Grolifant & GradleTest, I am not going to delve into that now.

The trick here is to use Groovy. Java & Kotlin is possible, but too verbose. Make sure you have a stable public API and then defer to dynamically compiled public method to switch between the legacy and new Gradle API. Sometimes it as simple as using GradleVersion.current () >= GradleVersion.version ('1.2.3'). Other times you have to use reflection to determine whether a class or method is available, something which is much simpler in Groovy.

It is possible that Grolifant might help you in the sense that it already has a solution to a pattern you want to implement. Otherwise by looking at it source code you might get ideas as well. I am happy to point you to code examples in Grolifant and other plugins as well if you have specific ‘how do I’ questions.

For this specific case Grolifant does have an implementation. It is used in anger by the newer Asciidoctor plugins. See Grolifant : A Library to Support Gradle Plugin Development. The example still shows the use of a closure, but I would strongly recommend using Action instances instead especially if you use static compilation.

As a side-note, there are cases where lazy-created tasks do not work (irrespective of the Grolifant API or the straight Gradle API). You have to think carefully about when the tasks will be instatiated. For instance if you used rules in the past to create tasks, you should still create those tasks on point and not delay their creation.

@Lance & @Schalk_Cronje

Thanks for the info.

I assume “in anger” in the above is a typo.

Under what circumstances do lazily created tasks not work? Do you mean that:

  1. some older APIs require an actual Task, so a Provider<Task> can’t be directly used
  2. lazily registering a task will sometimes fail (either with an exception or with an improperly registered task)
  3. something else

If 1, I can always just call Provider<Task>#get(), so it’s not that register won’t work, it’s just that it’s more concise (& possibly more performant) to just call create, right?

Also, what do you mean by “if you used rules”?

@Schalk_Cronje: @Lance mentioned your Gradle Test plugin. I haven’t had a chance to look much at its documentation, but is there any way to specify dynamic versions, e.g., ”5.6+”?

Also, how does one map from Gradle version to tests for that version? I saw some mention of src/gradleTest/<ver> directories; does that mean that each version must have its own directory containing all tests (& configs, etc.) for that version? Or is there some way to map multiple versions to one directory / test suite?

What if 90% of the tests for two versions are the same, but 10% is different? What is the best way to share tests / configs etc.?

Here is an example of Grolifant’s TaskProvider in use in Asciioctor.

This example also uses tasks.addRule so that let’s say there is a task call asciidoctorRevealJs, if you specify asciidoctorRevealsJsExport on the command-line it will try to create a non-existent task via a rule. In this case the latter task cannot be created via register but should use create instead. Logcially it actually makes scenes when you think about it, because if you specify the task on the command-line your intent is that it will be executed, where as the purpose of lazy-creation is to not use tasks which will not be used.

No, explictly define the versions you want to test i.e.

gradleTest {
  versions '3.5', '4.7', '5.5'

You can also override on the command-line if you want to focus on specific versions i.e.

./gradlew -DgradleTest.version=1.2.3,4.5.6

All of the tests in src/gradleTest will be executed. There is also a filter on the command-line

./gradlew gradleTest '-DgradleTest.include=*foo*'

You can also these two filters to run tests in parallel in CI. See asciidoctor-gradle-plugin/.gitlab-ci.yml at master · ysb33r/asciidoctor-gradle-plugin · GitHub (Thre is a bit more to it as you need to look at the content of too).

It is important to balance your tests. GradleTest is about testing full-blown compatibility. You write your tests as Gradle projects below src/gradleTest. It makes that part a lot easier for you. It also allows you to drop a build.gradle and build.gradle.kts in the same folder and both a Groovy DSL and KotLin DSL test will then be run over the same test project.

However you also need some integration tests based upon TestKit in which you can set a specific version and do focused testing on a specific feature. Because you can define a specific version inside TestKit if you need to focus on an issue in a specific version you can set up an integration test for it.

Having said all of that, you an also set up additional source sets for GradleTest where you define tests for a narrowed set of Gradle versions. This is described here