MSI is one of those technologies that has stood the test of time, and WiX is an increasingly popular way of creating these installer databases. There’s a perception that WiX is convoluted, complex and difficult to debug. Actually WiX really isn’t that daunting; you just need to understand that WiX has its own way of doing things, and there are some excellent practitioner resources on the web to help you -- for example http://wix.tramontana.co.hu.
My purpose today is slightly different. First of all, I want to concentrate not just on how you might produce an MSI for your Visual Studio solution, but why you might want to do it this particular way rather than some other way. You will need to do some plumbing to get Visual Studio, MSBuild and WiX all talking the same language, and you will also need to make some decisions about the way you harvest resources. Secondly, I want to show you exactly how you can fit the resultant MSIs into an Agile Build/Deployment process. And finally I want to look at a few real world problems and additional requirements that are likely to come up, and show you some ways to address them that worked for me.
What I’m not going to cover is the specific configuration of websites, virtual directories, Registry hives, etc. There are plenty of resources out there showing you how to do this, so all we will configure is the file system. I’m also going to overlook any kind of UI, since my installers typically need to be pushed out without any user intervention. What I will cover in extra depth though is how you can set up your installers to deploy environment-specific resources. This is a subject in itself, and something I’ll probably return to in a dedicated article.
Note: In this walkthrough I used Visual Studio 2012 and WiX 3.7, but my examples should work equally well with Visual Studio 2010 or 2008 and WiX 3.5 onward.
Adding Installer capabilities to an existing solution
It should come as no surprise that while it makes perfect sense to create installer solutions in tandem with application development, usually the requirement for a packaging and deployment mechanism comes far later. And if packaging and deployment is suddenly a high priority requirement, chances are there have been a few high-profile deployment failures of late. This makes for an awkward situation in which the need for process change is generally recognised, but no one wants to risk making things worse. In this context, the easiest and safest way to introduce automated packaging into a solution is via a custom build configuration. This means:
- Developers can continue working with their local configurations as you refine your packaging scripts
- Developers not interested in the setup solutions are not obligated to install the package-related tooling (eg. WiX 3.5)
- Application build times are not adversely impacted by the addition of packaging projects (WiX projects tend to add quite a bit of overhead, particularly when there are a lot of files to link)
So let’s start by creating something minimal for us to package.
- Open Visual Studio and create a new solution/C# Console Application called “MyApplication”
- Add a reference to System.Configuration
- Edit the app.config file and paste in the following beneath the <configuration> node:
<appSettings>
<add key="myConfiguration" value="Development"/>
</appSettings>
- Open Program.cs and add the following line to your Main method:
Console.WriteLine(System.Configuration.ConfigurationManager.AppSettings["myConfiguration"]);
Votive versus Visual Studio + WiX
Though WiX integrates with Visual Studio in the form of an add-in called “Votive”, there’s nothing to stop you using the two technologies together outside of Votive. Why might you want to do that? Well, Votive provides a lot of wiring behind the scenes, and while this is usually exactly what you want, there are times when you may want to override its behaviour. One of the things Votive does is enumerate the set of WiX files inside your installer project and ensure they all get passed to candle.exe and light.exe (WiX’s compilation and linker tools respectively). But what about dynamically generated resources? It’s too late trying to enumerate these at compile time to inject back into the project file as dependencies, because Votive has already created its list of files to process. In this case, you can get around the problem by dispensing with Votive entirely and perform all your packaging in a custom MSBuild script. But I think the better approach is to stick with Votive and instead add placeholders for any dynamic resources. These placeholders obviously need to contain valid WiX, but they will get overwritten with dynamic content when you build the project. As far as Votive is concerned, everything’s still fine; the list of files to compile and link hasn’t changed; the one gotcha is to make sure the placeholder files are editable (by default they probably won’t be when you pull them out of your Source control management system); otherwise their content won’t get updated.
Adding the Packaging Project
- Add a WiX Setup project called MyApplicationInstaller.
- Open the Product.wxs file and edit the “Manufacturer” attribute (which initially will be blank), putting in something appropriate.
- Right click "MyApplicationInstaller" , select "Project Dependencies" and check the "MyApplication" project check box. This ensures the installer project always packages the latest source
If you now try “Rebuild All” you will get an ICE71 warning, “The Media table has no entries.” That’s as expected, because while our project contains valid WiX resources, the MSI they’ve built doesn’t actually have anything in it.
Adding a custom Packaging configuration
- Right click on your solution, select “Properties”, then “Configuration Properties”, and click the “Configuration Manager” button.
- In the “Active solution configuration” drop down, select “<New>.” Call the new configuration “Package”, and elect to copy settings from the existing Release configuration.
- Also ensure “Create new project configurations” is checked, then click “OK.” When prompted whether you wish to save changes to the configuration, say yes. Finally, close Configuration Manager.
- In the Solution Property Pages dialog, select the “Debug” configuration for “MyApplicationInstaller”, uncheck “Build”, then click “Apply.”
- Repeat for the “Release” configuration. You have now excluded the packaging project from standard Debug/Release configurations.
At this point, if you were working collaboratively with other developers, you would check in your changes to the solution and project files, along with the new packaging project. Because you've chosen to restrict packaging activities to the "Package" configuration, they can continue working with their Debug/Release configurations without being affected by any changes you make to the packaging project. More importantly, they don't need to install WiX themselves -- the WiX projects will simply show as unavailable if they don't have WiX installed.
Harvesting with complex rules: the staging scaffold
At this point we can start adding files to be included in the MSI. If you know exactly which files need to be included, you could write this code manually, but that's tedious. Instead, you will want to use a file harvester of some kind. WiX ships with a tool called "heat", and that'll do just fine.
Run heat.exe from the command line, and you'll notice that among its many configuration options there are are various flags to change the way it processes the file system -- for example, to retain empty directories, or to override SourceDir with some other variable. All these flags exist because people need to configure how their files and folders get harvested at a highly granular level. In this respect, you can make things easier for yourself by staging your assets prior to packaging.
What do I mean by staging? I mean creating a temporary folder structure akin to the one you want to be installed. Include the files you want, exclude the ones you don't. Copy in additional resources and configuration files you might need from elsewhere. We'll add staging logic to our WiX project as follows:
- Add a BeforeBuild hook
- Remove the Debug and Release configurations
- Add a "Package" target
- Stage the MyApplication files that interest us (and exclude the ones that don't)
- Clean the staging directory
- Harvest the contents of the staging directory
To do this:
- Add a new WiX file to the project, calling it "HarvestedMyApplication.wxs." We'll use this to store the set of files harvested from MyApplication. Leave the file wth its default content because it will be overwritten each time we build the project.
- Open MyApplicationInstaller.wixproj in a text editor (the easiest way to do this is to right click the project, select "Unload project", then right click it again and select "Edit MyApplicationInstaller.wixproj"
- Change OutputName to MyApplication, rather than MyApplicationInstaller
- Remove the two PropertyGroup sections conditioned on the Debug|x86 and Release|x86 build configurations
- Delete everything after the line:
<Import Project="$(WixTargetsPath)" />
Replace it with:
<Target Name="BeforeBuild" DependsOnTargets="ExtractMyApplicationStagingFolder;PackageMyApplication" />
<Target Name="ExtractMyApplicationStagingFolder">
<!-- Convert the semicolon delimited list of key/vaue pairs into an ItemGroup -->
<ItemGroup>
<DefineConstantsKVPairs Include="$(DefineConstants)" />
</ItemGroup>
<!-- Evaluate each key/value pair with task batching, then make a conditional assignment -->
<PropertyGroup>
<MyApplicationStagingFolder Condition="$([System.String]::new('%(DefineConstantsKVPairs.Identity)').Contains('MyApplicationStagingFolder='))">$([System.String]::new('%(DefineConstantsKVPairs.Identity)').Split('=')[1])</MyApplicationStagingFolder>
</PropertyGroup>
</Target>
<Target Name="PackageMyApplication">
<!-- Collect the set of resources to package -->
<ItemGroup>
<MyApplicationFiles Include="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*.exe" />
<MyApplicationFiles Include="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*.config" />
<MyApplicationFiles Remove="$(MSBuildProjectDirectory)\..\MyApplication\bin\$(Configuration)\*vshost.*" />
</ItemGroup>
<!-- Clean the temporary directory -->
<RemoveDir Directories="$(MyApplicationStagingFolder)" />
<!-- Stage MyApplication's files -->
<Copy SourceFiles="@(MyApplicationFiles)" DestinationFiles="@(MyApplicationFiles -> '$(MyApplicationStagingFolder)\%(RecursiveDir)%(Filename)%(Extension)')" />
<!-- Remove the read-only attribute from the harvested files fragment for reconstitution in situ -->
<Exec Command="attrib -r $(ProjectDir)HarvestedMyApplication.wxs" />
<!-- Harvest MyApplication's files -->
<Exec Command=""$(WixToolPath)\heat.exe" dir "$(MyApplicationStagingFolder)" -cg MyApplicationFilesComponents -gg -scom -sreg -sfrag -srd -dr MYAPPLICATIONINSTALLFOLDER -var var.MyApplicationStagingFolder -out "$(MSBuildProjectDirectory)\HarvestedMyApplication.wxs"" />
</Target>
</Project>
Then save your changes and reload the project into Visual Studio.
The code we've added should be largely self-explanatory. It's longer than it needs to be though, and I want to explain why. I could, for example. have pointed heat.exe at the MyApplication output folder and harvested the files directly. Instead I chose both to stage the content and then to fiddle around with the source directory for harvesting (-var switch), installation folder (-dr switch) and ComponentGroup (-cg switch). Couldn't I have just used the defaults?
The answer is "It depends on what you are trying to achieve." In this cut-down example, I have indeed made life unnecessarily complicated for myself. But in the real-world solution upon which this example is based, I had some specific requirements:
- I wanted to create a single MSI per server type. Instead of installing a web service, a web project and perhaps some reference resources all as separate MSI packages, I wanted to install a single Webserver MSI. The problem is, neither my various services and resources nor their configuration shared an installation root. So I needed to make the various installation structures distinct in my WiX files. This in turn meant I needed to harvest them separately into different ComponentGroups
- I wanted complete control as to what resources were harvested; hence I chose to collect them into an ItemGroup first
- I also wanted to copy in configuration files and other resources external to the project, sometimes renaming them in the process; and finally:
- I wanted an installation structure distinct from my build structure, and using a staging intermediary was the most straightforward way to achieve this
Editing Product.wxs
We're virtually done with the various project files, but for our WiX installer to work there are a few things left we need to do:
- Create appropriately named Features with associated ComponentGroupRefs
- Define our overridden installation directory (MYAPPLICATIONINSTALLFOLDER)
- Embed all resources in a cabinet
Having made these changes, you'll end up with the following Product.wxs file.
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="89011200-0065-48AA-8341-75570EA7A79D" Name="MyApplication" Language="1033" Version="1.0.0.0" Manufacturer="Dunedin Software Ltd" UpgradeCode="396f2479-1721-4b35-9f67-751dafcb8d7c">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<Media Id="1" Cabinet="MyApplicationCab" EmbedCab="yes"/>
<Feature Id="ApplicationCopyFiles" Title="MyApplication.Installer.CopyFiles" Level="1000">
<ComponentGroupRef Id="MyApplicationFilesComponents"/>
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="INSTALLROOT">
<Directory Id="MYAPPLICATIONINSTALLFOLDER" Name="Application" />
</Directory>
</Directory>
</Product>
</Wix>
Testing the MSI and a note on nomenclature
Build the solution, open a command prompt and enter:
msiexec /i MyApplicationInstaller.msi ADDLOCAL="ApplicationCopyFiles" TARGETDIR="c:\temp" /l*V log.txt
You should now find the application referenced in the ARP, installed to c:\temp, and a full installation history saved in log.txt. Navigate to the c:\temp\Application folder and run MyApplication.exe. It should echo "Development" to the console. Was it really worth the effort? Well yes, because you've now put in place a general framework for staging, harvesting and deploying code assets. Notice that when we harvested from a project called MyApplication (let's call it [X] for clarity) we:
- added a build target called "Extract[X]StagingFolder" as a dependency of "BeforeBuild", which in turn copied the WiX preprocessor directive [X]StagingFolder to an MSBuild property of the same name
- added a build target called "Package[X]" as a dependency of "BeforeBuild"
- collected the files we wanted to stage within an ItemGroup under the name [X]Files
- staged our files at [X]StagingFolder
- Invoked heat.exe for a source folder [X]StagingFolder, creating a ComponentGroup called [X]FilesComponents, referencing an installation folder called [X]INSTALLFOLDER, with an overridden SourceDir of var.[X]StagingFolder
- Harvested the resultant files to a file called Harvested[X].wxs
Because of all this it's very straightforward to add more files from other projects to our MSI, and we can plumb in these new files in whichever way is most appropriate:
- as part of an existing staging directory
- as part of an existing Feature
- as part of an existing ComponentGroup
- or else in a discrete staging directory, Feature or ComponentGroup
Next steps
This is part 1 of a multi-part article. In the next article we pick up the pace a bit. Among other things, we will cover:
- Per-environment configuration
- Hosting multiple environments on the same box
- Versioning MSIs
- Managing environment-specific configuration information
- Deployment Batch Files and Continuous Deployment
And we will do all of this building on the very basic installer and associated scaffolding we have just created. Download the code we have created to date here.