Gradle: compile vs compileOnly vs compileInclude

Comparing the dependency management configurations.

By request, a blog to explain compile vs compileOnly vs compileInclude...

First it is important to understand that these are actually names for various configurations used during the build process, but specifically when it comes to the dependency management. In Maven, these are implemented as scopes.

Each time one of these three types are listed in your dependencies {} section, you are adding the identified dependency to the configuration.

The three configurations all add a dependency to the compile phase of the build, when your java code is being compiled into bytecode.

The real difference these configurations have is on the manifest of the jar when it is built.

compile

So compile is the one that is easiest to understand. You are declaring a dependency that your java code needs to compile cleanly.

As a best practice, you only want to list compile dependencies on those libraries that you really need to compile your code. For example, you might be needing group: 'org.apache.poi', name: 'poi-ooxml', version: '4.0.0' for reading and writing Excel spreadsheets, but you wouldn't want to spin out to http://mvnrepository.com/artifact/org.apache.poi/poi-ooxml/4.0.0 and then declare a compile dependency on everything POI needs. As transitive dependencies, Gradle will handle those for you.

When the compile occurs, this dependency will be included in the classpath for javac to compile your java source file.

Additionally, when it comes time to build the jar, packages that you use in your java code from POI will be added as Import-Package manifest entries.

It is this addition which will result in the "Unresolved Reference" error about the missing package if it is not available from some other module in the OSGi container.

For those with a Maven background, the compile configuration is the same as Maven's compile scope.

compileOnly

The compileOnly configuration is used to itemize a dependency that you need to compile your code, same as compile above.

The difference is that packages your java code use from a compileOnly dependency will not be listed as Import-Package manifest entries.

The common example for using compileOnly typically resolves around use of annotations. I like to use FindBugs on my code (don't laugh, it has saved my bacon a few times and I feel I deliver better code when I follow its suggestions). Sometimes, however, FindBugs gets a false positive result, something it thinks is a bug but I know it is exactly how I need it to be.

So the normal solution here is to add the @SuppressFBWarninsg annotation on the method; here's one I used recently:

@SuppressFBWarnings(value = "NP_NULL_PARAM_DEREF", justification = "Code allocates always before call.")
public void emit(K key) { ... }

FindBugs was complaining that I didn't check key for null, but it is actually emitting within the processing of a Map entry, so the key can never be null. Rather than add the null check, I added the annotation.

To use the annotation, I of course need to include the dependency:

compileOnly 'com.google.code.findbugs:annotations:3.0.0'

I used compileOnly in this case because I only need the annotation for the compile itself; the compile will strip out the annotation info from the bytecode because it is not a runtime annotation, so I do not need this dependency after the compile is done.

And I definitely don't want it showing up in the Import-Package manifest entry.

In OSGi, we will also tend to use compileOnly for the org.osgi.core and osgi.cmpn dependencies, not because we don't need them at runtime, but because we know that within an OSGi container these packages will always be provided (so the Manifest does not need to enforce it) plus we might want to use our jar outside of an OSGi container.

For those with a Maven background, the compileOnly configuration is similar to Maven's provided scope.

compileInclude

compileInclude is the last configuration to cover. Like compile and compileOnly, this configuration will include the dependency in the compile classpath.

The compileInclude configuration was actually introduced by Liferay and is included in Liferay's Gradle plugins.

The compileInclude configuration replaces the manual steps from my OSGi Depencencies blog post, option #4, including the jars in the bundle.

In fact, everything my blog talks about with adding the Bundle-ClassPath directive and the -includeresource instruction to the bnd.bnd file, well the compileInclude does this. Where compileInclude shines, though, is that it will also include some of the transitive dependencies into the module as well.

Note how I said that some of the transitive dependencies are included? I haven't quite figured out how it decides which transitive dependencies to include, but I do know it is not always 100% correct. I've had cases where it missed a particular transitive dependency. I do know it will not include optional dependencies and that may have been the cause in those cases. To fix it though, I would just add a compileInclude configuration line for the missing transitive dependency.

You can disable the transitive dependency inclusion by adding a flag at the end of the declaration. For example, if I only wanted poi-ooxml but for some reason didn't want it's transitive dependencies, I could use the following:

compileInclude group: 'org.apache.poi', name: 'poi-ooxml', version: '4.0.0', transitive:false

It's then up to you to include or exclude the transitive dependencies, but at least you won't need to manually update the bnd.bnd file.

If you're getting the impression that compileInclude will mostly work but may make some bad choices (including some you don't want and excluding some that you need), you would be correct. It will never offer you the type of precise control you can have by using Bundle-ClassPath and -includeresource. It just happens to be a lot less work.

For those who use Maven, I'm sorry but you're kind of out of luck as there is no corresponding Maven scope for this one.

Conclusion

I hope this clarifies whatever confusion you might have with these three configurations.

If you need recommendations of what configuration to use when, I guess I would offer the following:

  • For packages that you know will be available from the OSGi container, use the compile configuration. This includes your custom code from other modules. all com.liferay code, etc.
  • For packages that you do not want from the OSGi container or don't think will be provided by the OSGi container, use the compileInclude configuration. This is basically all of those third party libraries that you won't be pushing as modules to the container.
  • For all others, use the compileOnly configuration.

Enjoy!

Blogs

Thanks for the great explanation Dave! One question I have comes from your recommendation in the conclusion though. I like to use the Liferay blade templates to get started when I create my modules for projects and when they generate the build.gradle files, they use compileOnly for the Liferay library dependencies. Your recommendation is to change these to compile though?

Thanks for the article David, it gives me new insight.

 

I follow tutorials for developing web app in liferay docs, the portlet module use compileOnly for referencing service and api module generated by service builder. And when i check the the Import-Package in MANIFEST.MF file of portlet module, it lists service package referenced by portlet module. Is there any special treatment for this case as you state compileOnly depedency will not be listed as Import-Package manifest entries but in my observation it is listed?

 

I just try to understand the difference between compile and compileOnly. Thank you.

Thanks for sharing  the article David.  It is very usefull to unserdstand the difference between compile ,compileOnly and  compileInclude

Hello David,

 

we're currently migrating from Maven to Gradle. I really like it a lot as our bnd files are way smaller now. Unfortunately we're struggling with the compileInclude :/

 

We do have one dependency that uses a classifier ('frontend') and this one fails on a ./gradlew clean build.

Execution failed for task ':client-api:jar'. > Could not resolve all files for configuration ':client-api:compileInclude'.    > Could not find portal.backend:portal-backend-entities:.      Required by:          project :client-api

 

Interestingly if we use a compileOnly, a build and a clean build run successfully. And after that, we can change it back to compileInclude and builds (without clean) still run successfully.

 

Thanks, Bernd