Choosing OSGi Versions During Development

This one is for my good friend Milen... Sometimes he frustrates me, but he always forces me to think...

Introduction

So if you've done any Liferay 7.x CE or DXP development, you may have encountered something similar to the following in your build.gradle:

dependencies {
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.6.0"
  compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"
}

For the Maven users out there, I'm not going to include the pom equivalent, but I'm sure you can figure out how it would go.

One of the biggest mistakes that I see developers make is the selection of the version for modules they use. For example, I'm using portal-kernel version 2.6.0 above. This is really old; the Liferay Maven repo reports it was released in June, 2016.

And currently it looks like the latest available version is 2.62.0. You can see all of the available versions here.

So why do I pick such an old release? I certainly don't want to link to such an old version that likely has bugs or issues. Shouldn't I be picking the 2.62.0 version to get the latest?

Picking Old Versions

The Definitive Answer: No

Okay, so here's the skinny...

When you are building a module, you are not really linking to a jar. You are not including 2.6.0 or 2.62.0 or whatever into your module, you are only including meta information for the OSGi container that you need portal-kernel.

We as developers, we know that we always want to use the latest version available, when possible, so we get bugfixes, performance improvements, etc. So most of us want to grab 2.62.0 and use it as the version that we declare.

In OSGi, however, we never declare a specific version, we're always using a version range.

When I declare that my version is 2.6.0, the OSGi container thinks that I'm actually indicating that I want version [2.6.0,3.0.0). That's a version range from 2.6.0 up to (but not including) 3.0.0. This offers me a great bit of flexibility.

I can deploy my module to a Liferay 7.0 GA1 container or I could deploy it to the very latest Liferay 7.0 DXP Fix Pack 41. My version range means my module will work in all of these environments.

If instead I stuck with the 2.62.0 version, OSGi will treat that as [2.62.0,3.0.0). This version range is so narrow, it will not work in any of the 7.0 CE GAs and will actually only deploy to 7.0 DXP FP 40 or later.

So the version is not really the version you get, it is the minimum version you need to function. The OSGi container, it has some version of portal-kernel, you may or may not know what it is in your environment(s), the container will determine if the version it has is acceptable to your version range and may (or may not) start your module.

So, as a rule:

You always want to pick the oldest version you can get away with (relative to the major version number).

Wait, oldest you can get away with? What does that mean?

Oldest You Can Get Away With

The minor version numbers don't increment because you do a new build or antying like that. Liferay bumps the minor version when there is changed code in the API.

For example, the com.liferay.portal.kernel.dao.search.SearchContainer class. I recently was copying a chunk of code from a certain Liferay DisplayContext class to emulate how it was handling the paged search container views.

I had my portal-kernel version set to 2.6.0 like I always do, and the code I copied went along something like this:

SearchContainer searchContainer = new SearchContainer(_renderRequest, getIteratorURL(), null, "there-are-no-rules");

if (isShowRulesAddButton()) {
  searchContainer.setEmptyResultsMessageCssClass("there-are-no-rules");
}

Although I copied and only changes the strings, the IDE had the setEmptyResultsMessageCssClass() line marked as an error and it told me No Such Method errors. This made no sense because I literally lifted the code straight from the Liferay source, how could the method not exist.

Well, that comes back to the version range. Since I indicated 2.6.0, my code had to compile against 2.6.0 and this method doesn't exist in the SearchContainer interface from 2.6.0. I looked in Liferay's build.gradle file and found that they had used portal-kernel version 2.13.0. I changed my version to 2.13.0 and my compiler error went away.

So this tells me that, somewhere between portal-kernel version 2.6.0 and 2.13.0, this method was added to the SearchContainer interface. Since my code needs that method, the oldest version my code will work with is not 2.6.0, but more like 2.13.0.

Actually I guess it is possible it could have been added to the interface in 2.7.0, 2.8.0, etc, up to 2.13.0 so, if I were really a stickler about it, I could find the actual version where the method was introduced. I tend to value my time a lot, so instead I just settled on the version Liferay was also using.

Now, though, most of my modules use the version as 2.6.0 except for those that use this SearchContainer container code, this one uses version 2.13.0.

Why not just use 2.13.0 and keep everything the same?

Using Different Versions for Different Modules

Well, it comes down to flexibility. Most of my modules will work fine using the older version. This particular portlet uses a function in a SearchContainer that maybe I do or maybe I don't really need. If a client asks to run my portlet in their GA1 environment that might not have this version, well I can change the version in build.gradle, comment out the line using the etEmptyResultsMessageCssClass() method (and anything else that was not yet available) and then build and deploy my code.

In its current form it needed 2.13.0, but with a few modifications I could get a version that worked w/ 2.6.0.

Tripping

Or, how about this, this will really trip you out...

I create one bundle that has an Import-Package version range of [2.6.0,2.13.0) for com.liferay.portal.kernel.dao.search in one bundle that doesn't use setEmptyResultsMessageCssClass(), then on my second bundle I use an Import-Package version range [2.13.0,3.0.0) for the same package that does use the new method. Users can deploy both bundles into an environment, but OSGi will only enable the one that has the right version available.

It gets even better. You might deploy this to your GA1 envirronment where the first bundle starts. But then you upgrade to GA2 and, without changing anything from a deployment, the second bundle starts but the first does not. Completely transparent upgrades because my version ranges control which bundle will start and hence which code runs!

