Tag Archives: wix product version

Versioning Visual Studio Solutions The Easy Way

Versioning your assemblies on each test or release build is an essential best practice to ensure identifiable releases (for testing and support) and comply with Windows Installer standards if you produce MSIs (for example using WIX).

However many developers won’t have access to a fully setup and maintained “continuous integration” build environment, which typically provide versioning support built-in. And anyway there are the interim builds you may want to make quickly on a local developer PC, for example during WIX development. They probably won’t be done via a build sever for convenience and speed.

This brings a requirement for a simple local method to quickly increment the build across all projects and various configuration files. Here is a solution involving just a PowerShell script and text file, which you can add to your solution to achieve the same. You could also integrate this with a build server, providing the best of both solutions.

Firstly, here are the files you add to the root of your solution (in “Solution Items”):

  • Version.txt   – Stores the major and minor version parts which you will manually increment according to your target product and sprint/release versions, plus the build and revision parts which are automatically set by the script to the year, month, day and build number.
  • Version.ps1 – PowerShell script which reads the old Version.txt, generates a new version number then writes it back. Then it edits all necessary project files (e.g. AssemblyInfo.cs, Global.wxi, App.config, Web.config).
  • Version.cmd – Windows Command script file which calls the PowerShell script with the necessary parameters. Makes it easy for the developer or build script to invoke the PowerShell script.

So you start by creating a Version.txt file with your major and minor versions and any build and revision, for example:

2.5.0.0

Then create the Version.ps1 containing the following script, editing or extending the final subroutine calls to cover all of your relevant project files:

Write-Output "Version"
Write-Output "======="
Write-Output "Increments the version number stored in the Version.txt file,"
Write-Output "then applies it to all relevant source files in the solution."
Write-Output "Build is set to the UTC year and month in ""yyMM"" format."
Write-Output "Revision is set to the UTC day * 1000 plus a three digit incrementing number." 
Write-Output ""

trap
{
    Write-Error $_
    exit 1
}

function Update-Version ([Version]$Version)
{
    $date = (Get-Date).ToUniversalTime()
    $newBuild = $date.ToString("yyMM")
    $dayRevisionMin = $date.Day * 1000
    if (($Version.Build -lt $newBuild) -or ($Version.Revision -lt $dayRevisionMin)) { $newRevision = $dayRevisionMin + 1 } else { $newRevision = $Version.Revision + 1 }
    New-Object -TypeName System.Version -ArgumentList $Version.Major, $Version.Minor, $newBuild, $newRevision
}

function Get-VersionFile ([String]$File)
{
    Write-Host ("Reading version file " + $File)
    $versionString = [System.IO.File]::ReadAllText($File).Trim()
    New-Object -TypeName System.Version -ArgumentList $versionString
}

function Set-VersionFile ([String]$File, [Version]$Version)
{
    Write-Host ("Writing version file " + $File)
    [System.IO.File]::WriteAllText($File, $Version.ToString())
}

function Set-VersionInAssemblyInfo ([String]$File, [Version]$Version)
{
    Write-Host ("Setting version in assembly info file " + $File)
    $contents = [System.IO.File]::ReadAllText($File)
    $contents = [RegEx]::Replace($contents, "(AssemblyVersion\("")(?:\d+\.\d+\.\d+\.\d+)(""\))", ("`${1}" + $Version.ToString() + "`${2}"))
    $contents = [RegEx]::Replace($contents, "(AssemblyFileVersion\("")(?:\d+\.\d+\.\d+\.\d+)(""\))", ("`${1}" + $Version.ToString() + "`${2}"))
    [System.IO.File]::WriteAllText($File, $contents)
}

function Set-VersionInWixGlobal ([String]$File, [Version]$Version)
{
    Write-Host ("Setting version in WIX global file " + $File)
    $contents = [System.IO.File]::ReadAllText($File)
    $contents = [RegEx]::Replace($contents, "(\<\?define\s*ProductVersion\s*=\s*"")(?:\d+\.\d+\.\d+\.\d+)(""\s*\?\>)", ("`${1}" + $Version.ToString() + "`${2}"))
    [System.IO.File]::WriteAllText($File, $contents)
}

function Set-VersionInAssemblyReference ([String]$File, [String]$AssemblyName, [Version]$Version)
{
    Write-Host ("Setting version in assembly references of " + $File)
    $contents = [System.IO.File]::ReadAllText($File)
    $contents = [RegEx]::Replace($contents, "(["">](?:\S+,\s+){0,1}" + $AssemblyName + ",\s+Version=)(?:\d+\.\d+\.\d+\.\d+)([,""<])", ("`${1}" + $Version.ToString() + "`${2}"))
    [System.IO.File]::WriteAllText($File, $contents)
}

