І ще пару слів про 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" і дохідливо пояснювати, в чому він неправий. Зручно. Наочно. Вселяє.

Отже, з чекинами і форматування коду розібралися. Їдемо далі.

Джуніор регулярно ниє, що йому набридло писати одне і те ж. Регулярно виникають ситуації, коли в методі виправили опис, а в його п'яти перевантаженнях — забули. З цим нам допоможе…

<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, TagsIWithTags. Зручно. Здавалося б — ось воно, щастя! Ан немає.
Печаль #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" ім'я елемента="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("new Beginning 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 для проекту. Це добре;
  • По дорозі зробили правило для контролю недбайливих джуніорів, щоб вони швидше приходили до просвітління. Це теж добре;
  • Підтримувати і розвивати це буде хтось інший. Просто чудово.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.