How to use Maven for complex build processes

Don’t.

…but if you have to and you want Apache Maven to make your life easier rather than bring you eternal pain, you might find these 8 tips and tricks useful.

Tip #1: Use profile activation conditions to do different things based on Operating System

In the below example on a mvn clean the message I will only run on Linux is ran on Linux but not on Mac.

<profiles>  
  <profile>
    <id>platform-unix</id>
    <activation>
      <os>
        <family>unix</family>
        <name>!mac os x</name>
        <build>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-antrun-plugin</artifactId>
              <version>1.7</version>
              <executions>
                <execution>
                  <id>clean-message</id>
                  <phase>clean</phase>
                  <goals>
                    <goal>run</goal>
                  </goals>
                  <configuration>
                    <target>
                      <echo message="I will only run on Linux"/>
                    </target>
                  </configuration>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </build>
      </os>
    </activation>
  </profile>
</profiles>  
Tip #2: Use maven-dependency-plugin if you need to download dependencies to custom locations in your project

In the below example, every dependency with a scope set of runtime will be copied to lib directory of the project except for the artifacts with the ID of coreTest and libTest.

<plugin>  
  <artifactId>maven-dependency-plugin</artifactId>
  <version>2.5.1</version>
  <executions>
    <execution>
      <id>copy-dependencies</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <includeScope>runtime</includeScope>
        <outputDirectory>${project.basedir}/lib</outputDirectory>
        <excludeArtifactIds>coreTest,libTest</excludeArtifactIds>
      </configuration>
    </execution>
  </executions>
</plugin>  
Tip #3: You don’t need to have Maven dependencies specified to use maven-dependency-plugin to pull to custom locations

In the below example, myProject.jar and agent-solaris.sh is copied to lib directory of the project.

<plugin>  
  <artifactId>maven-dependency-plugin</artifactId>
  <version>2.5.1</version>
  <executions>
    <execution>
      <id>copy-special-libraries</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>copy</goal>
      </goals>
      <configuration>
        <artifactItems>
          <artifactItem>
            <groupId>com.mycompany</groupId>
            <artifactId>myProject</artifactId>
            <version>1.0.0</version>
            <type>jar</type>
            <destFileName>myProject.jar</destFileName>
          </artifactItem>
          <artifactItem>
            <groupId>com.mycompany</groupId>
            <artifactId>agent</artifactId>
            <version>1.0.0</version>
            <type>bin</type>
            <classifier>solaris-x86_64</classifier>
            <destFileName>agent-solaris.sh</destFileName>
          </artifactItem>
        </artifactItems>
        <outputDirectory>${project.basedir}/lib</outputDirectory>
      </configuration>
    </execution>
<plugin>  
Tip #4: You can download all artifacts matching a given groupId and artifactId list and even include their pom.xml

In this example, it downloads the pom.xml and jar for my-magic-plugin to lib/pom but strips the version from the jar.

<plugin>  
  <artifactId>maven-dependency-plugin</artifactId>
  <version>2.5.1</version>
  <executions>
    <execution>
      <id>copy-special-libraries</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
      <configuration>
        <includeGroupIds>com.mycompany.myapp</includeGroupIds>
        <includeArtifactIds>my-magic-plugin</includeArtifactIds>
        <copyPom>true</copyPom>
        <includeScope>runtime</includeScope>
        <stripVersion>true</stripVersion>
        <outputDirectory>${project.basedir}/pom</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugins>  
Tip #5: You can download specific files out of a jar and copy them to a location

In this example, we download the META-INF/MANIFEST.MF out of the my-core-app artifact and put it in target/tmp directory

