I’ve had a long outstanding requirement to add a windows installer to one of our products. Ideally this should be a standard .msi file, so that it can be easily managed remotely. My own requirement was that this must integrate into our maven2 build system, ideally without requring any extra installation other than by the standard maven dependency resolution mechanisms. I was also interested in maven2 plugins that could generate both .msi files and linux graphical installer applications (for the poor users). My initial investigations found — as always — many half-written and completely unusable maven plugins. But it’s easy enough to run commands through the mvn antrun plugin. After much investigation, I came across the open source Windows Installer XML toolkit. This is an open-source microsoft application. It does exactly what it is supposed to - creates .msi files from xml files. The XML format is sadly poorly documented, with the best example being a tutorial which is far too verbose for it’s own good. It also lacks what I consider basic functionality, such as performing simple string replacements in script files as they are installed. Anyway, we now have a working .msi compiler. This is performed in the build script with the following statements:

  1. We start by modifying the xml file, to include the correct version. This is defined as a property in the POM, rather than using the standard version property, since this must be in the form major.minor.build (no -SNAPSHOT allowed!).
    <replaceregexp file="src/assembly/installer.wxs\" match="Codepage=&quot;1252&quot; Version=&quot;.*&quot;>" replace="Codepage=&quot;1252&quot; Version=&quot;${windowsVersion}&quot;>" byline="true" />
  2. An execution of maven-dependency-plugin copies all the dependant jars to a target location (maven-jar-plugin is also configured to output here):
    <execution> <id>copy-dependencies</id> <phase>process-resources</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory> ${project.build.directory}/installer/lib </outputDirectory> <overWriteReleases>false</overWriteReleases> <overWriteSnapshots>false</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> <includeScope>runtime</includeScope> <silent>true</silent> </configuration> </execution>
  3. Next, we run the heat.exe tool. This is a pre-processor which generates a fragment wix xml file containing details of all the files we want to install (i.e. all the dependencies). We could have alternatively hand-crafted this code, but that would be far more susceptible to problems when the code is updated:
    <exec dir=\"target\" executable=\"heat.exe\" failonerror=\"true\"> <arg line=\"dir installer -sfrag -gg -out components.wxs -template:fragment\" /> </exec>
  4. The current version of heat generates an invalid xml fragment - it defines an invalid top-level directory, when it should just have a directory ref to the installdir directory. A simple regexp fixes this:
    <replaceregexp file="target/components.wxs" match="&lt;Directory Id=&quot;installer&quot;.*&gt;" replace="&lt;DirectoryRef Id=&quot;INSTALLDIR&quot;&gt;" byline="true" /> <replaceregexp file="target/components.wxs" match="^ &lt;\/Directory&gt;" replace="&lt;\/DirectoryRef&gt;" byline="true" />
  5. The XML is then ‘compiled’ into an intermediate form:
    <exec dir="target" executable="candle.exe" failonerror="true"> <arg line="..\src\assembly\installer.wxs components.wxs -ext WixUIExtension" /> </exec>
  6. The final .msi installer is generated from these intermediate files (like a linker):
    <exec dir="target" executable="${light}" failonerror="true"> <arg line="-ext WixUIExtension -cultures:en-us installer.wixobj components.wixobj -out ${project.name}-${project.version}.msi" /> </exec>
  7. As a final safety-net, we run the automatic msi testing tool. I’m not convinced that this does very much, but it’s cheap to include:
    <exec dir="target" executable="${smoke}" failonerror="true"> <arg line="radiusjobmanager-${project.version}.msi" /> </exec>