A very useful feature of Maven’s dependency management are the versions:display-dependency-updates and versions:display-plugin-updates tasks. These tasks help keep a project up to date with the latest versions the libraries that it depends on, which is a healthy activity.
I am unaware of similar mechanism built into Gradle. Previously I patched the checkLibVersions example, but it is not very robust. Instead of rewriting Gradle’s excellent dependency management, I wrote a task that uses it to determine the latest versions. This follows advice given by Adam Murdoch, which I was fortunate to stumble upon.
The code below shows a working implementation that could act as inspiration for a built-in report. The dependency lookup is slow if the resolution wasn’t previously cached. This is probably because Gradle resolves each sequentially. I also found that Gradle does not always evaluate the correct latest version, so sometimes it will report “1.1” is the latest release, warning that the declared dependency is newer at 1.2.
A trimmed sample of the report is included in addition to the code.
Update 1: Added support for buildscript dependencies, no longer resolves transitive dependencies, and displays unresolved dependencies Update 2: Added support for controlling the resolution strategy (integration, milestone, release) Update 3: Added reporting for downgrading a dependency to the latest version at the specified revision level Update 4: Now evaluates a subgraphs if added to all projects and run on a specific path (root for the entire graph, a project for the subgraph).
/**
* Adds a task that reports which dependencies have newer versions.
* <p>
* The <tt>revision</tt> project property controls the resolution strategy:
* <ul>
*
<li>release: selects the latest release
*
<li>milestone: select the latest version being either a milestone or a release (default)
*
<li>integration: selects the latest revision of the dependency module (such as SNAPSHOT)
* </ul>
*/
import org.gradle.api.internal.artifacts.version.LatestVersionSemanticComparator
task dependencyUpdates(description: "Displays the dependency updates for the project.",
group: "Help") << {
def current = getProjectAndBuildscriptDependencies()
def (resolved, unresolved) = resolveLatestDepedencies(current)
def (currentVersions, latestVersions, sameVersions, downgradeVersions, upgradeVersions) =
getVersionMapping(current, resolved)
displayReport(currentVersions, latestVersions, sameVersions,
downgradeVersions, upgradeVersions, unresolved)
}
/** Returns {@link ExternalDependency} collected from the project and buildscript. */
def getProjectAndBuildscriptDependencies() {
allprojects.collectMany{ project ->
(project.configurations + project.buildscript.configurations).collectMany { it.allDependencies }
}.findAll { it instanceof ExternalDependency }
}
/**
* Returns {@link ResolvedDependency} and {@link UnresolvedDependency} collected after evaluating
* the latest dependencies to determine the newest versions.
*/
def resolveLatestDepedencies(current) {
if (current.empty) {
return [[], []]
}
def unresolved = current.collect { dependency ->
dependencies.create(group: dependency.group, name: dependency.name,
version: "latest.${revisionLevel()}") {
transitive = false
}
}
def lenient = configurations.detachedConfiguration(unresolved as Dependency[])
.resolvedConfiguration.lenientConfiguration
[lenient.firstLevelModuleDependencies, lenient.unresolvedModuleDependencies]
}
/** Organizes the dependencies into version mappings. */
def getVersionMapping(current, resolved) {
def currentVersions = current.collectEntries { dependency ->
[label(dependency), dependency.version]
}
def latestVersions = resolved.collectEntries { dependency ->
["${dependency.moduleGroup}:${dependency.moduleName}".toString(), dependency.moduleVersion]
}
def sameVersions = currentVersions.intersect(latestVersions)
def comparator = new LatestVersionSemanticComparator()
def upgradeVersions = latestVersions.findAll { label, version ->
comparator.compare(version, currentVersions[label]) > 0
}
def downgradeVersions = latestVersions.findAll { label, version ->
comparator.compare(version, currentVersions[label]) < 0
}
[currentVersions, latestVersions, sameVersions, downgradeVersions, upgradeVersions]
}
/** Returns the dependency's group and name. */
def label(dependency) { "${dependency.group}:${dependency.name}".toString() }
/** Returns the resolution revision level. */
def revisionLevel() { project.properties.get("revision", "milestone") }
/* ---------------- Display Report -------------- */
/** Prints the report to the console. */
def displayReport(currentVersions, latestVersions, sameVersions,
downgradeVersions, upgradeVersions, unresolved) {
displayHeader()
displayUpToDate(sameVersions)
displayExceedLatestFound(currentVersions, downgradeVersions)
displayUpgrades(currentVersions, upgradeVersions)
displayUnresolved(unresolved)
}
def displayHeader() {
println """
|------------------------------------------------------------
|${project.path} Project Dependency Updates
|------------------------------------------------------------""".stripMargin()
}
def displayUpToDate(sameVersions) {
if (sameVersions.isEmpty()) {
println "\nAll dependencies have newer versions."
} else {
println "\nThe following dependencies are using the newest ${revisionLevel()} version:"
sameVersions.sort().each { println " - ${it.key}:${it.value}" }
}
}
def displayExceedLatestFound(currentVersions, downgradeVersions) {
if (!downgradeVersions.isEmpty()) {
println("\nThe following dependencies exceed the version found at the "
+ revisionLevel() + " revision level:")
downgradeVersions.sort().each { label, version ->
def currentVersion = currentVersions[label]
println " - ${label} [${currentVersion} <- ${version}]"
}
}
}
def displayUpgrades(currentVersions, upgradeVersions) {
if (upgradeVersions.isEmpty()) {
println "\nAll dependencies are using the latest ${revisionLevel()} versions."
} else {
println("\nThe following dependencies have newer ${revisionLevel()} versions:")
upgradeVersions.sort().each { label, version ->
def currentVersion = currentVersions[label]
println " - ${label} [${currentVersion} -> ${version}]"
}
}
}
def displayUnresolved(unresolved) {
if (!unresolved.isEmpty()) {
println("\nFailed to determine the latest version for the following dependencies:")
unresolved.sort { a, b -> label(a.selector) <=> label(b.selector) }.each {
println " - " + label(it.selector)
logger.info "The exception that is the cause of unresolved state:", it.problem
}
}
}
------------------------------------------------------------
: Project Dependency Updates
------------------------------------------------------------
The following dependencies are using the newest release version:
- com.google.code.findbugs:jsr305:2.0.1
- com.google.inject.extensions:guice-multibindings:3.0
- com.google.inject.extensions:guice-servlet:3.0
- com.google.inject:guice:3.0
The following dependencies exceed the version found at the release revision level:
- org.scalatra:scalatra [2.3.0-SNAPSHOT <- 2.2.0-RC1]
- org.scalatra:scalatra-auth [2.3.0-SNAPSHOT <- 2.2.0-RC1]
- org.scalatra:scalatra-specs2 [2.3.0-SNAPSHOT <- 2.2.0-RC1]
The following dependencies have newer release versions:
- com.amazonaws:aws-java-sdk [1.3.21.1 -> 1.3.26]
- com.beust:jcommander [1.27 -> 1.30]