Используем Cake для сборки C# кода

в 12:02, , рубрики: .net, build, C#, cake, метки:

Всем привет! Я хочу рассказать о таком инструменте как Cake (C# Make).

Cake build

Итак, что такое Cake?

Cake — это кроссплатформенная система сборки, использующая DSL с синтаксисом C# для того, что осуществлять в процессе сборки такие вещи, как сборка бинарников из исходных кодов, копирование файлов, создание/очищение/удаление папок, архивация артефактов, упаковка nuget-пакетов, прогоны юнит-тестов и многое другое. Так же Cake имеет развитую систему аддонов (просто C# классы, зачастую упакованные в nuget). Стоит отметить, что большое количество полезных функций уже встроены в Cake, а еще больше, практически на все случаи жизни, написаны сообществом и довольно успешно распространяются.

Сake использует модель программирования называемую dependency based programming, аналогично другим подобным системам вроде Rake или Fake. Суть этой модели в том, что мы для исполнения нашей программы мы определяем задачи и зависимости между ними. Подробнее про данную модель можно почитать у Мартина Фаулера.

Подобная модель заставляет нас представлять наш процесс сборки как некоторые задачи (Task) и зависимости между ними. При этом логически исполнение идет в обратном порядке: мы указываем задачу, которую хотим выполнить и ее зависимости, Cake же определяет, какие задачи могут быть выполнены (для них разрешены или отсутствуют зависимости) и исполняет их.

dependency based programming example

Так, например, мы хотим исполнить A, однако она зависит от B и C, а B зависит от D. Таким образом Cake исполнит их в следующем порядке:

  1. С или D
  2. B
  3. A

Задача же (Task) в Cake обычно представляет собой законченный кусок работы по сборке/тестированию/упаковке. Объявляется следующим образом

Task("A") // Название
.Does(() =>
{
    //Реализация Task A
});

Указать же, что задача A зависима от, например, задачи B можно с помощью метода IsDependentOn:

Task("A") // Название
.IsDependentOn("B")
.Does(() =>
{
    //Реализация Task A
});

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

Task("B") // Название
.IsDependentOn("C")
.WithCriteria(DateTime.Now.Second % 2 == 0)
.Does(() =>
{
    //Реализация Task A
});

Если же какая-то задача зависит от задачи B, а критерий принимает значение false, то задача B не выполнится, однако поток исполнения пойдет дальше и исполнит задачи, от которых зависит B.
Существует также перегрузка метода WithCriteria, принимающая в качестве параметра функцию, которая возвращает bool. В этом случае выражение будет посчитано только тогда, когда до задачи дойдет очередь, а не в момент выстраивания дерева задач.

Cake также поддерживает некоторые специфичные препроцессорные директивы, среди которых load, reference, tool и break. Подбробнее о них можно почитать на соответствующей странице документации.

Думаю, что людей, которые собирают свои проекты руками в эру DevOps, уже не так уж много. Преимущество любой системы сборки в сравнении с ручной сборкой очевидно — автоматически настроенный процесс всегда лучше ручных манипуляций.

Преимущества Cake при разработке на C

Зачем использовать именно Cake, раз существует множество альтернатив? Если вы не разрабатываете на C#, то, скорее всего, не за чем. А если разрабатываете, то выглядит разумным писать скрипты сборки на тем же языке, на котором написан и основной проект, поскольку не нужно изучать еще один язык программирования и плодить их зоопарк в рамках одной кодовой базы. Потому и стали появляться системы сборки типа Rake (Ruby), Psake (Powershell) или Fake (F#).

Cake — безусловно не единственный способ собрать проект на C#. Как варианты, можно привести в пример чистый MSBuild, Powershell, Bat-скрипты или CI Server типа Teamcity или Jenkins, однако все они имеют как преимущества, так и недостатки:

  • Скрипты на Powershell, равно как Bat/Bash довольно сложно поддерживать
  • DSL на основе C# приятнее по синтаксису DSL на основе XML из MSBuild. К тому же поддержка MSBuild в Linux/Mac появилась не так давно.
  • CI-сервер накладывает Vendor-lock и зачастую требует "программирования мышкой", следовательно и отвязывает версию процесса сборки от версии кода в репозитории, хотя некоторые CI системы и позволяют хранить файлы с описанием процесса сборки вместе с кодом.
  • Использование CI не позволяет собирать код локально так же, как и на CI-сервере

Установка Cake

Теперь поговорим о том, как же исполнять скрипты с задачами. У cake есть загрузчик, который все сделает за вас. Скачать его можно либо вручную, либо следующей powershell командой:

Invoke-WebRequest http://cakebuild.net/download/bootstrapper/windows -OutFile build.ps1

Скачанный файл build.ps1 затем сам загрузит необходимый cake.exe, если он еще не загружен, и исполнит cake-скрипт (по-умолчанию это build.cake), если мы вызовем его следующей командой:

Powershell -File ".build.ps1" -Configuration "Debug"

Мы можем также передать в build.ps1 аргументы командной строки, которые потом исполнятся. Они могут быть как встроенными, например, configuration, который обычно отвечает за конфигурацию сборки, так и заданные самостоятельно — в этом случае есть два способа:

  • Передать как значение параметра ScriptArgs:
    Powershell -File ".build.ps1" -Script "version.cake" -ScriptArgs '--buildNumber="123"'
  • Изменить build.ps1 таким образом, чтобы он пробрасывал переданный аргумент cake.exe.

Примеры

Что же, теперь перейдем к практике. Легко можно представить типичный цикл сборки nuget-пакета:

nuget pack pipeline

  • Собираем с помощью MSBuild из исходников dll
  • Прогоняем юнит-тесты
  • Собираем все в nuget по nuspec-описанию
  • Пушим в nuget feed

Сборка dll

Чтобы собрать из исходников наш solution, необходимо сделать 2 вещи:

  • Восстановить nuget-пакеты, от которых зависит наш solution с помощью функциии NuGetRestore
  • Собрать solution по умолчанию встроенной в cake функцией DotNetBuild, передав ей параметр configuration.

Опишем задачу по сборке solution на cake-dsl:

var configuration = Argument("configuration", "Debug");

Task("Build")
.Does(() =>
{
    NuGetRestore("../Solution/Solution.sln");
    DotNetBuild("../Solution/Solution.sln", x => x
        .SetConfiguration(configuration)
        .SetVerbosity(Verbosity.Minimal)
        .WithTarget("build")
        .WithProperty("TreatWarningsAsErrors", "false")
    );
});

RunTarget("Build");

Конфигурация сборки, соответственно, считывается из аргументов командой строки с помощью функции Argument со значением по умолчанию "Debug". Функция RunTarget запускает указанную задачу, так что мы сразу можем проверить правильность работы нашего cake-скрипта.

Юнит-тесты

Чтобы запустить юнит-тесты, совместимые с nunit v3.x, нам нужна функция NUnit3, которая не входит в поставку Cake и поэтому требует подключения через препроцессорную директиву #tool. Директива #tool позволяет подключать инструменты из nuget-пакетов, чем мы и воспользуемся:

#tool "nuget:?package=NUnit.ConsoleRunner&version=3.6.0"

При этом сама задача оказывается предельно проста. Не забываем, конечно, что она зависит от задачи Build:

#tool "nuget:?package=NUnit.ConsoleRunner&version=3.6.0"

Task("Tests::Unit")
.IsDependentOn("Build")
.Does(()=> 
{
    NUnit3(@"..SolutionMyProject.Testsbin" + configuration + @"MyProject.Tests.dll");
});

RunTarget("Tests::Unit");

Пакуем все в nuget

Чтобы упаковать нашу сборку в nuget, воспользуемся следующей nuget-спецификацией:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
    <metadata>
        <id>Solution</id>
        <version>1.0.0</version>
        <title>Test solution for demonstration purposes</title>
        <description>
            Test solution for demonstration purposes
        </description>
        <authors>Gleb Smagliy</authors>
        <owners>Gleb Smagliy</owners>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <tags></tags>
        <references>
            <reference file="MyProject.dll" />
        </references>
    </metadata>
    <files>
        <file src=".MyProject.dll" target="libnet45"/>
        <file src=".MyProject.pdb" target="libnet45"/>
    </files>
</package>

Положим ее в папку со скриптом build.cake. При исполнении задачи Pack перенесем все необходимые артефакты для упаковки в папку "...artefacts". Для этого убедимся, что она есть (а если нет — создадим) с помощью функции EnsureDirectoryExists и очистим ее с помощью функции CleanDirectory, встроенных в Cake. С помощью же функций по копированию файлов переместим нужные нам dll и pdb в папку с арефактами.

По умолчанию собранный nupkg попадет в текущую папку, поэтому укажем в качестве OutputDirectory папку "..package", которую мы так же создали и очистили.

Task("Pack")
.IsDependentOn("Tests::Unit")
.Does(()=> 
{
    var packageDir = @"..package";
    var artefactsDir = @"...artefacts";

    MoveFiles("*.nupkg", packageDir);

    EnsureDirectoryExists(packageDir);
    CleanDirectory(packageDir);

    EnsureDirectoryExists(artefactsDir);
    CleanDirectory(artefactsDir);
    CopyFiles(@"..SolutionMyProjectbin" + configuration + @"*.dll", artefactsDir);
    CopyFiles(@"..SolutionMyProjectbin" + configuration + @"*.pdb", artefactsDir);
    CopyFileToDirectory(@".Solution.nuspec", artefactsDir);

    NuGetPack(new FilePath(artefactsDir + @"Solution.nuspec"), new NuGetPackSettings
    {
        OutputDirectory = packageDir
    });
});

RunTarget("Pack");

Публикуем

Для публикации пакетов используется функция NuGetPush, которая принимает путь до nupkg файла, а также настройки: ссылку на nuget feed и API key. Конечно же, мы не будем хранить API Key в репозитории, а передадим снаружи опять же с помощью функции Argument. В качестве же nupkg возьмем просто первый файл в директории package, подходящий по маске с помощью GetFiles. Мы можем так сделать, поскольку директория была предварительно очищена перед упаковкой. Итак, задача по публикации описывается следующим dsl:

var nugetApiKey = Argument("NugetApiKey", "");

Task("Publish")
.IsDependentOn("Pack")
.Does(()=> 
{
    NuGetPush(GetFiles(@"..package*.nupkg").First(), new NuGetPushSettings {
        Source = "https://www.nuget.org/api/v2",
        ApiKey = nugetApiKey
    });
});

RunTarget("Publish");

Упрощаем себе жизнь

Во время отладки cake-скрипта, да и просто для отладки nuget-пакета, можно не публиковать его каждый раз в удаленный feed. Тут-то нам на помощью и придет функция WithCriteria, которую мы рассматривали. Будем передавать скрипту параметром флаг PublishRemotely (по-умолчанию выставленный в false), чтобы по значению этого флага определять, выложить ли пакет в удаленный feed. Однако cake не выполнит скрипт, если мы пропустим задачу, которую указали функции RunTarget. Поэтому заведем фиктивную пустую задачу BuildAndPublish, которая будет зависеть от Publish:

Task("BuildAndPublish")
.IsDependentOn("Publish")
.Does(()=> 
{
});

RunTarget("BuildAndPublish");

И добавим условие к задаче Publish:

var nugetApiKey = Argument("NugetApiKey", "");
var publishRemotely = Argument("PublishRemotely", false);

Task("Publish")
.IsDependentOn("Pack")
.WithCriteria(publishRemotely)
.Does(()=> 
{
    NuGetPush(GetFiles(@"..package*.nupkg").First(), new NuGetPushSettings {
        Source = "https://www.nuget.org/api/v2",
        ApiKey = nugetApiKey
    });
});

Скрипт для сборки и публикации nuget-пакета почти готов, осталось только совместить все задачи воедино. Окончательную версию кода можно найти в репозитории на github.

Заключение

Мы рассмотрели простейший пример использования cake. Сюда можно было бы добавить интеграцию со slack, мониторинг покрытия кода тестами и еще много всего. Имея богатую систему аддонов, активное сообщество, а также довольно неплохую документацию, cake явлляется весьма неплохой альтернативой CI-системам и MSBuild для сборки С# кода.

Автор: glebsmagliy

Источник


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


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