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 here. Update: 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…
Enter the build agent properties for your build server
Create a new build definition by selecting Builds> New Build Definition…
Enter a name for the build definition
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
Create the project file (TFSBuild.proj) by selecting the version control folder where you want it stored and selecting Create…
Select the Visual Studio solution to build and click Next
Select the configuration(s) to build and click Next
If you want to run unit tests or code analysis during the build, configure it and click Finish
Now that the project file is created, click OK
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
Enter a share where the build files will be dropped and click OK
Configure what triggers the build, either manually, on a schedule or when files are checked in (continuous integration)
Test the build by highlighting the build definition and selecting Queue New Build…
Click Queue
At this point you should have a working build, but it doesn’t package the wsp file yet
Next, we’ll modify the build script to package the wsp file. The build steps are as follows:
- Clean up any workspaces / files left by the previous build packaging
- Build the Visual Studio solution
- Delete the workspace that was created by the build
- Create a workspace for the wsp packaging build
- Open a second instance of the IDE and package the solution using the /package switch
- 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
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
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
<ProjectDefaultTargets="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 --><UsingTaskTaskName="Microsoft.TeamFoundation.Build.Tasks.DeleteWorkspaceTask"AssemblyFile="$(TeamBuildRefPath)\Microsoft.TeamFoundation.Build.Tasks.VersionControl.dll"/><UsingTaskTaskName="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><SolutionToBuildInclude="$(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 --><TargetName="BeforeInitializeWorkspace"><!-- Delete temporary workspace (if left by previous build) --><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Message="Deleting temporary workspace "$(TempWorkspaceName)" (if left by previous build)."><OutputTaskParameter="Id"PropertyName="StepId"/></BuildStep><DeleteWorkspaceTaskTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Name="$(TempWorkspaceName)"DeleteLocalItems="true"/><DeleteWorkspaceTaskTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Name="$(TempWorkspaceName)"DeleteLocalItems="false"/><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Id="$(StepId)"Status="Succeeded"/><!-- Error Occurred --><OnErrorExecuteTargets="MarkBuildStepAsFailed"/></Target>
Add the error handling target. If any of the other steps fail, they will call this target using ExecuteTargets
<!-- Handles custom errors --><TargetName="MarkBuildStepAsFailed"><BuildStepTeamFoundationServerUrl="$(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 --><TargetName="AfterCompile"><!-- Delete build workspace --><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Message="Deleting build workspace "$(WorkspaceName)"."><OutputTaskParameter="Id"PropertyName="StepId"/></BuildStep><DeleteWorkspaceTaskTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Name="$(WorkspaceName)"DeleteLocalItems="false"/><BuildStepTeamFoundationServerUrl="$(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 --><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Message="Creating temporary workspace "$(TempWorkspaceName)"."><OutputTaskParameter="Id"PropertyName="StepId"/></BuildStep><CreateWorkspaceTaskTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"BuildDirectory="$(BuildDirectory)"SourcesDirectory="$(SolutionRoot)"Name="$(TempWorkspaceName)"Comment="Temporary workspace"></CreateWorkspaceTask><BuildStepTeamFoundationServerUrl="$(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 --><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Message="Packaging " Contoso Intranet solution"."><OutputTaskParameter="Id"PropertyName="StepId"/></BuildStep><!-- The extensions modify files in the pkg directory, so those files cannot read only--><ExecCommand="attrib -R "$(BuildDirectory)\Contoso\Intranet\Deployment\pkg\*.*" /S /D"/><!-- Open a second instance of the dev environment and build using /package switch --><ExecCommand=""$(IDEPath)\devenv" "$(BuildDirectory)\Contoso\Intranet\Intranet.sln" /deploy debug /package"/><!-- Copy to drop location --><ExecCommand="xcopy "$(BuildDirectory)\Contoso\Intranet\Deployment\bin\debug\Contoso.Intranet.wsp" "$(DropLocation)\$(BuildNumber)\" /E /Y /R"/><BuildStepTeamFoundationServerUrl="$(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 --><BuildStepTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Message="Deleting temporary workspace "$(TempWorkspaceName)"."><OutputTaskParameter="Id"PropertyName="StepId"/></BuildStep><DeleteWorkspaceTaskTeamFoundationServerUrl="$(TeamFoundationServerUrl)"BuildUri="$(BuildUri)"Name="$(TempWorkspaceName)"DeleteLocalItems="false"/><BuildStepTeamFoundationServerUrl="$(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