I have spent some time binging for solutions to the following issue: You have a product which leverages visual studio setup and deployment packages. You wish to build the MSI as part of your automated build process in Team Build 2010, and you want the built MSI to include transformed configuration files.
So there were two distinct facets to the aforementioned functionality – first is building the installers via your automated Team Build process, and the second is ensuring the MSI that gets built includes the transformations we expect.
The first part of functionality is covered very well here, and I have (roughly) implemented what is described in Jakob’s blog post, with a few tweaks along the way.
The second part of functionality is the interesting one however, and requires quite a bit of fiddling in order to get it working as you would expect. I would like to thank Vishal Joshi for pointing me in the right direction to pursue initially with this.
So lets take a look at how we go about getting it all working:
After you get the creation of the setup package working within your automated build process, you will notice upon installing it that not only does it not contain transformed configuration files, but that it also contains all of your .Debug.config and .Release.config files (Just like if you built it via visual studio).
The reason for this is that when being built, the setup package considers configuration files to be ‘content’ files, so copies them as-is from your solution. When the setup package is being built, the first thing it does is build your solution using MSBuild as it is on the disk, and then it packages the output that it is configured to package (build output, content files, additional files).
So what we want to do is ensure the configuration files on the disk that the setup is going to package are transformed BEFORE the packaging step (during the MSBuild step) – so that when the setup package is created it will grab the transformed .config files.
To do that, we need to ensure the MSBuild step executes our transformations. The tricky part is that you can’t supply parameters to the MSBuild step here – it is internal to the construction of the setup package, and handled by devenv. So we need to make sure our transformations are executed when the package is built regardless of whether we supply arguments to MSBuild or not. To accomplish this, you can add the following to your applications csproj file (in the first property group will do nicely):
Code Snippet
- <DeployDefaultTarget>TransformWebConfig</DeployDefaultTarget>
- <DeployOnBuild>True</DeployOnBuild>
From what Vishal tells me, what this does is trigger deployment as part of the build process (but only in a command line / Team Build environment, not when built in the IDE), and then uses the deployment to trigger the TransformWebConfig task (instead of the Package task it would usually default to), which is a standard task which executes our configuration transformations. Pretty handy!
Now our transformations will be executed during the build process without having to append extra parameters, lets move on to the next steps.
What we now want to do is copy the transformed configuration files (which reside under the $(ProjectDir)\obj\Release) to their normal locations in your web application project’s folder structure, and overwrite the existing ones. The existing ones will currently be read-only, as they have been retrieved by the build agent, and not checked out. What we are going to do is check out the required files so they become writable, copy our transformed files over the top of them, trigger the setup package construction via devenv, then undo the checkouts we just performed.
I had originally put the first two parts of functionality (check out and copy) within a PostBuildEvent inside my web applications build configuration (csproj), as I found it easier to hack in there. However I felt this fragmented what I was doing too much in the end, so I pulled it out of there and added it to the build process template workflow (xaml).
The original looked something like this:
Code Snippet
- <PostBuildEvent>
- <!--
- In here we want to check out any .config file we have transformed during build using XDT, and write over it with the transformed version.
- All checkouts made by the build service will be undone at the end of a build process by a workflow item in the build definition.
- The ping command is a command line sleep hack. It will halt execution for 10 seconds, and allow file locks to be released.
- -->
-
- ping -n 10 127.0.0.1>nul
-
- cd /d "$(ProjectDir)"
-
- "C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\tf.exe" checkout "web.config"
- copy "$(ProjectDir)$(IntermediateOutputPath)web.config" "$(ProjectDir)" /y
-
- "C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\tf.exe" checkout "Config\ConnectionStrings.config"
- copy "$(ProjectDir)$(IntermediateOutputPath)Config\ConnectionStrings.config" "$(ProjectDir)Config\" /y
-
- </PostBuildEvent>
The completed version (including all of the functionality mentioned) now looks like this:


