суббота, 16 марта 2019 г.

Первые шаги в Bazel - системе сборки от Google

С тех пор, как нога человека впервые ступила на землю больших модульных систем, вопрос автоматизации сборки встал острее, чем когда-бы то ни было.
Много копий было сломано в попытке найти ту самую, идеальную систему сборки, удовлетворяющую всем, даже самым смелым фантазиям использующих ее разработчиков.
Тем не менее, самые упорные из них не отчаиваются и каждые несколько лет появляется очередная революционно-современная система сборки, которая обещает нам молочные реки и кисельные берега, успех в личной жизни и на работе и все такое прочее. 
Несколько лет назад вопросом занялась великая и могучая корпорация Google, которая не раз радовала нас отличными и нестандартными решениями типичных проблем разработки программного обеспечения. Можно как угодно относиться к Google, но, я думаю, в целях расширения кругозора полезно ознакомиться поближе с ее очередным детищем. 
Поэтому спешу представить вам быструю, масштабируемую, мультиязычную, надежную систему сборки - Bazel!


Запутанная история
История систем сборки полна неожиданных тупиков, невероятных твистов и прочих элементов качественного триллера с элементами фантастики. Но обо всем по порядку.
Наверное, одной из первых попыток автоматизировать сборку больших проектов можно назвать написание shell/bat/cmd/... скриптов, содержащих последовательность команд, которые нужно выполнить для того, чтобы получить из исходного кода конечный продукт. 
Это безобразие продолжалось аж до 1977 года, пока Стюарт Фельдман из Bell Labs не придумал утилиту Make. Несмотря на то, что make не сильно далеко ушла от shell, она до сих пор является одной из самых популярных утилит сборки программ на C/C++, в основном благодаря своей распространенности в nix-системах.

Бурное развития Java привело к созданию Ant, который по сути был тот же make, только с XML-скриптами и на Java. Тем не менее, Ant имел как минимум два важных преимущества по сравнению с make:
  1. Независимость от платформы (камон, ребят, это же Java);
  2. Ясный и дружественный к разработчику XML-синтаксис;
Например, простейший Ant-скрипт сборки выглядит примерно так:
<?xml version="1.0"?>
<project default="build" basedir=".">
    <property name="name" value="AntBuildJar"/>
    <property name="src.dir" location="${basedir}/src"/>
    <property name="build" location="${basedir}/build"/>
    <property name="build.classes" location="${build}/classes"/>
    <path id="libs.dir">
 <fileset dir="lib" includes="**/*.jar"/>
    </path>
    <!-- Сборка приложения -->
    <target name="build" depends="clean" description="Builds the application">
        <!-- Создание каталогов -->
        <mkdir dir="${build.classes}"/>

        <!-- Компиляция исходных файлов -->
        <javac srcdir="${src.dir}"
               destdir="${build.classes}"
               debug="false"
               deprecation="true"
               optimize="true" >
            <classpath refid="libs.dir"/>
        </javac>

        <!-- Копирование необходимых файлов -->
        <copy todir="${build.classes}">
            <fileset dir="${src.dir}" includes="**/*.*" excludes="**/*.java"/>
        </copy>

        <!-- Создание JAR-файла -->
        <jar jarfile="${build}/${name}.jar">
            <fileset dir="${build.classes}"/>
        </jar>
    </target>

    <!-- Очистка -->
    <target name="clean" description="Removes all temporary files">
        <!-- Удаление файлов -->
        <delete dir="${build.classes}"/>
    </target>
</project>
Даже человек, который впервые видит подобный скрипт, но в целом знаком с этапами сборки проектов на Java, приблизительно поймет смысл описанных действий.
Богатый практический опыт применения Ant принес разработчикам понимание его слабых сторон:
  1. Излишняя детализация
    Ant устроен таким образом, что разработчики раз за разом описывали в скриптах мельчайшие этапы сборки, несмотря на то, что эти шаги довольно типичны;
  2. Императивность
    Поскольку проекты в целом собираются одинаково, приходилось из проекта в проект копировать скрипты по компиляции, упаковке, развертыванию, очистке и т.п.;
  3. Отсутствие механизмов управления зависимостями
