Monthly Archives: June 2010

Setting up a self-contained build

Here is something you may have experienced already: As a newcomer on an existing project, you check out the code from source-control and discover that the build is broken. When you ask around no-one else seems to have that problem but a helpful collegue is kind enough to tell you that you can find the installers for the missing dependencies at location X (Let’s not even mention the places where those installers are not available *sigh*).

Anway, in order to avoid such a situation you could organize your solution in such a way that all the dependencies (libraries and tools) are part of it. A typical folder structure would look like this:

screenshot of typical solution folder organization

In order to get those files out of the installer and in your solution (instead of installed under %Program Files%) you could do an administrative install of the msi (eg: msiexec /a Blah.msi) but i find it easier to use Qwerty.Msi.

Here are a couple of settings you may want to add to your build configuration in order to make your self-contained build work:

<!-- Configure solution directories -->
<basePath Condition="'$(BasePath)'==''">$(MSBuildThisFileDirectory)..</basePath>
<buildPath>$(BasePath)\build</buildPath>
<sourcePath>$(BasePath)\src</sourcePath>
<toolsPath>$(BasePath)\tools</toolsPath>

<!-- Configure tool directories -->
<!-- the ending \ is required for the extension pack -->
<extensionTasksPath>$(ToolsPath)\MSBuild.ExtensionPack\</extensionTasksPath>
<msbuildCommunityTasksPath>$(ToolsPath)\MSBuildCommunityTasks</msbuildCommunityTasksPath>
<ilmergeToolPath>$(ToolsPath)\ILMerge</ilmergeToolPath>
<svnToolPath>$(ToolsPath)\Subversion\bin</svnToolPath>
<!-- wix will use this property to determine the location of other files -->
<wixToolPath>$(ToolsPath)\Wix</wixToolPath>

<!-- Configure target file paths -->
<commonTargetsPath>$(BuildPath)\common.targets</commonTargetsPath>
<msbuildCommunityTasksTargetsPath>$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets</msbuildCommunityTasksTargetsPath>
<extensionPackTargetsPath>$(ExtensionTasksPath)MSBuild.ExtensionPack.tasks</extensionPackTargetsPath>

<!-- Configure WIX -->
<wixTargetsPath>$(WixToolPath)\Wix.targets</wixTargetsPath>
<wixTasksPath>$(WixToolPath)\WixTasks.dll</wixTasksPath>
<wixTargetsPath>$(WixToolPath)\Wix.targets</wixTargetsPath>
<wixTasksPath>$(WixToolPath)\WixTasks.dll</wixTasksPath>
<luxTargetsPath>$(WixToolPath)\Lux.targets</luxTargetsPath>
<luxTasksPath>$(WixToolPath)\LuxTasks.dll</luxTasksPath>
<luxTargetsPath>$(WixToolPath)\Lux.targets</luxTargetsPath>
<luxTasksPath>$(WixToolPath)\LuxTasks.dll</luxTasksPath>

With this solution in place the next ‘new guy’ does not have to waste time trying to figure out where those dependencies are ;)

Convention over configuration with MSBuild

A while ago i blogged that i was using the TemplateFile task from the MSBuild Community Tasks Project to generate configuration files. Each project that required templating would have modified it’s csproj file as following:

<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. -->
<import Project="$(MSBuildProjectDirectory)\config.msbuild" />
<target Name="BeforeBuild">
 <callTarget Targets="GenerateConfigurationFiles" />
</target>

And each of these config.msbuild files looked as following:

<templateFile Template="web.template.config" OutputFileName="web.config" Tokens="@(TemplateTokens)" />
<templateFile Template="Config\WcfClients.config" OutputFileName="Config\WcfClients.config" Tokens="@(TemplateTokens)" />
</target>

As you can notice the convention here is that each template file has ‘.template.’ in it’s name, and the name of an output file is the template file name without ‘.template.’.

 <!-- valide input -->
 <error Condition="'$(SourceFile)'==''" Text="Missing SourceFile" />
 <!-- calculate destination file -->
 <regexReplace Input="$(SourceFile)" Expression="(\.template)\." Replacement="." Count="1">
  <output TaskParameter="Output" PropertyName="DestinationFile" />
 </regexReplace>
 <!-- generate file -->
 <templateFile Template="$(SourceFile)" OutputFileName="$(DestinationFile)" Tokens="@(TemplateTokens)" />
</target>

Now that we can do it for one file, we can do it for many files too:

 <!-- valide input -->
 <error Condition="'$(SourceDir)'==''" Text="Missing SourceDir" />
 <!-- find all template files -->
 <itemGroup>
  <templateFiles Include="$(SourceDir)\**\*.template.*" Exlude="$(SourceDir)\**\*.svn*" />
 </itemGroup>
 <!-- process each template file -->
 <msbuild Projects="$(MSBuildProjectFile)" Targets="ProcessTemplate" Properties="SourceFile=%(TemplateFiles.FullPath)" />
</target>

After these core improvements we wrote a common.proj.targets file as following:

 <!-- import global variables -->
 <import Project="$(MSBuildThisFileDirectory)\configuration.proj" />

 <propertyGroup>
  <buildDependsOn>CommonBeforeBuild;$(BuildDependsOn);CommonAfterBuild</buildDependsOn>
 </propertyGroup>

 <target Name="CommonBeforeBuild">
  <msbuild Projects="$(CommonTargetsPath)" Targets="ProcessTemplates" Properties="SourceDir=$(MSBuildProjectDirectory)" />
 </target>

 <target Name="CommonAfterBuild">
  <!--<msbuild Projects="$(CommonBuildTargetsPath)" Targets="PEVerify" Properties="SourceFile=$(TargetPath)" />-->
 </target>
</project>

Now we only need to import our common.proj.targets file in projects that have template files and focus on real business problems ;)