I recently had the pleasurable and painful experience of learning WiX to build a windows installer for a Windows Mobile application. The pain came first: there is quite a learning curve to both WiX and the underlying Windows Installer technology; and the documentation is a little thin on WiX 3, the latest version as I write this. But after about two weeks struggling, the switch flipped, the sun shone, the chorus of angels began singing – I understood how it worked and saw that its design is very well thought out. It became a joy to use WiX. And it’s free. And it has pretty good Visual Studio support.
That’s not to say there aren’t problems. The documentation is definitely a problem, one to which I’ll try to make a small contribution in solving. I intend to post more about WiX in the future, but for now, I’ll cover one small thing which is setting properties for your WiX project in an automated build with MSBuild.
Part of the pretty good Visual Studio support is it’s own MSBuild-based project file. It has custom tasks that wrap the underlying command-line utilities, candle, lit, light, etc. This makes it really easy to make it part of your daily build or CI builds. One thing that often gets taken over by the build process is setting the version number of your assemblies. Versioning is especially important when it comes to installers, so keeping the installer version in sync with the application version is definitely a good practice.
To do this with your WiX installer project, we first have turn the version value to a variable. (Forgive me if I use the incorrect terms here, I don’t know the formal WiX names for these things. I’ll gladly change it if someone corrects me.)
Suppose we have the following, default wxs file given to us by Votive in Visual Studio:
<?xml version="1.0" encoding="UTF-8"?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Product Id="*" Name="WixProject1" Language="1033" Version="1.0.0.0" Manufacturer="WixProject1" UpgradeCode="c93e09b9-9e8f-444c-a35b-84beb2c3788f"> <Package InstallerVersion="200" Compressed="yes" /> <Media Id="1" Cabinet="WixProject1.cab" EmbedCab="yes" /> <Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="ProgramFilesFolder"> <Directory Id="INSTALLLOCATION" Name="WixProject1"> <!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. --> <!-- <Component Id="ProductComponent" Guid="51acaaef-c2fb-4ef3-a641-9475c81ac948"> --> <!-- TODO: Insert files, registry keys, and other resources here. --> <!-- </Component> --> </Directory> </Directory> </Directory> <Feature Id="ProductFeature" Title="WixProject1" Level="1"> <!-- TODO: Remove the comments around this ComponentRef element and the Component above in order to add resources to this installer. --> <!-- <ComponentRef Id="ProductComponent" /> --> </Feature> </Product> </Wix>
The property we’re concerned with is the Version attribute on the Product element. Right now it’s set to 1.0.0.0. The first thing we need to do is change this into a variable so that we can set the value from elsewhere. To do that we change the Version attribute above into the following:
<Product Id="*" Name="WixProject1" Language="1033" Version="$(var.ProductVersion)" Manufacturer="WixProject1" UpgradeCode="c93e09b9-9e8f-444c-a35b-84beb2c3788f"> ... </Product>
This creates a preprocessor variable called ProductVersion (you can call it whatever you want). If you build the installer now, you get an error saying “Error 1 Undefined preprocessor variable ‘$(var.ProductVersion)’. “
Now we have to define that elsewhere. And where else but the project file!
To edit that, right click on the project node in solution explorer, select Unload Project… Then right click again and select Edit <ProjectName>.wixproj. You’ll get the proj file loaded as XML in the editor. Let me draw your attention to one of the PropertyGroups:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> <DefineConstants>Debug</DefineConstants> </PropertyGroup>
This is a property group that is only defined if the Configuration property is Debug and Platform is x86. You see that DefineConstants property set to Debug? That’s where we want to define our ProductVersion variable, like so:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> <DefineConstants>Debug;ProductVersion=1.0.0.0</DefineConstants> </PropertyGroup>
In general, you want to define all your properties in Name=Value; format. OK, we have that entered, so close the file, right click on the project node again in SOlution Explorer, select Reload Project. This makes Visual Studio read the project file again. Build again. No errors! Neat, eh?
Now we’ve managed to move setting the of the ProductVersion property into the MSBuild file, but remember what I said about that property group? It’s only defined in Debug builds. If we switched our project to Release, then we’d get the Undefined variable error again. (This is actually a big usability problem in VS, IMO; this happens all the time for many properties and settings).
What we really want is the ProductVersion property defined for all build configurations. But while we’re developing our installer, we want to use Visual Studio, so we don’t have to constantly go into the proj file to change things. No problem, I got you covered.
The good folks developing WiX for all of us obviously have our interests in mind by setting that Debug property by default, so we’re going to want to save that. We also want to define ProductVersion for Release builds. Let’s consider all the PropertyGroups in the wixproj file, the answer is practically shouting at us:
<PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> <ProductVersion>3.0</ProductVersion> <ProjectGuid>{9b84d861-ac5d-4559-bc9c-0e59cb95ad90}</ProjectGuid> <SchemaVersion>2.0</SchemaVersion> <OutputName>WixProject1</OutputName> <OutputType>Package</OutputType> <WixTargetsPath Condition=" '$(WixTargetsPath)' == '' ">$(MSBuildExtensionsPath)\Microsoft\WiX\v3.0\Wix.targets</WixTargetsPath> <Name>WixProject1</Name> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> <DefineConstants>Debug;ProductVersion=1.0.0.0</DefineConstants> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> </PropertyGroup>
First thing we should do is add the DefineConstants property with our ProductVersion property in Release:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> <DefineConstants>ProductVersion=1.0.0.0</DefineConstants> </PropertyGroup>
Got it, now: check out the Configuration and Platform properties in the first PropertyGroup. See what they do? They define a default if empty. Now why couldn’t we do that? We’ll define another property in the first property group called Version (you can call it what you like):
<Version Condition=" '$(Version)' == '' ">1.0.0.0</Version>
Alright, one more step, we replace the value in our DefineConstants declarations. I’ll do the Debug version, you do the Release:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <OutputPath>bin\$(Configuration)\</OutputPath> <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> <DefineConstants>Debug;ProductVersion=$(Version)</DefineConstants> </PropertyGroup>
Got the Release section finished? Awesome. Reload the project. Build. Awesome.
It works!
Now, we can build this project on the command line and set any version we want.
msbuild installer.proj /p:Version=3.1.0.0
One thing to note though. Ideally, you’re going to mix and match your product assemblies and product installer. Well, maybe not, but if you have any code-based custom actions, you’re probably going to have a C# or VB project in you product installer solution.
The bad news is that the CSharp.targets file, the file with all the default C# build tasks also defines a DefineConstants property. It’s used for setting DEBUG and TRACE and all those. They conform to a certain format; one that doesn’t include Name=Value; formatting. So you get unnecessary warnings.
I’m not sure why they decided to reuse a well-known property name. Was it really so hard to type <PreprocessorVariable>?