<plugin>  
  <artifactId>maven-dependency-plugin</artifactId>
  <version>2.5.1</version>
  <executions>
    <execution>
    <execution>
      <id>unpack-version</id>
      <phase>compile</phase>
      <goals>
        <goal>unpack</goal>
      </goals>
      <configuration>
        <artifactItems>
          <artifactItem>
            <groupId>com.myexample</groupId>
            <artifactId>my-core-app</artifactId>
            <version>${project.version}</version>
            <includes>META-INF/MANIFEST.MF</includes>
            <outputDirectory>
              ${project.build.directory/tmp
            </outputDirectory>
          </artifactItem>
        </artifactItems>
      </configuration>
    </execution>
  </executions>
</plugins>  
Tip #6: If you prefer Apache Ant over Apache Maven you can still use it from within Maven

In this example, we call Apache Ant from within Maven to do various things:

  • Delete lib/thirdparty directory and lib/generated.jar files as part of a mvn clean

  • Copy src/main/my-app and script.sh to the location defined by ${my.staging} before a Maven compilation

  • Create a zip artifact which contains various files and folders at target/<artifactId>-<version>.zip

  • Copy the staged MANIFEST.MF from the previous example to target/version.txt

<plugin>  
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-antrun-plugin</artifactId>
  <version>1.7</version>
  <executions>
    <execution>
      <id>clean-project</id>
      <phase>clean</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <target>
          <delete dir="${project.basedir}/lib/thirdparty" />
          <delete file="${project.basedir}/lib/generated.jar" />
        </target>
      </configuration>
    </execution>
    <execution>
      <id>create-staging-area</id>
      <phase>process-resources</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <target>
          <copy todir="${my.staging}">
            <fileset dir="${basedir}/src/main/my-app" />
          </copy>
          <copy file="${basedir}/scipt.sh" todir="${my.staging}" />
        </target>
      </configuration>
    </execution>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <target>
          <mkdir dir="${project.build.directory}" />
          <zip destfile="${project.build.directory}/${project.build.finalName}.zip">
            <fileset dir="${project.basedir}">
              <include name="lib/**" />
              <include name="my-app.cmd" />
              <include name="my-app" />
            </fileset>
            <fileset dir="${project.build.directory}">
              <include name="documentation/**" />
              <include name="xsd/**" />
              <include name="version.txt" />
            </fileset>
          </zip>
        </target>
      </configuration>
    </execution>
    <execution>
      <id>copy-versionnumber</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <target>
          <copy tofile="${project.build.directory}/version.txt">
            <fileset file="${project.build.directory}/tmp/META-INF/MANIFEST.MF" />
          </copy>
        </target>
      </configuration>
    </execution>
  </executions>
</plugin>  
Tip #7: You can attach any file to be included as an artifact in the built project

For the zip we created in the previous example, attach it to the generated Artifact.

<plugin>  
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <version>1.2</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>attach-artifact</goal>
      </goals>
      <configuration>
        <artifacts>
          <artifact>
           <file>       
           ${project.build.directory}/${project.build.finalName}.zip
           </file>
           <type>zip</type>
          </artifact>
        </artifacts>
      </configuration>
    </execution>
  </executions>
</plugin>  
Tip #8: If you want to simplify the install of your product, consider building for a packaging tool like RPM, NPM, Docker etc

In the example, below we are building an RPM for Red Hat Linux / Fedora / CentOS to allow for our CLI application to be easily installed with yum install <app name>.rpm or yum install <app name> if we publish to a Yum repository.

<plugin>  
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>rpm-maven-plugin</artifactId>
  <version>2.1-alpha-4</version>
  <executions>
    <execution>
      <id>attach-rpm</id>
      <goals>
        <goal>attached-rpm</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <license>All rights reserved</license>
    <group>My Company</group>
    <needarch>x86_64</needarch>
    <version>${project.version}</version>
    <name>a-cli-app</name>
    <vendor>My Company</vendor>
    <summary>My CLI appplication turns water into wine.</summary>
    <description>Through an effective propaganda campaign we will make you believe that water = wine all with a single metaphorical push button on your Linux terminal</description>
    <defineStatements>
      <defineStatement>__os_install_post %{nil}</defineStatement>
    </defineStatements>
    <mappings>
      <mapping>
        <directory>${app.home}/lib</directory>
        <filemode>755</filemode>
        <username>cliuser</username>
        <groupname>cligroup</groupname>
        <sources>
          <source>
            <location>${project.basedir}/lib</location>
          </source>
        </sources>
      </mapping>
      <mapping>
        <directory>${app.home}</directory>
        <filemode>755</filemode>
        <username>cliuser</username>
        <groupname>cligroup</groupname>
        <sources>
          <source>
            <location>${project.basedir}/cli-app</location>
          </source>
        </sources>
      </mapping>
      <mapping>
        <directory>${app.home}</directory>
        <filemode>755</filemode>
        <username>cliuser</username>
        <groupname>cligroup</groupname>
      </mapping>
    </mappings>
  </configuration>
</plugin>‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍  

Hope you found this useful! As a final remark, I want to be clear that what is complex in Maven is generally simple in other build tools so you should consider if you have the right tool for the job.

If you use Gradle, Docker or even Make to build and package your source code into some meaningful releasable artifact you will likely have less road blocks along the way but then again, if you are a pure Java shop using Maven to build Java EARs, WARs or JARs, frankly I think there is no good reason for you to not use Maven. Or is there? Leave a comment if you want and Good luck!

Craig Barr

I am a Software Engineer with a decade of experience empowering Enterprises in Banking, Logistics, Manufacturing with Service-Oriented Architecture, Microservices and Cloud Computing.

Brisbane, Australia https://twitter.com/craigbarrau