Keep Gradle Build Scripts DRY With Closures


Every now and then I was wondering how a more complex Gradle build script can be written in a manner, which does not violate the DRY (do not repeat yourself) principle.

Let me show how DRY is easily violated within even a simple build script:

Build Script Violating the DRY Principle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
apply plugin: 'ivy-publish'

buildscript {
  repositories {
    ivy {
      name  = 'Local Repository'
      url   = 'file:///home/myhome/localRepository/'
    }

    ivy {
      name  = 'Company Artifacts Repo'
      url   = 'https://company-repo/artifacts/'
    }

    ivy {
      name  = 'Third Party Artifacts Repo'
      url   = 'https://company-repo/third-party/'
    }
  }
}


repositories {
  ivy {
    name = 'Local Repository'
    url = 'file:///home/myhome/localRepository/'
  }

  ivy {
    name = 'Company Artifacts Repo'
    url = 'https://company-repo/artifacts/'
  }

  ivy {
    name = 'Third Party Artifacts Repo'
    url = 'https://company-repo/third-party/'
  }
}


publishing {
  publications {
    ivyJava( IvyPublication ) {
      // Stuff
    }
  }

  repositories {
    ivy {
      name = 'Local Repository'
      url = 'file:///home/myhome/localRepository/'
    }

    ivy {
      name = 'Company Artifacts Repo'
      url = 'https://company-repo/artifacts/'
    }
  }
}

// more stuff

It is easy to see, that DRY is violated multiple times here, as all three repositories are declared multiple times in exactly the same way.

Now imagine this to be part of a complex build system of a software company where the build logic is split into smaller scripts. In the worst case all those repository declarations are spread across multiple files, so having to change them is quite a bit of search and replace.

But how can this be cleaned up? The maybe obvious idea would be using variables for the repositories names and URLs, however this would still be not very DRY. Fortunately Gradle is just Groovy, after all! And Groovy is perfect for using closures.

So what is this closure thing? A closure is really just a block of code which can take in arguments, returns a value and can be assigned to a variable. This is what closures look like:

Some Groovy Closures
1
2
3
4
def addOneClosure               = { number -> number + 1 }
def addOneClosureAlternative    = { it + 1 }
def multiply                    = { a, b -> a * b }
def answerQuestionOfEverything  = { -> 42 }

Groovy closures have an implicit argument if nothing is given, which is called it. If the closure is not supposed to have any input argument then an empty argument list has to be declared explicitly like this: { -> //code }. Groovy also allows a closure to access visible variables from the surrounding scope.

That was the easy part about closures however Groovy has another interesting concept: the delegate of a closure. The delegate is a freely definable object which the closure will use. Here is a rather simple example:

Closure With Delegate Example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Server {
  String name
  String ip
}

class DeveloperPC {
  String name
  String ip
}

def webServer = new Server( name: 'webserver', ip: '192.168.1.42' )
def steffsPC  = new DeveloperPC( name: 'steffs_pc', ip: '192.168.2.42' )

def getIp = { -> ip } // Equivalent to { -> delegate.ip }

getIp.delegate = webServer
assert printIp() == '192.168.1.42'

getIp.delegate = steffsPC
assert printIp() == '192.168.2.42'

As seen in the example above the closure first uses the object of type Server to get the ip and then the other object of type DeveloperPC for the same thing. This works for all objects which have a String property named ip, there is no need for a common superclass declaring the ip property. Also Groovy does use the delegate transparently, i.e. it is not necessary to explicitly use it for calling it’s properties/methods. A more detailed explanation of this feature can be found in the Groovy Documentation.

With this knowledge it is easy to understand how Gradle actually works, it is simply making heavy use of Groovy’s closures. Basically every time you see a {} pair in a script this almost certainly is actually a closure. Gradle also makes heavy use of the delegate feature. Have a look at Project’s Javadoc, there are a lot of methods which just point this fact out.

And now we know everything needed to make the build script DRY! Here is the solution with Groovy closures:

DRY Build Script
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
apply plugin: 'ivy-publish'


buildscript {

  def companyRepoURL = 'https://company-repo'

  gradle.ext.localRepository = { ->
    name  = 'Local Repository'
    url   = 'file:///home/myhome/localRepository/'
  }

  gradle.ext.companyRepository = { ->
    name  = 'Company Artifacts Repo'
    url   = "$companyRepoURL/artifacts/"
  }

  gradle.ext.thirdPartyRepository = { ->
    name  = 'Third Party Artifacts Repo'
    url   = "$companyRepoURL/third-party/"
  }

  repositories {
    ivy gradle.ext.localRepository
    ivy gradle.ext.companyRepository
    ivy gradle.ext.thirdPartyRepository
  }
}


repositories {
  ivy gradle.ext.localRepository
  ivy gradle.ext.companyRepository
  ivy gradle.ext.thirdPartyRepository
}


publishing {
  publications {
    ivyJava( IvyPublication ) {
      // Stuff
    }
  }

  repositories {
    ivy gradle.ext.localRepository
    ivy gradle.ext.companyRepository
  }
}

// A lot of other stuff

Now the repositories are declared once and used where needed. Changing the URL of a repository has become a breeze as there is only one place where the change needs to be done. This works because Gradle will set the closure’s delegate to an instance of IvyArtifactRepository in this example.

The closures can also reside in their own script which is applied where needed. In this case it makes sense to use ext instead of gradle.ext.

This solution works for many other cases in a complex build script. It is also possible to use closures within other closures like this:

repositoryClosures.gradle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def companyRepoURL = 'https://company-repo'

def localRepository = { ->
  name  = 'Local Repository'
  url   = 'file:///home/myhome/localRepository/'
}

def companyRepository = { ->
  name  = 'Company Artifacts Repo'
  url   = "$companyRepoURL/artifacts/"
}

def thirdPartyRepository = { ->
  name  = 'Third Party Artifacts Repo'
  url   = "$companyRepoURL/third-party/"
}

ext.pluginRepositories = { ->
  companyRepository.delegate = delegate
  companyRepository()

  thirdPartyRepository.delegate = delegate
  thirdPartyRepository()
}

ext.projectRepositories = { ->
  localRepository.delegate = delegate
  localRepository()

  companyRepository.delegate = delegate
  companyRepository()
}

In the example above the actual repositories are now only visible within the script itself. However the repository groups like pluginRepositories can be used by other build scripts. Each of these groups first assign the closure’s delegate to their own delegate and then execute it.