Team Build: How do you deal with dependencies in builds?

A question came up yesterday from Tim Mulholland on the MSDN forums around directory structure when you need to share assemblies between different teams.  As I was throwing in my two cents, I realized that this might make a good post, so here’s Tim’s original question and my comments.  I’ve also linked to the entire thread as there are other comments that are worth the read.

Tim’s Question:

“I am trying to set up my build machine to be able to build some of our projects (we’ve moved to TFS for source/version control and work item tracking, but not yet for builds).

What are the “best practices” for having all of the required files on the machine to be able to build the sources. Not everything is included in source control for the project being built.

Examples of things i’m talking about might be:

  • Output of other projects developed by our team.
  • Output of other projects developed by other teams (or 3rd parties).
  • Includes/Libs from SDKs – WM SDK is the one i’m dealing with right now.

I imagine that this can be done quick and dirty by just mimicking the directory structure on the dev machine, but i’d prefer to make it “cleaner” and have some type of best practices to follow.”

My comments:

I can’t say that these are “Best Practices”, but only that they are “My Practices.” 

I am configuring our build as we speak.  Our system is composed of (currently) 4 Team Projects with about a dozen solutions housing 100+ projects.  This is our initial build under Team Build, it will get much worse.

To deal with the question of “How do we access the output of other projects”, we (the dev leads and I) decided on a “well-known” location for all references.  We created a local folder (C:ReferenceLibrary) that would be mapped to the R: drive letter(to make it machine-agnostic).  We then added a post-build task to each project to copy the project’s output to the R: drive in a folders structure like:

R:[TeamProjectName][AssemblyName][AssemlyVersion]assemblyname.dll

The post-build task was a simple xcopy like this:

xcopy “$(TargetDir)$(TargetName).*” “R:Common$(TargetName)1.0” /Y

This allowed us to setup a project’s references to the assemblies on R: drive.  (To make this work, you have to set the reference’s SpecificVersion properety to True).  I then added a bit of MSBuild magic code to each project file to let it search the R: drive for referenced assemblies from the Team Projects that it uses.  Because of this searching, it is imperative that the SpecificVersion property on the reference is True.  The “RetrieveCommonReferenceFolders” (for dependencies in the Common Team Project) target is hooked into the project by overriding the “BeforeResolveReferences” target like so:

<Target Name="BeforeResolveReferences"
DependsOnTargets=
"RetrieveCommonReferenceFolders" />

Then it was just a matter of creating the RetrieveCommonReferenceFolders target to recurse through the folder list on the R: drive and add all of the found folders to the standard “AssemblySearchPath” property.

<Target Name="RetrieveCommonReferenceFolders">
<CreateItem Include="R:Common***.*">
<Output ItemName="Common"
TaskParameter="Include" />

CreateItem>
<CreateItem Include=
"@(Common->'%(RootDir)%(Directory)')">

<Output ItemName="CommonReferencePath"

TaskParameter="Include" />

CreateItem>


<CreateProperty Value=
"@(CommonReferencePath->'%(FullPath)');
$(AssemblySearchPaths)"
>
<Output TaskParameter="Value"
PropertyName="AssemblySearchPaths"/>

CreateProperty>




<CreateProperty Value=
"@(CommonReferencePath->'%(FullPath)');
$(ReferencePath)"
>
<Output TaskParameter="Value"

PropertyName="CommonReferencePath" />

CreateProperty>


<Message Text="GetCommonReferences:
RefPath=$(CommonReferencePath)"

Importance
="High" />


<Message Text="GetCommonReferences:
AssySearchPathBefore=$(AssemblySearchPaths)"
Importance="High" />



Target>

Since we prepended our folders to the existing AssemblySearchPath property, MSBuild will look in the R: drive folders first.  When we have references in multiple Team Projects, we just add another target for the next Team Project and then add that target’s name to the BeforeResolveReferences target’s DependsOnTargets list.  Also to make adding this to all of these project file easier, I put the targets into their own References.targets file and then just used an “Import” in the project file.  This cuts down on the places that I have to change when a new Team Project is added.

Addidional Comments: To make this work you need to be able to run the SUBST command to map the drive letter “R” to your ReferenceLibrary folder (which must be shared).  Because we reflect over these “referenced” assemblies in our code generation, we had to run the SUBST command under the System account so that if could be seen during code generation.  Luckily I work with some very bright and ingenious people that solved this problem by creating a small Windows Service that runs at Startup and does the SUBST on the System account for us.