Dávkové vytvoření dokumentace kódu pomocí nástroje Sandcastle

Není třeba připomínat (nebo ano ? Wink), že veškerý kód, především však společne knihovy by měly být řádně dokumentované. K tomu slouží standardní C# komentáře. Pomocí nástroje Sandcastle a MSBuild scriptu lze z těchto komentářů dávkově generovat dokumentaci do souboru v CHM formátu nebo v podobě HTML stránek, které se umístí na web server. Vzhled je velmi podobný stylu MSDN dokumentace.

Co je k tomu potřeba

Struktura ukázkových knihoven

Pro ukázku budou použity dvě knihovny Example.ClassLibrary1 a Example.ClassLibrary2, kde každá obsahuje jednu třídu, s tím že Example.ClassLibrary2 má referenci na Example.ClassLibrary1. Pomocí C# komentářů lze popisovat třídy, metody, parametry, fieldy, property atd. s vyjímkou namespaců. Z MSDN dokumentace však víme, že obsahuje i popis namespaců. Ten lze řešit dvěma způsoby. Buď deklarací speciální NamespaceDoc třídy nebo pomocí XML souborů s popisem namespaces. V ukázce bude použit druhý způsob.

Sandcastle Help File Builder Project

Tento GUI nástroj vyvtáří projekt s příponou .shfbproj, ve kterém se nastavují jednotlivé vlastnosti generované dokumentace (záhlaví, patička, styl) a především seznam asssemblies a XML documentation souborů, ze kterých se bude dokumentace generovat. Reference na tyto soubory lze sice ručně naklikat, pro dávkové zpracování kde se předpokládá, že se počet dokumentovaných knihoven bude měnit se to ale příliš nehodí. Lze jej ovšem použít k vytvoření základní šablony. Takto vypadá:

Vytvořený Example.shfbproj soubor pak vypadá takto:

Dávkové zpracování pomocí MSBuild

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <PropertyGroup>
    <!-- The configuration and platform will be used to determine which
         assemblies to include from solution and project documentation
         sources -->
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{0304AA01-21FC-413C-87CA-6235BA927A4E}</ProjectGuid>
    <SHFBSchemaVersion>1.9.3.0</SHFBSchemaVersion>
    <!-- AssemblyName, Name, and RootNamespace are not used by SHFB but Visual
         Studio adds them anyway -->
    <AssemblyName>Documentation</AssemblyName>
    <RootNamespace>Documentation</RootNamespace>
    <Name>Documentation</Name>
    <!-- SHFB properties -->
    <OutputPath>.\Help\</OutputPath>
    <HtmlHelpName>Example</HtmlHelpName>
    <Language>en-US</Language>
    <HelpTitle>Example Libraries Documentation</HelpTitle>
    <NamingMethod>MemberName</NamingMethod>
    <DocumentationSources>
      <DocumentationSource sourceFile="Example.ClassLibrary2\bin\Debug\Documentation.XML" />
      <DocumentationSource sourceFile="Example.ClassLibrary1\bin\Debug\Documentation.XML" />
      <DocumentationSource sourceFile="Example.ClassLibrary1\bin\Debug\Example.ClassLibrary1.dll" />
      <DocumentationSource sourceFile="Example.ClassLibrary2\bin\Debug\Example.ClassLibrary2.dll" />
    </DocumentationSources>
    <CopyrightText>Copyright &#169%3b 2012 Petr Vones</CopyrightText>
    <KeepLogFile>False</KeepLogFile>
    <MissingTags>Parameter, Returns, AutoDocumentCtors, TypeParameter, AutoDocumentDispose</MissingTags>
    <SyntaxFilters>CSharp</SyntaxFilters>
    <ProjectSummary>Example Libraries Documentation</ProjectSummary>
    <HelpFileFormat>HtmlHelp1</HelpFileFormat>
    <FooterText>Example footer</FooterText>
    <RootNamespaceContainer>True</RootNamespaceContainer>
    <RootNamespaceTitle>Example Libraries Documentation</RootNamespaceTitle>
  </PropertyGroup>
  <!-- There are no properties for these groups.  AnyCPU needs to appear in
       order for Visual Studio to perform the build.  The others are optional
       common platform types that may appear. -->
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|Win32' ">
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|Win32' ">
  </PropertyGroup>
  <!-- Import the SHFB build targets -->
  <Import Project="$(SHFBROOT)\SandcastleHelpFileBuilder.targets" />
</Project>

