В некоторых проектах сборке отводится роль Золушки. Основные усилия команда сосредоточивает на разработке кода. А самой сборкой могут заниматься люди, далёкие от разработки (например, отвечающие за эксплуатацию, либо за развёртывание). Если сборка хоть как-то работает, то её предпочитают не трогать, и речь об оптимизации не заходит. Вместе с тем в больших гетерогенных проектах сборка оказывается достаточно сложной и к ней вполне можно подходить как к самостоятельному проекту. Если же относиться к сборке как к второстепенному проекту, то в результате будет получен неудобоваримый императивный скрипт, поддержка которого будет в значительной степени затруднена.


В этой заметке мы рассмотрим, по каким критериям мы выбирали инструментарий, а в следующей — каким образом этот инструментарий используем. (Есть также перевод на английский.)


CI/CD (opensource.com)


Общая модель сборки проектов


Модель сборки проектов во всех рассматриваемых инструментах представляет собой направленный граф без циклов (орграф, DAG), а не иерархическую структуру, типичную для структурного подхода (когда процедура вызывает другие процедуры, а затем пользуется результатами). Связано это с тем, что при разработке проекта вносятся небольшие изменения и бо?льшая часть операций по сборке не требуется. То есть организация проекта в форме орграфа является основой для того, чтобы выполнялись только те действия, которые необходимы для ближайшей задачи, тем самым часто используемые операции будут выполняться достаточно быстро.


Узлами в графе являются цели или задачи. Цели — результаты, которые необходимо достичь; а задачи — операции, которые надо выполнить, чтобы достичь текущей цели. При этом задача может быть запущена только в том случае, когда все зависимости удовлетворены.


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


Поверх этой основной модели в ряде инструментов реализованы более высокоуровневые модели.


Декларативный или императивный стиль


При написании программ часто противопоставляются императивный и декларативный стили программирования. Под императивным стилем понимается описание последовательности действий, описывающих, как прийти к какому-то результату. Причём сам результат не описывается. При использовании декларативного стиля описывается желаемый результат, что мы хотим получить. Последовательность действий при этом выбирается инструментом самостоятельно.


Такое разделение в некоторой степени условно. Сравним, например, такие программы:


fun loadPerson(file: File): Person = TODO()
val ivan = loadPerson("ivan")

и


fun personDefinedInFile(file: File): Person = TODO()
val ivan = personDefinedInFile("ivan")

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


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


Выбор инструмента


При старте проекта иногда бывает можно оценить, насколько сложной окажется интеграция проекта. В нашем случае оказалось, что требуется собирать несколько модулей node.js, несколько go-lang, осуществлять развёртывание нескольких модулей terraform (и ни одного jvm-модуля).


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


Для нового проекта мы задумались, какой инструмент взять за основу системы сборки. Оценили такие варианты:


  • make,
  • maven,
  • sbt,
  • gradle/groovy,
  • gradle/kotlin.

Make


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


Плюсы


  • наличие компетенции;
  • похож на shell.

Минусы


  • поддерживается только базовая модель целей и задач, нет поддержки понятия проекта;
  • запутанный синтаксис;
  • глобальные состояния;
  • нет поддержки плагинов, трудно повторно использовать части кода;
  • отсутствует возможность декларативного описания сборки; только императивный код.

В отсутствии ограничений, в скриптах сборки можно встретить


  • генерацию шаблонов с помощью perl;
  • установку недостающих исполняемых файлов путём исполнения .sh скриптов из интернета
    при каждом запуске скрипта;
  • вызов make-файлов для подпроектов с нестандартными названиями целей;
  • отсутствие согласованной обработки ошибок.

Такие неожиданности затрудняют поддержку и делают сборку небезопасной операцией.


Maven


Maven стал революцией в системах сборки в момент своего появления. Идеи декларативного описания проектов, широкого применения конвенций, использования плагинов, хранения артефактов в репозиториях, использования системы идентификации, включающей версии — всё это обеспечило признание и широкое использование во многих JVM-проектах по сей день.


Плюсы


  • наличие компетенции (часть команды имеет богатый опыт работы с Maven-проектами);
  • развитая модель проектов и подпроектов;
  • поддержка плагинов;
  • декларативная модель;
  • maven wrapper.

Минусы


  • слабая поддержка других технологий;
  • наличие трудно-преодолеваемых ограничений;
  • трудоёмкость реализации плагинов;
  • отсутствие удобной возможности реализации императивных скриптов;
  • жёсткая структура жизненного цикла;
  • не очень удобный XML-формат.

Отсутствие императивных скриптов является и плюсом и минусом. С одной стороны, декларативный подход обеспечивает жёсткое разделение кода и модели, с другой стороны, задачи сборки зачастую требуют отдельных вставок императивной логики, и решение таких задач в maven'е мучительно.


Sbt


Sbt — инструмент сборки Scala-проектов. Появился примерно в то же время, что и gradle.


Плюсы


  • развитый язык;
  • поддержка плагинов и императивных вставок;
  • поддержка инкрементной сборки;
  • поддержка непрерывной сборки по мере изменения;
  • параллельное исполнение.

Минусы


  • неожиданная модель (вместо задач — "настройки");
  • неявные зависимости через .value;
  • слабая поддержка других технологий (go, node.js);
  • неожиданный синтаксис.

Sbt в целом выглядит как специализированная система для сборки Scala-проектов, нежели чем универсальный инструмент сборки любых проектов.


Gradle


