Tags: continuous deployment, разработка ПО, CI/CD, DevOps, системы сборки, gradle

PAA: ???

Labels: gradle, kotlin

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

Если сборка хоть как-то работает, то её предпочитают не трогать, и речь об оптимизации

не заходит.

Вместе с тем в больших гетерогенных проектах

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

к самостоятельному проекту. Если же относиться к сборке как к второстепенному проекту,

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

будет в значительной степени затруднена.

В этой заметке мы рассмотрим, по каким критериям мы выбирали инструментарий, а

[в следующей](build-systems-2-gradle-howto-ru.md) —

каким образом этот инструментарий используем.

[![CI/CD (opensource.com)](https://opensource.com/sites/default/files/uploads/devops_pipeline_pipe-2.png)](https://opensource.com/article/19/7/cicd-pipeline-rule-them-all)

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

Модель сборки проектов во всех рассматриваемых инструментах

представляет собой [направленный граф без циклов](https://ru.wikipedia.org/wiki/Ориентированный_ациклический_граф)

([орграф, DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph)), а не иерархическую структуру,

типичную для структурного подхода

(когда процедура вызывает другие процедуры, а затем пользуется результатами).

Связано это с тем, что при разработке проекта вносятся небольшие изменения и бо?льшая часть

операций по сборке не требуется. То есть организация проекта в форме орграфа является

основой для того, чтобы выполнялись только те действия,

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

будут выполняться достаточно быстро.

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

а задачи — операции, которые надо выполнить, чтобы достичь текущей цели. При этом задача может

быть запущена только в том случае, когда все зависимости удовлетворены.

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

а несколько разных целей, на основе общей системы целей/задач. Эти несколько разных целей

сосуществуют вместе и их подзадачи пересекаются. Например, сборка, тестирование и

генерация документации взаимосвязаны как раз таким образом.

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

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

При написании программ часто противопоставляются императивный и декларативный стили программирования.

Под *императивным* стилем понимается описание последовательности действий, описывающих, **как**

прийти к какому-то результату. Причём сам результат не описывается.

При использовании *декларативного* стиля описывается желаемый результат, **что** мы хотим получить.

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

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

```kotlin

fun loadPerson(file: File): Person = TODO()

val ivan = loadPerson("ivan")

```

и

```kotlin

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.

<cut/>

### 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

за конструктивную критику черновика статьи.