Эти и прочие минусы привели в мире Java к созданию Maven, который:
  1. Декларативный
    Больше не надо описывать что делать, нужно лишь настроить параметры типовых фаз сборки проекта;
  2. Унифицирует жизненный цикл сборки
    Maven имеет заданную встроенную последовательность шагов сборки. Это позволяет унифицировать организацию кода и уменьшить количество настроек;
  3. Управляет зависимостями из коробки
    Был создан центральный репозиторий библиотек и их версий, на которые можно ссылаться. В итоге, для того, чтобы использовать какую либо библиотеку, нужно лишь прописать ее в pom.xml.
Я считаю, что именно Maven принес нам более глубокое и полное понимание того, какой должна быть любая система сборки. Конечно, он не лишен своих минусов, но, тем не менее, в большинстве случаев он отлично справляется со своей работой, облегчая труд разработчиков на порядок. 
Вот так, например, выглядит дескриптор небольшого проекта, который собирается в jar-файл:
<project xmlns="http://maven.apache.org/POM/4.0.0" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.blogspot.toolkas</groupId>
    <artifactId>xxx</artifactId>
    <packaging>jar</packaging>
    <version>1.5-SNAPSHOT</version>
    <name>xxx</name>

    <dependencies>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.4</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
Описание проекта намного лаконичнее и отображает лишь существенные аспекты: название проекта, версию и зависимости.

Появление Maven-а породило волну различных систем сборок, каждая из которых в чем-то улучшала Maven или рассматривала процесс сборки немного под другим углом: Buildr, Gradle, Pants, Buck и некоторые другие.

И тут мы приходим к Bazel.
Итак, что же побудило Google создать свою собственную систему сборки при таком обилии уже существующих. На сайте Bazel на этот вопрос дается такой ответ:

Почему не Make?
Слишком низкоуровневый и подробный. Bazel позволяет высокоуровнево описывать сборку.

Почему не Ant или Maven?
Ant и Maven разработаны в основном для Java-проектов, а Bazel поддерживает многоязычность, что актуально для больших проектов.

Почему не Gradle?
Bazel имеет более структурированную систему конфигурации проектов, что позволяет добиться более высокой производительности за счет параллелизма.

Почему не Pants и Buck?
Pants и Buck были разработаны бывшими сотрудниками Google, которые спроектировали эти инструменты после Bazel, но имеют не так много возможностей, чтобы конкурировать с Bazel.
(довольно слабый аргумент, IMHO - прим. Toolkas)

Тем не менее, становится приблизительно понятно, почему Google взялся за создание нового инструмента:
  1. Google хочет собирать многоязычные проекты, что типично для компании, имеющей дело со сверхвысокой нагрузкой и гигантскими объемами данных;
  2. Google хочет собирать большие многоязычные проекты;
  3. Google хочет очень быстро собирать большие многоязычные проекты;
  4. Google хочет собирать проекты по-своему=).
Ну или как-то так...
Теперь, когда мы приблизительно в курсе причин, приведших к созданию Bazel, установим его на компьютер.

Установка Bazel
На официальном сайте Bazel подробно расписана процедура установки на компьютер в зависимости от операционной системы. 
Например, для Windows:
  1. Установить MSYS2 Shell. Папку установки обозначим как %MSYS2%;
  2. Установить Microsoft Visual C++ Redistributable for Visual Studio 2015;
  3. Скачать bazel с GitHub (ищите ссылку типа bazel-<version>-windows-x86_64.exe, на момент написания статьи актуальна ссылка), сохраните его с именем bazel.exe в папку в файловой системе, которую будем называть %BZ%;
  4. Добавьте в переменную PATH путь до папки %BZ%;
  5. Добавьте в переменную PATH путь до папки %MSYS2%/usr/bin;
  6. Добавьте новую переменную BAZEL_SH, которая равна %MSYS2%/usr/bin/bash.exe;
  7. Добавьте переменную JAVA_HOME, которая равна пути до папки, куда установлена Java Development Kit.

"Hello, world" для Bazel
По великой традиции, восходящей к Кернигану и Ричи, создадим программу, которая выводит "Hello, world!" на Java и соберем эту программу с помощью Bazel.
Для тех, кто видит Java впервые, приведу ее код:
package com.blogspot.toolkas.bazel;

import static java.lang.System.*;

public class HelloWorld {
    public static void main(String[] args) {
        out.println("Hello, world!");
    }
}
Теперь создадим папку проекта helloworld с такой структурой:
helloworld
├── WORKSPACE
├── BUILD
└── src
    └── main
        └── java
            └── com
                └── blogspot
                    └── toolkas
                        └── bazel
                            └── HelloWorld.java
Папка src содержит исходники и ее структура полностью идентична таковой в Maven.
Теперь разберемся с новыми файлами в структуре проекта: WORKSPACE и BUILD.

Файл WORKSPACE
Файл WORKSPACE должен находиться в корне файловой структуры проекта, помечая его, таким образом, как проект Bazel. 
Обычно этот файл содержит глобальные настройки проекта, ссылки на репозитории внешних артефактов и тому подобное. 
Наш проект очень простой, никакие внешние артефакты ему не нужны, поэтому оставим файл WORKSPACE пока пустым.

Файл BUILD
Файл BUILD (их может быть несколько) содержит информацию, как собирать различные части проекта. Папку, в которой лежит файл BUILD называют пакетом (package).

Файл BUILD использует правила сборки (build rule), которые описывают, как именно собирать отдельные части проекта. Конкретное именованное использование правила сборки называется цель (target), ее можно вызвать отдельно. 
Содержимое файла BUILD пишется на языке программирования Starlark, специально разработанном для Bazel. Starlark можно считать диалектом Python, но он не является языком общего назначения. 
Файл BUILD нашего простого проекта будет иметь вид:
java_library(
    name = "helloworld-lib",
    srcs = glob(["src/main/java/com/blogspot/toolkas/bazel/HelloWorld.java"]),
)

java_binary(
    name = "helloworld",
    main_class = "com.blogspot.toolkas.bazel.HelloWorld",
    runtime_deps = [":helloworld-lib"],
)

Файл BUILD использует два встроенных правила: java_library и java_binary.
java_library компилирует исходники и упаковывает классы в jar-файл.
Для того, чтобы использовать правило java_library, нужно вызвать по имени цель, которая использует это правило.
Выглядит это так:

bazel build :helloworld-lib
Эту команду нужно выполнить в корне проекта.
В результате Bazel соберет jar-файл:
Loading: 
Loading: 0 packages loaded
Analyzing: target //:helloworld-lib (1 packages loaded, 0 targets configured)
Analyzing: target //:helloworld-lib (12 packages loaded, 242 targets configured)
INFO: Analysed target //:helloworld-lib (12 packages loaded, 522 targets configured).
INFO: Found 1 target...
[0 / 4] [-----] BazelWorkspaceStatusAction stable-status.txt
[1 / 4] SkylarkAction external/bazel_tools/tools/jdk/platformclasspath_classes/DumpPlatformClassPath.class; 1s local
[2 / 4] SkylarkAction external/bazel_tools/tools/jdk/platformclasspath.jar; 0s local
[2 / 4] SkylarkAction external/bazel_tools/tools/jdk/platformclasspath.jar; 1s local
[3 / 4] Building libhelloworld-lib.jar (1 source file); 1s worker
Target //:helloworld-lib up-to-date:
  C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/libhelloworld-lib.jar
INFO: Elapsed time: 9.444s, Critical Path: 6.50s
INFO: 3 processes: 2 local, 1 worker.
INFO: Build completed successfully, 4 total actions
INFO: Build completed successfully, 4 total actions
Обратите внимание, что при вызове цели мы используем имя, которое прописано в атрибуте name правила java_library.
Именно по имени мы можем вызывать цели Bazel, поэтому имена целей в файле BUILD должны быть уникальны.

Теперь рассмотрим цель helloworld. Она использует правило java_binary, которое предназначено для того, чтобы создать запускаемый модуль.
Если вы используете Bazel в операционной системе Windows, то результатом работы правила java_binary станет запускаемый jar-файл и exe-файл. Удобно, не правда ли?=)

