Archive for May, 2010

Maven Recipe: Delivering applications as RPMs

In the Java world, the standard unit of delivery is the jar/war/(x)ar file. For applications which don’t fit the ‘single file’ delivery model, there is no standard alternative.  To provide a solution, I will show you how to package your application with Maven as an RPM.

As we are talking about Maven, the solution starts with finding the right plugin – in our situation the rpm-maven-plugin. We include the plugin as follows:

 <groupId>org.codehaus.mojo</groupId>
 <artifactId>rpm-maven-plugin</artifactId>
 <version>2.0.1</version>

The goal we need to execute is rpm – and it attaches itself to the package phase by default – so far so good.

The configuration that is usually done in a RPM spec file is done inside the plugin’s configuration block. First, we list some required descriptive elements:

<configuration>
  <copyright>2010, NoBody</copyright>
  <group>Development</group>
  <description>Maven Recipe: RPM Package.</description>

Now we get to the core of the RPM – listing the files to deploy. As we want to deploy our application’s classes and the ones from our dependencies, we create a mapping:

<mappings>
  <mapping>
    <directory>${app.home}/lib/</directory>
    <dependency/>
  <artifact/>
</mapping>

This is a very compact way of declaring that we want all dependencies – the dependency tag – and our primary build artifact – the artifact tag – to be deployed to the lib directory of our application.

The next step is to take the app.properties file which is checked into source control and deploy it as a sample file in the conf directory, so that we have a reference at hand when configuring the application:

<mapping>
  <directory>${app.home}/conf</directory>
  <configuration>true</configuration>
  <sources>
    <source>
      <location>${project.build.outputDirectory}/app.properties</location>
      <destination>app.sample.properties</destination>
    </source>
  </sources>
</mapping>

Using location and destination we have absolute control over what is included and where it is placed. Also note the usage of the configuration tag, which translates in to the %config RPM macro, which means that changes to the file are preserved when updating or removing RPM  ( details on the RPM %config macro ).

Last, we need to generate an empty logs directory for our application. This is done simply by declaring a mapping with no sources.

<mapping>
  <directory>${app.home}/logs</directory>
</mapping>

Right now we can build a RPM out of our application. I’ve added hibernate 3.3.2.GA as a dependency, since it brings in a few transitive dependencies.

robert@neghvar:~/workspace/rpm-package> mvn clean package
[INFO] Scanning for projects...                          
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Recipe: RPM Package                                      
[INFO]    task-segment: [clean, package]                                       
[INFO] ------------------------------------------------------------------------
.........
[INFO] Wrote: /home/robert/workspace/rpm-package/target/rpm/rpm-package/RPMS/rpm-package-0.0.1-SNAPSHOT20100520205409.noarch.rpm   
...........
[INFO] ------------------------------------------------------------------------                                                     
[INFO] BUILD SUCCESSFUL                                                                                                             
[INFO] ------------------------------------------------------------------------                                                     
[INFO] Total time: 5 seconds                                                                                                        
[INFO] Finished at: Thu May 20 23:54:11 EEST 2010                                                                                   
[INFO] Final Memory: 15M/174M                                                                                                       
[INFO] ------------------------------------------------------------------------

Let’s verify that the RPM file indeed contains all that we asked it to:

robert@neghvar:~/workspace/rpm-package> rpm -qlp /home/robert/workspace/rpm-package/target/rpm/rpm-package/RPMS/rpm-package-0.0.1-SNAPSHOT20100520205409.noarch.rpm
/opt/app/conf                                                                                                                                                      
/opt/app/conf/app.sample.properties                                                                                                                                
/opt/app/lib                                                                                                                                                       
/opt/app/lib/antlr-2.7.6.jar                                                                                                                                       
/opt/app/lib/commons-collections-3.1.jar                                                                                                                           
/opt/app/lib/dom4j-1.6.1.jar                                                                                                                                       
/opt/app/lib/hibernate-core-3.3.2.GA.jar                                                                                                                           
/opt/app/lib/jta-1.1.jar                                                                                                                                           
/opt/app/lib/rpm-package-0.0.1-SNAPSHOT.jar                                                                                                                        
/opt/app/lib/slf4j-api-1.5.8.jar                                                                                                                                   
/opt/app/lib/xml-apis-1.0.b2.jar                                                                                                                                   
/opt/app/logs

We have reached our goal of building an RPM with our applications dependencies, classes and configuration files. By using the rpm-maven-plugin we have managed to keep using Maven as our build tool interface, and also reused the information available in the POM file for declaring dependencies.

The complete source code for this article is available at http://github.com/rombert/Maven-Recipe–RPM-Package . If you have any suggestions or corrections, please comment. Or better yet, fork me.

Comments (25)

Maven Recipe: Building an aggregate jar

Maven builds follow the “one project, one artifact” rule. This means that although it’s possible to build more than one artifact from a Maven project,  it’s not a good idea. It also means that while it’s also possible to build one artifact from multiple projects, it’s not entirely straightforward, and that’s what this post is about.

