Alright, now for how we actually automate the installer build! Everything mentioned here lives in the ‘installer’ profile. This allows your developers to still run the package command without invoking a lengthy installer build.
Required Properties
There is a set of required properties that can be set in your application’s pom or some parent pom.
- installer.product: The name of your application. This is what will appear in add/remove programs, etc.
- installer.productCode: A guid to use as a product code only when building locally on a developer machine. Jenkins will inject a random product code on each build.
- installer.upgradeCode: The guid upgrade code for your application. This is what uniquely identifies your application across all versions. It should never change.
- installer.upgradeableWixFiles: A comma-delimited list of WiX files names that the build process will run paraffin on before creating the installer. It is assumed all of these files live in a standard directory.
- installer.wixFiles: A comma-delimited list of WiX files that the build process will not run paraffin on but will still include in the installer.
- installer.manufacturer: Your company, included in add/remove programs, etc.
- environment: The default environment to build, we use test.
- installer.windows_installer_version: The minimum required Windows Installer version, depends on what features you use.
So now that we’ve set our required properties, let’s get into the required plugins.
Required Plugins
This plugin creates a timestamp build number that will be appended to the actual pom version of our application.
<execution>
<phase>package</phase>
<goals>
<goal>create</goal>
</goals>
</execution>
<configuration>
<format>{0, date,yyDDD.HHmm}</format>
<items>
<item>timestamp</item>
</items>
</configuration>
The build helper plugin let’s parse out the major and minor versions of our application.
<execution>
<id>parse-version</id>
<phase>package</phase>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
The assembly plugin will assemble our application. We keep our configuration properties for injection in standard folders, an “-all” file for all environments and another file for each environment we deploy to. The assembly descriptor for each application is also kept in a standard location.
<configuration>
<filters>
<filter>${basedir}/src/main/assembly/configuration-all.properties</filter>
<filter>${basedir}/src/main/assembly/configuration-${environment}.properties</filter>
</filters>
<descriptors>
<descriptor>${basedir}/src/main/assembly/assembly.xml</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
The resources plugin copies all of our WiX files after filtering them. As a convention, all files under the ‘auto’ folder will have paraffin run on them before being build. The ‘manual’ folder is for WiX files that will fairly infrequently.
<executions>
<execution>
<id>copy-wix-files</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<escapeWindowsPaths>false</escapeWindowsPaths>
<outputDirectory>${project.build.directory}/installer_temp</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/wix/wxs/auto</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>${basedir}/src/main/wix/wxs/manual</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
This is the really cool part. We use a groovy script to call the WiX binaries (candle, light, paraffin) and build the installer. This is where we use the WiX file listings we provided as properties.
This is the execution of the plugin:
<execution>
<id>groovy-magic</id>
<phase>package</phase>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<![CDATA[ source code below ]]>
</configuration>
</execution>
And now the actual groovy script. Sorry if it’s rough, it’s my first groovy script…
def updateableFiles = project.properties['installer.updateableWixFiles'].split(",")
def ant = new AntBuilder()
for ( file in updateableFiles ) {
log.info("running paraffin on " + file)
ant.exec(failonerror: "true",
executable: "paraffin",
dir: project.build.directory + "\\installer_temp") {
arg(line:"-update " + file )
}
def fileToMove = file.substring(0, (file.lastIndexOf('.') + 1)) + "PARAFFIN"
log.info("moving " + fileToMove + " to " + file)
ant.copy(file: project.build.directory + "\\installer_temp\\" + fileToMove,
tofile: project.build.directory + "\\installer_temp\\" + file,
overwrite: 'true')
def templateFile = file.substring(0, (file.lastIndexOf('.'))) + "Template.xsl"
templateFile = pom.basedir.getAbsolutePath() + "\\src\\main\\wix\\xsl\\" + templateFile
if ( new File(templateFile).exists()) {
log.info("running msxsl on " + file + " with " + templateFile)
ant.exec(failonerror: "true",
executable: "msxsl",
dir: project.build.directory + "\\installer_temp") {
arg(line: "\"" + file + "\" \"" + templateFile +"\" -o \"" + project.build.directory + "\\installer_temp\\" + file + "\"")
}
} else {
log.info("template file for " + file + " at " + templateFile + " did not exist")
}
}
def filesToCandle = project.properties['installer.wixFiles'].replace(',',' ') + " ";
filesToCandle += project.properties['installer.updateableWixFiles'].replace(',',' ')
log.info("running candle on " + filesToCandle);
ant.exec(failonerror: "true",
executable: "candle",
dir: project.build.directory + "\\installer_temp") {
arg(line: filesToCandle)
}
def filesToLight = ""
for ( file in filesToCandle.split(" ")) {
filesToLight += "\"" + project.build.directory + "\\installer_temp\\" + file.substring(0, (file.lastIndexOf('.') + 1)) + "wixobj\" "
}
log.info("running light on " + filesToLight)
ant.exec(failonerror: "true",
executable: "light",
dir: project.build.directory + "\\" + project.build.finalName + "\\" + project.build.finalName) {
arg(line: "-out \"" + project.build.directory + "\\installer\\" +
project.properties['installer.product'] + "-" +
project.properties["environment"] + "-" +
project.properties["parsedVersion.majorVersion"] + "." +
project.properties["parsedVersion.minorVersion"] + "." +
project.properties["buildNumber"] + ".msi\" " + filesToLight)
}
The only part that probably isn’t clear is the template file. For each WiX source file you can include, by convention, an xsl file that when applied will turn various Windows Installer features on or off. We use it make sure DiskIds and KeyPaths are set. Here is a sample xsl file we use on our lib folders:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wi="http://schemas.microsoft.com/wix/2006/wi"
exclude-result-prefixes="xsl wi">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node() | @*" />
</xsl:copy>
</xsl:template>
<xsl:template match="wi:Component">
<Component xmlns="http://schemas.microsoft.com/wix/2006/wi">
<xsl:attribute name="Id"><xsl:value-of select="@Id" /></xsl:attribute>
<xsl:attribute name="Guid"><xsl:value-of select="@Guid" /></xsl:attribute>
<xsl:attribute name="KeyPath">no</xsl:attribute>
<xsl:attribute name="UninstallWhenSuperseded">yes</xsl:attribute>
<!-- <xsl:attribute name="DiskId"><xsl:value-of select="@DiskId" /></xsl:attribute> -->
<xsl:attribute name="DiskId">1</xsl:attribute>
<xsl:apply-templates select="*|node()" />
</Component>
</xsl:template>
<xsl:template match="wi:File">
<File xmlns="http://schemas.microsoft.com/wix/2006/wi">
<xsl:attribute name="Id"><xsl:value-of select="@Id" /></xsl:attribute>
<xsl:attribute name="KeyPath">yes</xsl:attribute>
<xsl:attribute name="Source"><xsl:value-of select="@Source" /></xsl:attribute>
</File>
</xsl:template>
</xsl:stylesheet>
My next post will be the last one in this series. It’ll show how Jenkins is set up to build all of our installers!