Creating versioned installers using Wix and TFS 2010

Recently I created a Wix installer for a product and wanted to include the version number in the installer name – e.g. “My Product.1.2.3.msi”. I found this post which describes how to edit the .wixproj file to add a pre-build task to update the output filename to include a version. This worked perfectly when building the project locally, but not as a part of our TFS 2010 automated build. The reason for this is that TFS 2010 outputs builds to the /Binaries/ directory rather than their local /bin/, so the relative paths in the solution given break. In this post I describe how I edited the .wixproj file to work both locally and as a part of a TFS 2010 automated build.

I also wanted my MSI to use a three part version number rather than the default four part used.

The existing solution

Firstly, let’s take a look at the a solution based on the Tentacle Software blog post. Right click on your installer project, select “Unload” and then right click again and select “Edit X.wixproj”. Then modify the BeforeBuild target found at the bottom of the file.

<Target Name="BeforeBuild">
  <!-- Get version info from an assembly and store it in AssemblyVersions -->
  <GetAssemblyIdentity AssemblyFiles="$(SolutionDir)MyProduct\bin\$(Platform)\$(Configuration)\MyProduct.dll">
    <Output TaskParameter="Assemblies" ItemName="AssemblyVersions" />
  </GetAssemblyIdentity>

  <!-- Append the version to the filename - e.g. MyProduct.1.2 - and store in TargetName -->
  <CreateProperty Value="$(OutputName).%(AssemblyVersions.Version)">
    <Output TaskParameter="Value" PropertyName="TargetName"/>
  </CreateProperty>

  <!-- Append the extension to the filename and store in TargetFileName -->
  <CreateProperty Value="$(TargetName)$(TargetExt)">
    <Output TaskParameter="Value" PropertyName="TargetFileName"/>
  </CreateProperty>

  <!-- Finally, create a fully qualified path and update the TargetPath property used by MSBuild -->
  <CreateProperty Value="$(TargetDir)$(TargetFileName)">
    <Output TaskParameter="Value" PropertyName="TargetPath"/>
  </CreateProperty>
</Target>

When a build is run in TFS 2010 it uses two folders – /Binaries and /Sources. The /Binaries folder is used as the output directory for all projects, while your solution files are kept in the /Sources folder. This means that the relative path between your installer project and MyProduct.dll will change, meaning the GetAssemblyIdentity task will not work. Using this approach it is not possible to create a solution that works both locally and as part of an automated build.

An updated solution

In order to get around this problem I simply defined two relative paths in my BeforeBuild target and used conditions to get the assembly version based on which path actually existed. This is a fairly crude approach but works locally and as a part of the automated build.

<Target Name="BeforeBuild">

    <!-- Relative paths to the DLL locally and on the CI server -->
    <PropertyGroup>
      <LocalAssembly>..\..\MyProduct\bin\$(Configuration)\MyProduct.dll</LocalAssemvly>
      <TfsAssembly>..\..\..\..\..\..\Binaries\MyProduct.dll</TfsAssembly>
    </PropertyGroup>

    <!-- Get the version number from whichever assembly exists-->
    <GetAssemblyIdentity Condition="Exists($(LocalAssembly))" AssemblyFiles="$(LocalAssembly)">
      <Output TaskParameter="Assemblies" ItemName="ProductAssembly"/>
    </GetAssemblyIdentity>
    <GetAssemblyIdentity Condition="Exists($(TfsAssembly))" AssemblyFiles="$(TfsAssembly)">
      <Output TaskParameter="Assemblies" ItemName="ProductAssembly"/>
    </GetAssemblyIdentity>

      <!-- Append the version to the filename - e.g. MyProduct.1.2 - and store in TargetName -->
    <CreateProperty Value="$(OutputName).%(ProductAssembly.Version)">
      <Output TaskParameter="Value" PropertyName="TargetName"/>
    </CreateProperty>

    <!-- Append the extension to the filename and store in TargetFileName -->
    <CreateProperty Value="$(TargetName)$(TargetExt)">
      <Output TaskParameter="Value" PropertyName="TargetFileName"/>
    </CreateProperty>

    <!-- Finally, create a fully qualified path and update the TargetPath property used by MSBuild -->
    <CreateProperty Value="$(TargetDir)$(TargetFileName)">
      <Output TaskParameter="Value" PropertyName="TargetPath"/>
    </CreateProperty>
</Target>

I have used relative paths for both local and TFS scenarios, but you could still use the $(SolutionDir) based approach from the original solution for $(LocalAssembly) if you wanted.

Using a three part version number

As MSIs essentially ignore the fourth part of versions numbers we tend to use three part version numbers and I wanted our installer to use a three part version too, whereas our current solution will include a four part version number (the last digit always being zero). So my final tweak was to remove that final zero. Handily in MSBuild versions greater than four properties can be manipulated like instances of the String class, meaning we have access to methods like IndexOf and Substring.

<Target Name="BeforeBuild">

    <!-- Relative paths to the DLL locally and on the CI server -->
    <PropertyGroup>
      <LocalAssembly>..\..\MyProduct\bin\$(Configuration)\MyProduct.dll</LocalAssemvly>
      <TfsAssembly>..\..\..\..\..\..\Binaries\MyProduct.dll</TfsAssembly>
    </PropertyGroup>

    <!-- Get the version number from whichever assembly exists-->
    <GetAssemblyIdentity Condition="Exists($(LocalAssembly))" AssemblyFiles="$(LocalAssembly)">
      <Output TaskParameter="Assemblies" ItemName="ProductAssembly"/>
    </GetAssemblyIdentity>
    <GetAssemblyIdentity Condition="Exists($(TfsAssembly))" AssemblyFiles="$(TfsAssembly)">
      <Output TaskParameter="Assemblies" ItemName="ProductAssembly"/>
    </GetAssemblyIdentity>

    <!-- Remove the fourth part of the version number -->
    <CreateProperty Value="%(ProductAssembly.Version)">
      <Output TaskParameter="Value" PropertyName="RawTargetVersion"/>
    </CreateProperty>
    <CreateProperty Value="$(RawTargetVersion.Substring(0, $(RawTargetVersion.LastIndexOf('.')) ))">
      <Output TaskParameter="Value" PropertyName="TargetVersion"/>
    </CreateProperty>

      <!-- Append the version to the filename - e.g. MyProduct.1.2 - and store in TargetName -->
    <CreateProperty Value="$(OutputName).$(TargetVersion)">
      <Output TaskParameter="Value" PropertyName="TargetName"/>
    </CreateProperty>

    <!-- Append the extension to the filename and store in TargetFileName -->
    <CreateProperty Value="$(TargetName)$(TargetExt)">
      <Output TaskParameter="Value" PropertyName="TargetFileName"/>
    </CreateProperty>

    <!-- Finally, create a fully qualified path and update the TargetPath property used by MSBuild -->
    <CreateProperty Value="$(TargetDir)$(TargetFileName)">
      <Output TaskParameter="Value" PropertyName="TargetPath"/>
    </CreateProperty>
</Target>

This update adds two new targets which take the assembly version and then use IndexOf and Substring to remove the final, redundant zero from the version number.

With these changes made our product installers now include a three part version number in the filename when built both locally and as a part of our automated build.

Comments