As mentioned above, the Assembly plugin provides multiple ways of
creating many archive formats. Distribution archives are typically very
good examples of this, since they often combine modules from a
multi-module build, along with their dependencies and possibly, other
files and artifacts besides these. The distribution aims to include all
these different sources into a single archive that the user can
download, unpack, and run with convenience. However, we also examined
some of the potential drawbacks of using the
moduleSets
section of the assembly descriptor -
namely, that the parent-child relationships between
POMs in a build can prevent the availability of
module artifacts in some cases.
Specifically, if module POMs reference as their parent the POM that contains the Assembly-plugin configuration, that parent project will be built ahead of the module projects when the multi-module build executes. The parent’s assembly expects to find artifacts in place for its modules, but these module projects are waiting on the parent itself to finish building, a gridlock situation is reached and the parent build cannot succeed (since it’s unable to find artifacts for its module projects). In other words, the child project depends on the parent project which in turn depends on the child project.
As an example, consider the assembly descriptor below, designed to be used from the top-level project of a multi-module hierarchy:
<assembly> <id>distribution</id> <formats> <format>zip</format> <format>tar.gz</format> <format>tar.bz2</format> </formats> <moduleSets> <moduleSet> <includes> <include>*-web</include> </includes> <binaries> <outputDirectory>/</outputDirectory> <unpack>true</unpack> <includeDependencies>true</includeDependencies> <dependencySets> <dependencySet> <outputDirectory>/WEB-INF/lib</outputDirectory> </dependencySet> </dependencySets> </binaries> </moduleSet> <moduleSet> <includes> <include>*-addons</include> </includes> <binaries> <outputDirectory>/WEB-INF/lib</outputDirectory> <includeDependencies>true</includeDependencies> <dependencySets> <dependencySet/> </dependencySets> </binaries> </moduleSet> </moduleSets> </assembly>
Given a parent project - called app-parent - with three modules
called app-core
, app-web
, and
app-addons
, notice what happens when we try to
execute this multi-module build:
$ mvn package
[INFO] Reactor build order:
[INFO] app-parent <----- PARENT BUILDS FIRST
[INFO] app-core
[INFO] app-web
[INFO] app-addons
[INFO] ---------------------------------------------------------------
[INFO] Building app-parent
[INFO] task-segment: [package]
[INFO] ---------------------------------------------------------------
[INFO] [site:attach-descriptor]
[INFO] [assembly:single {execution: distro}]
[INFO] Reading assembly descriptor: src/main/assembly/distro.xml
[INFO] ---------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ---------------------------------------------------------------
[INFO] Failed to create assembly: Artifact:
org.sonatype.mavenbook.assemblies:app-web:jar:1.0-SNAPSHOT (included by module) does not have \
an artifact with a file. Please ensure the package phase is run before the assembly is \
generated.
...
The parent project - app-parent
- builds first.
This is because each of the other projects lists that
POM as its parent, which causes it to be forced to
the front of the build order. The app-web
module,
which is the first module to be processed in the assembly descriptor,
hasn’t been built yet. Therefore, it has no artifact associated with it,
and the assembly cannot succeed.
One workaround for this is to remove the executions section of the
Assembly-plugin declaration, that binds the plugin to the
package
lifecycle phase in the parent
POM, keeping the configuration section intact. Then,
execute Maven with two command-line tasks: the first,
package
, to build the multi-module project graph, and
a second, assembly:assembly
, as a direct invocation
of the assembly plugin to consume the artifacts built on the previous
run, and create the distribution assembly. The command line for such a
build might look like this:
$ mvn package assembly:assembly
However, this approach has several drawbacks. First, it makes the distribution-assembly process more of a manual task that can increase the complexity and potential for error in the overall build process significantly. Additionally, it could mean that attached artifacts - which are associated in memory as the project build executes - are not reachable on the second pass without resorting to file-system references.
Instead of using a moduleSet
to collect the
artifacts from your multi-module build, it often makes more sense to
employ a low-tech approach: using a dedicated distribution project
module and inter-project dependencies. In this approach, you create a
new module in your build whose sole purpose is to assemble the
distribution. This module POM contains dependency
references to all the other modules in the project hierarchy, and it
configures the Assembly plugin to be bound the
package
phase of its build lifecycle. The assembly
descriptor itself uses the dependencySets
section
instead of the moduleSets
section to collect module
artifacts and determine where to include them in the resulting assembly
archive. This approach escapes the pitfalls associated with the
parent-child relationship discussed above, and has the additional
advantage of using a simpler configuration section within the assembly
descriptor itself to do the job.
To do this, we can create a new project structure that’s very
similar to the one used for the module-set approach above, with the
addition of a new distribution project, we might end up with five
POMs in total: app-parent
,
app-core
, app-web
,
app-addons
, and app-distribution
.
The new app-distribution
POM looks
similar to the following:
<project> <parent> <artifactId>app-parent</artifactId> <groupId>org.sonatype.mavenbook.assemblies</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>app-distribution</artifactId> <name>app-distribution</name> <dependencies> <dependency> <artifactId>app-web</artifactId> <groupId>org.sonatype.mavenbook.assemblies</groupId> <version>1.0-SNAPSHOT</version> <type>war</type> </dependency> <dependency> <artifactId>app-addons</artifactId> <groupId>org.sonatype.mavenbook.assemblies</groupId> <version>1.0-SNAPSHOT</version> </dependency> <!-- Not necessary since it's brought in via app-web. <dependency> [2] <artifactId>app-core</artifactId> <groupId>org.sonatype.mavenbook.assemblies</groupId> <version>1.0-SNAPSHOT</version> </dependency> --> </dependencies> </project>
Notice that we have to include dependencies for the other modules
in the project structure, since we don’t have a modules section to rely
on in this POM. Also, notice that we’re not using an
explicit dependency on app-core
. Since it’s also a
dependency of app-web
, we don’t need to process it
(or, avoid processing it) twice.
Next, when we move the distro.xml
assembly
descriptor into the app-distribution
project, we must
also change it to use a dependencySets
section, like
this:
<assembly> ... <dependencySets> <dependencySet> <includes> <include>*-web</include> </includes> <useTransitiveDependencies>false</useTransitiveDependencies> <outputDirectory>/</outputDirectory> <unpack>true</unpack> </dependencySet> <dependencySet> <excludes> <exclude>*-web</exclude> </excludes> <useProjectArtifact>false</useProjectArtifact> <outputDirectory>/WEB-INF/lib</outputDirectory> </dependencySet> </dependencySets> ... </assembly>
This time, if we run the build from the top-level project directory, we get better news:
$ mvn package
(...)
[INFO] ---------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ---------------------------------------------------------------
[INFO] module-set-distro-parent ...............SUCCESS [3.070s]
[INFO] app-core .............................. SUCCESS [2.970s]
[INFO] app-web ............................... SUCCESS [1.424s]
[INFO] app-addons ............................ SUCCESS [0.543s]
[INFO] app-distribution ...................... SUCCESS [2.603s]
[INFO] ---------------------------------------------------------------
[INFO] ---------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ---------------------------------------------------------------
[INFO] Total time: 10 seconds
[INFO] Finished at: Thu May 01 18:00:09 EDT 2008
[INFO] Final Memory: 16M/29M
[INFO] ---------------------------------------------------------------
As you can see, the dependency-set approach is much more stable and - at least until Maven’s internal project-sorting logic catches up with the Assembly plugin’s capabilities, - involves less opportunity to get things wrong when running a build.