Gradle появился в 2007 году в качестве ответа на основные ограничения maven — отсутствие императивного кода, трудность реализации плагинов, неудобство при нестандартных операциях. Gradle базируется на идеях, предложенных Maven'ом, развивает их и меняет акценты. Основными частями модели gradle являются:


  • задачи (Task) — выполняемые операции, узел графа зависимостей, имя+описание;
  • проект — логическая единица организации кода, совокупность и scope задач, точка подключения плагинов;
  • плагин — возможность или фича, добавляемая в проект. Среди прочего — набор задач;
  • зависимости, проверка up-to-date.

Важным усовершенствованием стало использование DSL (domain specific language),
основанного на императивном языке, с помощью которого формируются элементы декларативной модели,
а также решаются императивные задачи.


Плюсы


  • поддержка модели проектов;
  • поддержка декларативного (основанного на модели) и императивного подхода одновременно;
  • поддержка инкрементной сборки;
  • непревзойденная гибкость;
  • превосходная документация (для двух диалектов сразу);
  • очень быстрая работа (даже определения задач выполняются только в случае необходимости);
  • кросс-платформенность — работает везде;
  • gradle wrapper — небольшой скрипт для загрузки и запуска gradle правильной версии; разработчикам не требуется вручную настраивать утилиты и обновлять при изменении версии в репозитории;
  • удобный и понятный DSL.

Минусы


  • отсутствие компетенции (до этого gradle широко не применялся членами команды);
  • насколько мне известно, отсутствуют механизмы защиты от чрезмерного использования императивного кода. Необходимы дисциплина и следование рекомендованным практикам при разработке скриптов сборки во избежание макаронного кода;
  • не поддерживаются иные механизмы зависимостей, кроме JVM (maven-repository, ivy2);
  • необходимость предпринимать определённые усилия для того, чтобы каждая задача поддерживала проверку up-to-date (полезную для инкрементной сборки). В частности, для каждой задачи необходимо описать входные и выходные данные. В принципе, обычные возможности DAG доступны без усилий, но gradle позволяет достичь ещё более высокой скорости работы при условии указания входов и выходов.

Выбор диалекта — gradle/groovy или gradle/kotlin


Изначально gradle использовался с помощью groovy-DSL. В дальнейшем был разработан DSL на основе Kotlin'а.


Плюсы Kotlin'а


  • компилируемый строго-типизированный язык:
    • защита от ошибок на этапе компиляции,
    • поддержка intelli-sense,
    • безопасный рефакторинг,
  • хорошая поддержка DSL;
  • простой синтаксис, меньше бойлерплейта, по сравнению с Java;
  • достаточно много сахара.

Минусы


  • на просторах интернета большинство примеров — для groovy, вначале бывает трудно сообразить, как переписать пример на kotlin'е;
  • изначально был сделан gradle/groovy DSL, поэтому некоторые элементы неидеально представлены в kotlin'е (extra, имена задач, ...);
  • немного повышается порог входа в связи с необходимостью освоения нового языка.

Заключение


По итогам сравнения имеющихся инструментов сборки проекта мы решили попробовать в нашем проекте реализовать сборку и CI/CD с использованием gradle/kotlin. Этот вариант обладает рядом преимуществ в сравнении с реализацией сборки проекта на основе make/shell.


Gradle/kotlin vs make


Ниже приведены сравнительные преимущества gradle/kotlin по отношению к make:


Преимущества:


  • высочайшая скорость работы. Команда Gradle прикладывает постоянные усилия в направлении улучшения скорости и в реализации инструментов, способствующих реализации высокоскоростных скриптов. Можно добиться того, что все задачи, не требующие выполнения, будут пропущены. А задачи, требующие выполнения — выполнены только для изменившихся файлов.
  • унификация языка. Все задачи решаются в рамках одного компилируемого строго-типизированного языка с согласованным и продуманным синтаксисом — Kotlin. Нет необходимости изучать особенности режимов make, различия версий shell-интерпретаторов, варианты обработки параметров командной строки в разных утилитах, отдельные языки программирования для шаблонов (php?, perl?). За счёт использования современного языка со статической типизацией, исключается множество классов ошибок, характерных для скриптовых языков.
  • декларативная модель проектов/подпроектов и плагинов поверх декларативного орграфа задач. В make — только непосредственно императивные задачи.
  • возможность комбинирования декларативного и императивного подхода. Несмотря на то, что декларативный подход обеспечивает понятность и чистоту кода, лёгкость поддержки, возможности комбинирования компонентов, императивный подход может оказаться незаменимым в силу своей гибкости. Новые задачи можно вначале решить императивным способом (ad-hoc), а затем обобщить и выделить в форме декларативных конфигурируемых плагинов.
  • возможность создания повторно-используемых плагинов. Причем написание таких плагинов не вызывает особых сложностей и плагинам предоставляется удобный API с широкими возможностями. В случае make стандартных механизмов не предусмотрено, из-за чего возникает дублиирование кода и переизобретение велосипедов.
  • платформа JVM, на которой реализованы библиотеки на все случаи жизни. В make некоторые задачи требуют установки платформо-специфических приложений

Недостатки:


  • неудобно вызывать shell-команды. Для каждой команды надо создать и настроить task.
  • более высокие требования к инженерной культуре — язык со статической типизацией, декларативная модель проекта, использование развитых концепций (свойства, вывод зависимостей, проверки up-to-date).

В следующей части рассмотрим некоторые особенности применения gradle/kotlin к сборке не-JVM проектов.


Благодарности


Хотелось бы поблагодарить nolequen, Starcounter, tovarischzhukov за конструктивную критику черновика статьи.