Building and Packaging SharePoint Solutions
In my last post I described a strategy for: Planning SharePoint Solution Packages. In this post we'll construct a sample solution template, use that template to construct a real solution, package this as a WSP, and finally (once you have many WSPs) wrap the entire process with a routine to automate the build. You can download the sample code from the WSPSolution project on CodePlex. Let's get started.
The strategy uses Visual Studio 2008 to build the assemblies and WSPBuilder to generate SharePoint packages. You can vary the locations and specifics to match your standards. For example it's fine to use your My Documents\Visual Studio 2008\Projects subtree rather than c:\dev, especially if multiple developers need to share a development server. When each developer has an individual server, I like to keep paths brief and consistent so we can share build and backup scripts, but any good source control app (like TFS) will manage these paths for you. The steps here will use a brief path (c:\dev) to illustrate the concepts.
Preparing the development machine
Create a standard development subtree:
1. Create a development folder (e.g. c:\dev).
2. Create a utility app folder (e.g. c:\dev\utils).
3. Create a folder for WSP Builder (e.g. c:\dev\utils\wsp).
4. Download WSPBuilder and install or unpack it so the executable is in your c:\dev\utils\wsp folder
5. Download SPDisposeCheck and install or unpack it so the executable is in your c:\dev\utils folder.
6. Create a c:\dev\projects\SampleProject\Deployment subtree.
The sample project will contain the empty templates for typical SharePoint projects (e.g. web parts, content types, etc.) that you will copy each time you want to use these elements in real solutions. You can either build these projects yourself or start with samples generated by STSDEV.
7. Add a script to your sample deployment folder to run SPDisposeCheck against your generated assemblies:
c:\dev\projects\SampleProject\Deployment\CallDisposeCheck.bat
\dev\utils\SPDisposeCheck .\gac
\dev\utils\SPDisposeCheck .\80\bin
pause
8. Add a script to create a solution package. Note that setting clean-up to false will leave a copy of the generated manifest.xml in the Deployment folder. The manifest shows you everything being packaged, and since feature and assembly names can change over time, it should be reviewed to make sure old names and versions aren't being packaged.
c:\dev\projects\SampleProject\Deployment\CreatePackage.bat
\dev\utils\wsp\wspbuilder -WSPName PackageName.wsp -SolutionId Paste-A-Solution-Id-Here-Without-Braces -Cleanup false
pause
9. Add a script to install your package. Note that this script is intended for a development machine and installs the solution directly (-local) rather than with a timer job. Remove the -local switch to deploy to a farm with multiple WFEs.
c:\dev\projects\SampleProject\Deployment\install.bat
stsadm -o addsolution -filename PackageName.wsp
stsadm -o deploysolution -name PackageName.wsp -local -allowgacdeployment -force
pause
9. Add a script to uninstall your package.
c:\dev\projects\SampleProject\Deployment\uninstall.bat:
stsadm -o retractsolution -name PackageName.wsp -local
stsadm -o deletesolution -name PackageName.wsp
pause
10. Add folders for the WSP deployment targets (per WSPBuilder's documentation). In practise these can be created as you go rather than up-front. Not all paths are used in every package so it reduces clutter to only add the subtrees you deploy files into.
c:\dev\projects\SampleProject\Deployment\12
c:\dev\projects\SampleProject\Deployment\80\bin
c:\dev\projects\SampleProject\Deployment\GAC
Building SharePoint solutions in Visual Studio
1. Create the project folder that will contain all of your solutions.
c:\dev\projects\MyProject
2. Create a folder for your first solution.
c:\dev\projects\MyProject\MyProject1Global
3. Create and configure the deployment subtree
a) Copy the sample deployment subtree (created earlier) into your solution's subtree.
c:\dev\projects\MyProject\MyProject1Global\Deployment\...
b) Update the deployment script placeholders with the real names
- Replace PackageName.wsp with the real name (e.g. MyProject1Global.wsp)
- Generate a solution GUID and paste it into CreatePackage.bat
3. Create a new Visual Studio solution.
The trick is to create the solution with the name you want for the folder, and then to rename it to the way you want it to be displayed in Visual Studio. Here the name you want for the folder is the root of your namespace (e.g. MyCompany), and to display this as MyProject1Global in Visual Studio.
a. Click File, New Project, Other Project Types, Visual Studio Solutions, Blank Solution.
b. Name: MyCompany
Location: c:\dev\projects\MyProject\MyProject1Global\
Click OK.
c. Right-click Solution 'MyCompany' in the Solution Explorer, and click Rename.
d. Type MyProject1Global and press Enter.
You now have the following folder structure with one folder to build the deployment package, and one to reflect the top of your namespace which will contain your VS solution:
c:\dev\projects\MyProject\MyProject1Global\Deployment\
c:\dev\projects\MyProject\MyProject1Global\MyCompany\
Adding a Project to the Solution
Add physical folders (not virtual Visual Studio folders) to reflect the namespace down to where you will create projects. As when creating the sample deployment sub-tree, it's best to do this as you actually create projects, since you won't need every type in every project.
As an example, let's add a web part project with these characteristics:
Namespace: MyCompany.ProjectName.SharePoint.WebParts
Class: SuperCoolWP
Assembly: MyCompany.ProjectName.SharePoint.WebParts
Physical path: c:\dev\projects\MyProject\MyProject1Global\MyCompany\ProjectName\SharePoint\WebParts\SuperCoolWP.cs
Steps to make this happen:
1. Create the physical folders where all web parts will go:
c:\dev\projects\MyProject\MyProject1Global\MyCompany\ProjectName\SharePoint\
2. Either -
a) Copy an empty web part project into this location and you're done, or
b) Create a new project:
i. Right-click the solution in the Solution Explorer
ii. Click Add, New project
iii. Click Visual C# (or whatever your language of choice), Class library
iv. Enter the details (note: always Browse to the location to be sure you created it correctly):
Name: WebParts
Location: c:\dev\projects\MyProject\MyProject1Global\MyCompany\ProjectName\SharePoint
Click OK.
c) Delete the auto-generated Class1.cs file and either copy in your own web part template, or build a new web part from scratch. Add your project references and using statements, change the default class to inherit from WebPart, add the standard methods, and so on.
d) Create the feature files.
i. Add a 12 subtree to the project. Note that I cheat here and skip the "SharePoint" part of the namespace in the feature's folder name, unlike in the namespace for the source code, because here it's redundant. You can keep it for absolute consistency, but if your goal is to be able to locate deployed features in the 12 hive, you can do without it.
...\WebParts\WebPart1\12\TEMPLATE\FEATURES\MyCompany.ProjectName.WebParts\
ii. Create (or copy) your feature.xml, elements.xml and supporting files (e.g. SuperCoolWP.webpart) into the feature folder.
f) If you're copying the folder out of your sample project, then back in Visual Studio you can just highlight your project and click the Show All Files icon at the top of the Solution Explorer to see the files you've added, expand the 12 subtree, and right-click "Include in Project" for each file you want in the project.
Note that if you followed these steps, then every web part you create for the business project will be added as a new class to the same WebParts project in Visual Studio, and they will share an assembly. An alternative would be to create each web part as a separate project in Visual Studio, so that each would have its own assembly and you could combine them as elements into Features as you like. If you need to activate or deactivate your web parts as separate features, more projects might be a good idea. It would also fit with the recommended strategy to "build more features in fewer solutions."
Just as we first named the solution according to the physical path we wanted to create, and then renamed it so the display name would be meaningful in Visual Studio, we do the same with each project. Usually this means naming the folder according to the simple class name (e.g. Uls), and then changing the display name to reflect the full namespace (e.g. Company.SharePoint.Diagnostics.Logging.Uls). For SharePoint elements where developers tend to create a new project for every element (like web parts), rather than create-then-renaming, it also works to sort projects into virtual folders (e.g. ApplicationPages, ContentTypes, WebParts) and then to move projects into these. The same steps apply as above, the physical path will be the same as above (and will reflect the actual namespace), but when the goal is to visually group Web Parts together, virtual folders are great.
Create the Post-build Events
The final bit of magic is to build a bridge from the code to the packaging. In post-build events we'll copy the project's output into the Deployment subtree, and from there we'll be able to build a package any time with the little script we wrote earlier.
1. Right-click the project title in Solution Explorer, click Properties.
2. Click the Build Events tab
3. Paste in the appropriate code snippet. For example, to deploy a web part feature and install its assembly to the GAC:
REM Deploy features and supporting files to the 12 hive
xcopy /E /Y "$(ProjectDir)12\*.*" "$(SolutionDir)..\Deployment\12\"
xcopy /E /Y "$(TargetDir)$(TargetFileName)" "$(SolutionDir)..\Deployment\gac\"
4. If you don't already have a project template, copy your project subtree over to your sample project now!
The beauty here is that you can work each of your projects individually, and see how they will deploy because you take the time to recreate the 12 hive local to each. Then every project in the solution is aggregated into the Deployment subtree, where WSPBuilder generates the complete total package. Visual Studio aliases provide another bit of magic as you can re-use the same snippets every time you create a feature of a certain type. When it comes time to change from "debug" to "release" mode these aliases automatically grab the right output from each project into the next built package.
Sample post-build snippets:
REM Deploy features and supporting files to the 12 hive
xcopy /E /Y "$(ProjectDir)12\*.*" "$(SolutionDir)..\Deployment\12\"
REM Deploy assemblies to the GAC
xcopy /E /Y "$(TargetDir)$(TargetFileName)" "$(SolutionDir)..\Deployment\gac\"
REM Deploy assemblies to the BIN
xcopy /E /Y "$(TargetDir)$(TargetFileName)" "$(SolutionDir)..\Deployment\80\bin\"
REM Application Page with assembly (from the project root, assumes no files are in the project's 12 subtree)
xcopy "$(ProjectDir)*.aspx" "$(SolutionDir)..\Deployment\12\TEMPLATE\LAYOUTS\MyProject\"
xcopy "$(ProjectDir)*.xml" "$(SolutionDir)..\Deployment\12\TEMPLATE\LAYOUTS\MyProject\"
xcopy /E /Y "$(ProjectDir)12\*.*" "$(SolutionDir)..\Deployment\12\"
xcopy /E /Y "$(TargetDir)$(TargetFileName)" "$(SolutionDir)..\Deployment\gac\"
Automating the build
Maybe I wrote "final bit of magic" too soon, because this is pretty cool. Let's say you used this strategy to build a set of solutions and now you want to make it all happen in one step. No problem. Your script (okay, batch file) will look something like this:
1. Create a build folder: c:\dev\projects\MyProject\build
2. Add the following script to the build folder, edit to taste (lines are REM'ed out to build additional solutions).
c:\dev\projects\MyProject\build\Build.bat:
ECHO Rebuilding Visual Studio solutions . . .
"C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" ..\MyProject1Global\MyCompany\MyProject1Global.sln /Rebuild | find "Rebuild All"
REM "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" ..\MyProject2SPAdmin\MyCompany\MyProject2SPAdmin.sln /Rebuild | find "Rebuild All"
REM "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" ..\MyProject3SPBase\MyCompany\MyProject3SPBase.sln /Rebuild | find "Rebuild All"
REM "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" ..\MyProject4SiteTypeThis\MyCompany\MyProject4SiteTypeThis.sln /Rebuild | find "Rebuild All"
REM "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" ..\MyProject4SiteTypeThat\MyCompany\MyProject4SiteTypeThat.sln /Rebuild | find "Rebuild All"
REM This version can be used for Visual Studio 2005
REM "c:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.com" ..\MyProject1Global\MyCompany\MyProject1Global.sln /Rebuild | find "Rebuild All"
ECHO Building WSP files. . .
cd ..\MyProject1Global\Deployment\
call CreatePackage.bat
xcopy /Y MyProject1Global.wsp ..\..\build\
REM cd ..\..\MyProject2SPAdmin\Deployment\
REM call CreatePackage.bat
REM xcopy /Y MyProject2SPAdmin.wsp ..\..\build\
REM cd ..\..\MyProject3SPBase\Deployment\
REM call CreatePackage.bat
REM xcopy /Y MyProject3SPBase.wsp ..\..\build\
REM cd ..\..\MyProject4SiteTypeThis\Deployment\
REM call CreatePackage.bat
REM xcopy /Y MyProject4SiteTypeThis.wsp ..\..\build\
REM cd ..\..\MyProject4SiteTypeThat\Deployment\
REM call CreatePackage.bat
REM xcopy /Y MyProject4SiteTypeThat.wsp ..\..\build\
pause
3. Enjoy.
Conclusion and Downloads
To encourage the use of the standards described here I've launched a CodePlex project called WSPSolution. Downloads include sample solutions, snippets, and more. Contributions, questions and new ideas are always welcome.