The Blog

Sep 15, 2007

Build Tools: Maven and ColdFusion 

by Maxim Porges @ 12:47 AM | Link | Feedback (4)

We had some discussions at work today surrounding Maven and Buildr, and the build strategy I've been working on. One item I hadn't covered yet was dependency management and build automation for ColdFusion projects.

The thing with ColdFusion is that there isn't really any code to compile; you just have a bunch of .cfm files, and usually a set of CFCs that you'll want to invoke. Some of these CFCs will be in libraries that you have written, while others will be third party libraries (such as ColdSpring and Fusebox).

The issues with resolving dependencies in CF aren't really that much different from resolving them in Java. The biggest difference is that at deployment time, Java web applications will bundle up a WAR file with all the library dependencies in the WEB-INF/lib directory, whereas CF apps just need a "web root" directory with all the CF code, and the root of the package(s) for any CFCs that the app needs at the same height of the directory tree.

There is, of course, another option for CF apps, which is to use WAR-based deployment. We're thinking about this for a few of our apps, but the problem with this option is that you have to bundle the entire CF server with the application. The advantages are that we can have a pristine classpath for the application, and use Maven/Buildr to resolve library dependencies for Java libraries, but the disadvantage is that the resultant WAR ends up very large (CF by itself is about 100 MB, slightly less without the CF administrator).

Since both Maven and Buildr support WAR-based builds and deployment, WAR-based CF apps would be the easiest to take care of. All we'd need to do is follow Maven's standard convention for the project file system layout, and put the .cfm files and CFC packages in the directory where Maven expects JSP files (in the root of the web app). However, since we're not sure if we want to go this route yet, I wondered how hard it would be to have Maven resolve dependencies for ColdFusion and build the entire app without having to resort to the WAR-based approach.

It turns out it wasn't that hard. The first thing I needed to do was create a few projects: two "library" projects to hold CFCs to be used as dependencies, and another "application" project to rely on the library dependencies.

In order to have Maven resolve the dependencies, I needed to install the libraries in to Artifactory. Artifactory can store any archive format, so to keep things simple I just left them as JARs. I set up a basic Maven build for a JAR. However, since there is no need to compile any code, I overrode the location of the "resources" directory and just put my top-level CFC package inside. In Maven, anything you put in this "resources" directory just ends up in the root of the JAR, so by doing this, Maven would JAR up my CFCs, putting the top-level CFC package in the root of the JAR file, just like it would put the top-level Java package in a JAR to be picked up by the classpath.

So, the directory structure for all of my ColdFusion Maven projects (for both the libraries and the application) look like the following:

-- project root
-- pom.xml
-- src
-- com
-- packagename
-- packagename
-- Component.cfc


The build file looks like this (ignore the package and artifact naming referencing "coldspring", I was just pretending to have a real CF library of some kind).

<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>coldfusion.library</groupId>
<artifactId>coldspring</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<build>
<resources>
<resource>
<directory>src</directory>
<includes>
<include>*/**</include>
</includes>
</resource>
</resources>
</build>
<distributionManagement>
<repository>
<id>localArtifactory</id>
<name>Local Artifactory Repository</name>
<url>http://localhost:8081/artifactory/cfi-releases</url>
</repository>
<snapshotRepository>
<id>localArtifactory</id>
<name>Local Artifactory Repository</name>
<url>http://localhost:8081/artifactory/cfi-snapshots</url>
</snapshotRepository>
</distributionManagement>
</project>


The <resources/> section simply says "all my code is in a directory in the root of the project called src." The <distributionManagement/> section defines my local Artifactory instance, where I want to deploy my libraries. I run the command mvn deploy to package my JAR and deploy it to the repository. I created a second mock library for Transfer, used the same Maven build file, and deployed that to the Artifactory repository also. The build file for this library looks the same as the other one, but has the following opening block:

<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>coldfusion.library</groupId>
<artifactId>transfer</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
...


Next, I needed to create an application to hold some .cfm pages that would make use of my libraries. I set up the same build file again, but this time, added dependencies for the two libraries. I also added an override to the assembly plugin so that it would resolve the dependencies at build time and bundle them in to the final JAR. This means that the final JAR will contain both the application code pages, and the dependent libraries, in the root of the JAR file. Simply unzipping this JAR file would result in a directory ready to be uploaded to a CF server and executed.

This build file looks like the following. To package up my application in to a JAR containing all of the source for the app and the dependencies, and deploy it to Artifactory, I simply run the command mvn deploy again.

<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>coldfusion.application</groupId>
<artifactId>cf-application</artifactId>
<packaging>jar</packaging>
<version>0.0.1</version>
<build>
<resources>
<resource>
<directory>src</directory>
<includes>
<include>*/**</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>
jar-with-dependencies
</descriptorRef>
</descriptorRefs>
</configuration>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>localArtifactory</id>
<name>Local Artifactory Repository</name>
<url>http://localhost:8081/artifactory/cfi-releases</url>
</repository>
<snapshotRepository>
<id>localArtifactory</id>
<name>Local Artifactory Repository</name>
<url>http://localhost:8081/artifactory/cfi-snapshots</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>coldfusion.library</groupId>
<artifactId>coldspring</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>coldfusion.library</groupId>
<artifactId>transfer</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
</project>


You'll notice that there is quite a bit of replication in these files for the <distributionManagement/> and <resources/> sections. This is easy to remove with Maven's project inheritance model, but since this is just a POC I replicated the code blocks between build files.

The advantage of using Maven's POMs for the build process is that I get all the benefits that Maven provides out of the box: I can create "snapshot" builds for code under development, and easily manage multiple versions of libraries, regardless of whether I created them or I have downloaded them from third parties. I can also integrate with SVN or any other source control system that Maven supports, and take advantage of multiple profiles for staging to different environments or modifying the build process in some way (such as copying in differing configuration files for each environment, like we need to do with our Java apps).

It's also conceivable that if I wanted to, I could write Maven plugins in Java to manipulate or scan my CF code. For example, I could write a CF code coverage plugin, and run it when building my code to create a report of inconsistencies. Alternatively, I could add a plugin that runs the cfcompile executable to precompile my code. I have no plans to write either of these plugins right now, but it's nice to know the facility is there in Maven if I want it.

One item we'll definitely want to do is have the code stage itself to a local development environment, or to an integration testing environment. We can use the ability to switch profiles to set up a different distribution method for integration builds, during which the code will be compiled and then FTP'd to the appropriate server for testing to begin.

Dan's going to be porting my Maven scripts over to Buildr next week, so hopefully by week's end we'll have an idea of where we're headed next. Assuming we get all the items migrated, I'd really like to make a decision on a build platform and set a new standard for builds in our environment the week of September 24th.