So the first thing we need to do is to locate the “Build Code” sequence container and rename it to “Build Code and Setup” – this more accurately describes what it will be doing.
To this sequence container we want to add two variables. So select the container, then click the “Variables” tab in the bottom left.
The first will be called “TransformedConfigurations”, will be of type IEnumerable<String>, and will have a value of your transformed configuration files relative to the root web applicatino project directory. Mine has the value {"web.config", "Config\ConnectionStrings.config"} – the formatting is required for the IEnumerable value.
The second will be called “ProjectDirectory”. In MSBuild you can leverage a property called $(IntermediateOutputPath) which will return the full path of the obj\release folder for the project currently being built. Unfortunately we can’t get to this via the build definition (Since the MSBuild step is essentially atomic), so we will have to construct our own property for this purpose. We need this directory so we can tell the tf.exe tasks to execute within it, so it can establish which workspace we are working in. The value of this property will be localProject.Substring(0, localProject.LastIndexOf("\") + 1) + "My.WebApplication.Project.Directory\" – a little hacky, but still fairly resiliant to change unless you rename your web app project’s root directory often.
Now what we will do is add the first custom task below the existing Run MSBuild for Project task. Drop a ForEach sequence container here, set its name to “Checkout and Copy Configuration Files”, its TypeArgument property to String, and its Values property to your TransformedConfigurations variable. Within this, drop a sequence container.
Finally, drop two InvokeProcess tasks into the sequence container.
The first task (sequentially) we will call “Checkout File”, and set FileName property to the location of tf.exe (mine is "C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"). In the Arguments property, append "checkout """ + item + """" (gotta love escaping strings in VB). Finally, set the WorkingDirectory property to your ProjectDirectory variable. This will ensure tf.exe gets executed in the root folder of your web application, which is what we want.
For the second task, name it “XCopy File”, and set the FileName property to “xcopy.exe”. Set the Arguments property to """" + ProjectDirectory + "obj\Release\" + item + """ " + """" + ProjectDirectory + item + """ " + "/Y" – this will copy our transformed configuration files over their non-transformed counterparts in the web applications directory structure. Finally once again set the WorkingDirectory property to your ProjectDirectory variable.
Now we are ready for devenv to create the setup package and consume our transformed configuration files. The creation and configuration of this particular task can be found here, as I mentioned previously.
After the setup package has been generated, we can undo the checkouts / changes we forced previously. Drop another InvokeProcess task below the Invoke DevEnv task, and name it “Undo Workspace Checkouts”. We will again set its FileName property to tf.exe’s location. Set the Arguments property to "undo /recursive /noprompt /workspace:" + WorkspaceName + " $/". We are leveraging the already existing workspace name variable being used by our automated build process for the undo. Finally, set the WorkingDirectory property to your ProjectDirectory variable.
We are very close to being finished now! Not quite done yet though. Now you have a setup project copied to your drop location that when installed, will be installed with your transformed configuration files. Great! However you will also notice that your other .Debug.config, .Release.config etc files are still being copied across too!
This is easily fixed however. Open your solution in visual studio, and right click your deployment project and select View > File System. Select The Web Application Folder node to the left, then right click your Content Files from <WebApplicationProjectName> node on the right and select Exclude Filter. Exclude any build configuration types you have, mine looks like so:

This will ensure none of these files get packaged with the setup, leaving you only with your transformed configuration files.
Phew! We’re done! Now if I go to my build’s drop location, I have an installer copied within it that when installed, will have transformed configuration files. Lovely.
It would actually be pretty awesome to turn all of this into a custom task or two that could be hosted on codeplex – but that is a job for another day.
Disclaimer: There are other techniques to accomplish this like this one, and to tell you the truth I’m not sure which one is less ugly. I think when going down this road you just have to accept the hackiness of the available solutions, and then plan to migrate your installer to something more malleable (and less crap) in the near future such as WIX or Install Shield.
Print | posted on Tuesday, 21 June 2011 1:09 PM