Keep Gradle Build Scripts DRY With Closures
Date: 25 Mar, 2018
Reading Time: 6 min
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:
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:
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:
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:
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:
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.