Wednesday, May 13, 2009

MSDN-Style Documentation with Sandcastle and NAnt

kick it on DotNetKicks.com

Sandcastle is a code documentation utility, which you run against your compiled .NET assembly to produce “accurate, MSDN style, comprehensive documentation by reflecting over the source assemblies and optionally integrating XML Documentation Comments” [http://sandcastle.codeplex.com/]. NAnt is a .NET-based build automation utility, which allows you to set up build scripts to perform pretty much any task, from compiling assemblies, to file operations, to executing tasks [http://nant.sourceforge.net/]. Both are available for download, absolutely free. Consider the introduction complete.

While NAnt documentation and samples are readily available, Sandcastle does not have as much content devoted to it. Luckily, with the install, several examples are provided – both as MSBuild and batch files. I have used the MSBuild examples to create a single, reusable NAnt script for documenting any assembly (with some caveats which will be pointed out). This will create a CHM file, which will be our end result documentation.

Prerequisites

These examples require the following:

Overview

The script itself is fairly straightforward. First, we will make the property declarations. Then, we will have a series of tasks grouped into “targets”. Each target will depend on the previous step to be complete before it will itself run.

<?xml version="1.0" encoding="utf-8" ?>

<project xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd"

         name="Document.Assembly" default="All">

    <!--

    <property name="project.name" value="MyAssembly"/>

    <property name="project.bin.dir" value="Path\"/>

    <property name="chm.destination.dir" value="Path\"/>

    -->

 

 

</project>

We’ll be setting up a NAnt script that is expecting three properties passed in: the project or assembly name (“project.name”), the directory in which the assembly is located (“project.bin.dir”), and the destination directory for the resulting CHM file (“chm.destination.dir”). The end result is that a file named projectname.chm will be placed in the destination directory.

Step 1: Property declarations

Here, we are assuming the DLL and XML comments file have the same name as the project. If necessary, you can change the script to expect additional inputs, declaring the comment file name and/or the assembly name.

The properties we declare in the NAnt script will make use of the three input properties, and also define environment- or process-specific details.

    <property name="project.dll.file" value="${project.bin.dir}\${project.name}.dll"/>

    <property name="project.xml.file" value="${project.bin.dir}\${project.name}.xml"/>

 

    <!-- Build environment -->

    <property name="build.root.dir" value="E:\Builds\_BuildArea\"/>

    <property name="build.docs.dir" value="${build.root.dir}Docs\${project.name}\"/>

    <property name="build.dxtmp.dir" value="${build.docs.dir}dxtmp\"/>

 

    <!-- Documentation ("dx") info -->

    <property name="dx.presentation.style" value="vs2005"

              unless="${property::exists('dx.presentation.style')}" />

    <property name="dx.reflection.base.xml.file" value="${build.dxtmp.dir}reflection_base.xml"/>

    <property name="dx.reflection.xml.file" value="${build.dxtmp.dir}reflection.xml"/>

    <property name="dx.manifest.xml.file" value="${build.dxtmp.dir}Output\manifest.xml"/>

    <property name="dx.toc.xml.file" value="${build.dxtmp.dir}Output\toc.xml"/>

 

    <!-- Tools / Utilities -->

    <property name="hhc.root.dir" value="C:\Program Files\HTML Help Workshop\"/>

    <property name="sc.root.dir" value="C:\Program Files\Sandcastle\"/>

    <property name="sc.tools.dir" value="${sc.root.dir}ProductionTools\"/>

    <property name="sc.transforms.dir" value="${sc.root.dir}ProductionTransforms\"/>

    <property name="sc.presentation.dir" value="${sc.root.dir}Presentation\${dx.presentation.style}\"/>

 

    <!-- NAnt-specific -->

    <property name="nant.onfailure" value="Failure"/>

    <property name="nant.onsuccess" value="Success"/>

Step 2: Preparing the environment

Once we have our properties defined, we’ll need to prepare the environment. This includes creating directories, and moving any files we need to.

    <target name="Prepare" description="Initializes working area">

        <!-- Remove directories (if needed) -->

        <delete file="${build.docs.dir}${project.name}.chm" failonerror="false"/>

        <delete dir="${build.dxtmp.dir}" failonerror="false" />

        <delete dir="${build.docs.dir}" failonerror="false" />

 

        <!-- Create directories -->

        <mkdir dir="${build.dxtmp.dir}"/>

        <mkdir dir="${build.dxtmp.dir}chm\"/>

        <mkdir dir="${build.dxtmp.dir}Intellisense\"/>

        <mkdir dir="${build.dxtmp.dir}Output\"/>

        <mkdir dir="${build.dxtmp.dir}Output\html\"/>

        <mkdir dir="${build.dxtmp.dir}Output\icons\"/>

        <mkdir dir="${build.dxtmp.dir}Output\media\"/>

        <mkdir dir="${build.dxtmp.dir}Output\scripts\"/>

        <mkdir dir="${build.dxtmp.dir}Output\styles\"/>

 

        <!-- Copy documentation content -->

        <copy todir="${build.dxtmp.dir}Output\icons\">

            <fileset basedir="${sc.presentation.dir}icons\">

                <include name="**.*"/>

            </fileset>

        </copy>

        <copy todir="${build.dxtmp.dir}Output\scripts\">

            <fileset basedir="${sc.presentation.dir}scripts\">

                <include name="**.*"/>

            </fileset>

        </copy>

        <copy todir="${build.dxtmp.dir}Output\styles\">

            <fileset basedir="${sc.presentation.dir}styles\">

                <include name="**.*"/>

            </fileset>

        </copy>

 

        <!-- Copy comments file to build area -->

        <copy file="${project.xml.file}"

              todir="${build.dxtmp.dir}" />

        <move file="${build.dxtmp.dir}${project.name}.xml"

              tofile="${build.dxtmp.dir}comments.xml" />

 

    </target>

Now we are ready to start executing tasks which will generate our documentation. You will notice that each of the following steps utilize <exec/> tasks, which have the working directory set. This is important! If you are manually executing these programs from the command line, you must be running them from the documentation working directory.

Step 3: Reflection file generation

This is where the real work begins. We must run a reflection tool against the assembly, which identifies types, classes, methods, and everything else. Once we’ve reflected the information, we will transform it according to our presentation style (in the properties, we’ve chosen “vs2005”).

    <target name="GenerateReflection" depends="Prepare" description="Generates reflection data">

        <exec program="${sc.tools.dir}MRefBuilder.exe"

              workingdir="${build.dxtmp.dir}">

            <arg value="${project.dll.file}" />

            <arg value="/out:${dx.reflection.base.xml.file}" />

        </exec>

 

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}"

              if="${dx.presentation.style == 'prototype'}">

            <arg value="/xsl:&quot;${sc.transforms.dir}ApplyPrototypeDocModel.xsl&quot;" />

            <arg value="/xsl:&quot;${sc.transforms.dir}AddGuidFilenames.xsl&quot;" />

            <arg value="${dx.reflection.base.xml.file}" />

            <arg value="/out:${dx.reflection.xml.file}" />

        </exec>

 

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}"

              if="${dx.presentation.style == 'vs2005'}">

            <arg value="/xsl:&quot;${sc.transforms.dir}ApplyVSDocModel.xsl&quot;" />

            <arg value="/xsl:&quot;${sc.transforms.dir}AddFriendlyFilenames.xsl&quot;" />

            <arg value="${dx.reflection.base.xml.file}" />

            <arg value="/out:${dx.reflection.xml.file}" />

            <arg value="/arg:IncludeAllMembersTopic=true" />

            <arg value="/arg:IncludeInheritedOverloadTopics=true" />

        </exec>

 

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}"

              if="${dx.presentation.style == 'hana'}">

            <arg value="/xsl:&quot;${sc.transforms.dir}ApplyVSDocModel.xsl&quot;" />

            <arg value="/xsl:&quot;${sc.transforms.dir}AddFriendlyFilenames.xsl&quot;" />

            <arg value="${dx.reflection.base.xml.file}" />

            <arg value="/out:${dx.reflection.xml.file}" />

            <arg value="/arg:IncludeAllMembersTopic=false" />

            <arg value="/arg:IncludeInheritedOverloadTopics=true" />

        </exec>

    </target>

