Important Note: This guide is meant to be a general overview and by no means is exhaustive. For detailed information about Site Definitions, Features and Solution Packages see the Windows SharePoint Services SDK (http://msdn2.microsoft.com/en-us/library/ms441339.aspx) and Chapter 9 of Ted Pattison's book "Inside Windows SharePoint Services 3.0".
Understanding SharePoint Solutions and Deployments
Site Definitions
A site definition is the top-level component in WSS that aggregates smaller, more modular definitions to create a complete site template that can be used to provision sites. For example, a site definition usually includes a custom page template for the site's home page and can additionally reference external features to add support for other types of site-specific elements such as custom lists, secondary pages, and web parts. Creating custom site definitions enables you to develop site templates for creating new sites that act as prepackaged business solutions.
Features
Features are the preferred packaging mechanism for SharePoint components. Features are easy to activate within existing sites and also have object and event models that are very powerful. A feature can be activated with a WSS site in five ways:
- Site Settings Web Pages
- STSADM command line
- Site definition references
- Feature activation dependencies
- Feature stapling
Deployment Using Solution Packages
From Chapter 9 of Inside Windows SharePoint Services 3.0 by Ted Pattison:
Because SharePoint solutions are deployed on WSS or MOSS installations that range in size from a single standalone Web server to large enterprise server farms, there needs to be a mechanism to deploy them as a single unit. By deploying a single unit, we can have a supported, testable, and repeatable deployment mechanism. The deployment mechanism that SharePoint uses is the solution package.
Solution packages are critical components for deployment in enterprise scenarios and use the same Visual Studio project formats that you work with on your development box. For all deployments outside of your development environment, you want to use WSS solution packages. Solution packages enable your system administrators to create scriptable installs, an important requirement for any project.
It is also important to test solution package deployments in a controlled WSS environment (that is not the same as your development environment), such as a clean Virtual PC image, to test installation of features, site definitions, assemblies, and configuration changes using solution packages. Solution packages are generally language neutral without localized resources, supplemented by solution language packs that contain the language-specific resources.
A solution package is a compressed .cab file with a .wsp extension containing the components to be deployed on a target web server. A solution package also contains additional metadata that enable the WSS runtime to uncompress the .cab file and install its components. In the case of server farm installations, WSS is able to automate pushing the solution package file out to each Web server in the farm.
Solution packages are deployed by using two steps. The first step is installation, in which WSS copies the .wsp file to the configuration database. The second step is the actual deployment, in which WSS creates a timer job that is processed by all front-end Web servers in the server farm. This greatly simplifies the installation across farm servers and ensures a consistent deployment.
The .wsp file for a solution package can be built using the MAKECAB operating system utility by reading the definition from a .ddf (Diamond Directive File) file. The .ddf file defines the output structure of the .wsp file by referencing each file in its source location and its destination location in the .wsp file. This is one of the more tedious aspects of WSS development because you will likely need to create and maintain the .ddf file by hand.
The metadata for a solution package is maintained in a file named manifest.xml that must be added to the root of the .wsp file. It is the manifest.xml file that tells the WSS runtime which template files to copy into the WSS sytem directories. The manifest.xml file can also instruct WSS to install features and assembly DLL as well as add entries to one or more web.config files for SafeControl entries and code access security settings.
An article on MSDN titled "Solution Deployment with SharePoint 2007" (http://msdn.microsoft.com/msdnmag/issues/07/08/officespace/default.aspx) provides a sample project and walks through creating a solution package. Figure 1 shows a sample project in Visual Studio.NET. Notice the two files in the Solution directory, Cab.ddf and manifest.xml. The file Cab.ddf, see Listing 1, defines which files are placed into the .cab file and their locations. The manifest.xml file instructs the WSS runtime how to read the files in the cab and how to create the files on the file system.
Sample DDF
.OPTION EXPLICIT ; Generate errors
.Set CabinetNameTemplate=OfficeSpaceFeature.wsp
.Set DiskDirectory1=Package
.Set Cabinet=on
.Set MaxDiskSize=0
.Set CompressionType=MSZIP;
.Set DiskDirectoryTemplate=CDROM;
Solution\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
bin\Debug\OfficeSpaceFeature.dll OfficeSpaceFeature.dll
; end of DDF file
Sample manifest.xml
<?xml
version="1.0"
encoding="utf-8" ?>
<Solution
SolutionId="{FCCAEEF6-A49C-488f-BA44-434239FBF4E6}"
xmlns="http://schemas.microsoft.com/sharepoint/">
<FeatureManifests>
<FeatureManifest
Location="HelloWorld\feature.xml" />
</FeatureManifests>
</Solution>
Configure Visual Studio to compile the .wsp
Visual Studio can be configured to compile the feature files into a .wsp whenever the project is built. The following steps show how to configure this.
Prerequisite
You need to download and install The MSBuild Community Tasks Project from http://msbuildtasks.tigris.org/. This includes a set of custom tasks for use in our project.
Create the SharePointFeaturePackage.targets file
MSBuild can be extended by creating .targets files and configuring the project to call specific targets in the file. In this case we will create a file names SharePointFeaturePackage.targets and configure the file to compile the project into a .wsp.
Start out by creating a new file in the solution by right-clicking on the solution name and select Add New Item.:
Select XML File and name it SharePointFeaturePackage.targets:
Create an XML element named Project as follows:
<Project
xmlns=http://schemas.microsoft.com/developer/msbuild/2003>
Because we plan on using the MSBuild Community Tasks in our solution, let's add a reference to them in our .targets file:
<Import
Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
The goal of this .targets file is to use the MakeCab utility to compress all the feature files into a single .wsp file. In order to do this we need to create a property that tells MSBuild where the utility resides in the file system. We also need to pass the output directory into MakeCab so that it knows where to place the compressed file. To accomplish this, we need to create an MSBuild PropertyGroup element and specify these properties:
<PropertyGroup>
<MAKECAB>"C:\Windows\System32\makecab.exe"</MAKECAB>
<DiskDirectory1>"$(OutDir)"</DiskDirectory1>
<DiskDirectory1
Condition="HasTrailingSlash($(OutDir))">"$(OutDir)."</DiskDirectory1>
</PropertyGroup>
Notice two important items about the DiskDirectory1 property. First, we set the default value of DiskDirectory1 to an MSBuild variable called OutDir. On a developer virtual machine, the OutDir variable will be either bin\debug\ or bin\release\. On the build server, OutDir will be the full path to the compilation directory, i.e. D:\DATA\TeamBuilds\MyTFSProject\Sample\Binaries\Debug\. Second, we conditionally add a trailing dot if the OutDir ends with a slash. This is required because MakeCab can't handle the path ending with a slash.
With the properties in place, we are ready to create the MSBuild target for the .wsp creation. MakeCab makes uses the .ddf file created in your project, so make sure you have that in place before trying to compile the project. To create the target, create a new xml element called Target:
<Target
Name="SharePointFeaturePackage">
We next need to consider the path to any assemblies referenced by the DDF. The MakeCab utility allows us to pass parameters, however, I have been unable to correctly pass paths with spaces in them to MakeCab. To get around this, we need to modify the Cab.ddf file in order to set the correct path to the assemblies. We will use a custom MSBuild task from the MSBuild Community Tasks project to accomplish this. First make a copy of the original .ddf file:
<Copy
SourceFiles="DeploymentFiles\Cab.ddf"
DestinationFiles="DeploymentFiles\WorkingCab.ddf" />
Next, let's make sure our .ddf file has some tokens in it that we can replace with the correct path. Replace any hard-coded paths to your assembly (i.e. bin\debug\MyAssembly.dll MyAssembly.dll) with the following:
#AssemblyFilePath# #AssemblyFileName#
Now, back to our .targets file. After the Copy task we can use the custom FileUpdate task to replace the tokens with real values:
<FileUpdate
Files="DeploymentFiles\WorkingCab.ddf"
Regex="#AssemblyFilePath# #AssemblyFileName#"
ReplacementText="%22$(OutDir)$(AssemblyName).dll%22 %22$(AssemblyName).dll%22" />
Now, within the target we can use the Exec task to call the MakeCab utility:
<Exec
Command="$(MAKECAB) /F DeploymentFiles\WorkingCab.ddf /D CabinetNameTemplate=$(MSBuildProjectName).wsp /D DiskDirectory1=$(DiskDirectory1)" />
Additionally, I like to call the Exec task a second time when compiling in debug mode to create another version of the package with a .cab extension. This makes it much easier to look inside the file to see exactly what was added and the folder structure within it:
<Exec
Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU'"
Command="$(MAKECAB) /F DeploymentFiles\WorkingCab.ddf /D CabinetNameTemplate=$(MSBuildProjectName).cab /D DiskDirectory1=$(DiskDirectory1)" />
Configure the Visual Studio project to call the SharePointFeaturePackage target
We now need to configure our Visual Studio project to call the SharePointFeaturePackage target after the project has been compiled. Alternatively, if the project does not contain any class files that need to be compiled, we can configure the project to call our custom target instead of compiling. To do this we have to edit the .csproj file and add import our .targets file. Since a .csproj file is just an MSBuild file, this process should look familiar.
Right-click the project name in Solution Explorer and select "Unload Project":
Once the project has unloaded, right-click the project name in Solution Explorer and select "Edit [your project name].csproj"
Look for the <Import
Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> element and add the following right after it:
<Import
Project="SharePointFeaturePackage.targets" />
You may need to alter the path to the .targets file based on where on the file system you created the file.
The next step is to tell MSBuild to invoke our custom SharePoinFeaturePackage target after the build completes. To do this, locate and uncomment the AfterBuild target and add a call to our custom target:
<Target
Name="AfterBuild">
<CallTarget
Targets="SharePointFeaturePackage" />
</Target>
Compile your project and verify the outputs
At this point, you can now compile your project and verify that the .wsp has been compiled correctly. If everything has gone smoothly both a .wsp and .cab file with the same name as the project should now be placed in the bin\debug or bin\release directory for your project.
Be sure to open your .cab file with Windows Explorer to verify that all of your project files were placed into the package correctly.
Using TFS to Create and Install Solution Packages
Once your project has been configured to compile the project into solution packages, you are now ready to configure Team Foundation Build to copy these packages to the correct location and install them on the server.
Overview of Team Build
Team Foundation Build provides the functionality of a public build lab and is part of Visual Studio Team Foundation Server. With Team Foundation Build, enterprise build managers can synchronize the sources, compile the application, run associated unit tests, perform code analysis, release builds on a file server, and publish build reports. Build result data is propagated to the warehouse for historical reporting. Team Foundation Build works with other Team System tools during the build process, including source control, work item tracking, and with test tools.
Team Build introduces the topic of Customizable Team Build Targets (see http://msdn2.microsoft.com/en-us/library/aa337604.aspx). These targets are extension points allowing us to inject our own processing into the Team Build process.
Team Build Configuration
We will walk through creating and configuring a team build project and will cover the following steps:
- Create new Team Build definition.
- Update version information for assemblies and packages
- Compile the projects in the solution
- Copy build outputs to network file share
- Install any .wsp's on the development SharePoint server
Create new Team Build definition
To create a new Team Build definition, open Visual Studio and connect to your Team Foundation Server. Open the solution you want to build and choose Build, New Build Definition…
You will now be required to enter configuration information for your build. For detailed information about creating a new build see the MSDN Walkthrough: Creating a Build Definition in Team Foundation Build at http://msdn2.microsoft.com/en-us/library/ms181286.aspx.
Once you configure the Team Build you will have a solution that does the following:
- Retrieves the latest version of the source code onto the build server.
- Compiles all projects in the solution and configuration selected to build.
- Copies the build outputs to the network file share.
The next step is to start tweaking the build process to do what we need.
Modify Team Build definition
We now need to open the team build definition to add additional configuration information. To open the team build for editing open TFS source control and navigate to the TeamBuildTypes\[Your Build Name] node. Right-click on your .proj file and choose Check Out for Edit…
Next, open the .proj file for editing. We can now start to configure the MSBuild project file to do exactly what we need it to do.
Automatic Assembly Versioning
Note: After additional testing, we decided against using automatic assembly versioning. It become too problematic because of references to assembly version numbers in feature.xml files, .webpart files, etc. This information is left here for historical purposes.
Our first task is to set a version number for our assemblies and SharePoint solution packages. To do this, we will utilize several of the Team Build extensibility points.
Before we begin we need to add a reference to the MSBuild Community Tasks project. Find the existing Import node and place the following right below it:
<Import
Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
When versioning assemblies, we use the AssemblyVersion attribute and utilize the build number attribute to relate each build to a ChangeSet in TFS Source Control. MSDN documentation explains how to set the AssemblyVersion attribute:
<major version>.<minor version>.<build number>.<revision>
In order to set the AssemblyVersion Attribute, we first need to define attributes to store this information as well as some information about where to find TFS. We also set some additional information about where the URL these packages will be deployed to. To do this we need to create an MSBuild PropertyGroup within the .proj file:
<PropertyGroup>
<!-- Assembly version properties. Add others here -->
<!-- TF.exe -->
<TF>"$(TeamBuildRefPath)\..\tf.exe"</TF>
<TFSClientLocation>$(TeamBuildRefPath)</TFSClientLocation>
<STSADM>"c:\program files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe"</STSADM>
<Major
Condition="'$(Major)'==''">1</Major>
<Minor
Condition="'$(Minor)'==''">0</Minor>
<Build
Condition="'$(Build)'==''">0</Build>
<Revision
Condition="'$(Revision)'==''">0</Revision>
<SolutionDeploymentUrl>http://mysamplesite</SolutionDeploymentUrl>
</PropertyGroup>
Note: This process will overwrite any values set in the AssemblyInfo.cs file.
Now that we have some properties to store the version information, we need to set these values. We first need to create a task that will set the Build property:
<Target
Name="GetTFSVersion">
<TfsVersion
LocalPath="$(SolutionRoot)"
TfsLibraryLocation="$(TFSClientLocation)">
<Output
TaskParameter="Changeset"
PropertyName="Revision"/>
</TfsVersion>
<Message
Text="TFS ChangeSet: $(Revision)" />
</Target>
Next, we will utilize the AfterGet extension target to update the source files with the revision information. This task will checkout all AssemblyInfo and feature.xml files within the solution's source code and update the files with the new version information:
<Target
Name="AfterGet"
Condition="'$(IsDesktopBuild)'!='true'"
DependsOnTargets="GetTFSVersion">
<!-- Set the AssemblyInfoFiles items dynamically -->
<CreateItem
Include="$(SolutionRoot)\**\AssemblyInfo.cs;$(SolutionRoot)\**\feature.xml$(SolutionRoot)\**\manifest.xml">
<Output
ItemName="VersionInfoFiles"
TaskParameter="Include" />
</CreateItem>
<Exec
WorkingDirectory="$(SolutionRoot)"
Command="$(TF) checkout /recursive AssemblyInfo.cs feature.xml manifest.xml"/>
<!-- Now that the files are checked out, we can update the version numbers -->
<FileUpdate
Files="@(VersionInfoFiles)"
Regex="(\d+)\.(\d+)\.(\d+)\.(\d+)"
ReplacementText="$(Major).$(Minor).$(Build).$(Revision)" />
</Target>
We now need to handle what happens after a successful and an unsuccessful build. When the build is successful, we will check in the modified file. When unsuccessful, we will undo the checkout of the files:
<Target
Name="AfterCompile"
Condition="'$(IsDesktopBuild)'!='true'">
<Exec
WorkingDirectory="$(SolutionRoot)"
Command="$(TF) checkin /comment:"Auto-Build: Version Update" /noprompt /override:"Auto-Build: Version Update" /recursive AssemblyInfo.cs feature.xml manifest.xml"/>
</Target>
<!-- In case of Build failure, the AfterCompile target is not executed. Undo the changes -->
<Target
Name="BeforeOnBuildBreak"
Condition="'$(IsDesktopBuild)'!='true'">
<Exec
WorkingDirectory="$(SolutionRoot)"
Command="$(TF) undo /noprompt /recursive AssemblyInfo.cs feature.xml manifest.xml"/>
</Target>
Automatically Installing Feature Packages on the Build Server
The next step in the build process is to install the feature packages on the build server. We will utilize the AfterDropBuild extension target to invoke our tasks. The first step is to manually copy the .wsp and .cab file to the drop directory and remove any non-wsp's from the drop directory:
<Target
Name="AfterDropBuild">
<CreateItem
Include="$(DropLocation)\$(BuildNumber)\**\*.*"
Exclude="$(DropLocation)\$(BuildNumber)\**\*.wsp;$(DropLocation)\$(BuildNumber)\**\*.cab">
<Output
ItemName="NotSolutionPackages"
TaskParameter="Include" />
</CreateItem>
<Message
Text="Attempting to delete anything that is not a solution package or a cab file" />
<Delete
Files="@(NotSolutionPackages)" />
<CallTarget
Targets="InstallSolutionPackages" />
</Target>
Next, we will write MSBuild code to install the solution packages onto the development SharePoint server. The main job of this task is to create a batch file that retracts and installs all of the solution packages using the STSADM utility and logs any messages to an output file (see sample batch files below):
Sample Batch File: DeletePackages.bat
@SET STSADM="c:\program files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe"
@SET SolutionDeploymentUrl="http://mysamplesite"
@Set LOGFILE="\\ny17nt0017\drop\Sample_20080220.7\InstallationLog.txt"
REM -----------------------
ECHO RetractingSolution HelloWorld.wsp >> %LOGFILE%
%STSADM% -o retractsolution -name "HelloWorld.wsp" -immediate >> %LOGFILE%
ECHO RetractingSolution OfficeSpaceFeature.wsp >> %LOGFILE%
%STSADM% -o retractsolution -name "OfficeSpaceFeature.wsp" -url %SolutionDeploymentUrl% -immediate >> %LOGFILE%
ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%
ECHO Deleting Solution HelloWorld.wsp >> %LOGFILE%
%STSADM% -o deletesolution -name "HelloWorld.wsp" >> %LOGFILE%
ECHO Deleting Solution OfficeSpaceFeature.wsp >> %LOGFILE%
%STSADM% -o deletesolution -name "OfficeSpaceFeature.wsp" >> %LOGFILE%
Sample Batch File: InstallPackages.bat
@SET STSADM="c:\program files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe"
@SET SolutionDeploymentUrl="http://mysamplesite"
@Set LOGFILE="\\ny17nt0017\drop\Sample_20080220.7\InstallationLog.txt"
REM -----------------------
ECHO Adding Solution HelloWorld.wsp >> %LOGFILE%
%STSADM% -o addsolution -filename "\\ny17nt0017\drop\Sample_20080220.7\Debug\HelloWorld.wsp" >> %LOGFILE%
ECHO Adding Solution OfficeSpaceFeature.wsp >> %LOGFILE%
%STSADM% -o addsolution -filename "\\ny17nt0017\drop\Sample_20080220.7\Debug\OfficeSpaceFeature.wsp" >> %LOGFILE%
ECHO Deploying Solution HelloWorld.wsp >> %LOGFILE%
%STSADM% -o deploysolution -name "HelloWorld.wsp" -local -allowGacDeployment >> %LOGFILE%
ECHO Deploying Solution OfficeSpaceFeature.wsp >> %LOGFILE%
%STSADM% -o deploysolution -name "OfficeSpaceFeature.wsp" -url %SolutionDeploymentUrl% -local -allowGacDeployment >> %LOGFILE%
ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%
Since this Target is fairly involved I will explain the steps in detail as we go along. For the impatient, here's the entire Target:
<Target
Name="InstallSolutionPackages">
<!--
This target searches for any soltuion packages in the drop location and
attempts to install them on the MOSS environment on the build server.
-->
<BuildStep
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Name="Install Solution Packages"
Message=" SharePoint Solution Packages are being installed">
<Output
TaskParameter="Id"
PropertyName="MyBuildStepId" />
</BuildStep>
<Message
Text="Attempting to delete and install the solution packages" />
<CreateItem
Include="$(DropLocation)\$(BuildNumber)\**\*.wsp;">
<Output
ItemName="SolutionPackages"
TaskParameter="Include" />
</CreateItem>
<Message
Text="All Solution Packages being installed: @(SolutionPackages)" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Overwrite="true"
Lines="@SET STSADM=$(STSADM)"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="@SET SolutionDeploymentUrl=%22$(SolutionDeploymentUrl)%22"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="@Set LOGFILE=%22$(DropLocation)\$(BuildNumber)\InstallationLog.txt%22" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="REM -----------------------" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Overwrite="true"
Lines="@SET STSADM=$(STSADM)"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="@SET SolutionDeploymentUrl=%22$(SolutionDeploymentUrl)%22"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="@Set LOGFILE=%22$(DropLocation)\$(BuildNumber)\InstallationLog.txt%22" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="REM -----------------------" />
<!-- Retract the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO RetractingSolution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o retractsolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 -immediate >> %LOGFILE%" />
<!-- Execute timer jobs -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%" />
<!-- Delete the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO Deleting Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o deletesolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 >> %LOGFILE%" />
<!-- Add the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Adding Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o addsolution -filename %22%(SolutionPackages.FullPath)%22 >> %LOGFILE%" />
<!-- Deploy the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Deploying Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o deploysolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 -local -allowGacDeployment >> %LOGFILE%" />
<!-- Execute timer jobs -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%" />
<FileUpdate
Files="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Regex="(retractsolution|deploysolution) -name %22%(UrlDeploySolutions.Identity)%22"
ReplacementText="$1 -name %22%(UrlDeploySolutions.Identity)%22 -url %SolutionDeploymentUrl%"
Encoding="Windows-1252" /> <!-- Default ANSI code page -->
<FileUpdate
Files="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Regex="(retractsolution|deploysolution) -name %22%(UrlDeploySolutions.Identity)%22"
ReplacementText="$1 -name %22%(UrlDeploySolutions.Identity)%22 -url %SolutionDeploymentUrl%"
Encoding="Windows-1252" /> <!-- Default ANSI code page -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\RedeployPackages.bat"
Lines="CALL $(DropLocation)\$(BuildNumber)\DeletePackages.bat" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\RedeployPackages.bat"
Lines="CALL $(DropLocation)\$(BuildNumber)\InstallPackages.bat" />
<Exec
Command="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
ContinueOnError="True" />
<Exec
Command="$(DropLocation)\$(BuildNumber)\InstallPackages.bat" />
<BuildStep
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(MyBuildStepId)"
Status="Succeeded" />
</Target>
Now to explain what that thing does. The first step in the target is the BuildStep task. This task notifies TeamBuild of Build Steps so that it can update the Build Explorer:
<BuildStep
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Name="Install Solution Packages"
Message="SharePoint Solution Packages are being installed">
<Output
TaskParameter="Id"
PropertyName="MyBuildStepId" />
</BuildStep>
The next step creates a list of solution packages to install and places them in a variable for us to use:
<CreateItem
Include="$(DropLocation)\$(BuildNumber)\**\*.wsp;">
<Output
ItemName="SolutionPackages"
TaskParameter="Include" />
</CreateItem>
Now on to the fun stuff. We now need to create two batch files and fill them with our STSADM commands. We'll start with adding variables to our batch files. We'll first create DeletePackages.bat, then InstallPackages.bat:
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Overwrite="true"
Lines="@SET STSADM=$(STSADM)"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="@SET SolutionDeploymentUrl=%22$(SolutionDeploymentUrl)%22"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="@Set LOGFILE=%22$(DropLocation)\$(BuildNumber)\InstallationLog.txt%22" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="REM -----------------------" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Overwrite="true"
Lines="@SET STSADM=$(STSADM)"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="@SET SolutionDeploymentUrl=%22$(SolutionDeploymentUrl)%22"/>
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="@Set LOGFILE=%22$(DropLocation)\$(BuildNumber)\InstallationLog.txt%22" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="REM -----------------------" />
Now that we have the files created and some variables set, we can start creating commands to retract the solutions if they have already been installed. At this point we start taking advantage of MSBuild batching construct. MSBuild has the ability to divide item collections into different categories, or batches, based on item metadata, and run a target or task one time with each batch. What this means is that each of the WriteLinesToFile tasks will execute for each file in our SolutionPackages variable created at the top of this task.
<!-- Retract the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO RetractingSolution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o retractsolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 -immediate >> %LOGFILE%" />
<!-- Execute timer jobs -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%" />
<!-- Delete the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Lines="ECHO Deleting Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o deletesolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 >> %LOGFILE%" />
Now that the solutions have been retracted and deleted, we can install the solutions using the same batching contruct as above:
<!-- Add the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Adding Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o addsolution -filename %22%(SolutionPackages.FullPath)%22 >> %LOGFILE%" />
<!-- Deploy the Solutions -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Deploying Solution %(SolutionPackages.Filename)%(SolutionPackages.Extension) >> %LOGFILE%
%STSADM% -o deploysolution -name %22%(SolutionPackages.Filename)%(SolutionPackages.Extension)%22 -local -allowGacDeployment >> %LOGFILE%" />
<!-- Execute timer jobs -->
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Lines="ECHO Executing all administrative timer jobs immediately >> %LOGFILE%
%STSADM% -o execadmsvcjobs >> %LOGFILE%" />
At this point we now have 2 batch files that will do the solution deletion and installation. However, SharePoint requires that solutions that make changes to the web.config file (i.e. by adding SafeControl entries) be activated and retracted using the URL Parameter. The first step is to identify the solutin packages that required URL activation. This can be accomplished by looking for SafeControl entries in the solution manifest.xml file. Once the packages have been identified you will need to add an ItemGroup element under the PropertyGroup element created on page 13:
<ItemGroup>
<UrlDeploySolutions
Include="OfficeSpaceFeature.wsp" />
<!-- Add additional .wsp's that require URL activation here
<UrlDeploySolutions Include="PackageA.wsp" />
<UrlDeploySolutions Include="PackageB.wsp" />
<UrlDeploySolutions Include="PackageC.wsp" />
-->
</ItemGroup>
Now that we have a variable containing all the solution packages that require URL activation, we need to modify our batch files using a Regular Expression:
<FileUpdate
Files="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
Regex="(retractsolution|deploysolution) -name %22%(UrlDeploySolutions.Identity)%22"
ReplacementText="$1 -name %22%(UrlDeploySolutions.Identity)%22 -url %SolutionDeploymentUrl%"
Encoding="Windows-1252" /> <!-- Default ANSI code page -->
<FileUpdate
Files="$(DropLocation)\$(BuildNumber)\InstallPackages.bat"
Regex="(retractsolution|deploysolution) -name %22%(UrlDeploySolutions.Identity)%22"
ReplacementText="$1 -name %22%(UrlDeploySolutions.Identity)%22 -url %SolutionDeploymentUrl%"
Encoding="Windows-1252" /> <!-- Default ANSI code page -->
At this point we'll create one more batch file that simple calls the other two to allow an administrator to run both batch files manually:
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\RedeployPackages.bat"
Lines="CALL $(DropLocation)\$(BuildNumber)\DeletePackages.bat" />
<WriteLinesToFile
File="$(DropLocation)\$(BuildNumber)\RedeployPackages.bat"
Lines="CALL $(DropLocation)\$(BuildNumber)\InstallPackages.bat" />
OK, now we are ready to have MSBuild execute the batch file. First we will execute DeletePackages.bat and allow it to continue if there are error. Second we will execute InstallPackages.bat and allow any errors to bubble up through the build process.
<Exec
Command="$(DropLocation)\$(BuildNumber)\DeletePackages.bat"
ContinueOnError="True" />
<Exec
Command="$(DropLocation)\$(BuildNumber)\InstallPackages.bat" />
The last step in this target is to finish the Build Step:
<BuildStep
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(MyBuildStepId)"
Status="Succeeded" />

Verifying the build
At this point you should have a working build process. To test the build, open your project in Visual Studio and select Build, Queue New Build. Set your configuration options, or just keep the default and click Queue.
The Build Explorer will open and you can double-click on your build to view the progress of the build. Once the build completes, you can click the Log link to view any output messages from the STSADM commands.
Tips & Tricks
Getting site column and content type xml
Andrew Connell has created stsadm extensions to aid in the development of the required xml files for site columns and custom content type. This can be a real time saver:
http://andrewconnell.com/blog/articles/MossStsadmWcmCommands.aspx