function Set-VersionInBindingRedirect ([String]$File, [String]$AssemblyName, [Version]$Version)
{
    Write-Host ("Setting version in binding redirects of " + $File)
    $contents = [System.IO.File]::ReadAllText($File)
    $oldVersionMax = New-Object -TypeName "System.Version" -ArgumentList $Version.Major, $Version.Minor, $Version.Build, ($Version.Revision - 1)
    $pattern = "([\s\S]*?<assemblyIdentity\s+name=""" + $AssemblyName + """[\s\S]+?/>[\s\S]*?<bindingRedirect\s+oldVersion=""\d+\.\d+\.\d+\.\d+-)(?:\d+\.\d+\.\d+\.\d+)(""\s+newVersion="")(?:\d+\.\d+\.\d+\.\d+)(""[\s\S]*?/>)"
    $contents = [RegEx]::Replace($contents, $pattern, ("`${1}" + $oldVersionMax.ToString() + "`${2}" + $Version.ToString() + "`${3}"))
    [System.IO.File]::WriteAllText($File, $contents)
}

$scriptDirectory =  [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)

$versionFilePath = $scriptDirectory + "\Version.txt"
$version = Get-VersionFile -File $versionFilePath
Write-Host ("Old Version: " + $version.ToString())

$newVersion = Update-Version -Version $version
Write-Host ("New Version: " + $newVersion.ToString())
Set-VersionFile -File $versionFilePath -Version $newVersion

$otherVersionFilePath = $scriptDirectory + "\Dependencies\Other.Component.Version.txt"
$otherVersion = Get-VersionFile -File $otherVersionFilePath
Write-Host ("Other Component Reference Version: " + $otherVersion.ToString())

Set-VersionInAssemblyInfo -File ($scriptDirectory + "\MyProduct.Assembly1\Properties\AssemblyInfo.cs") -Version $newVersion
Set-VersionInAssemblyInfo -File ($scriptDirectory + "\MyProduct.Assembly2\Properties\AssemblyInfo.cs") -Version $newVersion
Set-VersionInAssemblyInfo -File ($scriptDirectory + "\MyProduct.Assembly3\Properties\AssemblyInfo.cs") -Version $newVersion
Set-VersionInAssemblyReference -File ($scriptDirectory + "\MyProduct.Assembly3\App.config") -AssemblyName "MyProduct.Assembly2" -Version $newVersion
Set-VersionInAssemblyInfo -File ($scriptDirectory + "\MyProduct.Assembly4\Properties\AssemblyInfo.cs") -Version $newVersion
Set-VersionInBindingRedirect -File ($scriptDirectory + "\MyProduct.Assembly4\Web.config") -AssemblyName "Other.Component" -Version $otherVersion
Set-VersionInWixGlobal -File ($scriptDirectory + "\MyProduct.Setup\Global.wxi") -Version $versionString

exit 0

Lastly the Version.cmd invokes the PowerShell Version.ps1 correctly from the command line, e.g. I usually open a command prompt using the Visual Studio Power Tools toolbox menu item then run either my build script (which calls Version.cmd) or just Version.cmd to increment the version alone.

@powershell -File "%~dp0Version.ps1"

Most of this script remains static. The functions provided in this example will edit the “AssemblyVersion” attribute in an “AssemblyInfo.cs” or a WIX “ProductVersion” variable in a WIX file (typically stored in a shared include file named Global.wxi). Finally the last lines of the script are customized for your actual solution, calling the relevant functions to update the version number in files of your solution.

Now each time you build your solution you have an identifiable version which is exactly same across all assemblies and even the MSI. For example, the third build of the above solution on the 23rd April 2013 will produce the following version:

2.5.1303.23003

This is broken down as follows:

  • 2.5 comes from the static major and minor version you typed in the Version.txt.
  • 1303 comes from the year and month, 2013 and 03 for March.
  • 23003 is the day, the 23rd, multiplied by 1000 to give us three predictable digits for the build number, 003 is the third build on this day, with up to 999 possible before clashing with builds on the next day (should never occur then).

Following this pattern, you could add the version files to every solution you create and maintain it simply by adding a line or two each time you add projects and calling it from any build script or system you may have. If you have new types of files to edit add your own functions and call them at the end. The general solution of using regular expression replacements should suffice for most use cases. Other more complex requirements could use an XPath query to edit configuration files where it is not possible to identify the version by string patterns alone (when a structured XML path is more appropriate to accurately locate the version entries).

I like this solution because it allows you to use best practice versioning without the overhead. And even if you do have a proper continuous build system in place, using this in place of any proprietary versioning add-on or Visual Studio extension is much simpler, flexible (e.g. use offline or when build server is out of service)  and easy to maintain, reducing your 3rd party product dependencies.