К тому же, посредством атрибута runtime_deps мы указываем список зависимостей, которые необходимы для работы.

Чтобы собрать запускаемый модуль, нужно выполнить команду:
bazel build :helloworld
в результате чего Bazel соберет jar и exe файлы:
Loading: 
Loading: 0 packages loaded
Analyzing: target //:helloworld (0 packages loaded, 0 targets configured)
INFO: Analysed target //:helloworld (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
[0 / 1] [-----] BazelWorkspaceStatusAction stable-status.txt
Target //:helloworld up-to-date:
  C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/helloworld.jar
  C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/helloworld.exe
INFO: Elapsed time: 4.186s, Critical Path: 0.07s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Запустить программу на выполнение можно командой:
bazel run :helloworld

Loading: 
Loading: 0 packages loaded
Analyzing: target //:helloworld (0 packages loaded, 0 targets configured)
INFO: Analysed target //:helloworld (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
[0 / 1] [-----] BazelWorkspaceStatusAction stable-status.txt
Target //:helloworld up-to-date:
  C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/helloworld.jar
  C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/helloworld.exe
INFO: Elapsed time: 4.117s, Critical Path: 0.06s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Running command line: C:/users/toolkas/_bazel_toolkas/tclzheym/execroot/__main__/bazel-out/x64_windows-fastbuild/bin/helloworld.exe
INFO: Build completed successfully, 1 total action
Hello, world!
Красным цветом выделена строчка, которую напечатал класс HelloWorld.

Модульный проект
Тестовые примеры, вроде нашего HelloWorld, хорошо демонстрируют принцип, но совершенно оторваны от реальности. Хочется попробовать Bazel на немного более сложном проекте. Допустим, у нас есть интерфейс кода приветствия:
package com.blogspot.toolkas.greeting.api;

public interface Greeting {
    String getMessage();
}
И два варианта реализации приветствия, на английском:
package com.blogspot.toolkas.greeting.en;

import com.blogspot.toolkas.greeting.api.*;

public class EnGreeting implements Greeting {
    public String getMessage() {
        return "Hello, world!";
    }
}
и на русском:
package com.blogspot.toolkas.greeting.ru;

import com.blogspot.toolkas.greeting.api.*;

public class RuGreeting implements Greeting {
    public String getMessage() {
        return "Привет, мир!";
    }
}
Все три класса мы хотим организовать в виде отдельных jar-файлов. Есть много способов получить такой результат, рассмотрим один из них:
greeting
├─ WORKSPACE
├─ greeting-api
│  ├─ BUILD
│  └─ src
│      └─ main
│         └─ java
│            └─ com
│               └─ blogspot
│                  └─ toolkas
│                     └─ greeting
│                        └─ api
│                           └─ Greeting.java
│
├─ greeting-en
│  ├─ BUILD
│  └─ src
│     └─ main
│        └─ java
│           └─ com
│              └─ blogspot
│                 └─ toolkas
│                    └─ greeting
│                       └─ en
│                          └─ EnGreeting.java
└─ greeting-ru
   ├─ BUILD
   └─ src
      └─ main
         └─ java
            └─ com
               └─ blogspot
                  └─ toolkas
                     └─ greeting
                        └─ ru
                           └─ RuGreeting.java
Поскольку нам не требуется никаких глобальных настроек, то и в этом случае файл WORKSPACE пустой.

Ни один из трех классов не является запускаемым, значит нам не нужно использовать правило java_binary. Все три класса должны просто собираться в jar-файлы, поэтому мы используем правило java_library.
Поскольку greeting-api должно быть доступно во время сборки проектам greeting-en и greeting-ru, для проекта greeting-api необходимо указать видимость. Таким образом, файл greeting-api/BUILD будет выглядеть так:
package(default_visibility = ["//visibility:public"])

java_library(
    name = "greeting-api",
    srcs = glob(["src/main/java/**/*.java"]), 
)
greeting-ru и greeting-en - это пакеты, которые содержат конкретные реализации, поэтому этим пакетам не нужна публичная видимость. Единственное, что им нужно - это greeting-api. Поэтому файл greeting-en/BUILD будет выглядеть так:
package