When refactoring a monolithic build into a modular one, often the downstream consumers are not prepared to consume multiple artifacts, so the build still needs to create a single jar with all the classes. This type of change is what I like to call build refactoring, similar to code refactoring. If code refactoring deals in restructuring an existing body of code, build refactoring aims to restructure an existing build.

Assuming that we have split a large project into multiple modules:

This high-complexity project used to contain two classes, but since they were unrelated we decided to split each into its own module.

When running mvn package, each module creates its individual jar:

robert@neghvar:~/workspace/aggregate-jar> mvn clean package                                                             
[INFO] Scanning for projects...  
(snip...)
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------------------
[INFO] Maven Recipe: Aggregate jar ........................... SUCCESS [1.641s]
[INFO] Maven Recipe: Aggregate jar - first module ............ SUCCESS [2.066s]
[INFO] Maven Recipe: Aggregate jar - second module ........... SUCCESS [0.697s]
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Sat May 15 00:59:01 EEST 2010
[INFO] Final Memory: 23M/251M
[INFO] ------------------------------------------------------------------------

Inspecting the resulting jar files confirms this assertion:

robert@neghvar:~/workspace/aggregate-jar> find -name \*.jar
./aggregate-first-module/target/aggregate-first-module-1.0.0-SNAPSHOT.jar
./aggregate-second-module/target/aggregate-second-module-1.0.0-SNAPSHOT.jar

Of course, we can always use a custom shell command, or an ant file, or even the maven-antrun-plugin to combine the two jars. While easy to do correctly and even in a cross-platform manner,  the downside is that we lose the simplicity and modularity of a pure Maven build.

Following the “one project, one artifact” rule, it becomes clear that the solution is to add another project, which generates the distribution jar. We will add a new aggregate-dist module to the build, which will use the maven-assembly-plugin to combine the all resulting classes into a single jar. The module section therefore becomes

While the assembly plugin is well known and used, there are a few considerations which must be observed when aggregating the results of a multi-module build.

The distribution module should list the modules to assemble as dependencies:

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aggregate-first-module</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aggregate-second-module</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
 </dependencies>

The assembly plugin should be bound to the package lifecycle phase, and invoke the ‘single’ goal:

<plugin>

 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.2-beta-5</version>
 <executions>
 <execution>
 <id>package-all</id>
 <phase>package</phase>
 <goals>
 <goal>single</goal>
 </goals>
 <configuration>
 <descriptors>
 <descriptor>src/main/assembly/all-jar.xml</descriptor>
 </descriptors>
 </configuration>
 </execution>
 </executions>
 </plugin>

The assembly descriptor should include the unpacked non-transitive dependencies:


<assembly
 xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
 <id>all-jar</id>
 <formats>
 <format>jar</format> <!-- the result is a jar file -->
 </formats>

 <includeBaseDirectory>false</includeBaseDirectory> <!-- strip the module prefixes -->

 <dependencySets>
 <dependencySet>
 <unpack>true</unpack> <!-- unpack , then repack the jars -->
 <useTransitiveDependencies>false</useTransitiveDependencies> <!-- do not pull in any transitive dependencies -->
 </dependencySet>
 </dependencySets>
</assembly>

With this setup, we can invoke maven again:

robert@neghvar:~/workspace/aggregate-jar> mvn clean package                                                             
[INFO] Scanning for projects...  
(snip...)
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------------------
[INFO] Maven Recipe: Aggregate jar ........................... SUCCESS [1.562s]
[INFO] Maven Recipe: Aggregate jar - first module ............ SUCCESS [1.315s]
[INFO] Maven Recipe: Aggregate jar - second module ........... SUCCESS [0.286s]
[INFO] Maven Recipe: Aggregate jar - distribution ............ SUCCESS [1.312s]
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Sat May 15 01:26:47 EEST 2010
[INFO] Final Memory: 23M/301M
[INFO] ------------------------------------------------------------------------

And indeed a third jar has been created

robert@neghvar:~/workspace/aggregate-jar> find -name \*.jar
./aggregate-first-module/target/aggregate-first-module-1.0.0-SNAPSHOT.jar
./aggregate-second-module/target/aggregate-second-module-1.0.0-SNAPSHOT.jar
./aggregate-dist/target/aggregate-dist-1.0.0-SNAPSHOT-all-jar.jar

We verify that it contains the two classes

robert@neghvar:~/workspace/aggregate-jar> jar tf aggregate-dist/target/aggregate-dist-1.0.0-SNAPSHOT-all-jar.jar | grep \.class
ro/lmn/maven/recipe/ClassOne.class
ro/lmn/maven/recipe/ClassTwo.class

At this point, we have achieved or goal. By creating a dedicated distribution module, we have isolated the packaging logic from the rest of the project, and used the maven-assembly-plugin to package the non-transitive dependencies, which we declared to be the modules to be packaged.

One indirect conclusion of this article is that, although Maven is convention-based and seemingly rigid ( or opinionated , to meet the buzzword quota per posting ), there are often idiomatic Maven solutions which solve unsual problems in an elegant way.

The complete source code for this article is available at http://github.com/rombert/Maven-Recipe—Aggregate-Jar . If you have any suggestions or corrections, please comment. Or better yet, fork me.

Comments (14)