Saturday 9 May 2009

NAnt, .Net, Sandcastle and Documentation

Documentation is great, it makes people feel good about themselves. After they have created a particularly cunning peice of work, it's nice to throw together some documentation so that others can see your genius. But wait, that means you need to go through all your work and come up with some documentation of your own! Gasp, shock - horror!!

Lets face it, for a lot of people, creating standardised documentation after they have finished their project etc isnt at the fore front of their minds. This is a shame, because with flippancy aside, people do really like documentation. It enables developers to get on with their work without asking people for info on an API etc and it lets managers see that there is "stuff being done with documentation". And dont forget, standardisation is key here - you dont want a hundred different developers all creating documentation in different ways, it would be a nightmare.

There are a lot of ways documentation can be generated at build time. One popular way is to use NDoc and this is catered for within NAnt. However, this doesnt support .Net 2.0 let alone 3.5 and it hasnt seen a release since 2005... So, there appears to be a massive gap for generation of documentation, that is if you don't count Sandcastle. This is a fairly useful, if slightly complex method of generating documentation. There are lots of examples on how to use Sandcastle via batch files and powershell scripts (the version I downloaded came packed with them). In an effort to understand the generation process, I decided to put together a NAnt task so this could be automated as part of continuous integration.

Sandcastle itself is not really one application. Right now, Sandcastle is a number of programs working together. It consits of:

  • MRefBuilder.exe
  • XslTransform.exe
  • BuildAssembler.exe
  • CHMBuilder.exe
  • DBCSFix.exe

Each of these programs are used in turn to generate first XML documentation followed by CHM (should you need it.). The following examples give an indication of how you could put a NAnt task together to make use of these programs for all your documentation needs.

All we need to do is create a suite of tasks that represent each of these programs. Essentially, all we are doing here is capthuring the arguments we need to run the apps themselves, there isnt much going on in task per se. For example, here is the complete code for MRefBuilder:

using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;
using Custom.NantTasks.Common;

namespace Custom.NantTasks.Sandcastle
{
/// <summary>
/// NAnt task to make use of Sandcastle from MS
/// </summary>
[TaskName("mrefbuilder")]
class MRefBuilder : ExternalProgramBase
{
private string _assemblyPath;
[TaskAttribute("assembly-path", Required = true)]
public string AssemblyPath
{
get { return _assemblyPath; }
set { _assemblyPath = value; }
}
private string _outputPath;
[TaskAttribute("output-path", Required = true)]
public string OutPutPath
{
get { return _outputPath; }
set { _outputPath = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return AssemblyPath + " /out:" + OutPutPath; }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Running MRefBuilder");
Task();
}

private void Task()
{
try
{
base.ExecuteTask();
}
catch (Exception e)
{
throw new BuildException(e.Message);
}
}
}
}


As you can see, this is pretty much just an arguments gathering exercise. Things become a little trickier for XslTransform. This app specifies multiple transformations, so we need to be able to specify at least one transformation in the task itself - but reserve the ability to specify a second. I am not going to go into exactly how Sandcastle works here, all I would be doing is copying information. For an in depth explanation of how Sandcastle works, take a look at the Sandcastle blog. So, for the XslTransform task, I simply specify two attributes for the .xsl files:


private string _xslPath;
[TaskAttribute("xsl-path", Required = true)]
public string XslPath
{
get { return _xslPath; }
set { _xslPath = value; }
}
private string _xslPath2;
[TaskAttribute("xsl-path2", Required = false)]
public string XslPath2
{
get { return _xslPath2; }
set { _xslPath2 = value; }
}

You may wish to use more than one .xsl - I don't, the example batch file in the version I downloaded didnt seem to make use of more than two. However, if you need to add more then its a very simple task to add this to the task itself. Once the arguments have been captured, we need to then use them with the app itself. We need to be able to provide at least one .xsl in the argument we use with the app and if a second is specified, then we need to be able to provide this also. To achieve this, I used the following:


private string checkXSLPath()
{
string _arg;
if (XslPath2 == null)
{
_arg = "/xsl:\"" + XslPath + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}
else
{
_arg = "/xsl:\"" + XslPath + "\" " + "/xsl:\""
+ XslPath2 + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}

}

All this does is simply check to see if a second xsl file has been specified. If it has, then it returns an argument that makes use of it, if not, then it returns an argument that doesnt. The full source looks like this:


using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;

