Непрерывная интеграция проектов .NET: NAnt и/или MSBuild?

в 13:25, , рубрики: .net, continuous integration, msbuild, nant, Программирование, разработка, метки: , , , ,

Привет всем читателям !

Не так давно я начал использовать для сборки сервером непрерывной интеграции некоторых проектов NAnt наряду с уже освоенным MSBuild. Как всегда, в процессе работы обнаруживаются бонусы с разными знаками (как плюс, так и минус). Тех, кому интересны детали сборки разными движками (MSBuild, NAnt) в контексте сервера CI, с удовольствием приглашаю под кат.

Непрерывная интеграция проектов .NET: NAnt и/или MSBuild?

Процесс сборки

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

  1. чистка сборочной папки от результатов предыдущей сборки
  2. инициализация процесса сборки (например, установка нужной версии в файлы кода)
  3. компиляция файлов с кодом
  4. прогон тестов
  5. создание файлов специального назначения на основе бинарных файлов (инсталляторы msi, пакеты NuGet и т.п.)
  6. публикация результатов сборки (выкладка на FTP, nuget push)

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

Поддержка процесса сборки в MSBuild

В виду того, что MSBuild является штатным сборщиком для IDE (Visual Studio, SharpDevelop), то изначально работали с ним.
Для сборки проекта целиком использовалась пара дополнительных скриптов. Почему именно пара, а не один, спросите вы. Всё достаточно просто: MSBuild не умеет подгружать targets-файлы в процессе выполнения скрипта. Он сначала импортирует все заданные в скрипте файлы, а затем стартует процесс сборки. Поэтому первый скрипт отвечал только за скачивание дополнительных targets-файлов из репозитория NuGet, а второй, пользуясь всем свалившимся на него богатством, выполнял сценарий сборки.

Для сборки отдельных проектов использовались сгенерированные IDE файлы-csproj с лёгким обогащением их своими переменными. Оказалось, что с наследованием проектов у MSBuild дело обстоит туго. Для правильного прогона родительский скрипт должен был полностью передавать весь набор параметров внутрь дочернего, простого определения свойства в теле родителя было недостаточно. Это, конечно, не смертельно, но заставляет нас делать лишнюю работу.

Если с тремя первыми фазами, особых проблем нет, то реализация оставшихся требует некоторой сноровки. Т.к. файлы csproj используются IDE для управления проектом, то сильно менять их нельзя. Выходом стало написание в главном скрипте сборки сценария всех оставшихся фаз. Плюсы такого решения в том, что всё описано в одном месте. Однако, когда число production и парных им unit-test проектов возрастает до десятка, поддерживать такой скрипт сборки становится сложно.

IDE любят генерировать отдельные выходные папки на каждую комбинацию Platform/Configuration, поэтому определить в процессе сборки, в какой папке у тебя находятся dll-ки от Reference-проекта не всегда является тривиальной задачей.

Если мы решаем использовать у себя в проекте репозиторий пакетов NuGet, то тут же получаем дублирование. Ссылка на dll из пакета будет присутствовать в двух местах, а именно в файлах csproj и packages.config. У клиента Nuget имеются известные проблемы при поддержании синхронности двух этих файлов. В частности, обновление пакетов не всегда приводит к желаемому результату.

Поддержка процесса сборки в NAnt

NAnt на сегодняшний день имеет актуальную версию 0.92 (релиз лета 2012 года), поэтому в своих изысканиях я опирался именно на неё.

Здесь мы, пожалуй, начнём с выявленных недостатков.

Основные IDE (Visual Studio, SharpDevelop) не любят NAnt и «из коробки» формат его проектов не поддерживают. Хотя, ради полноты картины можно отметить, что одна из старых версий SharpDevelop NAnt поддерживала. Почему в текущей ветке развития SharpDevelop поддержка NAnt свёрнута в ноль для меня пока остаётся загадкой.

NAnt не обладает некоторыми проприетарными бонусами, позволяющими ему собирать некоторые типы проектов без помощи MSBuild (например, WPF). О минимизации последствий работы с MSBuild скажем чуть позже.

Что же мы имеем из приятных бонусов?

Работа с файлами по маскам позволяет нам минимизировать объём работы по поддержке единожды написанных скриптов.

Наследование свойств между проектами позволяет нам не заботиться о явной передаче всех значений между проектами разных уровней. Например, легко организовать сбор всех результатов компиляции в одной папке на уровне Solution вместо размножения папок bin, obj в каждом отдельном проекте.

Императивность работы NAnt позволяет легко модифицировать отдельные target'ы проектов по своему усмотрению. Реализация типовой задачки: скопировать помимо библиотек еще и пару самодельных конфигов при помощи BeforeBuild и AfterBuild событий (внутри них не используются задачи MSBuild, а обычно используются cmd- или Powershell-скрипты) смотрится «костыльной» по сравнению с явным использованием родной задачи NAnt copy.

Система вывода штатного сборщика Java Ant давно и хорошо парсится на сборочных серверах, соответственно с NAnt особых проблем нет. Для сервера сборки Jenkins (Hudson) наличествует отдельный плагин для NAnt.

NAnt, в отличие от MSBuild, умеет динамически загружать наборы дополнительных задач при явном на то указании внутри скриптов или загружает их автоматически, если их нужным образом расположить рядом с NAnt.exe. Таким образом, сам сборщик может распространяться способом «xcopy», что значительно облегчает использование единой конфигурации сборщика параллельно на нескольких машинах. Из полезных библиотек с дополнениями штатного набора задач хочется отметить:

  • NAnt Contrib большой набор дополнений для NAnt, обеспечивающий в том числе взаимодействие с MSBuild и Subversion
  • Wix позволяет собирать инсталляторы MSI
  • Nuget бонусы для работы с менеджером пакетов

По итогам всего выше сказанного для меня оптимальным сочетанием стало такое: сборка MSBuild при работе в IDE, запуск NAnt при работе сервера CI.

Сборка проекта WPF

В заключение своего поста хотел рассказать о ложке мёда в бочке дёгтя, а именно о сборке проекта WPF при помощи NAnt. Скажу сразу, волшебства по полному преодолению двойной компиляции (xaml -> cs -> dll) не будет. Однако, обучить MSBuild ходить в лоточек под зорким присмотром NAnt всё-таки можно.

При запуске MSBuild в контексте сборки NAnt у нас в наличии две ключевые проблемы:

  1. MSBuild ошибочно думает, что проекты зависимостей нужно собирать
  2. MSBuild ищет dll с зависимостями там, где они должны были оказаться после сборки проектов-зависимостей

Первая проблема решается установкой ключа сборки /p:BuildProjectReferences=false.

Вторая в лоб не решается, поэтому подойдём немного с иной стороны. Содержать статическую версию файла csproj для прогона сборки через NAnt нерентабельно, т.к. поддерживать придётся обе версии. Значит, нужно генерировать такую версию «на лету». Возьмём и напишем небольшое преобразование XSL, запустим его через задачу style NAnt и скормим MSBuild скорректированный проект. При запуске задачи style передаётся один параметр, содержащий путь к уже скомпилированным файлам DLL.

Преобразование XSL

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msbuild="http://schemas.microsoft.com/developer/msbuild/2003">
    <xsl:output indent="yes" method="xml"/>
    
    <xsl:param name="dllPath"/>

    <xsl:template match="/">
        <xsl:apply-templates />
    </xsl:template>
        
    <xsl:template match="//msbuild:ItemGroup/msbuild:ProjectReference">
        <xsl:element name="Reference" namespace="http://schemas.microsoft.com/developer/msbuild/2003">
            <xsl:attribute name="Include">
                <xsl:value-of select="msbuild:Name/text()"/>
            </xsl:attribute>
            <xsl:element name="HintPath" namespace="http://schemas.microsoft.com/developer/msbuild/2003">
                <xsl:value-of select="$dllPath"/>
                <xsl:value-of select="msbuild:Name/text()"/>.dll</xsl:element>
        </xsl:element>
    </xsl:template>

    <xsl:template match="*|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Таким образом, нам удаётся добиться того, что MSBuild собирает свой проект изолированно, ничего не подозревая о других проектах. Как говорят люди «меньше знаешь, лучше спишь», что для управления MSBuild весьма актуально.

Место (вместо) выводов

Как и чем пользоваться всегда надо решать самому или внутри команды, тщательно взвесив все «за» и «против».

Начать лучше всего с MSBuild, т.к. он обычно уже установлен на машине разработчика и не требует лишних телодвижений по основной настройке.

Работа с NAnt тоже не слишком сложна, но требует от разработчика некоторого «поворота» мозга.

Автор: unlocker

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js