*** Note ***

The reflection tool must be pointed at the assembly. For those familiar with reflection, assembly references must be readily available to accurately reflect the contents. Because I want a script that is reusable, I cannot pass in all required references to the location. Therefore, your assembly’s referenced DLLs must be in the same location as the assembly!

Step 3: Generate the manifest

This transforms the reflection XML file into a manifest file, which will be used to generate the HTML, which will eventually be compiled into the CHM.

    <target name="GenerateManifest" depends="GenerateReflection" description="Generates manifest">

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}">

            <arg value="/xsl:&quot;${sc.transforms.dir}ReflectionToManifest.xsl&quot;"/>

            <arg value="${dx.reflection.xml.file}"/>

            <arg value="/out:${dx.manifest.xml.file}"/>

        </exec>

    </target>

Step 4: Generate the HTML

Since we have the manifest, we can then produce the HTML. This can be a time-consuming step, depending on the size of your assembly.

    <target name="GenerateHTML" depends="GenerateManifest" description="Generates HTML for CHM">

        <exec program="${sc.tools.dir}BuildAssembler.exe"

              workingdir="${build.dxtmp.dir}">

            <arg value="/config:&quot;${sc.presentation.dir}configuration\sandcastle.config&quot;" />

            <arg value="${dx.manifest.xml.file}" />

        </exec>

    </target>

Step 5: Generate the table of contents

