21 апреля 2016 г.

Этот пост появился не из ниоткуда. Однажды, я, активно использующий CodeContracts, все таки перевез свой блог (тот самый, который вы читаете) на Continuous Integration сервер. Мой выбор пал на Visual Studio Team Services. Ничего не предвещало беды до тех пор, пока безупречно выполняющиеся на моей локальной машине тесты проекта не стали падать с ужасающими ошибками при попытке их запуска на VSTS.

В результате небольшого проведенного расследования, удалось выяснить причину ошибок - предусловия библиотеки CodeContracts, а именно предусловия с заданным типом исключением Contract.Requires<TException>. Дело в том, что при использовании предусловий с заданным типом исключения, сборка не просто может, а должна быть переписана с помощью утилиты ccrewrite.

Разбираемся в причинах

Прежде всего найдем агента, выполняющего сборку на сервере <Ваш VSTS аккаунт>.visualstudio.com/_admin/_AgentPool: в списке его системных переменных нет упоминания CodeContracts



Как следствие, получаемая сборка при построении не подвергается переписыванию утилитой CodeContracts ccrewrite, поставляемой при установке CodeContracts. Значит, нужно установить CodeContracts на CI сервер, выполняющий сборку. Однако, как установить CodeContracts на неподконтрольную разработчику машину?

Создание локальной копии CodeContracts

Предоставим необходимые утилиты для работы CodeContracts рядом с исходными кодами через систему контроля версий По соглашению, все дополнительные утилиты будем располагать в директории Tools корня репозитория: Скопируйте содержимое установленных CodeContracts на вашей машине (по умолчанию C:\Program Files (x86)\Microsoft\Contracts) в директорию проекта Tools/CodeContracts.

Для работы CodeContracts на VSTS не обязательно копировать все содержимое установленных CodeContracts. Достаточно лишь утилит в каталоге Bin, файлов MsBuild для используемой версии и каталога Languages, однако, я предпочитаю иметь полноценную копию CodeContracts в репозитории с лицензией, примерами и документацией.

Пришла пора сделать коммит, включающий локальную копию CodeContracts, на удаленный сервер:
git add Tools/CodeContracts
git commit -m "CodeContracts"
git push
Прежде чем переходить к дальнейшим действиям, проверьте, что все содержимое CodeContracts было скопировано на сервер. Я практически уверен, что в удаленном репозитории отсутствуют файлы директории Tools/CodeContracts/Bin.

Если это так, добавьте в конец файла .gitignore строку !Tools/CodeContracts/Bin. Добавьте в индекс файлы этой директории и повторите коммит, на этот раз содержимое каталога должно успешно скопироваться на удаленный репозиторий.

Дело в том, что файл .gitignore, создаваемый Visual Studio по умолчанию, содержит исключение для каталога Bin, обычно содержащем результаты билдов, которым не место в системе контроля версий. Но в данном случае в этом каталоге содержатся необходимые для сборки утилиты, поэтому строкой !Tools/CodeContracts/Bin задается исключение из общего правила.

В результате удается в каком-то смысле установить CodeContracts на VSTS в виде локальной утилиты.

Дополнительная настройка

Если вы попробуете собрать сборку, результат будет, внезапно, абсолютно тем же самым - ошибка построения, Действительно, если бы CodeContracts были установлены "официально", инсталлятор CodeContracts добавил бы дополнительный шаг сборки, заключающийся в итоговом переписывании сборки утилитой ccrewrite, в процесс построения приложения. Поэтому придется добавлять шаг переписывания сборки через ccrewrite вручную.

Все действия, которые выполняет CodeContracts при сборке проектов описаны в файле Tools/CodeContracts/MsBuild/<Номер версии>/Microsoft.CodeContracts.targets Достаточно импортировать этот файл в файл файлы ваших проектов и CodeContracts наконец-то будут работать на VSTS. Файл проекта .csproj по сути - обычный xml файл, поэтому его легко можно изменить в вашем любимом текстовом редакторе. Добавьте перед закрывающим </Project> тегом строку следующего содержания:
<Import Project="$(SolutionDir)\Tools\CodeContracts\MSBuild\v$(MSBuildToolsVersion)\Microsoft.CodeContracts.targets" />
Наконец, нужно не забыть указать путь к локальной версии CodeContracts, поместив перед строками импорта установку переменных окружения:
<PropertyGroup>
  <CodeContractsInstallDir>$(SolutionDir)\Tools\CodeContracts\</CodeContractsInstallDir>
  <DontImportCodeContracts>True</DontImportCodeContracts>
</PropertyGroup>
Первая переменная CodeContractsInstallDir указывает путь к локальной версии CodeContracts. Вторая переменная отменяет автоматическое включение CodeContracts для предотвращения дублирования шага построения, добавленного нами вручную и автоматического, добавляемого CodeContracts при их установке, в противном случае при сборке проекта на локальной машине вы будете видеть предупреждения о дублирующих шагах построения. Хотя сборка успешно собирается, я всегда стараюсь не допускать предупреждений при компиляции, приравнивая их к ошибкам. К сожалению, придется сделать это для всех проектов, в которых используются CodeContracts.

Возможно, что существует способ создания некоторого "глобального" для всех проектов файла с установкой переменных окружения, но я не нашел такого способа.

Сохраните изменения в репозитории, и выполните построение сборки, результаты которого должны вас обрадовать:


Подводя итоги

Итак, описанным нехитрым способом возможен запуск на CI сервере любых утилит и программ, не предусмотренных стандартной поставкой CI сервера. В общем случае требуется выполнить всего два шага:
  1. Скопировать на удаленный репозиторий все необходимые для работы файлы (для данного случая - каталог с установленными CodeContracts);
  2. Настроить файлы проектов: установка переменных окружения, добавление шагов сборки и т.п. (для CodeContracts - установка переменных окружения CodeContractsInstallDir и DontImportCodeContracts).
Признаюсь, я потратил половину своего свободного выходного для того, чтобы подружить CodeContracts и VSTS. Надеюсь, что описанный мною способ сможет помочь кому-либо сохранить свое время, и вдобавок не разочароваться ни в CodeContracts, ни в VSTS, ни в CI.