Locking Dependency Versions in Gradle

The ./gradlew build command should produce the exact same output each time you run it–even when you run it on someone else’s computer. But the use of dependency version ranges in Gradle makes it possible for ./gradlew build to resolve different versions of dependencies during each execution, which makes the output of your builds unpredictable.

Version ranges are a great convenience, but they allow you to shoot yourself in the foot. Fortunately, there’s a solution that gives you the best of both worlds.

Using a Lockfile

One of the great innovations in dependency management is the lockfile introduced by Ruby’s Bundler as the Gemfile.lock. A lockfile is generated by a dependency manager to enforce explicit version numbers for dependencies even when version ranges are used in your dependency configuration.

The lockfile creates predictable and reproducible builds. It prevents the situation where a developer runs ./gradlew build and resolves Apache HttpClient 4.5.3, but a CI server runs the same command and resolves version 4.5.2.

This type of version conflict is uncommon in Gradle and Maven builds because most dependencies uses explicit versions. In the early days of Maven it was common to support ranges, but they introduced so many problems that some of the facilities for using ranges were removed in Maven 3.

Gradle supports dynamic version ranges, but it does not provide a lockfile. Fortunately, there’s a plugin for that.

Using the Dependency Lock Plugin from Netflix

You can use dynamic dependency versions and still lock them to specific versions with the Gradle Dependency Lock Plugin from Netflix’s Nebula Plugins project.

From the project’s documentation:

Some project teams may prefer to have their build.gradle dependencies reflect their ideal world. A latest.release for internal dependencies. A major.+, major.minor.+, or a range [2.0.0, 4.0.0). Many also want to lock to specific versions for day to day development, having a tagged version always resolve identically, and for published versions to have specific dependencies.

To use the plugin, add the following to your build.gradle:

plugins {
  id "nebula.dependency-lock" version "2.2.4"
}

Then run:

$ ./gradlew generateLock saveLock

This will generate a build/dependencies.lock file and copy it (save it) into your root project directory. The file contains explicit versions for every one of your dependencies, and the transitive dependencies of those libraries.

You can add this file to Git, to ensure consistent across environments, by running:

$ git add dependencies.lock
$ git commit -m "Add lock file"

The next time you run ./gradlew build, the plugin will force Gradle to honor the dependency versions locked in the dependencies.lock files.

Predicable and reproducible builds are essential to delivering secure and correct software. With such little overhead, the Gradle Dependency Lock Plugin is an easy win for your projects.