Vytvořený projekt by bylo možné samozřejmě ručně spustit z GUI, ovšem to není zrovna to pravé, pokud chceme dokumentaci generovat dávkově na build serveru, například pomocí CruiseControl.NET. Dále se předpokládá, že dokumentovaný zdrojový kód knihoven je již předtím přeložen se zapnutým generováním XML souboru z komentářů. Stejně tak je poměrně nepříjemná nutnost editovat po každém přidání nové knihovny daný projektový .shfbproj soubor a aktualizovat tak obsah elementu DocumentationSources.

Pro automatické zpracování lze využít následující postup:

  1. definovat jediný seznam solution souborů, ze kterých se vytváří dokumentace, plus případné XML soubory s popisem namespaců
  2. přeložit všechny tyto solution se zapnutým generováním XML souboru z komentářů (bez dočasného zásahu do jejich .csproj souborů)
  3. vygenerovat dočasný Temp.shfbproj soubor ze šablony Example.shfbproj, kde elementy DocumentationSource budou generovány na základě seznamu solution v bodu 1.
  4. vygenerovat dokumentaci na základě dočasného Temp.shfbproj souboru z bodu 3.

MSBuild script pro celý tento postup vypadá následovně

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Tasks from http://msbuildtasks.tigris.org/ -->
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />

  <PropertyGroup>
    <Path_ProjectRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)'))</Path_ProjectRoot>
  </PropertyGroup>

  <ItemGroup>
    <!-- Define list of solutions to build to create documentation -->
    <DocumentationSolutionToBuild Include="$(Path_ProjectRoot)\Example.ClassLibrary1\Example.ClassLibrary1.sln" />
    <DocumentationSolutionToBuild Include="$(Path_ProjectRoot)\Example.ClassLibrary2\Example.ClassLibrary2.sln" />
    <!-- Define list of namespace summaries -->
    <DocumentationSummary Include="$(Path_ProjectRoot)\ExampleNamespaceSummary.xml" />
  </ItemGroup>

  <Target Name="CreateSandcastleDocumentation">
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="BuildAndCreateSandcastleDocumentation" Properties="sandcastleProjectTemplateFilename=$(Path_ProjectRoot)\Example.shfbproj;sandcastleOutputPath=$(Path_ProjectRoot)\DocOutput" />
  </Target>

  <Target Name="BuildAndCreateSandcastleDocumentation">
    <Error Condition="$(sandcastleProjectTemplateFilename) == ''" Text="Parameter sandcastleProjectTemplateFilename not defined" />
    <Error Condition="$(sandcastleOutputPath) == ''" Text="Parameter sandcastleOutputPath not defined" />
    <PropertyGroup>
      <DocumenationBuildConfiguration>Debug</DocumenationBuildConfiguration>
      <TemporarySandcastleProjectFilename>$(Path_ProjectRoot)\Temp.shfbproj</TemporarySandcastleProjectFilename>
      <XmlDocumentationFilename>Documentation.XML</XmlDocumentationFilename>
    </PropertyGroup>
    <!-- Build all solutions with Debug configuration and generate Documentation.XML documentation file -->
    <MSBuild Projects="@(DocumentationSolutionToBuild)" Properties="Configuration=$(DocumenationBuildConfiguration);Platform=Any CPU;PostBuildEvent=;DocumentationFile=bin\$(DocumenationBuildConfiguration)\$(XmlDocumentationFilename)" Targets="Clean;Build">
      <Output TaskParameter="TargetOutputs" ItemName="ProjectOutputs" />
    </MSBuild>
    <Message Text="@(ProjectOutputs->'DEBUG ProjectOutputs: %(FullPath)%0D%0A')" />
    <!-- Create distinct list of project output files -->
    <ItemGroup>
      <ProjectOutputsDistinct Include="%(ProjectOutputs.Identity)" />
    </ItemGroup>
    <Message Text="@(ProjectOutputsDistinct->'DEBUG ProjectOutputsDistinct: %(FullPath)%0D%0A')" />
    <!-- Create DocumentationSource elements to replace in the template -->
    <PropertyGroup>
      <BinaryFiles>@(ProjectOutputsDistinct->'<DocumentationSource sourceFile="%(FullPath)" />')"</BinaryFiles>
      <DocumentationFiles>@(ProjectOutputsDistinct->'<DocumentationSource sourceFile="%(RootDir)%(Directory)$(XmlDocumentationFilename)" />')</DocumentationFiles>
      <SummaryFiles>@(DocumentationSummary->'<DocumentationSource sourceFile="%(FullPath)" />')</SummaryFiles>
      <AllFiles>$(BinaryFiles);$(DocumentationFiles);$(SummaryFiles)</AllFiles>
    </PropertyGroup>
    <ItemGroup>
      <Tokens Include="DocumentationSources">
        <ReplacementValue>$(AllFiles.Replace(";","%0D%0A"))</ReplacementValue>
      </Tokens>
    </ItemGroup>
    <!-- Create temporary Temp.shfbproj from the template and replace the token with previously created DocumentationSource elements -->
    <TemplateFile Template="$(sandcastleProjectTemplateFilename)" OutputFile="$(TemporarySandcastleProjectFilename)" OutputFilename="$(TemporarySandcastleProjectFilename)" Tokens="@(Tokens)" />
    <!-- Build the documentation using Sandcastle -->
    <MSBuild Projects="$(TemporarySandcastleProjectFilename)" Properties="OutputPath=$(sandcastleOutputPath)" Targets="Build" />
    <Delete Files="$(TemporarySandcastleProjectFilename)" />
  </Target>


Dale je je ještě třeba upravit Example.shfbproj na obecnou šablonu, kde je seznam souborů v elementech DocumentationSource nahrazen textem ${DocumentationSources}, který bude v bodu 3. pak automaticky nahrazen skutečným seznamem souborů pomocí tasku TemplateFile z MSBuild Community Tasks.

    <DocumentationSources>
${DocumentationSources}
    </DocumentationSources>


Popis jednotlivých kroků MSBuild scriptu

Seznam souborů solution je v item DocumentationSolutionToBuild a popisy namespaces v DocumentationSummary (krok 1.).

Target CreateSandcastleDocumentation volá "interní" target BuildAndCreateSandcastleDocumentation, do kterého předává jméno souboru šablony sandcastleProjectTemplateFilename a adresář sandcastleOutputPath ve kterém bude uložen výstup ze Sandcastle. BuildAndCreateSandcastleDocumentation target může být společný pro více různých projektů.

Target BuildAndCreateSandcastleDocumentation nejdříve přeloží všechny solution s konfiguraci Debug|Any CPU a C# dokumentací do souboru bin\Debug\Documentation.XML (krok 2.). Seznam výsledných binárních souborů je uložen v ProjectOutputs. Díky tomu, že Example.ClassLibrary2 má referenci na Example.ClassLibrary1, je v seznamu Example.ClassLibrary1.dll dvakrát. To se pro další zpracování nehodí, proto je nutné vytvořit seznam ProjectOutputsDistinct.

Následná sekce v PropertyGroup vytváří obsah elementu DocumentationSources, který bude nahrazen v šabloně Example.shfbproj a provede vygenerování dočasného Temp.shfbproj (krok 3.), který je pak použit pro Sandcastle compiler (krok 4.) kde se volá jeho target Build.

Pokud vše dobře dopadlo, je v ukázkovém příkladu CHM soubor v adresáři DocOutput\Example.chm.

SHFB : warning BE0063

Když si pozorně prohlédnete MS build log (v příkladu v souboru MSBuild.log), tak v něm najdete warning:

SHFB : warning BE0063: '...Example.ClassLibrary2\bin\Debug\Documentation.XML' matches a previously copied comments filename.  The duplicate will be copied to a unique name to preserve the comments it contains. [...Temp.shfbproj]

Tento warning znamená, že se v SHFB projektu nachází více souborů se stejným jménem, jen v jiných adresářích. Vzhledem k tomu, že nechceme dočasně modifikovat .csproj soubory jednotlivých projektů a zadávat jim jedinečná jména pro XML soubor s dokumentací (krok 2.) je toto jediný způsob a ničemu to nevadí. Protože build solution Example.ClassLibrary2.sln provede zároveň build referencované Example.ClassLibrary1, je nutné použít pro build všech projektů v solution stejné relativní jméno XML souboru.

Příklad je ke stažení zde DocumentationBuild.zip (9,90 kb)

Komentáře (2) -

  • Famózní. Ještě by mě zajímalo, jestli tam nejde přidat něco úplně obecného, nějaká titulka helpu + třeba nějaké podstránky. Dovedu si představit, že bych do stejného chm nageneroval i nějakou analýzu, pár schémátek apod. To se samozřejmě do kódu dát nedá.
Komentáře jsou uzavřeny