What if my build system could be clever enough to do just the activities it must, skip the others and still ensure the overall build integrity ? Gradle came with a promise to go a long way doing just that, so obviously I had to try it out.
Sample multiproject:
- dumpling : Root multiproject (packaged as pom in maven )
- dumpling-util : A utility project (jar)
- dumpling-core : Application domain (jar, depends on dumpling-util)
- dumpling-webapp: Web app (depends on dumbling-core, and dumpling-util transitively)
To see how gradle and maven behaved I performed some simple trials. For each step I observed the main activities that was reported.
Sample scenario 1: Non breaking change
Precondition:
- maven: mvn clean install
- gradle: gradle clean build
Changed a source file in dumpling-util. The change is internal to the class. Then for maven and gradle I did following:
- maven: mvn install
- gradle: gradle build
dumpling-util observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | skipped |
compile | compiled changed file | compiled changed file |
test resources | copied resources | skipped |
test compile | no work | compiled test sources |
jar | created | created |
install | installed to local repo | n/a |
dumpling-core observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | skipped |
compile | none | full recompile (due to input file dumpling-core jar had changed) |
test resources | copied resources | skipped |
test compile | no work | full recompile (due to input file dumpling-core jar had changed) |
jar | created | skipped (input classfiles and resources same checksum as previous run) |
install | installed to local repo | n/a |
dumpling-webapp observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | skipped |
compile | none | full recompile (due to transitive input file dumpling-core jar had changed) |
test resources | copied resources | skipped |
test compile | no work | full recompile (due to transitive input file dumpling-core jar had changed) |
war | created | created (due to transitive input file dumpling-core jar had changed) |
install | installed to local repo | n/a |
Comparing the two one might at first conclude that both did unnecessary work.
Maven: Copied resources and test resources for all subprojects eventhough none of them had changed. For dumpling-core it created the jar eventhough not a single file in the jar had changed.
For dumpling-core and dumpling-webapp it didn't do any work compiling java sources ar test sources. This might seem ok, but next scenario will prove this to be untrue.
Gradle: It seems unnecessary to do some of the activities that gradle does, but there is a reasonable explanation behind. In gradle there is a concept of inputs and outputs for tasks. A task may have a defined set of inputs (files) and a defined set of outputs (files). For each execution of a task gradle calculates checksums for the inputs and the outputs. If gradle finds that the checksums have changed since last run, the tasks will be run (unless other factors such as explicit disabling of tasks has been configured). So an example would be dumpling-util:test compile. This task is executed because its input files from dumpling-util:compile has changed.
Sample scenario 2: Breaking change
I changed the interface of a method for a class in dumpling-util, and updated its test case accordingly. The method in question is used by a class in dumpling-core, so dumpling-core needs to change...
Then for maven and gradle I did following:
- maven: mvn install
- gradle: gradle build
dumpling-util observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | skipped |
compile | compiled changed file | compiled changed file |
test resources | copied resources | skipped |
test compile | compiled changed test source file | compiled test sources |
jar | created | created |
install | installed to local repo | n/a |
dumpling-core observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | skipped |
compile | none | full recompile (due to input file dumpling-core jar had changed). BUILD Failed due to compiler error on class in dumpling-core that uses changed interface method of class from dumpling-util |
test resources | copied resources | n/a |
test compile | no work | n/a |
jar | created | n/a |
install | installed to local repo | n/a |
dumpling-webapp observations:
Task | mvn observation | gradle observation |
---|---|---|
resources | copied resources (not needed) | n/a |
compile | none | n/a |
test resources | copied resources | n/a |
test compile | no work | n/a |
war | created | n/a |
install | installed to local repo | n/a |
It instantly becomes obvious that the incremental build features of maven is rather limited. Gradle on the other hand fails fast where it should. Obviously maven has never advertised great support for incremental builds, but the comparison helps put things into perspective.
Parting words
The potential of huge savings in time due to incremental builds is obvious. However vitally important for the feature is the level of trust you can put into it as well. In gradle you have great flexibility of creating your own tasks, if these rely on file inputs and outputs be sure to consider using the inputs/outputs mechanisms available to you. But do take care when defining them, otherwise you might end up with a trust issue and reverting back to "clean build" :-)