Monday, 3 October 2016

Gradle Multi-Project Build with local SNAPSHOT dependency resolution

Note - 2016-10-22 - I missed Gradle Composite Builds which do something very similar

I often find myself on a project with multiple applications depending on common libraries, so I tend to end up with a super project looking like this:

|
|-app1
|-app2
|-lib

All three projects are separate git projects, separately built and deployed; the apps are on a continuous deployment pipeline, the lib requires a decision to cut a release to move it from a SNAPSHOT version to a fixed version. The top level project is just a convenience to allow checking them all out in one shot and building them all with one command.

During development of a feature that requires a change to the lib, I would update the dependency in the app that needs the feature to X.X.X-SNAPSHOT and work on them both at the same time.

In Maven this worked OK for development - both Maven and most IDEs would successfully resolve any SNAPSHOT dependencies locally if possible. Then after cutting a release of the app you only had to delete the -SNAPSHOT bit from the dependency version and job done.

However, Gradle does not do this by default; you have to specify the dependency as being part of your multi-module build as so:

dependencies {
  ...
  compile project(':lib')
  ...
}


This is much more invasive - changing to a release version of the lib now requires replacing that with:

dependencies {

  ...
  compile 'mygroup:lib:1.2.3'
  ...
}

So you have to add the group of the lib, and know which precise version to specify, rather than just deleting '-SNAPSHOT' from the version. This makes it harder to automate changing the dependency - ideally, I would like to release the lib automatically as part of the continuous deployment process of the app after pushing a commit of the app which references a SNAPSHOT version of the lib.

I'm experimenting with a way around this by manipulating the top level gradle build as so:


subprojects.each { it.evaluate() }

def allDependencies = subprojects.collect { it.configurations.collect { it.dependencies }.flatten() }.flatten()
def localDependencies = allDependencies.findAll { dependency ->
    subprojects.any { it.name == dependency.name && it.version == dependency.version && it.group == dependency.group }
}

subprojects {
    configurations.all {
        resolutionStrategy.dependencySubstitution {
            localDependencies.each {
                substitute module("${it.group}:${it.name}:${it.version}") with project(":${it.name}")
            }
        }
    }
}

This effectively gives the Maven behaviour - and IntelliJ at least respects it correctly and resolves the dependencies to the same workspace

You can play with an example here:
https://github.com/lidalia-example-project/parent

1 comment: