По мотивам только-только проскочившей публикации «Sandcastle и SHFB» решил поделиться своими болями и печалями, а также и success-story при работе с этим продуктом.
В тексте не будет скриншотов с подписями "нажмите кнопку ДОБАВИТЬ" и описания настроек/плагинов.
В тексте будет описание процесса реализации конкретного кейса: сборки документации SHFB в TFS.
Итак, имеющееся окружение:
- Team Foundation Server 2013
- VisualStudio 2014
В чём проблема
Моей первой и главной заботой при организации документации было максимально отдаление разработчика от дальнейшего процесса построения документации. Т.е. чтоб условный джуниор мог писать код, коммитить его в TFS, а документация сама собиралась уже при удачном билде релизной версии.
Так мы подходим к первой проблеме. Заключается она как раз-таки в это джуниор-разработчике. Как заставить его ставить комментарии? С этим нам поможет…
Stylecop
А точнее StyleCop checkin policy. Мне пришлось немного допилить его, чтоб забирал конфиг-файл прям из TFS (чтоб не разливать всем разработчикам новую версию каждый раз). Но в целом принцип понятен, да? Настраиваем нужные нам правила, касающиеся документации, включаем policy и настраиваем в TFS оповещение на каждый Policy override — мы не можем совсем запретить его (технически можем, но случаи, когда действительно нужно будет сделать override, превратятся в совершенно запредельную боль), но можем вырисовываться изниоткуда над плечом разработчика через минуту после того, как он нажал "Override policy" и доходчиво объяснять, в чём он неправ. Удобно. Наглядно. Внушает.
Итак, с чекинами и форматированием кода разобрались. Едем дальше.
Джуниор регулярно ноет, что ему надоело писать одно и то же. Регулярно возникают ситуации, когда в методе исправили описание, а в его пяти перегрузках — забыли. С этим нам поможет…
<inheritdoc />
SHFB поддерживает тэг <inheritdoc />. Он позволяет избавиться от массы копи-пасты в атрибутах описания. Ради его приемлимого функционирования надо немного поплясать с бубном, поугадывать его возможности (потому что официальная документация довольно пространная и не вдаётся в технические детали реализации этой функции — мне пришлось покопаться в сырцах, чтоб отловить, откуда же он, например, берёт списки файлов для генерации дерева унаследованных типов).
Для примера, имеем класс:
/// <inheritdoc />
/// <summary>Имплементация логгера для NLog.</summary>
public class NLogWrapper : ILogger, IWithTags
{
/// <inheritdoc />
public virtual bool IsTraceEnabled
{
get { return InnerLogger.IsTraceEnabled; }
}
/// <inheritdoc />
public string Name { get; set; }
/// <inheritdoc cref="IWithTags.Tags"/>
public HashSet<string> Tags { get; set; }
...
}
В результирующей документации по классу NLogWrapper описания пропертей IsTraceEnabled и Name будут унаследованы от ILogger, а Tags — от IWithTags. Удобно. Казалось бы — вот оно, счастье! Ан нет.
Печаль #1 с этим inheritdoc заключается в том, что работает он на эфирных материях через астральные тела и практически никогда нельзя быть уверенным, что какой-то из кейсов будет работать, пока не попробуешь. Для примера:
- Нельзя наследовать описания перегруженных методов в рамках одного класса/интерфейса;
- Не всегда достаточно повесить метку у унаследованного метода — иногда требуется ещё поставить его на самом классе, чтоб SHFB догадался, что его предков надо бы просканировать;
- Необходимо руками добавлять библиотеки с базовыми классами в DocumentationSources (об этом ниже);
- Необходимы дополнительные манипуляции для IntelliSense, потому что «из коробки» в результирующем .xml получаются эти самые <inheritdoc/>, которых Visual Studio не ест.
И тд. В целом штука полезная, но надо хорошо подумать, прежде чем её использовать.
Ну, на этом с подготовкой закончили, давайте уже приступать к основному.
TFS Build
Итак, что мы хотим? А хотим мы, чтоб для нашей сборки вместе со всеми проектами в ней была документация.
Для начала ставим SHFB на сервер, где крутится наш билд-агент. Иначе работать не будет. Он использует переменные окружения, кучу своих локальных файлов… В-общем надо ставить.
Далее открываем gui SHFB, настраиваем проект, добавляем в качестве Documentation Sources наш .sln-файл, сохраняем. Читаем инструкцию. Всё выглядит довольно тривиально. Создаём файлик build.proj по инструкции, чтоб обмануть пляски с OutputDir (без него пробовал — там такой ад с путями начинается, что правда — лучше сделать лишний .proj-обёртку):
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Target Name="Build">
<!-- Build source code docs -->
<MSBuild Projects="My.Api.shfbproj"
Properties="Configuration=Release;Platform=AnyCPU;OutDir=$(OutDir)" />
</Target>
</Project>
Запускаем:
SHFB : error BE0040: Project assembly does not exist
Эмммм, чо? Ты кто такой?
А это, друзья, грабли: файлик sfhbproj хоть и является по сути msbuild-проектом, и даже позволяет оперировать .sln-файлами в качестве источников, вот только саму сборку он не делает. Т.е. он этот файл .sln он использует только для того, чтоб найти список проектов, а в них найти OutputFolder для указанной конфигурации и оттуда уже взять готовые .dll/xml-файлы.
Вот ведь ленивая скотинка-то. Ладно, сейчас обучим новым трюкам. Лезем в файл, видим там
<Import Project="$(SHFBROOT)SandcastleHelpFileBuilder.targets" />
Ага. После довольно быстрого озарения понимаем, что $(SHFBROOT) это ни что иное, как папка установки бинарников самого SHFB. Там и находим этот файл. Смотрим, куда бы нам вклиниться… Ага, вот оно:
<PropertyGroup>
<BuildDependsOn>
PreBuildEvent;
BeforeBuildHelp;
CoreBuildHelp;
AfterBuildHelp;
PostBuildEvent
</BuildDependsOn>
</PropertyGroup>
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)" />
Возьмём, например, BeforeBuildHelp. Ещё один кусок документации, который нам поможет жить, находится здесь. Слегка модифицируем наш build.proj:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Target Name="Build">
<!-- Build source code docs -->
<MSBuild
Projects="My.Api.shfbproj"
Properties="Configuration=Release;Platform=AnyCPU;OutDir=$(OutDir);CustomAfterSHFBTargets=$(MSBuildThisFileDirectory)shfbcustom.targets" />
</Target>
</Project>
(добавили CustomAfterSHFBTargets) и создаём вот такой файлик shfbcustom.targets:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Target Name="BeforeBuildHelp">
<XmlPeek Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" XmlContent="<root>$(DocumentationSources)</root>" Query="//msb:DocumentationSource[@configuration]/@sourceFile">
<Output TaskParameter="Result" ItemName="Peeked" />
</XmlPeek>
<MSBuild Projects="@(Peeked)" Properties="Configuration=Doc;Platform=Any CPU;OutDir=$(OutDir)" />
</Target>
</Project>
Здесь немножко магии. В файле My.Api.shfbproj в свойстве <DocumentationSources> хранится… XML. Строкой. Вот такой хитрый ход. Супротив него мы можем применить только такой же хитрый ход: наша перегрузка таргета BeforeBuildHelp берёт эту строку, скармливает её в XmlPeek таск и забирает оттуда все @sourceFile с нод, у которых есть @configuration. Затем скармливает этот массив в таск MSBuild.
Да, при этом мы теряем по-проектные настройки Configuration|Platform, которые могли быть указаны в SHFB для этих источников, но эту боль я смог пережить просто: для документации используется специальная конфигурация сборки под названием Doc (как видно выше в коде). Это копия релиза, с отключенными тестовыми проектами и прочими лишними вещами, которые иначе мешали бы генерировать нормальную доку. Т.е. можно было бы сделать этот файлик в три раза толще, разбирать для каждого .sln его параметры, но в нашем случае оно того не стоило.
Запускаем ещё раз… Ух ты — собирается!
Так, т.е. у нас уже есть проект, который можно настраивать в SHFB, включая новые .sln, а потом просто запускать билд в TFS и получать на выходе chm + html?! Прекрасно. Смотрим… ой, что такое? В логе ошибки:
SHFB: Warning GID0002: No comments found for cref 'T:System.Web.Http.Dependencies.IDependencyResolver' on member 'T:My.Api.Server.DependencyResolver'
Смотрим код:
/// <summary>
/// DependencyResolver для Unity
/// </summary>
/// <inheritdoc cref="System.Web.Http.Dependencies.IDependencyResolver" />
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "IDisposable реализован в базовом классе.")]
public class DependencyResolver : DependencyScope, IDependencyResolver
{
/// <inheritdoc />
public DependencyResolver(IUnityContainer container)
: base(container)
{
}
/// <inheritdoc />
public IDependencyScope BeginScope()
{
Log.Trace("Beginning new scope");
return new DependencyScope(Container.CreateChildContainer());
}
}
Вроде, всё чисто, <inheritdoc /> есть, прописан нормально — должен находиться!
[ вырезано ]
Выше вырезаны несколько часов поисков, ковыряний в настройках, затем в исходниках самого SHFB и его кусков… В итоге выяснилось:
В качестве источника для <inheritdoc /> берутся ИСКЛЮЧИТЕЛЬНО данные, указанные в DocumentationSources. При этом они должны быть прописаны прямо в файле.
Никакие плагины не помогут. Никакие References не учитываются. Никакая магия MSBuild, позволяющая на лету модифицировать переменные, тоже не поможет. Потому что в конце концов запускается файлик GenerateInheritedDocs.exe, который тупо парсит файл .shfbproj, достаёт через XPath из него содержимое ноды и перебирает указанные там файлы. Всё, приехали. Я попытался, было, распилить это мракобесие, но там на каждом шагу вставлена прямая работа с файлом — каждый компонент сам по себе лезет в него и читает то, что ему надо — ни о каком общем контексте речи не идёт. Так что я эту затею забросил.
Так что если хотите, чтоб в вашу документацию вставились строчки из компонентов, которые вы используете в проекте (в данном случае я хотел, чтоб там было описание методов из System.Web.Http), то придётся включить эти компоненты в DocumentationSources.
Да, можно включать не саму сборку, а только .xml-файл от неё. От этого не сильно легче.
На этом месте мы явно получаем геморрой с поддержкой файла .shfbproj — надо обновлять его каждый раз, когда используются новые компоненты. Надо обновлять его каждый раз, когда обновляем nuget-пакет — потому как меняется путь к файлу! Ужас-ужас. И никак не автоматизировать же.
Нет, конечно, можно сделать такой target, чтоб перебирал содержимое /packages/** и вытаскивал оттуда все .xml… А, нет, нельзя — каждый пакет же может содержать несколько версий под разные версии .net runtime. Значит, надо заходить с другого конца — после сборки каждого проекта перебирать всё содержимое $(OutDir), и все xml/dll-файлы оттуда вписывать в… А вот куда?
Здесь можно немного обыграть: поддерживается включение .shfbproj в качестве Documentation Source. Так что можно на лету создать файл минимального содержимого, в котором будет только DocumentationSources, а его держать единственным включением в основной файл… Но чем-то попахивает от этого, мне кажется.
К (не-)счастью я всем этим занимался в качестве факультатива и из личной заинтересованности, вскоре пришлось заняться другим проектом, а это всё так и осталось в таком виде — собирается, публикуется, но вот обновлять/поддерживать — боль.
Что в остатке?
- По кнопке «Build project» (или само по правилу Continuos Integration) собирается и публикуется документация в .chm и html для проекта. Это хорошо;
- По пути сделали правило для контроля нерадивых джуниоров, чтоб они быстрее приходили к просветлению. Это тоже хорошо;
- Поддерживать и развивать это будет кто-то другой. Просто прекрасно.
Автор: justmara