Nerd For Tech
Published in

Nerd For Tech

Gradle: Managing scope and platform-specific dependencies

From mobile apps to micro-services, managing dependencies is one of the most crucial yet tedious tasks. Various build tools like gradle, maven, ant, grunt, etc exist to make us developers’ lives easy.

Gradle is one of the most popular such build tool with a focus on build automation and support for multi-language development. Linkedin, Netflix, etc all use gradle as their build tool.

The dependency labyrinth

source: https://solidsoft.wordpress.com/2016/06/07/gradle-tricks-display-buildscript-dependencies/

As the project size gets larger the number of dependencies increase. In distributed systems, this can quickly get out of hand. Managing dependencies itself is a formidable task let alone managing scope and platform-specific dependencies.

How do we manage so many dependencies? After all, we don’t need all of them to be packaged inside the prod build. Some of them are dev only, test specific etc which won’t be needed in the final jar.

This is where gradle steps in. Gradle makes this a little easier with something called configuration.

Configuration

Every dependency declared for a Gradle project applies to a specific scope. For example, some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a configuration. Every configuration can be identified by a unique name.

Configuration is nothing but a logical grouping of dependencies.

You can notice the named buckets like implementation, testImplementation, testRuntime. These are nothing but configurations.

When working with a java project adding the Java plug-in introduces six configurations:

  1. archives
  2. default
  3. implementation
  4. runtime
  5. testImplementation
  6. testRuntime

This helps to segregate dependencies under different named buckets/categories based on when and where they are required.

Not just that but you can even create your own configurations and use them to separate the scope of dependencies for whatever reason you need. You can do so by simply adding your custom configuration name to the configuration block as follows:

configurations {
devOnly
}

You can already figure out from the configuration name itself how useful this can be. I don’t have to worry about my devOnly dependencies getting packed with the war or jar file.

The cherry on top is that you can extend from current configurations when creating a new custom one. This means you will inherit all the dependencies from the configuration you extended from. You can even extend from other custom configurations as well.

This is extremely useful. To understand that let us take an example straight from the gradle docs.

source: gradle docs

Configuration inheritance is heavily used by Gradle core plugins like the Java plugin. For example, the testImplementation configuration extends the implementation configuration. The configuration hierarchy has a practical purpose: compiling tests requires the dependencies of the source code under test on top of the dependencies needed to write the test class. A Java project that uses JUnit to write and execute test code also needs Guava if its classes are imported in the production source code.

Now that we have understood well how we can manage scopes of dependencies using custom configurations let us look at how they can also be used to manage platform-specific dependencies.

Configurations in play

For example, how do you configure Tomcat Native?

Let us explore some of the options:

  1. One solution would be to create an explicit if condition based on the platform
if(Os.isFamily(Os.FAMILY_MAC)
runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.29.Final:'osx-x86_64'
else
runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.29.Final:'linux-x86_64'

Note: We import org.gradle.internal.os.OperatingSystem for checking the current operating system.

Though this doesn't scale well as the number of dependencies increase we will end up with huge if blocks with no central or out of the box solution for new projects.

2. If we have only a few such dependencies, we can do it inline as follows.

runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.29.Final:${Os.isFamily(Os.FAMILY_MAC) ? 'osx-x86_64' : 'linux-x86_64'}"

3. Using custom configurations

To understand it well let us look at the example of using file watcher in Micronaut, it has macOS specific dependency for the following reason:

The native JVM implementation of the WatchService interface for Mac OS X using polling is slow. To improve file watch performance add the following dependencies to your build if you are using OS X.

io.micronaut:micronaut-runtime-osx,
net.java.dev.jna:jna,
io.methvin:directory-watcher

The first dependency is native to macOS and unlike the first example, there isn’t a different version for other platforms for it in fact we don’t even need it on other platforms. It is only required on macOS.

How do we deal with it?

Gradle provides a much cleaner and extensible approach to tackle this situation using custom configuration as follows :

configurations {
devOnly
macDevOnly.extendsFrom(devOnly) //descriptive conf name
}
sourceSets {
devOnly {
kotlin.srcDirs = ['dev-src']
resources.srcDirs = ['dev-res']
java.srcDirs = [] // disable java devOnly dirs
compileClasspath += sourceSets.main.runtimeClasspath
}
}
dependencies {
devOnly platform("io.micronaut:micronaut-bom:$micronautVersion")

macDevOnly "io.micronaut:micronaut-runtime-osx"
devOnly "net.java.dev.jna:jna"
devOnly "io.methvin:directory-watcher"
}
run.classpath += configurations.macDevOnlytasks.withType(JavaExec) {
classpath += configurations.devOnly
//add to classpath when current platform is macOS
if (OperatingSystem.current().isMacOsX())
classpath += configurations.macDevOnly
classpath += sourceSets.devOnly.runtimeClasspath
jvmArgs('-noverify',
'-XX:TieredStopAtLevel=1',
'-Dcom.sun.management.jmxremote',
"-Dlogback.configurationFile=logback-dev.xml",
'-Dmicronaut.environments=dev')
}

We have created a separate configuration for macOS specific dev dependencies (they will only be resolved when we are on macOS platform). The reason we extended from the devOnly configuration is that our macDevOnly “io.micronaut:micronaut-runtime-osx" dependency depends upon another dev dependency “io.micronaut:micronaut-bom:$micronautVersion”.

The dependency tree looks like this once we have our custom configurations devOnly and macDevOnly in place.

Custom configuration macDevOnly

For other developers it is now obvious from the configuration itself that they should be prefix every macOS dev dependency with macDevOnly. Plus this is also much cleaner than the previous approach.

Conclusion

Dependency management is one of the most crucial aspects of any application.

Gradle is a highly mature build tool that provides an out of the box solution to manage platform and environment-specific dependencies.

You can explore more about gradle here.

Hope you enjoyed it :)

Happy learning!!!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store