In this walkthrough, I’ll demonstrate how to achieve automated builds and continuous integration by creating a build script for VSEWSS 1.3 solutions.  The patterns and practices SharePoint Guidance  (11 drop) contains an example build script that this article is based on, but I found some issues with it in practice that I’d like to share.  Before we get started, I’d like to talk about the design of the extensions and some of the problems you may encounter with them. 

VSEWSS 1.3 stores information used to build the wsp file in a directory named “pkg” that is not included in the project.  This works fine for individual development on a single machine, but not so well for team development and build automation.  The pkg directory must be manually included in the project so files needed to build the wsp are included in source control.  If you forget to do this, your teammates won’t be able to build the wsp, and you risk losing deployment configuration.  Furthermore, if you later add a new feature, you also have manually add the new feature subdirectory to the project.  In addition to storing configuration information, the pkg directory is also where solution files (feature.xml for example) are automatically generated.  When you package the wsp (or refresh in the wsp view), the files in the pkg directory must be checked out to be re-generated.  This also causes problems on the build server because the files in the pkg directory are read-only because they are checked into source control.  An improved design would be to store wsp configuration information in a folder/file that is part of the project and use the pkg directory only for generating packages based on the wsp configuration files.  So now that you are aware of some of the team development “gotchas” with VSEWSS 1.3, I’ll show you, step-by-step, how to configure automated builds.

If you just want to see the build script, get it hereUpdate: get a simplified build script here.

If you have not installed TFS Build, you will need to install it using the TFS installation media.  You will also need to configure a service account such as TFSBuild, and add the account to the TFS server group Team Foundation Licensed Users and the Team Project(s) group Build Services.

To get started, configure the build agent.  In the team project select Builds > Manage Build Agents…

Manage Build Agents

Enter the build agent properties for your build server

Build Agent Properties

Create a new build definition by selecting Builds > New Build Definition…

New Build Definition

Enter a name for the build definition

Build Definition General 

Configure the workspace.  Note: all files in the source control folder will be downloaded to the local folder you specify, so be sure the location you specify has enough free space

Build Definition Workspace

Create the project file (TFSBuild.proj) by selecting the version control folder where you want it stored and selecting Create…

Build Definition Project File

Select the Visual Studio solution to build and click Next

MSBuild Project File Creation Wizard Selections

Select the configuration(s) to build and click Next

MSBuild Project File Creation Wizard Configurations

If you want to run unit tests or code analysis during the build, configure it and click Finish

MSBuild Project File Creation Wizard Options

Now that the project file is created, click OK

Build Definition Project File

Configure how many builds you would like to retain and click OK.  Note: you should at least keep the latest of failed or partially succeeded builds or the build log file you need to troubleshoot will be deleted

Build Definition Retention Policy

Enter a share where the build files will be dropped and click OK

Build Definition Defaults

Configure what triggers the build, either manually, on a schedule or when files are checked in (continuous integration)

Build Definition Trigger

Test the build by highlighting the build definition and selecting Queue New Build…

Queue New Build

Click Queue

Queue Build

At this point you should have a working build, but it doesn’t package the wsp file yet

Build Results

Next, we’ll modify the build script to package the wsp file.  The build steps are as follows:

  1. Clean up any workspaces / files left by the previous build packaging
  2. Build the Visual Studio solution
  3. Delete the workspace that was created by the build
  4. Create a workspace for the wsp packaging build
  5. Open a second instance of the IDE and package the solution using the /package switch
  6. Copy the wsp to the drop folder

In addition to this process, a nice to have is to display the packaging steps in the GUI, so we’ll use the BuildStep task to accomplish that

To get started, get the latest version of the Team Build Types folder

Get Latest Version Team Build Types

Check out the TFSBuild.proj file.  Note: when developing the build script, you will have to check it out to work on it, and check it back in to test it

Check Out TFSBuild.proj

Add the custom tasks used by the build.  Although we could accomplish what we need to using TF commands, these tasks are installed with TFS and work well

<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">

  <!-- These tasks are used by the Team Build process defined in this file -->
  <UsingTask TaskName="Microsoft.TeamFoundation.Build.Tasks.DeleteWorkspaceTask"
           AssemblyFile="$(TeamBuildRefPath)\Microsoft.TeamFoundation.Build.Tasks.VersionControl.dll" />
  <UsingTask TaskName="Microsoft.TeamFoundation.Build.Tasks.CreateWorkspaceTask"
             AssemblyFile="$(TeamBuildRefPath)\Microsoft.TeamFoundation.Build.Tasks.VersionControl.dll" />

Add the variables needed for wsp packaging.  Note: there is probably a better way to determine the location of the IDE

  <PropertyGroup>

    <!--  
    Variables added for VSeWSS 1.3 builds 
    -->

    <!--  IDEPath
    The path in which DevEnv and TF reside 
    -->
    <IDEPath>C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE</IDEPath>

    <!-- TempWorkspaceName
    Workspace name, must not match an existing namespace name 
    -->
    <TempWorkspaceName>NightlyBuildTempWorkspace</TempWorkspaceName>
    
  </PropertyGroup>

