Gradle 7, Liferay and Version Catalogs...

Liferay is providing the Mega Jar and Target Platform BOMs to simplify version management of its dependencies. With Gradle 7, you can add Version Catalogs to simplify version management of your own dependencies...

Introduction

So recently I wrote the blog, Gradle 7 is here! Yay? where I showed how to update your workspace to Gradle 7.3.3, and I just kind of stopped there.

I mean, I kind of assumed that as developers we might just be interested in having our toolchains up to date (we all know how infosec teams react when they find out we're using outdated tools). So other than presenting how to update to Gradle 7, I didn't dig any deeper.

A comment posted by Andreas Schaeffer forced me to check my assumptions:

Thank you very much for this blog post.

We have upgraded one of our projects to Gradle 7 and can confirm it works. But of course for simple setups it's simple and for more complex setups it's more complex to upgrade.

What I really like is the versionCatalogs feature which has to be enabled:

enableFeaturePreview('VERSION_CATALOGS')

Andreas's comment made me realize that, yes upgrading our toolchain is important, but in addition there are probably some new goodies available that we should consider that will make our lives better.

Gradle version catalogs are one of those goodies, so I'm going to share it here and hopefully it will help you in some way with your projects going forward.

Liferay Dependency Management

Liferay has, for some time now, supported two means of managing your Liferay dependencies.

The first of which is the Target Platform. The Target Platform is basically a Maven Bill of Materials (BOM) which identifies all of the Liferay modules and version numbers, all bound to a specific Liferay version.

For example, when I set my Target Platform property liferay.workspace.product=dxp-7.4-u94 in the gradle.properties file, I can then exclude version numbers for my Liferay dependencies in my build.gradle files. I can use, for example:

dependencies {
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.impl"
}

I don't need to know, for example, that DXP U94 actually uses portal-impl.jar version 88.0.0 or 88.0.1 (or whatever the version actually is), the Target Platform BOM knows the right version number.

For single dependencies this may not be a big deal, but for a Service Builder module I could have dependencies for asset management, resource permissions, workflow, indexing, recycle bin, ... In this case the Target Platform is a godsend because I don't have to track individual versions.

It also means my code is easy to change to a new version. When it comes out, I could change my Target Platform value to dxp-7.4-u95 and recompile everything to find out if maybe I'm hitting up against a breaking change or something in my customizations. One property change, and all of those individual version numbers will change to go with it. Such a time saver...

The second means that Liferay uses to manage dependencies is the Mega Jars. The mega jars are a one-line replacement for individual Liferay dependency listing. For my Service Builder example above, instead of listing each of those modules separately, I just use the Mega Jar like:

dependencies {
  compileOnly group: "com.liferay.portal", name: "release.dxp.api"
}

With this, I no longer need to know the dependency jars for assets, resource permissions, workflow and the like. The mega jar includes all of apis, so my one line dependency includes everything I might need and then some.

As a reminder, there are 2 flavors of mega jars, there's the release.dxp.api for DXP developers and release.portal.api for CE developers.

So combining these two methods for dependency management, our mega jar determines which flavor of Liferay we're building for, and our Target Platform will pick the right version of the mega jar for the specific version we're targeting.

Managing Our Dependencies

So Liferay has given us some cool tools to help manage our Liferay dependencies, but it doesn't really do anything for our own custom dependencies, does it?

I mean, let's say for sake of argument that our project has 10 different Spring portlet wars.

That means I have 10 sub projects, each one is going to have something like:

dependencies {
  compileOnly group: "com.liferay.portal", name: "release.dxp.api"
  implementation group: 'com.liferay.portletmvc4spring', 
    name: 'com.liferay.portletmvc4spring.framework', version: '5.3.2'
  implementation group: 'org.springframework', 
    name: 'spring-core', version: '5.3.30'
  implementation group: 'org.springframework', 
    name: 'spring-beans', version: '5.3.30'
  implementation group: 'org.springframework', 
    name: 'spring-context', version: '5.3.30'
  implementation group: 'org.springframework', 
    name: 'spring-webmvc', version: '5.3.30'
  ...
}

And wow, isn't that going to be painful. Oh, and maybe 5.3.31 of Spring comes out next month that fixes some bug or security thing, now I'll have to go into all of my build.gradle files and make a lot of manual updates...

Well, obviously I'm trying to paint a dreary picture here, right? I mean we did have alternatives available, even for Gradle 6. We could define a property like springVersion and then change all of or version references to be version: '${springVersion}' and that way one property change updates everything, but it just doesn't feel right.

Well this is the niche that the Gradle 7 Version Catalogs is meant to fill...

Gradle 7 Version Catalogs

So effectively the version catalog is basically going to be a repository for all of your versions that you use in your modules, but it is managed in one location and you'd want to include all of your dependencies here.

This will help to enforce consistency in your modules, so for example in our Spring example above you don't have 10 different modules using some version of Spring 5.3.whatever, you can guarantee they will all use the version specified in your catalog.

Enabling The Version Catalog

Andreas's comment was correct, enabling the catalog is a one-line change.

In your settings.gradle, just add the following line:

enableFeaturePreview('VERSION_CATALOGS')
apply plugin: "com.liferay.workspace"

(I tucked mine in right before applying the workspace plugin).

According to the Gradle folks, the version catalogs feature was promoted to a stable feature in 7.4, so it is not necessary to enable the feature preview. That said, I'm using 7.6.2 and the enableFeaturePreview() did not fail, so it seems to be safe to include in any of the 7.x versions whether it is necessary or not.

Declaring The Versions

The master file used for your versions list is going to be in the gradle/libs.versions.toml file. Right now in the workspace the only thing you'll find in the gradle folder is the wrapper folder (used to hold the gradle wrapper stuff), so this will be a new file that you'll need to create.

The file itself is in a special format called Tom's Obvious Minimal Language or TOML. The syntax will seem very familiar if you've worked with INI files, but even if you haven't it is pretty easy to follow.

Basically you'll create this file and then start defining your various libraries and versions and stuff. There are four common sections you'll find in this file:

  • [versions] - For defining individual version properties.
  • [libraries] - For defining specific library coordinates (lib and version).
  • [plugins] - For Gradle plugin coordinates.
  • [bundles] - Defines groups of libraries.


From my Spring example above, I would likely replace those with a libs.versions.toml file like:

[versions]
portletmvc4spring = "5.3.2"
spring = "5.3.30"
spring-security = "5.8.7"

[libraries]

portletmvc4spring-framework = { 
  module="com.liferay.portletmvc4spring:com.liferay.portletmvc4spring.framework", 
  version.ref="portletmvc4spring" }
portletmvc4spring-security = { 
  module="com.liferay.portletmvc4spring:com.liferay.portletmvc4spring.security", 
  version.ref="portletmvc4spring" }
portletmvc4spring-webflow = { 
  module="com.liferay.portletmvc4spring:com.liferay.portletmvc4spring.webflow", 
  version.ref="portletmvc4spring" }
spring-beans = { module="org.springframework:spring-beans", version.ref="spring"}
spring-core = { module="org.springframework:spring-core", version.ref="spring"}
spring-context = { module="org.springframework:spring-context", version.ref="spring"}
spring-webmvc = { module="org.springframework:spring-webmvc", version.ref="spring"}
commons-math3 = { module="org.apache.commons:commons-math3", version="3.6.1"}
...

[bundles]

spring = ["spring-beans", "spring-core", "spring-context", "spring-webmvc"]
portletmvc4spring = ["portletmvc4spring-framework", "portletmvc4spring-security",
  "portletmvc4spring-webflow"]
Note: The word wrapping I've done here is just for making it look fine in the blog. In reality, each line must be complete and not wrap (otherwise you'll get errors).

