In my last post - TFS Builds for SharePoint Projects ... It's Hard! - I was just starting to look into what it would take to get TFS to build and deploy SharePoint Solutions.
It turns out that getting started on the right track isn't really as hard as I initially thought. I was also happy to find a lot of helpful information out there in the SharePoint community.
Deployability
I'm convinced that deployability is the biggest obstacle that stands in the way of achieving this. I don't think that's a real word - it has a red squiggly under it when I type it - but I take it to mean structuring your projects so that they can be packaged as SharePoint Solutions (WSP cabinet files).
Solutions make your life easier in multiple ways by enabling you to :
- Deploy assemblies to the GAC
- Deploy files to the 12 folder on all the WFEs in your farm
- Package multiple features together
- Bundle in CAS policies and changes to the SharePoint web.config
I've seen that if you don't structure your project around Solutions to begin with, it's hard to steer it back in the right direction. Take the time and do it right up front!
Not to trivialize this, but once you've nailed deployability, this just becomes an exercise in using MSBuild...
Dependencies
Another important thing you need to take care of is to make sure all your project dependencies are available to TFS during the build.
For example, Microsoft.SharePoint.dll doesn't exist on the TFS server by default. You can either GAC it - and other dependencies - on the TFS server, or include it in your solution.
In the walkthrough in this post, I'll add Microsoft.SharePoint.dll into a solution folder and reference it from there. If you use this approach, make sure you store these dependencies at a high enough level in your source tree, so that you only keep 1 copy of each that every project can reference.
Walkthrough
MSDN article written by Ted Pattison to put together a TFS build definition to build and deploy the project to a SharePoint server.
The most common approach I've seen involves using a custom build target in your Visual Studio solution to generate your SharePoint Solution WSP. This requires defining a new build target in your project's csproj or vbproj file and then including the necessary targets file in your Visual Studio solution.
Although you end splitting up pieces of your build script between the targets file and TFSBuild.proj, I'm ok with this because there's value in automatically generating the WSP during a "Desktop Build".
I'll show how you can extend this approach to allow TFS to build and deploy the SharePoint solution for you.
External Tools
The walkthrough uses the MSBuild Community Tasks project; this needs to be installed on the TFS server.
Although I'm not using them in the walkthrough, tools such as STSDEV and WSPBuilder have taken away the grunt work of handling the creation of the DDF and SharePoint WSP files. Definitely worth looking into. There's no reason why you can't integrate these tools into the MSBuild process.
OfficeSpaceFeature.targets and Cab.ddf
The use of a custom build target to run MAKECAB.exe and create a SharePoint WSP has been exhaustively discussed (see here and here), so I won't go into too much detail except to point out some changes.
When you compile in Visual Studio, binaries are dropped in bin\Debug|Release. However, when TFS compiles your code, it puts the binaries outside the root of your source code in Binaries\Debug|Release.
The DDF file used by MAKECAB.exe needs to point to a consistent location so the MAKECAB process handle both a desktop build and a TFS build.
In this case, in the AfterBuild target specified in the OfficeSpaceFeature.targets build target file, we copy the DLL to the DeploymentFiles directory.
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MAKECAB>"C:\Windows\System32\makecab.exe"</MAKECAB>
<DiskDirectory1>"$(OutDir)"</DiskDirectory1>
<DiskDirectory1 Condition="HasTrailingSlash($(OutDir))">"$(OutDir)."</DiskDirectory1>
</PropertyGroup>
<Target Name="OfficeSpaceFeaturePackage">
<Copy SourceFiles="$(TargetPath)" DestinationFiles="$(SourceDir)\DeploymentFiles\$(TargetFileName)"/>
<Exec Command="$(MAKECAB) /F DeploymentFiles\Cab.ddf /D CabinetNameTemplate=$(MSBuildProjectName).wsp /D DiskDirectory1=$(DiskDirectory1)" />
</Target>
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<UsingTask AssemblyFile="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.dll" TaskName="MSBuild.Community.Tasks.MAKECAB" />
</Project>
You can see that in the DDF file, we always pull OfficeSpaceFeature.dll from within the DeploymentFiles directory.
.OPTION EXPLICIT
.Set CabinetNameTemplate=OfficeSpaceFeature.wsp
.Set Cabinet=on
.Set MaxDiskSize=0
.Set CompressionType=MSZIP;
.Set DiskDirectoryTemplate=CDROM;
DeploymentFiles\manifest.xml manifest.xml
TEMPLATE\FEATURES\OfficeSpaceFeature\feature.xml OfficeSpaceFeature\feature.xml
TEMPLATE\FEATURES\OfficeSpaceFeature\elements.xml OfficeSpaceFeature\elements.xml
TEMPLATE\FEATURES\OfficeSpaceFeature\LetterTemplate.docx OfficeSpaceFeature\LetterTemplate.docx
TEMPLATE\LAYOUTS\OfficeSpace\LetterGenerator.aspx LAYOUTS\OfficeSpace\LetterGenerator.aspx
TEMPLATE\IMAGES\OfficeSpace\PithHelmet.gif IMAGES\OfficeSpace\PithHelmet.gif
DeploymentFiles\OfficeSpaceFeature.dll OfficeSpaceFeature.dll
Create a TFS Build Definition
Go through the New Build Definition wizard and create a simple TFS build definition for this project. Queue a new build, and take a look in the build's drop location. You can see that the WSP was automatically created during the build and deposited in the drop location.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CopySharePointWSPTo>\\SharePointWFE\Drop</CopySharePointWSPTo>
<SolutionDeploymentUrl>http://portal</SolutionDeploymentUrl>
<STSADM>"%commonprogramfiles%\microsoft shared\web server extensions\12\bin\stsadm.exe"</STSADM>
</PropertyGroup>
</Project>
By pulling out configuration settings - such as the portal Url - to a separate configuration file, you can use the same build definition in different environments. All you need to change are the values in Environment.proj.
In TFSBuild.proj, you can reference Environment.proj as follows:
<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<!-- Do not edit this -->
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="Environment.proj"/>
You can now refer to the variables in Environment.proj using the MSBuild syntax for variables, e.g. $(SolutionDeploymentUrl)
As the first step in our deployment project, we want to copy the WSP to a location on one of the SharePoint Web Front End servers. We define this network share in the CopySharePointWSPTo property.
We can add a custom BuildStep that outputs the progress to the TFS Build Explorer. We then use the CreateItem syntax to select all the WSP files in the drop location, and copy them to the location at $(CopySharePointWSPTo)\$(BuildNumber)
<Target Name="AfterDropBuild">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)" Name="Copy SharePoint Solution WSP to drop location"
Message="Copy SharePoint Solution WSP to drop location $(CopySharePointWSPTo)\$(BuildNumber)">
<Output TaskParameter="Id" PropertyName="CopyWSPStepId" />
</BuildStep>
<CreateItem Include="$(DropLocation)\$(BuildNumber)\**\*.wsp">
<Output ItemName="SolutionWSPs" TaskParameter="Include"/>
</CreateItem>
<Copy SourceFiles="@(SolutionWSPs)" DestinationFolder="$(CopySharePointWSPTo)\$(BuildNumber)"/>
</Target>
You can see the sequence of build steps in the TFS Build Explorer.