namespace Custom.NantTasks.Sandcastle
{
[TaskName("xsltransform")]
class XslTransform : ExternalProgramBase
{
private string _xslPath;
[TaskAttribute("xsl-path", Required = true)]
public string XslPath
{
get { return _xslPath; }
set { _xslPath = value; }
}
private string _xslPath2;
[TaskAttribute("xsl-path2", Required = false)]
public string XslPath2
{
get { return _xslPath2; }
set { _xslPath2 = value; }
}
private string _inputFile;
[TaskAttribute("inputfile", Required = true)]
public string InputFile
{
get { return _inputFile; }
set { _inputFile = value; }
}
private string _outPutPath;
[TaskAttribute("output", Required = true)]
public string OutputPath
{
get { return _outPutPath; }
set { _outPutPath = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return checkXSLPath(); }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Starting XslTransform");
Task();
}

private void Task()
{
try
{
base.ExecuteTask();
}
catch (Exception e)
{
throw new BuildException(e.Message);
}
}
private string checkXSLPath()
{
string _arg;
if (XslPath2 == null)
{
_arg = "/xsl:\"" + XslPath + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}
else
{
_arg = "/xsl:\"" + XslPath + "\" " + "/xsl:\""
+ XslPath2 + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}

}
}
}


Looking good so far. Now we have two NAnt tasks that will generate documentation as xml and then transform it. Now we get to the meat of the whole process, BuildAssembler. Again, I am not going to go into detail about what this does here - instead, check the Sandcastle blog entry on BuildAssembler here for a complete explanation. Whilst the process behind BuildAssembler is very involved, the NAnt task for it is quite simple, the complete source is below:


using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;

namespace Custom.NantTasks.Sandcastle
{
[TaskName("buildassembler")]
class BuildAssembler : ExternalProgramBase
{
private string _manifest;
[TaskAttribute("manifest", Required = true)]
public string Manifest
{
get { return _manifest; }
set { _manifest = value; }
}
private string _config;
[TaskAttribute("config", Required = true)]
public string Config
{
get { return _config; }
set { _config = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return " /config:\"" + Config + "\" " + "\"" + Manifest + "\""; }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Starting BuildAssember");
base.ExecuteTask();
}
}
}


Essentially, all we are doing is giving BuildAssembler a config file to work with as well as a manifest file we generated with the previous task, XslTransform. Now even though there are more steps to go through, we need to start looking at the NAnt script itself. On its own, Sandcastle wont do everything you want, there are things you need to initiate, some of these are the creation of directories as well as copying files etc. Lets take a look at the NAnt script, at this stage it should look something similar to this:


<target name="Sandcastle">
<echo message="Trying out my Sandcastle task" />
<echo message="Checking for output directory.." />
<mrefbuilder assembly-path="Custom.NantTasks.dll"
output-path="C:\somefolder\sandcastletest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\MrefBuilder.exe" />
<xsltransform xsl-path="C:\Program Files\Sandcastle\ProductionTransforms\ApplyVsDocModel.xsl"
xsl-path2="C:\Program Files\Sandcastle\ProductionTransforms\AddFriendlyFilenames.xsl"
inputfile="C:\somefolder\sandcastletest.xml"
output="C:\somefolder\refelction.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\xsltransform.exe" />
<xsltransform xsl-path="C:\Program Files\Sandcastle\ProductionTransforms\ReflectionToManifest.xsl"
inputfile="C:\somefolder\refelction.xml"
output="C:\somefolder\manifest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\xsltransform.exe" />
<mkdir dir="C:\somefolder\html" />
<mkdir dir="C:\somefolder\icons" />
<copy todir="C:\somefolder\icons">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\icons">
<include name="*" />
</fileset>
</copy>

<mkdir dir="C:\somefolder\scripts" />
<copy todir="C:\somefolder\scripts">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\scripts">
<include name="*" />
</fileset>
</copy>
<mkdir dir="C:\somefolder\styles" />
<copy todir="C:\somefolder\styles">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\styles">
<include name="*" />
</fileset>
</copy>
<mkdir dir="C:\somefolder\media" />
<mkdir dir="C:\somefolder\intellisense" />
<buildassembler config="C:\Program Files\Sandcastle\Presentation\vs2005\configuration\sandcastle.config"
manifest="C:\somefolder\manifest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\buildassembler.exe" />
</target>

As you can see, my script is very static. It specifies one model for Sandcastle (VS2005) and specifies one assembly to generate from. Also, as you can see all of the paths are static - but this doesnt stop you from creating a generic task to handle multiple assemblies as part of a CI process. Points to note in this script - simply the creation and population of directories prior to using BuildAssembler. These come directly from the location Sandcastle has been installed to (or from wherever you will choose to store them).

So, we are nearly there. By now, we have xml documentation generated, following the provided scripts in the Sandcastle download, the next step is to create some CHM file. Personally, I dont really need .chm files generated - I want my help files to go straight into a website/wiki.... To do that though, requires a bit more thought and code than I cover here. So stay tuned!!

No comments:

Post a Comment