Monday, May 25, 2009

Maven: Compile to a RAM-drive

While preparing to do a backup recently, I realized that I had over 2GB in 110.000 files which was the result of either an Ant build or Maven target. It occured to me that with Maven especially, this is nothing more than garbage to keep around, since Maven will copy the artefacts to its /.m2 folder anyway!

In a previous blog entry, I explained how to mount a RAM-drive on Ubuntu and use it as a development folder for much higher performance in the compile-test-run cycle. I have to admit, I don't use this for each and every project since it's a little complex and fragile.

But there's an interesting compromise to be had here. It turns out it is possible to instruct Maven to build artefacts to a RAM-drive. That way you not only gain some performance but also some automatic "garbage collection" in that by the next boot, you will have no traces of these build steps laying around. It would also be a very healthy thing to do if you're running from of an SSD drive, which are susceptible to wear-and-tear. The following will explain how to set this up with Maven.

Prerequisites
You are going to need a RAM-drive for this purpose. It is extremely easy to set one up on Ubuntu. In the following I set the permissions of the mount to that of the Ubuntu user "plugdev" which is the one being used for auto-mounting USB-drives etc. That way all users should have permission to use it:


sudo mkdir /media/ramdrive
sudo chgrp plugdev /media/ramdrive
sudo chmod g+w /media/ramdrive
sudo chmod +t /media/ramdrive
sudo mount tmpfs /media/ramdrive -t tmpfs


You can have your system automatically create and mount a TMPFS partition for you, by modifying your /etc/fstab file. Simply add the following to it:


none /media/ramdrive tmpfs defaults,user,size=1G,mode=0777 0 0


An alternative approach could be to have the RAM-drive mounted at compile time. This is perfectly possible by calling out to an Ant target in Maven, and have it execute a Bash script. The problem is though, in order to mount filsystems on Linux you need to be a super user. So for this approach to work, you would need to hardwire your SUDO password in the script and that would be pretty dumb. It is entirely possible there is a way to do it, but if there is I am unaware of it.

The POM modification
Basically all we need to do is instruct Maven where out build directory is, our output directory and our test directory. This can be done by simply specifying it within the build tag:



<project>
   <build>
       <directory>/media/ramdrive/maven-targets/${project.name}</directory>
       <outputDirectory>/media/ramdrive/maven-targets/${project.name}/classes</outputDirectory>
       <testOutputDirectory>/media/ramdrive/maven-targets/${project.name}/test-classes</testOutputDirectory>
       ...
   </build>
   ...
</project>


You'll notice I place everything under the sub-folder "maven-targets", this is so that none of my targets conflicts with other stuff on my RAM-drive. Also, the actual sub-folder for each individual project will be named the same as the project.

As Maven profile
While the above certainly works, it's arguably some heavy bending of Maven's conventions and we run the risk that other people not as fortunate as us (i.e. Windows users) won't have a RAM-drive nor the possibility to create one. To remedy this, we can encapsulate this custom behaviour in a Maven profile.

Sadly in a profile, we are not allowed to specify build paths like we just saw above. We can however cheat a bit, by going through some properties. Start by defining some global properties somewhere in a properties tag under the project tag:



<project>
   <properties>
       <build.dir>target</build.dir>
       <build.outputDir>target/classes</build.outputDir>
       <build.testOutputDir>target/test-classes</build.testOutputDir>
       ...
   </properties>
   ...
</project>


This complies with the standard Maven conventions. Then in the non-profile build tag, do like before but rather than hardwire the paths, use the properties we just defined. Like so:



<project>
   <build>
       <directory>${build.dir}</directory>
       <outputDirectory>${build.outputDir}</outputDirectory>
       <testOutputDirectory>${build.testOutputDir}</testOutputDirectory>
       ...
   </build>
   ...
</project>


The behaviour thus far should not differ in any way from the default Maven build cycle. The last thing we need to do now is to add a profile for emitting to the RAM-drive:



<project>
   <profile>
       <id>RAM-drive</id>
       <build>
           <plugins>
               <plugin>
                   <artifactId>maven-compiler-plugin</artifactId>
                   <inherited>true</inherited>
                   <configuration>
                       <source>1.6</source>
                       <target>1.6</target>
                   </configuration>
               </plugin>
           </plugins>
       </build>
       <properties>
           <build.dir>/media/ramdrive/maven-targets/${project.name}</build.dir>
           <build.outputDir>/media/ramdrive/maven-targets/${project.name}/classes</build.outputDir>
           <build.testOutputDir>/media/ramdrive/maven-targets/${project.name}/test-classes</build.testOutputDir>
       </properties>
   </profile>
   ...
</project>


That's it. Now you just select the profile "RAM-drive" in your IDE when you build, without it having negative consequences for your less fortunate collegues. Of course a similar setup can be made with Ant and most other build environments.

Post a Comment