Using NAnt to Build SharePoint Solutions

Andrew Connell wrote an excellent blog entry on building your WSS solution packages with MSBuild. My problem is that I can't stand MSBuild and find it crazy complicated for even the simplest of tasks. Andrew's post possibly led to the creation (or at least contribution) of STSDEV, a very interesting value-added tool by Ted Pattison and co. that helps ease the pain of building SharePoint solutions. However I found it has it's issues and doesn't really work the way I like to (for example I don't like having everything in one single assembly).

My choice of build tool these days is NAnt (although I'm starting to look at something like Rake or even Boo to make building easier using a DSL) and I find it easier (in my feeble brain anyway) to build and deploy SharePoint solutions with NAnt. I've blogged about it before, but that was v2.0 and here we are in 2008 with new shiny happy solution packages. So here we go.

First we'll start with a basic NAnt build script. When I start a project I create it and set a default target of "help" then in that describe the targets you can run. This provides some documentation on the build process and let's me get a build file up and running.

<?xml version="1.0" encoding="utf-8"?>
<project name="SharePointForums" default="help">
    <target name="help">
        <echo message="--------------------" />
        <echo message="targets in this file" />
        <echo message="--------------------" />
        <echo message="help - display targets in the nant script. This is the default target." />
        <echo message="clean - cleans up the temporary directories" />
        <echo message="init - sets up the temporary directories" />
        <echo message="compile - compiles the solution assemblies" />
        <echo message="test - compiles the solution assemblies then runs unit tests" />
        <echo message="build - builds the entire solution for packaging/installation/distribution" />
        <echo message="dist - creates a distribution zip file containing solution installer, wsp, and config files" />
        <echo message="---------------------------" />
        <echo message="targets in SharePoint.build" />
        <echo message="---------------------------" />
        <echo message="addsolution - installs the solution on the SharePoint server making it avaialble for deployment" />
        <echo message="deploysolution - deploy the solution to the local server for the first time" />
        <echo message="retractsolution - removes the deployed solution from the local server" />
        <echo message="deletesolution - removes the solution from the server completely. calls retractsolution first" />
    </target>
</project>

In fact, at this point I can check this into CruiseControl.NET and it'll build successfully.

Doesn't do much at this point, but it's our roadmap. You'll notice there are a few targets listed in a file called SharePoint.build. I've found that these are typical and never change, you just change the properties of the filenames they act on. Let's take a look at this file:

<?xml version="1.0" encoding="utf-8"?>
<project name="SharePoint">
 
  <!-- directory and file names, generally won't change -->
  <property name="build.dir" value="${root.dir}\build" />
  <property name="solution.dir" value="${source.dir}\solution" />
  <property name="deploymentfiles.dir" value="${solution.dir}\DeploymentFiles" />
  <property name="tools.dir" value="${root.dir}\tools" />
 
  <!-- executable files that shouldn't change -->
  <property name="makecab.exe" value="${tools.dir}\makecab\makecab.exe" />
  <property name="stsadm.exe" value="${tools.dir}\stsadm\stsadm.exe" />
 
  <target name="buildsolutionfile">
    <exec program="${makecab.exe}" workingdir="${solution.dir}">
      <arg value="/F" />
      <arg value="${deploymentfiles.dir}\${directives.file}" />
      <arg value="/D" />
      <arg value="CabinetNameTemplate=${package.file}" />
    </exec>
    <move 
      file="${deploymentfiles.dir}\${package.file}"
      tofile="${build.dir}\${package.file}" />
  </target>
 
  <!-- stsadm targets for deployment -->
  <target name="addsolution">
    <exec program="${stsadm.exe}" verbose="${verbose}">
      <arg value="-o" />
      <arg value="addsolution" />
      <arg value="-filename" />
      <arg value="${build.dir}\${package.file}" />
    </exec>
    <call target="spwait" />
  </target>
 
  <target name="spwait" description="Waits for the timer job to complete.">
    <exec program="${stsadm.exe}" verbose="${verbose}">
      <arg value="-o" />
      <arg value="execadmsvcjobs" />
    </exec>
  </target>
 
  <target name="deploysolution" depends="addsolution">
    <exec program="${stsadm.exe}" workingdir="${build.dir}"  verbose="${verbose}">
      <arg value="-o" />
      <arg value="deploysolution" />
      <arg value="-name" />
      <arg value="${package.file}" />
      <arg value="-immediate" />
      <arg value="-allowgacdeployment" />
      <arg value="-allcontenturls" />
      <arg value="-force" />
    </exec>
    <call target="spwait" />
  </target>
 
  <target name="retractsolution">
    <exec program="${stsadm.exe}" verbose="${verbose}">
      <arg value="-o" />
      <arg value="retractsolution" />
      <arg value="-name" />
      <arg value="${package.file}" />
      <arg value="-immediate" />
      <arg value="-allcontenturls" />
    </exec>
    <call target="spwait" />
  </target>
 
  <target name="deletesolution" depends="retractsolution">
    <exec program="${stsadm.exe}" verbose="${verbose}">
      <arg value="-o" />
      <arg value="deletesolution" />
      <arg value="-name" />
      <arg value="${package.file}" />
    </exec>
    <call target="spwait" />
  </target>
 
</project>

This file contains a few targets for directly installing and deploying solutions into SharePoint (using stsadm.exe). It simply calls makecab.exe or stsadm.exe (which are local to the project in a tools directory) and executes them with the appropriate filenames. The filenames are set as properties in your main build file, then that build file includes this one. This SharePoint.build file generally never has to change and you can use it from project to project.

You might notice a "DeploymentFiles" folder used as the "deploymentfiles.dir" property. This is taking a queue from STSDEV, using it as a root folder in the solution where the *.ddf and manifest.xml file live for the solution. There's also a RootFiles folder which contains various subfolders with the webparts, features, images, resources, etc. in it. Here's a look at the development tree:

A typical SharePoint solution tree

All source code that will be compiled into assemblies lives under "src". The "solution" folder is the root folder where DeploymentFiles and RootFiles lives as I consider things like feature and site defintions to be part of the solution and source code, just like say a SQL script. Under "src" I have "app" which contains the web parts, feature receivers, etc. and "test" which contains unit test assemblies. This allows you (as you'll see) to build each independently as I don't want my unit test code mixing up with my web parts or domain code. Under "src" they'll be many projects, but in the build file we collapse them all down to one assembly for testing purposes.

The "lib" folder contains external assemblies I reference (but not necessarily deploy) in the solution. This actually contains a copy of SharePoint.dll and SharePoint.Search.dll. You might wonder why I have copies of the files here while they exist in the GAC or buried in the 12 hive. It's because I prefer to have my solution trees to be self-contained. Anyone can grab this entire tree, no matter what version of what they have installed and build it (that includes the tools folder with all the tools they need to build it).

In the "tools" folder I have a copy of stsadm.exe (again, if the version changes on the server I'm protected and using the version I need), NAnt (for the build itself), makecab.exe to create the .wsp file and SharePoint Solution Installer, a really cool tool that runs against my WSP and lets you install and configure it without having to write an installer. You just edit a .config file and provide it the .wsp file.

Back to our project.build file. We'll setup some properties that will get used both in our build file and the SharePoint.build one (like the root directory where things are, etc.)

<!-- global properties, generally won't change -->
<property name="nant.settings.currentframework" value="net-2.0" />
 
<!-- filenames and directories, generally won't change -->
<property name="root.dir" value="${directory::get-current-directory()}" />
<property name="source.dir" value="${root.dir}\src" />
<property name="directives.file" value="${project::get-name()}.ddf" />
<property name="package.file" value="${project::get-name()}.wsp" />
<property name="dist.dir" value="${root.dir}\dist" />
<property name="lib.dir" value="${root.dir}\lib" />
 
<!-- properties that change from project to project but not often -->
<property name="webpart.source.dir" value="${source.dir}\app\SharePointForums.FeatureReceiver" />
<property name="feature.source.dir" value="${source.dir}\app\SharePointForums.WebParts" />
<property name="test.source.dir" value="${source.dir}\test" />
<property name="webpart.lib" value="${project::get-name()}.WebParts.dll" />
<property name="feature.lib" value="${project::get-name()}.Feature.dll" />
<property name="test.lib" value="${project::get-name()}.Test.dll" />
 
<!-- "typical" properties that change -->
<property name="version" value="2.0.0.0" />
<property name="debug" value="true" />
<property name="verbose" value="false" />

Then we'll include our SharePoint.build file:

<!-- include common SharePoint targets -->
<include buildfile="SharePoint.build" />

Recently I've switched to using patternsets inside of filesets in NAnt as it's more flexible. So we'll define a patternset for source files and assemblies, then use this in our filesets for the webpart, feature, and test sources and assembly files.

<!-- filesets and pattern sets for use instead of naming files in targets -->
<patternset id="cs.sources">
  <include name="**/*.cs" />
</patternset>
 
<patternset id="lib.sources">
  <include name="**/*.dll" />
</patternset>
 
<fileset id="feature.sources" basedir="${feature.source.dir}">
  <patternset refid="cs.sources" />
</fileset>
 
<fileset id="webpart.sources" basedir="${webpart.source.dir}">
  <patternset refid="cs.sources" />
</fileset>
 
<fileset id="test.sources" basedir="${test.source.dir}">
  <patternset refid="cs.sources" />
</fileset>
 
<fileset id="sharepoint.assemblies" basedir="${lib.dir}">
  <patternset refid="lib.sources" />
</fileset>
 
<fileset id="solution.assemblies" basedir="${build.dir}">
  <patternset refid="lib.sources" />
</fileset>

Finally here are the targets in our project build file.

Clean will just remove any temporary directories we built:

<target name="clean">
  <delete dir="${build.dir}" />
  <delete dir="${dist.dir}" />
</target>

Init will first call clean, then create the directories:

<target name="init" depends="clean">
  <mkdir dir="${build.dir}" />
  <mkdir dir="${dist.dir}" />
</target>

Compile will call init, then using the <csc> task, build our sources into assemblies. We're using a strongly named keyfile so we specify this in our csc task (otherwise when we deploy we'll get warnings about unsigned assembies, feature receivers must be put into the GAC and require signing).

<target name="compile" depends="init">
  <csc output="${build.dir}\${feature.lib}" target="library" keyfile="${project::get-name()}.snk"  debug="${debug}">
    <sources refid="feature.sources" />
    <references refid="sharepoint.assemblies" />
  </csc>
  <csc output="${build.dir}\${webpart.lib}" target="library" keyfile="${project::get-name()}.snk"  debug="${debug}">
    <sources refid="webpart.sources" />
    <references refid="sharepoint.assemblies" />
  </csc>
</target>

Test first calls compile to get all the web part, feature, and domain assemblies built then compiles the unit test assembly. Finally it will call our unit test runner (MbUnit.Cons.exe or whatever). The output of the unit test run can be used in a CI tool like CruiseControl.NET.

<target name="test" depends="compile">
  <csc output="${build.dir}\${test.lib}" target="library" debug="${debug}">
    <sources refid="test.sources" />
    <references refid="sharepoint.assemblies" />
    <references refid="solution.assemblies" />
  </csc>
  <!-- run unit tests with test runner (mbunit, nunit, etc.) -->
</target>

Our "build" task just calls test (to ensure everything compiles and works) then delegates to the SharePoint.build file to build the solution. This will create our .wsp file and put us in a position to deploy our solution.

<target name="build" depends="test">
  <call target="buildsolutionfile" />
</target>

Finally in this build file we have a dist task. This will build the entire solution then zip up the Solution Installer files and wsp file into a zip file that we'll distribute. End users just download this, unzip it, and run Setup.exe to install the solution.

<target name="dist" depends="build">
  <zip zipfile="${dist.dir}\${project::get-name()}-${version}.zip">
    <fileset basedir="${build.dir}">
      <include name="**\*.wsp" />
    </fileset>
    <fileset basedir="${tools.dir}\SharePointSolutionInstaller">
      <include name="**\*" />
    </fileset>
  </zip>
</target>

There's a lot of NAnt script here, but it's all pretty basic stuff. The nice thing is that from the command line I can build my system, install it locally, deploy it for testing, and event create my distribution for release on CodePlex (or whatever site you use). There's a go.bat file that lives in the root of the solution and looks like this:

@echo off
tools\nant\nant.exe -buildfile:SharePointForums.build %*

It simply calls NAnt with the buildfile name and passes any parameters to the build. For example from the command line here's the output of "go build deploysolution" which will compile my system, run all the unit tests, then add the solution to SharePoint and deploy it. After this I can simply browse to my website and do some integration testing.

NAnt 0.86 (Build 0.86.2898.0; beta1; 12/8/2007)
Copyright (C) 2001-2007 Gerry Shaw
http://nant.sourceforge.net
 
Buildfile: file:///C:/Development/Forums/SharePointForums.build
Target framework: Microsoft .NET Framework 2.0
Target(s) specified: build deploysolution
 
clean:
 
   [delete] Deleting directory 'C:\Development\Forums\build'.
   [delete] Deleting directory 'C:\Development\Forums\dist'.
 
init:
 
    [mkdir] Creating directory 'C:\Development\Forums\build'.
    [mkdir] Creating directory 'C:\Development\Forums\dist'.
 
compile:
 
      [csc] Compiling 22 files to 'C:\Development\Forums\build\SharePointForums.Feature.dll'.
      [csc] Compiling 182 files to 'C:\Development\Forums\build\SharePointForums.WebParts.dll'.
 
test:
 
      [csc] Compiling 41 files to 'C:\Development\Forums\build\SharePointForums.Test.dll'.
 
build:
 
buildsolutionfile:
 
     [exec] Microsoft (R) Cabinet Maker - Version (32) 1.00.0601 (03/18/97)
     [exec] Copyright (c) Microsoft Corp 1993-1997. All rights reserved.
     [exec]
     [exec] Parsing directives
     [exec] Parsing directives (C:\Development\Forums\src\solution\DeploymentFiles\SharePointForums.ddf: 1 lines)
     [exec] 140,309 bytes in 7 files
 
     [exec] Executing directives
     [exec]   0.00% - manifest.xml (1 of 7)
     [exec]   0.00% - SharePointForums.Feature.dll (2 of 7)
     [exec]   0.00% - SharePointForums.WebParts.dll (3 of 7)
     [exec]   0.00% - SharePointForums\Feature.xml (4 of 7)
     [exec]   0.00% - SharePointForums\WebParts.xml (5 of 7)
     [exec]   0.00% - SharePointForums\WebParts\SharePointForums.webpart (6 of 7)
     [exec]   0.00% - IMAGES\SharePointForums\SharePointForums32.gif (7 of 7)
     [exec] 100.00% - IMAGES\SharePointForums\SharePointForums32.gif (7 of 7)
     [exec]   0.00% [flushing current folder]
     [exec]  93.59% [flushing current folder]
     [exec]   5.60% [flushing current folder]
     [exec] 100.00% [flushing current folder]
     [exec] Total files:              7
     [exec] Bytes before:        140,309
     [exec] Bytes after:          50,736
     [exec] After/Before:            40.09% compression
     [exec] Time:                     0.04 seconds ( 0 hr  0 min  0.04 sec)
     [exec] Throughput:             349.34 Kb/second
     [move] 1 files moved.
 
addsolution:
 
     [exec]
     [exec] Operation completed successfully.
     [exec]
 
spwait:
 
     [exec]
     [exec] Operation completed successfully.
     [exec]
 
deploysolution:
 
     [exec]
     [exec] Timer job successfully created.
     [exec]
 
spwait:
 
     [exec]
     [exec] Executing solution-deployment-sharepointforums.wsp-0.
     [exec] Operation completed successfully.
     [exec]
 
BUILD SUCCEEDED
 
Total time: 22.2 seconds.

Hope this helps your build process. Creating SharePoint solutions is a complicated matter. There are features, web parts, solutions, manifests, various tools, and lots of command line tools to make it all go. NAnt helps you tackle this and these scripts boil the solution down to only a few simple commands you need to remember.

3 Comments

  • Hmm, for some reason my Google Reader did not pick up this post. It has the entries before and after, but not this one.

    I'm glad I stumbled upon this post as it is quite useful.

  • Hi Bil.
    I had problems with test target.
    Nant expects only one tag, so I had to change it for




    Which looks kind of ugly.....
    How can I make this work with the patternsets?

  • A small typo - "webpart.source.dir" points to features and "feature.source.dir" points to webpparts.

Comments have been disabled for this content.