Build Smells - Maven Assembly Plugin

If you are a software developer (for example in Java) you might have heard about code smells which indicate not really bugs, but usually are a kind of indicator for problems in the future. It´s usually a good practice to fix such code smells.

But sometimes if I take a deeper look into a Maven build I can observe things like Build Smells which could produce problems in future but not need to.

So the best practice for Code Smells as well as Build Smells is to fix such things as soon as possible. Let us start with simple example on the first glance.

The Distribution

It´s often the case that you like to create a kind of distribution package which contains the created artifacts. Apart from that it is sometimes also useful having supplemental files into such distribution packages as well.

The Obvious Solution

Let us assume having two modules which you like to package into the final zip distribution archive. This will bring you to use the maven-assembly-plugin with an appropriate assembly descriptor like this:

 1<assembly>
 2  <id>distribution</id>
 3  <formats>
 4    <format>zip</format>
 5  </formats>
 6
 7  <includeBaseDirectory>false</includeBaseDirectory>
 8
 9  <fileSets>
10    <fileSet>
11      <directory>${basedir}/../package-1/target/</directory>
12      <outputDirectory>/</outputDirectory>
13      <includes>
14        <include>*.jar</include>
15      </includes>
16    </fileSet>
17    <fileSet>
18      <directory>${basedir}/../package-2/target/</directory>
19      <outputDirectory>/</outputDirectory>
20      <includes>
21        <include>*.jar</include>
22      </includes>
23    </fileSet>
24  </fileSets>
25</assembly>

So on the first glance this descriptor looks very well. Ok. Let us take a look into the appropriate pom file which looks like this:

 1<project
 2  xmlns="http://maven.apache.org/POM/4.0.0"
 3  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5  <modelVersion>4.0.0</modelVersion>
 6
 7  <parent>
 8    <groupId>org.test.parent</groupId>
 9    <artifactId>root</artifactId>
10    <version>1.0.0-SNAPSHOT</version>
11  </parent>
12
13  <name>Packaging Test : Distribution</name>
14  <artifactId>dist</artifactId>
15  <packaging>pom</packaging>
16
17  <build>
18    <plugins>
19      <plugin>
20        <artifactId>maven-assembly-plugin</artifactId>
21        <executions>
22          <execution>
23            <id>make-bundles</id>
24            <phase>package</phase>
25            <goals>
26              <goal>single</goal>
27            </goals>
28            <configuration>
29              <descriptors>
30                <descriptor>proj1-assembly.xml</descriptor>
31              </descriptors>
32            </configuration>
33          </execution>
34        </executions>
35      </plugin>
36    </plugins>
37  </build>
38
39</project>

If you have the right pom file in the parent which will look like this then your build will work without any problems.

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project
 3  xmlns="http://maven.apache.org/POM/4.0.0"
 4  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 5  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 6  <modelVersion>4.0.0</modelVersion>
 7
 8  <parent>
 9    <groupId>com.soebes.smpp</groupId>
10    <artifactId>smpp</artifactId>
11    <version>6.0.5</version>
12  </parent>
13
14  <groupId>org.test.parent</groupId>
15  <artifactId>root</artifactId>
16  <version>1.0.0-SNAPSHOT</version>
17
18  <packaging>pom</packaging>
19
20  <name>Packaging Test</name>
21
22  <modules>
23    <module>package-1</module>
24    <module>package-2</module>
25    <module>dist</module>
26  </modules>
27</project>

The full code of the example can be found on github in the folder assemblies-with-files.

The Build Smells

Let us dive into the build and see if we find some kind of Build Smells.

The Order Smell

The first smell can be found if you change the order of the modules in the parent pom file from this:

1  <modules>
2    <module>package-1</module>
3    <module>package-2</module>
4    <module>dist</module>
5  </modules>

into the following:

1  <modules>
2    <module>dist</module>
3    <module>package-1</module>
4    <module>package-2</module>
5  </modules>

and your build will fail like this:

 1[INFO] 
 2[INFO] --- maven-assembly-plugin:2.4:single (make-bundles) @ dist ---
 3[INFO] Reading assembly descriptor: proj1-assembly.xml
 4[INFO] ------------------------------------------------------------------------
 5[INFO] Reactor Summary:
 6[INFO] 
 7[INFO] Packaging Test .................................... SUCCESS [0.947s]
 8[INFO] Packaging Test : Distribution ..................... FAILURE [0.470s]
 9[INFO] Packaging Test : Package-1 ........................ SKIPPED