Depending on the presentation style we’ve chosen, we will create a specific table of contents XML file, which the CHM requires for navigation.

    <target name="GenerateTOC" depends="GenerateHTML" description="Generates table of contents">

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}"

              if="${dx.presentation.style == 'prototype'}">

            <arg value="/xsl:&quot;${sc.transforms.dir}CreatePrototypeToc.xsl&quot;" />

            <arg value="${dx.reflection.xml.file}" />

            <arg value="/out:${dx.toc.xml.file}" />

        </exec>

 

        <exec program="${sc.tools.dir}XslTransform.exe"

              workingdir="${build.dxtmp.dir}"

              if="${dx.presentation.style != 'prototype'}">

            <arg value="/xsl:&quot;${sc.transforms.dir}CreateVSToc.xsl&quot;" />

            <arg value="${dx.reflection.xml.file}" />

            <arg value="/out:${dx.toc.xml.file}" />

        </exec>

    </target>

Step 6: Generate the CHM file

We have generated all necessary content for the CHM project to be created. We will now use Sandcastle tools to create the CHM project files, and the HTML Help Workshop to generate the CHM file itself.

    <target name="GenerateCHM" depends="GenerateTOC" description="Generates CHM file">

        <!-- Copy resources for CHM -->

        <copy todir="${build.dxtmp.dir}chm\icons\">

            <fileset basedir="${build.dxtmp.dir}Output\icons\">

                <include name="**.*" />

            </fileset>

        </copy>

        <copy todir="${build.dxtmp.dir}chm\scripts\">

            <fileset basedir="${build.dxtmp.dir}Output\scripts\">

                <include name="**.*" />

            </fileset>

        </copy>

        <copy todir="${build.dxtmp.dir}chm\styles\">

            <fileset basedir="${build.dxtmp.dir}Output\styles\">

                <include name="**.*" />

            </fileset>

        </copy>

 

        <!-- Create CHM -->

        <exec program="${sc.tools.dir}ChmBuilder.exe"

              workingdir="${build.dxtmp.dir}">

            <arg value="/project:${project.name}" />

            <arg value="/html:${build.dxtmp.dir}Output\html" />

            <arg value="/lcid:1033" />

            <arg value="/toc:${dx.toc.xml.file}" />

            <arg value="/out:${build.dxtmp.dir}chm\" />

        </exec>

 

        <exec program="${sc.tools.dir}DBCSFix.exe"

              workingdir="${build.dxtmp.dir}">

            <arg value="/d:${build.dxtmp.dir}chm\" />

            <arg value="/l:1033" />

        </exec>

 

        <exec program="${hhc.root.dir}hhc.exe"

              workingdir="${build.dxtmp.dir}"

              failonerror="false">

            <arg value="${build.dxtmp.dir}chm\${project.name}.hhp" />

        </exec>

    </target>

*** Notes ***

There are two items worth mentioning here. First, regardless of whether the HTML Help Workshop (hhc.exe) process succeeded, it will always return non-zero results. We indicate in the <exec/> task that we will not fail on error. This causes a potential headache with the second issue, which is odd…

Most assemblies are processed fine by the hhc.exe process, like “DotNetNuke.dll” or “DoyleITS.Samples.dll”. If your assembly name contains “.h”, the CHM will not be generated. “DotNetNuke.HttpModules.dll” will fail with numerous HHC3002 and HHC3004 errors and warnings, as image files used in the documentation are parsed for HTML. This has something to do with how the utility scans for .h* files (help files, HTML files, who knows).

Step 7: Success

Now that the CHM is created, we can copy it to our destination, and clean up the working area. This gets called by setting the NAnt property “nant.onsuccess” to the name of our target.

    <target name="Success" description="Cleans up after success">

        <copy file="${build.dxtmp.dir}chm\${project.name}.chm"

              todir="${chm.destination.dir}"/>

        <delete dir="${build.dxtmp.dir}" failonerror="false"/>

    </target>

The last thing you need is your initial target, which will be the default target, or get called externally.

    <target name="All" depends="GenerateCHM" description="Runs build" />

Running the script

The easiest way to execute the NAnt script is with a batch file, or a process to manage your builds like CruiseControl.NET [http://ccnet.thoughtworks.com]. Below is how you would execute the script at the command line:

"C:\Program Files\NAnt\nant-0.86-beta1\bin\nant.exe" /f:"E:\Visual Studio 2008 Projects\DoyleITS.Build\DoyleITS.Build.NAntScripts\Document.Assembly.build.xml" /D:project.name=DotNetNuke /D:project.bin.dir="E:\Downloads\DotNetNuke\DotNetNuke_Community_05.00.01_Source\Website\bin" /D:chm.destination.dir=E:\Builds\

The “/f” argument defines the NAnt script, while the “/D” arguments pass the expected input properties. The example uses my NAnt script “Document.Assembly.build.xml”, located at E:\Visual Studio 2008 Projects\DoyleITS.Build\DoyleITS.Build.NAntScripts\. I am documenting the DotNetNuke.dll assembly, which resides in the same directory as any dependent or referenced assemblies.

Closing thoughts

Overall, I am constantly impressed by the quality developer resources that others contribute to the landscape. Sandcastle will make your life simpler, once you have a consistent, reusable approach, which I believe NAnt (or any other automation tool) can provide. API developers should really pay attention to what Sandcastle provide – the exact documentation another developer needs to see!

3 comments: