Pull to refresh

И ещё пару слов о SandCastle, TFS и магии…

Reading time 8 min
Views 4.4K
По мотивам только-только проскочившей публикации «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 для проекта. Это хорошо;
  • По пути сделали правило для контроля нерадивых джуниоров, чтоб они быстрее приходили к просветлению. Это тоже хорошо;
  • Поддерживать и развивать это будет кто-то другой. Просто прекрасно.
Tags:
Hubs:
+9
Comments 2
Comments Comments 2

Articles