Pull to refresh

Внедряем StyleCop в MSBuild

Reading time 6 min
Views 13K
Всё чаще возникают задачи автоматизации разных процессов в рамках CI. Поковырявшись с MSBuild, я всё больше убеждаюсь, что это довольно мощный инструмент. При желании, им много чего можно сделать. Однако ни в рунете вцелом, ни конкретно на хабре я не нашёл статей по нему и решил позаполнять этот пробел по мере сил и наличия свободного времени.
Итак, сегодня мы будем готовить

StyleCop



Задача: реализовать тотальную принудительную проверку кода (C#) на соответствие правилам оформления.

Условие: тотально, принудительно. Т.е. весь код, попадающий на сборку, должен быть проверен в обязательном порядке. В случае обнаружения нарушений — build error и вперёд, рефакторить.

Инструменты: StyleCop, MSBuild (TFS или TeamCity — неважно).

Итак. Мы хотим, чтоб код прогонялся через StyleCop. Мы в курсе про лень и безответственность разработчиков, поэтому клиентской проверке мы не доверяем. Т.е. VSIX, который ставится из дистрибутива StyleCop, доступного по ссылке выше, и позволяет в студии просто жать правой кнопкой на проект/солюшн и делать Run Stylecop, нас не устраивает.
Всякие StyleCop Checkin Policy — тоже не наш путь. Потому как они завязаны на TFS, притом именно как репозиторий. Таким образом, запустив Git for TFS, мы уже потеряем эти policy. Не, не рассматриваем (хотя, чисто идеологически это и есть самый правильный способ — «грязный» код просто не должен попадать в репозиторий).

Значит, просто не пустить код мы не можем1. Остаётся только не дать ему собраться. Для этого у нас есть вполне изученные механизмы внедрения в MSBuild pipeline. Благо, бОльшую часть работы за нас уже сдедали — с nuget-пакете StyleCop.MSBuild есть все нужные файлы. Это и файлы самого StyleCop (класс StyleCopTask, реализующий нужный нам Task для MSBuild есть уже в самой StyleCop.dll), и дефолтовые настройки (StyleCop.Settings) и, что главное — StyleCop.MSBuild.targets. Вот этот последний нам больше всего и нужен.

Как будем внедряться?



Для MSBuild есть довольно-таки удобный механизм расширения через ImportBefore/After. Если вкратце:
...if you want to offer an extensibility point so that someone else can import a file without requiring you to explicitly add the file name to the importing file. For this purpose, Microsoft.Common.Targets contains the following line at the top of the file.
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\$(MSBuildThisFile)\ImportBefore\*" Condition="'$(ImportByWildcardBeforeMicrosoftCommonTargets)' == 'true' and exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\$(MSBuildThisFile)\ImportBefore')"/>


И точно также импортируюся автоматом файлы из ImportAfter. Т.е. при каждой сборке MSBuild импортирует файлы, лежащие в $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.Targets\ImportBefore\ и ImportAfter. Разница между ними — перед или после загрузки собираемого файла (sln/csproj/другой proj), это бывает актуально для… в другой раз расскажу.

Можно пойти по пути обезьяней работы: скачать .msi с сайта stylecop, установить его на машине, где крутится MSBuild, положить там же руками файл .targets…
Но у нас есть несколько машин с билд-агентами (раньше TFS, сейчас ещё и под TeamCity) и на все копировать/устанавливать руками как-то не хочется. Не дай бог ещё обновления потом проливать… нет уж, увольте.

Будем автоматизировать.


Раз уж у нас есть TFS/TeamCity, то можем пойти в лоб. Делаем репозиторий с такой структурой файлов:
/
├[lib]
│   ├mssp7en.dll
│   ├mssp7en.lex
│   ├Settings.StyleCop
│   ├StyleCop.CSharp.dll
│   ├StyleCop.CSharp.Rules.dll
│   ├StyleCop.dll
├[targets]
│   ├StyleCop.MSBuild.Targets
└build.proj


build.proj


Это просто project-файл в формате msbuild, который будет исполняться на агенте и совершать операции по раскладыванию файлов в нужные нам места.
Содержимое, допустим, такое
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="StyleCopUpdate" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

	<PropertyGroup>
		<StyleCopTargetsFolder Condition="'$(StyleCopTargetsFolder)' == ''">$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter</StyleCopTargetsFolder>
		<StyleCopFolder Condition="'$(StyleCopFolder)' == ''">$(MSBuildExtensionsPath)\StyleCop</StyleCopFolder>
	</PropertyGroup>

	<Target Name="StyleCopUpdate" DependsOnTargets="StyleCopClear">
		<ItemGroup>
			<StyleCopTargets Include="$(MSBuildThisFileDirectory)targets\ImportAfter\StyleCop*.targets" />
		</ItemGroup>
		<ItemGroup>
			<StyleCopLibs Include="$(MSBuildThisFileDirectory)lib\*.*" />
		</ItemGroup>

		<MakeDir Directories="$(StyleCopTargetsFolder)" Condition="!Exists('$(StyleCopTargetsFolder)')" />
		<Copy DestinationFolder="$(StyleCopTargetsFolder)" SourceFiles="@(StyleCopTargets)" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true" />

		<MakeDir Directories="$(StyleCopFolder)" Condition="!Exists('$(StyleCopFolder)')" />
		<Copy DestinationFolder="$(StyleCopFolder)" SourceFiles="@(StyleCopLibs)" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true" />
	</Target>

	<Target Name="StyleCopClear">
		<ItemGroup>
			<StyleCopToDelete Include="$(StyleCopTargetsFolder)\StyleCop*.targets" />
			<StyleCopToDelete Include="$(StyleCopFolder)\*.*" />
		</ItemGroup>

		<Delete Files="@(StyleCopToDelete)" TreatErrorsAsWarnings="true" Condition="@(StyleCopToDelete) != ''" />
	</Target>
</Project>



Запустив msbuild.exe build.proj мы получим:
0. если есть старые файлы — удаление
1. копирование файлов из targets в $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.targets\ImportAfter (в зависимости от версии запускаемого MSBuild это может быть что-то похожее на C:\Program Files (x86)\MSBuild\14.0\Microsoft.Common.Targets\ImportAfter
2. копирование файлов из lib в $(MSBuildExtensionsPath)\StyleCop (например C:\Program Files (x86)\MSBuild\StyleCop)

Настраиваем в TFS/TeamCity билд, который в режиме Continuous Integration реагирует на коммиты в этот репозиторий и делаем так, чтоб он прогнялся принудительно на каждом агенте. Таким образом после внесения изменений у нас все агенты будут автоматом обновляться до свежих скриптов/dll/конфигов StyleCop. Милота.

Настраиваем StyleCop



В StyleCop.MSBuild.targets нам важно поправить две вещи.
1. Включить StyleCop. Находим меняем false на true:
  <PropertyGroup Condition="'$(StyleCopEnabled)' == ''">
    <StyleCopEnabled>true</StyleCopEnabled>
  </PropertyGroup>
  <PropertyGroup Condition="'$(StyleCopTreatErrorsAsWarnings)' == ''">
    <StyleCopTreatErrorsAsWarnings>false</StyleCopTreatErrorsAsWarnings>
  </PropertyGroup>

(второе — чтоб результаты проверки вываливались в Error, а не Warn).
2. Поправить путь к StyleCop.dll
  <UsingTask AssemblyFile="$(MSBuildExtensionsPath)\StyleCop\StyleCop.dll" TaskName="StyleCopTask"/>

и к конфигу
  <PropertyGroup Condition="'$(StyleCopOverrideSettingsFile)' == '' and Exists('$(MSBuildExtensionsPath)\StyleCop\Settings.StyleCop')">
	<StyleCopOverrideSettingsFile>$(MSBuildExtensionsPath)\StyleCop\Settings.StyleCop</StyleCopOverrideSettingsFile>
  </PropertyGroup>

Здесь есть нюанс.

Файл Settings.StyleCop


Правила StyleCop по-умолчанию довольно спорны. Там есть некоторое количество как сомнительных так и просто неудобных нам. Править этот файл руками неудобно. Для этого проще поставить тот самый .msi с сайта и он установит заодно StyleCopSettingsEditor.exe — удобный GUI для редактирования списка правил.
Фокус в том, что по-умолчанию в StyleCop включено наследование настроек и дефолтовые берутся из того, который вам установился из msi (правда, предварительно ещё сканируется всё дерево каталогов вверх в поисках .stylecop-файла, чтоб унаследовать его автоматом тоже).
Так что открыв файл в своей какой-нибудь папке, поправив правила и сохранив, вы на самом деле сохраните только отличия от дефолта. И если вы в репозиторий положите только вот этот свой модифицированный файл, то у вас будет проверяться только то, что есть в нём. Включённых по-дефолту правил StyleCop под MSBuil'ом не получит, потому что у него нет того самого дефолтного файла.
Чтоб не править исходный файл и иметь возможность подменять его в случае чего, при этом сохранив свои модификации, я сделал так:

Изначальный файл Settings.StyleCop переименован в default.StyleCop и положен рядом.

Что в итоге?


Имеем принудительную проверку кода через StyleCop. Нарушения видны сразу в виде ошибок в билд-логе.
Имеем механизм, который автоматически обновляет stylecop на билд-машинах.

PS: Можно прогнать локально этот «msbuild.exe build.proj». Тогда все эти же ошибки будут видны сразу в студии. Т.е. msbuild импортирует эти ImportBefore/After даже когда из студии прогоняется. Без всяких установок плагинов и прочего, зато сразу в Error List — знай себе кликай по списку и правь там, куда студия курсор поставит. Без вычитки логов с билд-сервера.

1. Я честно пытался написать расширение для TFS через ISubscriber, как описано здесь, но в итоге утонул в скудной документации, полном отсутсвии инструментов для публикации ("drop the assembly in the plugins directory on your server C:\Program Files\Microsoft Team Foundation Server 12.0\Application Tier\Web Services\bin\Plugins. Then recycle your app pool and the plugin will be activated." — это смешно), весьма неочевидной и запутанной структуре классов/зависимостей в этой библиотеке Microsoft.TeamFoundation.Git.Server и даже несоответствии методов с их действиям. А как представил себе, что придётся это потом под TFS2015 пересобирать — сделал Close solution и отправился искать другие способы реализации.
Tags:
Hubs:
+17
Comments 7
Comments Comments 7

Articles