[Из песочницы] И ещё пару слов о SandCastle, TFS и магии…

По мотивам только-только проскочившей публикации «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» и доходчиво объяснять, в чём он неправ. Удобно. Наглядно. Внушает.Итак, с чекинами и форматированием кода разобрались. Едем дальше.

Джуниор регулярно ноет, что ему надоело писать одно и то же. Регулярно возникают ситуации, когда в методе исправили описание, а в его пяти перегрузках — забыли. С этим нам поможет…

SHFB поддерживает тэг. Он позволяет избавиться от массы копи-пасты в атрибутах описания. Ради его приемлимого функционирования надо немного поплясать с бубном, поугадывать его возможности (потому что официальная документация довольно пространная и не вдаётся в технические детали реализации этой функции — мне пришлось покопаться в сырцах, чтоб отловить, откуда же он, например, берёт списки файлов для генерации дерева унаследованных типов).Для примера, имеем класс:

/// ///

Имплементация логгера для NLog. public class NLogWrapper: ILogger, IWithTags { /// public virtual bool IsTraceEnabled { get { return InnerLogger.IsTraceEnabled; } }

/// public string Name { get; set; }

/// public HashSet Tags { get; set; } … } В результирующей документации по классу NLogWrapper описания пропертей IsTraceEnabled и Name будут унаследованы от ILogger, а Tags — от IWithTags. Удобно. Казалось бы — вот оно, счастье! Ан нет.Печаль #1 с этим inheritdoc заключается в том, что работает он на эфирных материях через астральные тела и практически никогда нельзя быть уверенным, что какой-то из кейсов будет работать, пока не попробуешь. Для примера:

Нельзя наследовать описания перегруженных методов в рамках одного класса/интерфейса; Не всегда достаточно повесить метку у унаследованного метода — иногда требуется ещё поставить его на самом классе, чтоб SHFB догадался, что его предков надо бы просканировать; Необходимо руками добавлять библиотеки с базовыми классами в DocumentationSources (об этом ниже); Необходимы дополнительные манипуляции для IntelliSense, потому что «из коробки» в результирующем .xml получаются эти самые , которых Visual Studio не ест. И тд. В целом штука полезная, но надо хорошо подумать, прежде чем её использовать.Ну, на этом с подготовкой закончили, давайте уже приступать к основному.

TFS Build Итак, что мы хотим? А хотим мы, чтоб для нашей сборки вместе со всеми проектами в ней была документация.Для начала ставим SHFB на сервер, где крутится наш билд-агент. Иначе работать не будет. Он использует переменные окружения, кучу своих локальных файлов… В-общем надо ставить.Далее открываем gui SHFB, настраиваем проект, добавляем в качестве Documentation Sources наш .sln-файл, сохраняем. Читаем инструкцию. Всё выглядит довольно тривиально. Создаём файлик build.proj по инструкции, чтоб обмануть пляски с OutputDir (без него пробовал — там такой ад с путями начинается, что правда — лучше сделать лишний .proj-обёртку):

Запускаем: SHFB: error BE0040: Project assembly does not exist Эмммм, чо? Ты кто такой? А это, друзья, грабли: файлик sfhbproj хоть и является по сути msbuild-проектом, и даже позволяет оперировать .sln-файлами в качестве источников, вот только саму сборку он не делает. Т.е. он этот файл .sln он использует только для того, чтоб найти список проектов, а в них найти OutputFolder для указанной конфигурации и оттуда уже взять готовые .dll/xml-файлы.

Вот ведь ленивая скотинка-то. Ладно, сейчас обучим новым трюкам. Лезем в файл, видим там 

Ага. После довольно быстрого озарения понимаем, что $(SHFBROOT) это ни что иное, как папка установки бинарников самого SHFB. Там и находим этот файл. Смотрим, куда бы нам вклиниться… Ага, вот оно: PreBuildEvent; BeforeBuildHelp; CoreBuildHelp; AfterBuildHelp; PostBuildEvent Возьмём, например, BeforeBuildHelp. Ещё один кусок документации, который нам поможет жить, находится здесь. Слегка модифицируем наш build.proj: (добавили CustomAfterSHFBTargets) и создаём вот такой файлик shfbcustom.targets: Здесь немножко магии. В файле My.Api.shfbproj в свойстве хранится… 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' Смотрим код: ///

/// DependencyResolver для Unity /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage («Microsoft.Design», «CA1063: ImplementIDisposableCorrectly», Justification = «IDisposable реализован в базовом классе.»)] public class DependencyResolver: DependencyScope, IDependencyResolver { /// public DependencyResolver (IUnityContainer container) : base (container) { }

/// public IDependencyScope BeginScope () { Log.Trace («Beginning new scope»); return new DependencyScope (Container.CreateChildContainer ()); } } Вроде, всё чисто, есть, прописан нормально — должен находиться![ вырезано ] Выше вырезаны несколько часов поисков, ковыряний в настройках, затем в исходниках самого SHFB и его кусков… В итоге выяснилось: В качестве источника для  берутся ИСКЛЮЧИТЕЛЬНО данные, указанные в 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 для проекта. Это хорошо; По пути сделали правило для контроля нерадивых джуниоров, чтоб они быстрее приходили к просветлению. Это тоже хорошо; Поддерживать и развивать это будет кто-то другой. Просто прекрасно.

© Habrahabr.ru