Conclusion

So I'm at the point where I hate the label "version". For a normal developer specifying a value in build.gradle, it means what version do I want to compile against. As we've seen, to the OSGi container it specifies the minimum version the module needs in order to start.

So even though it goes against your developer nature, avoid the latest version. Always use the oldest version your module needs.

Update

So I updated the blog post today after realizing that some of this might be a little jumbled, so just to clarify:

  • You define a compile time version, a single version, in your pom.xml or build.gradle file; although you can use a range, it is not really the same thing as the version range discussed above.
  • To specify a version range limitation in your bundle, you're going to use the Import-Package directive in your bnd.bnd file in a form like:
Import-Package:\
  com.liferay.portal.kernel.dao.search;version="[2.6.0,2.13.0)",\
  *

BND will stamp this range into your bundle's MANIFEST.MF file and OSGi will use this range when trying to satisfy dependencies in the bundle.

Take note of the wildcard '*' at the end of the Import-Package directive; you need that so BND will include all of the rest of your dependencies correctly.

Blogs
Great gob.
It seems that many people have encountered this problem.
You solved my problem in this post(https://web.liferay.com/zh/community/forums/-/message_boards/message/102513997).
This blog explains better.
I totally agree with you David. It should be enough to compile against the "oldest" version you would like to support. In terms of semantic versioning a minor version should not have any breaking changes, so it should not matter if you compile against an older version that the one currently running in your Liferay environment.

However when it comes to debugging, it is very useful to know the exact version of a module currently running in your Liferay development server. IMO it is very annoying when stepping through the code (of a dependency) and the line numbers do not match. In those cases I set the exact version number in my maven/gradle dependencies temporarily (determined by using the Gogo Shell).
So while your position is true and valid, it also happens to be moot.

For development, you just need to ensure you support the version range of dependencies in the container you're deploying to.

For debugging, you are also concerned somewhat about version, but because Liferay does not distribute individual modules separately, we don't have to worry about it. If you are using CE GA 2, then you use the source for CE GA 2 to debug against; when you change to CE GA 5, you change your source also.

Same goes for DXP; if you are on fixpack 27, you use the source for FP27 (also available in the downloads). When you update to FP41, you also use the source for FP41.

We can keep things simple like this until the day comes (if it comes) that individual modules are distributed separately. At that point things can definitely get weird if we all have some hodgepodge of bundle versions floating around in the container.

If that day comes, I think my personal recommendation would be to avoid it. Stick with Liferay GAs or Service Packs or whatever they want to call them, but avoid separate module updates as it will likely be too confusing to manage those containers.
Another great BLog David. I ran against this compile issue when using the MailMessage interface needing to set headrers that was added somewhere between portal.kernal 2.0.0 and 2.35.0 currently it is at 2.63.0 or somewhere.
the explanation between runtime and compile time decisions is useful.
What would be very useful though is a table/page/service that would let you find out which versions were in each FP or at least each SP (for DXP).
It seems the standard new module routines assume 2.0.0 which is rather old.
I don't know that such a table exists, but I wonder how useful it would be. I mean, if you are looking for a specific API method, you need to know when the method was introduced in the interface and then back track that to the bundle version of its introduction.

The only thing I've found that works is to pick a valid version number and then start decrementing until I get to a point where a clean compile breaks; it is super tedious, so I'll typically find some sort of version number that it works and call it the day. Like in your case, I'd go with 2.35.0 and be happy with it.

The "new module routines assume 2.0.0 which is rather old", while true, is the wrong way of thinking about it, the way that we as developers want the latest version.

Don't think of it as "this module uses old versions", think of it like "this module doesn't need any changes introduced since 2.0.0". Its what allows the module to be compatible with the earliest 7.0 GAs even if nobody is running GA1.

Hi David, - Import-Package: com.liferay.portal.kernel.dao.search;version="[2.6.0,2.13.0)" - 2.6.0 and 2.13.0 are the versions of the package included into portal-kernel.jar and NOT the versions of portal-kernel boundle.

 

The package version of "com.liferay.portal.kernel.dao.search" into portal-kernel-2.6.0.jar is 7.2.0.

 

In you article you talk about portal-kernel boundle version when you write 2.6.0 and 2.13.0...

Yep, my bad, kernel is a bad example because the packages version differently.  The "normal" modules typically version as a whole, so in general it would work.  I'll look up the package versions for 2.6.0 and 2.13.0 and update accordingly...

Very informative . Thanks David.

 

I was facing the issue recently for 2 different versions of  portal-kernel.jar of  this package "com.liferay.portal.kernel.portlet.bridges.mvc" which has different version from gradle home and server.  

Gradle Repository:  3.6.2

Tomcat/lib: 2.0.0

 

I didn't mention any version in build.gradle file and my .bnd file has:

 

Import-Package:\   com.liferay.portal.kernel.portlet.bridges.mvc;version="[2.0.0,3.6.2)",\   *

 

So compile time it takes 3.6.2 and run time .

 

can we follow the same approach for all the dependency issues? 

 

By default, manifest.mf file is generating some interval range. For example, it is generating the version 1.6.0 to 2.0.0  as mentioned here. how it took this range ?

 

"Import-Package: com.liferay.portal.kernel.portlet.bridges.mvc; version="[1.6.0,2.0.0)"