Here I've defined my version properties, so when Spring jumps to 5.3.31, one quick change here and I'm good to go.

I've also defined each of my libraries, so all of the coordinates are clear and they include the version reference.

And finally, I've defined a couple of bundles so I can just depend upon those and know that I'll get all of the relevant libraries I need when I use them.

If I were using some custom plugins such as code coverage analysis tools, etc. I could include their references here too in the [plugins] section so I can ensure that everywhere I use them, I'll be using the same version.

Using The Catalog

So now comes the cool part, the changes to the various build.gradle files to use the new dependencies.

My old build.gradle file is shown above, but my new build.gradle file is:

dependencies {
  compileOnly group: "com.liferay.portal", name: "release.dxp.api"
  implementation(libs.portletmvc4spring.framework)
  implementation(libs.spring)
  implementation(libs.commons.math3)
  ...
}

Sweet, isn't it?

I mean, my 10 Spring portlet wars still have their dependencies, but now my versions are managed in a single spot, I can bundle dependencies so I don't have to itemize all of the Spring jars separately, I'll be consistent in all versions used in my modules, ...

Other than adding yet another file to manage, what's not to love?

Conclusion

So here, thanks to Andreas, we've seen how upgrading our tool chain can benefit our Liferay projects by promoting consistency for dependency usage and simplifying our individual build.gradle files.

And the cool thing? We can mix old and new without even worrying about it. If I have one Spring portlet war that has, say, a dependency on iText, in that specific build.gradle file it would be fine just to add the implementation group: 'com.itextpdf', name: 'itextpdf', version: '5.5.13.3' right into the dependencies. I don't have to list it in the libs.versions.toml file if I don't want to, but given the advantages of a single catalog listing all of your dependencies and versions, why wouldn't you, yeah?

So that's about it for the Gradle 7 Versions Catalog stuff here. You can find lots of other references online if you are interested in pursuing the plugins version handling or if you want to define your catalogs manually in settings.gradle or pursue lots of other syntax options available.

This at least should help you get started with using Versions Catalog in your shiny new Gradle 7 workspaces.

Yet another cool thing - Intellij knows all about Version Catalogs and will offer completion assistance when editing your build.gradle files. I don't know if Eclipse does the same, but I bet there's probably a plugin for it.

Oh, and I should mention that Gradle 8 supports Versions Catalogs too, so whenever Liferay gets around to supporting Gradle 8, you won't have to change any of this out.

Finally, thanks to Andreas for giving me the idea to write this up in a blog. If he or anyone else out there has some cool Gradle 7 tricks that you'd like to see shared with the Liferay developers out there, please let me know and I'll be happy to explore them!