10[INFO] Packaging Test : Package-2 ........................ SKIPPED
11[INFO] ------------------------------------------------------------------------
12[INFO] BUILD FAILURE
13[INFO] ------------------------------------------------------------------------
14[INFO] Total time: 1.632s
15[INFO] Finished at: Sat Sep 28 13:24:55 CEST 2013
16[INFO] Final Memory: 23M/981M
17[INFO] ------------------------------------------------------------------------
18[ERROR] Failed to execute goal org.apache.maven.plugins:maven-assembly-plugin:2.4:single (make-bundles) on project dist: Failed to create assembly: Error creating assembly archive dist-assembly: You must set at least one file. -> [Help 1]
19[ERROR] 
20[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
21[ERROR] Re-run Maven using the -X switch to enable full debug logging.
22[ERROR] 
23[ERROR] For more information about the errors and possible solutions, please read the following articles:
24[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
25[ERROR] 
26[ERROR] After correcting the problems, you can resume the build with the command
27[ERROR]   mvn <goals> -rf :dist

The question is: Why does this happen? The simple answer is: Maven can not calculate the reactor build order in the right manner. The root cause of this is that the dependencies between the modules are not defined at all. The solution of the problem can be simply achieved by defining the appropriate dependencies to the modules you would like to pack into your distribution package like this:

 1  <dependencies>
 2    <dependency>
 3      <groupId>${project.groupId}</groupId>
 4      <artifactId>package-one</artifactId>
 5      <version>${project.version}</version>
 6    </dependency>
 7    <dependency>
 8      <groupId>${project.groupId}</groupId>
 9      <artifactId>package-two</artifactId>
10      <version>${project.version}</version>
11    </dependency>
12  </dependencies>

With this change it does not matter in which order you have defined the modules in your parent pom. Maven will calculate the reactor build order automatically.

The Assembly Descriptor File Smell

If we remember back to the maven-assembly-plugin descriptor which looks like this:

 1  <fileSets>
 2    <fileSet>
 3      <directory>${basedir}/../package-1/target/</directory>
 4      <outputDirectory>/</outputDirectory>
 5      <includes>
 6        <include>*.jar</include>
 7      </includes>
 8    </fileSet>
 9    <fileSet>
10      <directory>${basedir}/../package-2/target/</directory>
11      <outputDirectory>/</outputDirectory>
12      <includes>
13        <include>*.jar</include>
14      </includes>
15    </fileSet>
16  </fileSets>

At the first glance this descriptor looks all right. But what happens if you have ten modules which should be packed into the distribution archive instead of two? The assembly descriptor would look like this:

 1  <fileSets>
 2    <fileSet>
 3      <directory>${basedir}/../package-1/target/</directory>
 4      <outputDirectory>/</outputDirectory>
 5      <includes>
 6        <include>*.jar</include>
 7      </includes>
 8    </fileSet>
 9    <fileSet>
10      <directory>${basedir}/../package-2/target/</directory>
11      <outputDirectory>/</outputDirectory>
12      <includes>
13        <include>*.jar</include>
14      </includes>
15    </fileSet>
16    <fileSet>
17      <directory>${basedir}/../package-3/target/</directory>
18      <outputDirectory>/</outputDirectory>
19      <includes>
20        <include>*.jar</include>
21      </includes>
22    </fileSet>
23    ...
24    .
25    .
26    <fileSet>
27      <directory>${basedir}/../package-10/target/</directory>
28      <outputDirectory>/</outputDirectory>
29      <includes>
30        <include>*.jar</include>
31      </includes>
32    </fileSet>
33  </fileSets>

As you can see this descriptor will become quite long and you should not forget to add every dependency into the pom file as well, cause if do not do this, the reactor order will not be as you expect and your build might fail. In other words you need to maintain two areas, the dependencies as well as the descriptor. The full example can be found here.

The solution of this problem is to remember that maven has a reactor which can be used for such purposes instead of the file level. This will simplify the descriptor dramatically like this:

 1<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" 
 2xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
 4
 5  <id>dist-assembly</id>
 6
 7  <formats>
 8      <format>zip</format>
 9  </formats>
10
11  <includeBaseDirectory>false</includeBaseDirectory>
12
13  <dependencySets>
14      <dependencySet>
15          <outputDirectory>/</outputDirectory>
16          <useProjectArtifact>false</useProjectArtifact>
17          <unpack>false</unpack>
18          <scope>runtime</scope>
19      </dependencySet>
20  </dependencySets>
21</assembly>

This simple descriptor will work independent of the number of modules you have. The only thing you need to do is to maintain the dependencies in your distribution pom.xml file correctly.