mandag 28. februar 2011

Incremental builds - great potential, reliant on trust

The first time I evaluated gradle as a candidate build system I was intrigued by its incremental build feature. Having worked with multiprojects in maven previously I must admit I was pretty hardwired into using "mvn clean install". With maven that felt as the only safe bet way to ensure that any changes in subprojects didn't break the overall build. However for large multiproject builds it sure felt as I was spending a lot of time waiting for a range of build activities that shouldn't have been necessary.
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:
Taskmvn observationgradle 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" :-)




fredag 18. februar 2011

Gradle friends with Sonar

A little while ago a colleague of mine asked me if I had done a Sonar analysis of my current project. Well I hadn't so I started to investigate a little on what my options were given my build was setup with Gradle. After reading up a bit on the gradle forums, it appeared that it wasn't all that trivial. In particular getting my emma coverage available for sonar seemed to be a stumbling block. The sonar version at the time also seemed to be fairly tied to maven, a build system I sort of left behind last year.

Anyways yesterday I thought I'd check up on the announced plans for providing gradle support for sonar. Not quite there yet, but there was a snapshot version with ant support. Gradle and Ant being pretty good friends, I decided to give it a go.

Short story is that it worked. However I couldn't manage to get the testcoverage from emma in sonar, so I switched my setup to use cobertura (did slow my build quite a bit actually). Another remaining issue is all the annoying error messages from the pmd plugin in sonar. Lots of exceptions related to it for some reason insisting that I'm using jdk 1.4, but the fact is that I'm using jdk 1.6.

My build.gradle setup:
 apply plugin: 'java'  
 configurations {  
   sonarLibs  
    coberturaLibs {extendsFrom testRuntime}  
 }  
 dependencies {  
   //.. other deps omitted  
   coberturaLibs 'net.sourceforge.cobertura:cobertura:1.9.3'  
   sonarLibs "org.codehaus.sonar-plugins:sonar-ant-task:0.1-SNAPSHOT"  
 }  
 def cobSerFile = "${project.buildDir}/cobertura.ser"  
 def srcOriginal = "${sourceSets.main.classesDir}"  
 def srcCopy = "${srcOriginal}-copy"  
 test {  
   useTestNG()  
   maxParallelForks = 2  
   afterTest {descriptor, result ->  
     println "${descriptor.className}.${descriptor.name}"  
   }  
   /** Cobertura setup **/  
   if (project.hasProperty('coverage') && ['on', 'true'].contains(project.properties.coverage)) {  
     systemProperties["net.sourceforge.cobertura.datafile"] = cobSerFile  
     doFirst {  
       ant {  
         // delete data file for cobertura, otherwise coverage would be added  
         delete(file: cobSerFile, failonerror: false)  
         // delete copy of original classes  
         delete(dir: srcCopy, failonerror: false)  
         // import cobertura task, so it is available in the script  
         taskdef(resource: 'tasks.properties', classpath: configurations.coberturaLibs.asPath)  
         // create copy (backup) of original class files  
         copy(todir: srcCopy) {  
           fileset(dir: srcOriginal)  
         }  
         // instrument the relevant classes in-place  
         'cobertura-instrument'(datafile: cobSerFile) {  
           fileset(dir: srcOriginal,  
               includes: "**/*.class",  
               excludes: "**/*Test.class")  
         }  
       }  
     }  
     doLast {  
       if (new File(srcCopy).exists()) {  
         // replace instrumented classes with backup copy again  
         ant {  
           delete(file: srcOriginal)  
           move(file: srcCopy,  
               tofile: srcOriginal)  
         }  
         // create cobertura reports  
         ant.'cobertura-report'(destdir: "${project.buildDirName}/test-results",  
             format: 'html', srcdir: "src/main/java", datafile: cobSerFile)  
         ant.'cobertura-report'(destdir: "${project.buildDirName}/test-results",  
             format: 'xml', srcdir: "src/main/java", datafile: cobSerFile)  
       }  
     }  
   }  
 }  
 task runSonar << {  
   ant.taskdef(name: "sonar", classname: "org.sonar.ant.SonarTask", classpath: configurations.sonarLibs.asPath)  
   ant.sonar(workdir: ".", key: "no.sample:sampleapp", version: '1.0') {  
     sources {  
       path(location: "src/main/java")  
     }  
     tests {  
       path(location: "src/test/java")  
     }  
     // tell sonar to use existing cobertura reports  
     property(key: "sonar.dynamicAnalysis", value: "reuseReports")  
     property(key: "sonar.cobertura.reportPath", value: file("build/test-results/coverage.xml"))  
   }  
 }  

So to run the sonar goal with coverage I basically make the following command;

$gradle build runSonar -Pcoverage=on



And voila: