c a n d l a n d . n e t

Using nant and tallow to create usable WIX component.

Dusty Candland | |
    <p>Creating installer for web application can be a pain, but using tallow and an nant script element can make things a lot easier.</p>
    <p>Once my nant script was building my solution and web project with all the needed output files I begin to setup automating the build of the WIX component for the web application.</p>
    <p>As you know tallow output is not usable as is, some custom handling needs to be done. I've found the script element of nant to work well for this. I've also created a new class library project to keep the script block small, and keep the source in a project. I've included the class library in the solution build. </p>
    <p>Nant target:</p>
    <!-- code formatted by http://manoli.net/csharpformat/ -->
    <pre class="csharpcode">&lt;property name="wix.websitefragment.file" value="${build.output.temp}\website-fragment.wsx"/&gt;<br />&lt;target name="buildwebsitewsx" depends="solution"&gt;<br />    &lt;mkdir dir="${build.output.temp}"/&gt;<br />    &lt;exec program="${wix.bin}\tallow.exe" workingdir="${build.output.temp}" output="${wix.websitefragment.file}"&gt;<br />        &lt;arg value="-d &amp;quot;${compile.output.webapplication}&amp;quot;"/&gt;<br />        &lt;arg value="-nologo"/&gt;<br />    &lt;/exec&gt;<br />    &lt;script language="C#" verbose="true"&gt;<br />        &lt;references&gt;<br />            &lt;include name="${compile.output}\InstallHelper.dll"/&gt;<br />        &lt;/references&gt;<br />        &lt;imports&gt;<br />            &lt;import namespace="InstallHelper"/&gt;<br />        &lt;/imports&gt;<br />        &lt;code&gt;<br />        &lt;![CDATA[<br />        public static void ScriptMain(Project project) {<br />            string file = project.Properties["wix.websitefragment.file"];<br />            new FixTallow(file, "My.ComponentGroup.Name").UpdateFile();<br />        }<br />        ]]&gt;<br />        &lt;/code&gt;<br />    &lt;/script&gt;<br />&lt;/target&gt;<span class="kwrd"></span></pre>
    This calls the tallow application on the directory where the web application is built. Then loads and calls the InstallHelper.dll to modify the tallow output into something more usable. <br /><p>The FixTallow class loads the tallow output into an XML document and takes the following actions; adds a component group, creates guids for the components, renames the directory id's and changes the directoryRef element to point to the INSTALLDIR.</p><p>The component group needs to be created so a reference can be added to the main WIX file. This is because tallow creates a component for each directory (or each file) and because I don't want to update the main WIX file every time there is a directory added or removed from the web application. The group name is passed the the constructor of the FixTallow class. </p><p>FixTallow also renames the directory id's so that I can reference them if needed from the main WIX file, however if the directory is removed or renamed, the main WIX file will need to be updated. Also, the root DirectoryRef element is changed to reference INSTALLDIR which needs to be set in the main WIX file. This is a good idea as it allows the installation directory to be changed when installing.</p><p>FixTallow.cs</p><pre class="csharpcode"><span class="kwrd">    public</span><span class="kwrd">class</span> FixTallow<br />    {<br /><span class="kwrd">  private</span><span class="kwrd">readonly</span><span class="kwrd">string</span> _filename;<br /><span class="kwrd">    private</span><span class="kwrd">readonly</span><span class="kwrd">string</span> _componentGroupName;<br />        XmlDocument _document = <span class="kwrd">new</span> XmlDocument();<br />        XmlNamespaceManager _manager;<br />        XmlElement _compGroup;<br /><br /><span class="kwrd">   public</span> FixTallow(<span class="kwrd">string</span> filename, <span class="kwrd">string</span> componentGroupName)<br />        {<br />            _filename = filename;<br />            _componentGroupName = componentGroupName;<br /><br />            _manager = <span class="kwrd">new</span> XmlNamespaceManager(_document.NameTable);<br />            _manager.AddNamespace(<span class="kwrd">string</span>.Empty, <span class="str">"http://schemas.microsoft.com/wix/2003/01/wi"</span>);<br />            _manager.AddNamespace(<span class="str">"ns"</span>, <span class="str">"http://schemas.microsoft.com/wix/2003/01/wi"</span>);<br />        }<br /><br /><span class="kwrd"> public</span> FixTallow UpdateFile()<br />        {<br />            _document.Load(_filename);<br /><br />            AppendComponentGroupElement();<br /><br /><span class="kwrd">        foreach</span> (XmlElement element <span class="kwrd">in</span> _document.SelectNodes(<span class="str">"//ns:Component"</span>, _manager))<br />            {<br />                element.Attributes[<span class="str">"Guid"</span>].Value = Guid.NewGuid().ToString().ToUpper();<br />                AppendComponentToGroup(element.Attributes[<span class="str">"Id"</span>].Value);<br />            }<br /><br />            ChangeDirectoryRefId();<br /><br />            UpdateDirectoryIds();<br /><br />            _document.Save(_filename);<br /><br /><span class="kwrd">      return</span><span class="kwrd">this</span>;<br />        }<br /><br /><span class="kwrd">        private</span><span class="kwrd">void</span> UpdateDirectoryIds()<br />        {<br /><span class="kwrd">            foreach</span> (XmlElement directory <span class="kwrd">in</span> _document.SelectNodes(<span class="str">"//ns:Directory"</span>, _manager))<br />            {<br /><span class="kwrd">                string</span> directoryName = (directory.Attributes[<span class="str">"LongName"</span>] == <span class="kwrd">null</span>)<br />                                           ?<br />                                       directory.Attributes[<span class="str">"Name"</span>].Value<br />                                           :<br />                                       directory.Attributes[<span class="str">"LongName"</span>].Value;<br />                directory.Attributes[<span class="str">"Id"</span>].Value = directoryName + <span class="str">".Dir"</span>;<br />            }<br />        }<br /><br /><span class="kwrd">        private</span><span class="kwrd">void</span> ChangeDirectoryRefId()<br />        {<br />            _document.SelectSingleNode(<span class="str">"/ns:Wix/ns:Fragment/ns:DirectoryRef/@Id"</span>, _manager).Value = <span class="str">"INSTALLDIR"</span>;<br />        }<br /><br /><span class="kwrd">        private</span><span class="kwrd">void</span> AppendComponentToGroup(<span class="kwrd">string</span> componentId)<br />        {<br />            XmlElement componentRef = _document.CreateElement(<span class="str">"ComponentRef"</span>, _manager.DefaultNamespace);<br />            XmlAttribute componentRefId = _document.CreateAttribute(<span class="str">"Id"</span>);<br />            componentRefId.Value = componentId;<br />            componentRef.Attributes.Append(componentRefId);<br />            _compGroup.AppendChild(componentRef);<br />        }<br /><br /><span class="kwrd">        private</span><span class="kwrd">void</span> AppendComponentGroupElement()<br />        {<br />            _compGroup = _document.CreateElement(<span class="str">"ComponentGroup"</span>, _manager.DefaultNamespace);<br />            XmlAttribute compGroupId = _document.CreateAttribute(<span class="str">"Id"</span>);<br />            compGroupId.Value = _componentGroupName;<br />            _compGroup.Attributes.Append(compGroupId);<br />            _document.SelectSingleNode(<span class="str">"/ns:Wix/ns:Fragment"</span>, _manager).AppendChild(_compGroup);<br />        }<br />    }</pre><p>The main WIX file needs to include a few things for this to work. First, it much have a diretory element with INSTALLDIR as it's Id. Second, a ComponentGroupRef element pointing the component group name specified in the FixTallow constructor needs to be included in the Feature element.</p><pre class="csharpcode">...<br /><span class="kwrd">&lt;</span><span class="html">Directory</span><span class="attr">Id</span><span class="kwrd">="TARGETDIR"</span><span class="attr">Name</span><span class="kwrd">="SourceDir"</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;</span><span class="html">Directory</span><span class="attr">Id</span><span class="kwrd">="ProgramFilesFolder"</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;</span><span class="html">Directory</span><span class="attr">Id</span><span class="kwrd">="INSTALLDIR"</span><span class="attr">Name</span><span class="kwrd">="myapp"</span><span class="attr">LongName</span><span class="kwrd">="MyApplicationName"</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;/</span><span class="html">Directory</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;/</span><span class="html">Directory</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;/</span><span class="html">Directory</span><span class="kwrd">&gt;</span><br />...<br /><span class="kwrd">&lt;</span><span class="html">Feature</span><span class="attr">Id</span><span class="kwrd">="Complete"</span><span class="attr">Level</span><span class="kwrd">="1"</span><span class="kwrd">&gt;</span><br /><span class="kwrd">&lt;</span><span class="html">ComponentGroupRef</span><span class="attr">Id</span><span class="kwrd">="My.ComponentGroup.Name"</span><span class="kwrd">/&gt;</span><br /><span class="kwrd">&lt;/</span><span class="html">Feature</span><span class="kwrd">&gt;</span><br />...</pre>Lastly, the tallow output needs to be passed to the candle and light applications when building the msi file with something like this:<pre class="csharpcode"><span class="kwrd">&lt;</span><span class="html">exec</span><br /><span class="attr">program</span><span class="kwrd">="${wix.bin}\candle.exe"</span><br /><span class="attr">workingdir</span><span class="kwrd">="${build.output.install}"</span><br /><span class="attr">commandline</span><span class="kwrd">="Main.wsx website-fragment.wsx"</span><span class="kwrd">/&gt;</span><br />...<br /><span class="kwrd">&lt;</span><span class="html">exec</span><br /><span class="attr">program</span><span class="kwrd">="${wix.bin}\light.exe"</span><br /><span class="attr">workingdir</span><span class="kwrd">="${build.output.install}"</span><br /><span class="attr">commandline</span><span class="kwrd">="-out MyApplication.msi Main.wixobj website-fragment.wixobj"</span><span class="kwrd">/&gt;</span></pre><span class="kwrd">There was a lot of setup here, but once completed changes can be made to the web application without modifying the WIX files, resulting in an up to date MSI on each build.</span><br /><div style="clear: both; padding-bottom: 0.25em;"></div>

Webmentions

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: