I recently hit what I would call a fairly serious limitation of MSBuild and that is that it cant build Visual Studio Deployment Projects. This is a bit of a show stopper when your trying to build a one click release script.
After some Googling it turns out that there are two solutions, the correct solution and the hack.
Correct Solution : Move away from Visual Studio Deployment projects and use something like Wix. The downside is if you're already deeply invested in using Visual Studio Deployment projects this can be quite a lot of work and possible not a route you want to go down.
Hack : Use MSBuild to call Visual Studio from the command line asking it to build the Deployment Project for you. The downside of this is your build server will need to have Visual Studio installed.
In my case I went with the Hack as it was the quickest way to get things working with the existing architecture.
I already had my MSBuild script doing the following
- Grab latest source code from source control
- Increment Assembly version numbers
- Build the solution in release mode (using MSBuild task)
From here I wanted to increment the version number for my Deployment Project and build it in release mode.
*Warning* hacks and bad code ahead…
To increment the Deployment Projects version number I wrote a little inline C# task into the build script to edit the project file, the task takes a path to the Deployment Projects project file and a version number (in the format of 1.2.3) to set the installer to. It sets 3 variables inside the project file both ProductCode and PackageCode get set to new GUIDs and the ProductVersion gets set to the version passed in to the task. The code for this task looked like this….
<UsingTask TaskName ="SetInstallerVersion" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll"> <ParameterGroup> <InstallerPath ParameterType="System.String" Required="true"/> <Version ParameterType="System.String" Required="true"/> </ParameterGroup> <Task> <Code Type="Fragment" Language="cs"> var installerContents = ""; using (var sr = new System.IO.StreamReader(InstallerPath)) { installerContents = sr.ReadToEnd(); sr.Close(); } var reProductCode = new System.Text.RegularExpressions.Regex(@"(?:\""ProductCode\"" =\""8.){([\d\w-]+)}"); var rePackageCode = new System.Text.RegularExpressions.Regex(@"(?:\""PackageCode\"" =\""8.){([\d\w-]+)}"); var reProductVersion = new System.Text.RegularExpressions.Regex(@"""ProductVersion"" =""8:[0-9\.]*"""); installerContents = reProductCode.Replace(installerContents,"\"ProductCode\" = \"8:{" + Guid.NewGuid().ToString().ToUpper() + "}"); installerContents = rePackageCode.Replace(installerContents,"\"PackageCode\" = \"8:{" + Guid.NewGuid().ToString().ToUpper() + "}"); installerContents = reProductVersion.Replace(installerContents,"\"ProductVersion\" = \"8:" + Version + "\""); using(var sw = new System.IO.StreamWriter(InstallerPath,false)) { sw.Write(installerContents); sw.Close(); } </Code> </Task> </UsingTask>
OK so at this point the version numbers have been updated and we are ready to build the installer. I’d read how to do this by executing devenv.com from a few different sites and the suggested method was this
<DevEnvExePath>C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.com</DevEnvExePath> <DeploymentProjectPath>c:\temp\myinstaller.vdproj</DeploymentProjectPath> <Target Name="BuildInstaller"> <Exec Command=""$(DevEnvExePath)" "$(DeploymentProjectPath)" /build Release"/> </Target>
When I tried to run the build code above it just kept throwing exceptions saying missing dependencies. It turns out that if your installer references the output from other projects in the solution (As most do) you need to point DevEnv at the solution then specify the Deployment Project to build. This way it first builds all the projects in the solution the installer needs the output from, then it builds that actual installer. My working code for building the deployment project looked like this….
<DevEnvExePath>C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.com</DevEnvExePath> <SolutionPath>c:\temp\mysolution.sln</SolutionPath> <DeploymentProjectPath>c:\temp\myinstaller.vdproj</DeploymentProjectPath> <Target Name="BuildInstaller"> <Exec Command=""$(DevEnvExePath)" "$(SolutionPath)" /build quot;Release" /project "$(DeploymentProjectPath)""/> </Target>
At this point I removed the step I already had to build the solution as this is no longer needed now the solution and installer are being built in the same step. Its worth also noting that if you wanted to introduce logging you could add something like “> BuildLog.txt” to the end of the Exec command to pipe the output from Visual Studio in to a text file called BuildLog.txt.
I am in no way encouraging you to use DevEnv to build projects from MSBuild rather than the standard MSBuild tasks but in cases like this it's pretty much the only option.