java_library(
    name = "greeting-en",
    srcs = glob(["src/main/java/**/*.java"]), 
    deps = ["//greeting-api:greeting-api"]
)
а файл greeting-ru/BUILD так:
package

java_library(
    name = "greeting-ru",
    srcs = glob(["src/main/java/**/*.java"]), 
    deps = ["//greeting-api:greeting-api"]
)
Из этих примеров видно, как корректно делать ссылки на правила: //[относительный путь от корня проекта]:[имя правила в BUILD]. Теперь для сборки всех модулей достаточно выполнить команду:
bazel build //...
Starting local Bazel server and connecting to it...
Loading: 
Loading: 0 packages loaded
Loading: 0 packages loaded
    currently loading: greeting-api ... (3 packages)
Analyzing: 3 targets (3 packages loaded, 0 targets configured)
Analyzing: 3 targets (11 packages loaded, 96 targets configured)
Analyzing: 3 targets (13 packages loaded, 115 targets configured)
INFO: Analysed 3 targets (14 packages loaded, 526 targets configured).
INFO: Found 3 targets...
[0 / 5] [-----] BazelWorkspaceStatusAction stable-status.txt
[1 / 5] checking cached actions
INFO: Elapsed time: 12.264s, Critical Path: 0.50s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action

Визуализация зависимостей
Bazel хорош тем, что жестко контролирует зависимости пакетов. Это сделано для воспроизводимости результатов сборки в любом окружении.
Чтобы сгенерировать дерево зависимостей greeting-en, нужно в корне проекта выполнить команду:
bazel query  --nohost_deps --noimplicit_deps "deps(//greeting-en:greeting-en)" --output graph

Loading: 0 packages loaded
digraph mygraph {
  node [shape=box];
"//greeting-en:greeting-en"
"//greeting-en:greeting-en" -> "//greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java"
"//greeting-en:greeting-en" -> "//greeting-api:greeting-api"
"//greeting-api:greeting-api"
"//greeting-api:greeting-api" -> "//greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java"
"//greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java"
"//greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java"
}
Loading: 0 packages loaded
Loading: 0 packages loaded

Если мы хотим представить дерево зависимостей более наглядно, необходимо скопировать код графа
digraph mygraph {
  node [shape=box];
"//greeting-en:greeting-en"
"//greeting-en:greeting-en" -> "//greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java"
"//greeting-en:greeting-en" -> "//greeting-api:greeting-api"
"//greeting-api:greeting-api"
"//greeting-api:greeting-api" -> "//greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java"
"//greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java"
"//greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java"
}
и вставить его в GraphViz, который покажет нам вот такую симпатичную картинку:

mygraph //greeting-en:greeting-en //greeting-en:greeting-en //greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java //greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java //greeting-en:greeting-en->//greeting-en:src/main/java/com/blogspot/toolkas/greeting/en/EnGreeting.java //greeting-api:greeting-api //greeting-api:greeting-api //greeting-en:greeting-en->//greeting-api:greeting-api //greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java //greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java //greeting-api:greeting-api->//greeting-api:src/main/java/com/blogspot/toolkas/greeting/api/Greeting.java

Заключение
Итак, Bazel мне показался вполне хорошим годным инструментом для своих целей. 
Поскольку в одной статье удалось лишь кратко рассмотреть основные принципы его работы, для дальнейшего погружения отсылаю вас к официальному ресурсу, там все очень подробно расписано.
Хотя Bazel существует уже 4 года, он не сильно распространен.
Тем не менее, если вы разрабатываете в IntelliJ Idea, то есть плагин от Google.

Bazel - правильный выбор для больших многомодульных и многоязыковых проектов, которые нужно собирать очень быстро и жестко контролировать дерево зависимостей. 
Если ваш проект именно такой - добро пожаловать=)

Исходный код тестовых проектов тут и тут

На этом моменте я прощаюсь, стабильных вам сборок и пусть ваши проекты будут большими и сложными! Adieu!


Комментариев нет:

Отправить комментарий