Add the solution to build.  Note: this will build the binaries, but will not generate the wsp file

  <ItemGroup>

    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../Intranet/Intranet.sln">
        <Targets></Targets>
        <Properties></Properties>
    </SolutionToBuild>

  </ItemGroup>

Add the target to clean up any files left by the previous build packaging (if the previous build failed or was cancelled).  This target is called during the build just before the workspace is created to build the solution

  <!-- Before the build workpace is initialized -->
  <Target Name="BeforeInitializeWorkspace" >

    <!-- Delete temporary workspace (if left by previous build) -->
    <BuildStep
        TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
        Message="Deleting temporary workspace &quot;$(TempWorkspaceName)&quot; (if left by previous build).">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    <DeleteWorkspaceTask
          TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
          BuildUri="$(BuildUri)"
          Name="$(TempWorkspaceName)"
          DeleteLocalItems="true" />

    <DeleteWorkspaceTask
          TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
          BuildUri="$(BuildUri)"
          Name="$(TempWorkspaceName)"
          DeleteLocalItems="false" />

    <BuildStep
              TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
              BuildUri="$(BuildUri)"
              Id="$(StepId)"
              Status="Succeeded" />

    <!-- Error Occurred -->
    <OnError ExecuteTargets="MarkBuildStepAsFailed" />

  </Target>

Add the error handling target.  If any of the other steps fail, they will call this target using ExecuteTargets

    <!-- Handles custom errors -->
  <Target Name="MarkBuildStepAsFailed">
    <BuildStep
        TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
        Id="$(StepId)"
        Status="Failed" />
  </Target>

In the AfterCompile target, add the step to delete the workspace that was automatically created by the build

<!-- After the solutions are compiled -->
  <Target Name="AfterCompile" >

    <!-- Delete build workspace -->
    <BuildStep
            TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
            BuildUri="$(BuildUri)"
            Message="Deleting build workspace &quot;$(WorkspaceName)&quot;.">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    <DeleteWorkspaceTask
          TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
          BuildUri="$(BuildUri)"
          Name="$(WorkspaceName)"
          DeleteLocalItems="false" />

    <BuildStep
              TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
              BuildUri="$(BuildUri)"
              Id="$(StepId)"
              Status="Succeeded" />
    <!-- END Delete build workspace -->

Just below that, add the build step to create the temporary workspace

    <!-- Create temporary workspace -->
    <BuildStep
        TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
        Message="Creating temporary workspace &quot;$(TempWorkspaceName)&quot;.">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    <CreateWorkspaceTask
          TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
          BuildUri="$(BuildUri)"
          BuildDirectory="$(BuildDirectory)"
          SourcesDirectory="$(SolutionRoot)"
          Name="$(TempWorkspaceName)"
          Comment="Temporary workspace">
    </CreateWorkspaceTask>

    <BuildStep
              TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
              BuildUri="$(BuildUri)"
              Id="$(StepId)"
              Status="Succeeded" />
    <!-- END Create temporary workspace -->

Add the step to package the solution.  This first marks all files in the pkg directory as not read only--the packaging will not be able to overwrite these files otherwise.  Then it runs the IDE and uses the /package switch to generate the wsp.  Finally, it copies the wsp to the drop folder.  If you had multiple wsp files to build, you could repeat the steps below or refactor into a target

    <!-- Place projects to package here -->

    <!--  Build and package solution -->
    <BuildStep
        TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
        Message="Packaging &quot; Contoso Intranet solution&quot;.">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    <!-- The extensions modify files in the pkg directory, so those files cannot read only-->
    <Exec Command="attrib -R &quot;$(BuildDirectory)\Contoso\Intranet\Deployment\pkg\*.*&quot; /S /D" />

    <!-- Open a second instance of the dev environment and build using /package switch  -->
    <Exec Command="&quot;$(IDEPath)\devenv&quot; &quot;$(BuildDirectory)\Contoso\Intranet\Intranet.sln&quot; /deploy debug /package" />

    <!-- Copy to drop location -->
    <Exec Command="xcopy &quot;$(BuildDirectory)\Contoso\Intranet\Deployment\bin\debug\Contoso.Intranet.wsp&quot; &quot;$(DropLocation)\$(BuildNumber)\&quot; /E /Y /R" />

    <BuildStep
              TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
              BuildUri="$(BuildUri)"
              Id="$(StepId)"
              Status="Succeeded" />
    <!-- END Build and package solution -->

Add the build step to delete the temporary workspace we created

    <!-- Delete temporary workspace -->
    <BuildStep
        TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
        Message="Deleting temporary workspace &quot;$(TempWorkspaceName)&quot;.">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    
    <DeleteWorkspaceTask
          TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
          BuildUri="$(BuildUri)"
          Name="$(TempWorkspaceName)"
          DeleteLocalItems="false" />

    <BuildStep
              TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
              BuildUri="$(BuildUri)"
              Id="$(StepId)"
              Status="Succeeded" />
    <!-- END Delete temporary workspace -->

Check the project back in to source control, and queue the build to test it