Время сборки проекта оказывает значительное влияние на эффективность разработки команды. Чем больше кодовая база, тем больше времени требуется для сборки. И чем дольше время сборки, тем хуже становится опыт разработчика.

Хотя SBT — отличный инструмент сборки, его конструкция (в частности, отсутствие надежного кэша сборки с адресацией по содержимому) делает его не очень подходящим для крупных проектов.

В этом сообщении в блоге рассказывается о Bazel, системе сборки для быстрой сборки даже в репозиториях масштаба Google путем создания простых приложений Scala.

Что такое Базель?

Прежде всего, давайте изучим концепцию Bazel. Bazel — это система сборки, девиз которой: «{Быстро, правильно} выберите два». Чтобы реализовать эти свойства, Bazel разработан как «система сборки на основе артефактов».

В этом разделе мы узнаем о природе Bazel, рассмотрев, что такое система сборки на основе артефактов и что означает «{Быстро, правильно} выберите два»!

Система сборки на основе артефактов

Традиционные системы сборки, такие как Ant и Maven, называются системами сборки на основе задач. В конфигурации сборки для систем сборки на основе задач мы описываем обязательный набор задач, например выполнить задачу A, затем выполнить задачу B, а затем выполнить задачу C.

С другой стороны, в системах сборки на основе артефактов, таких как Buck, Pants и Bazel, мы описываем декларативный набор артефактов для сборки, список зависимостей и ограниченные параметры сборки.

Итак, основная идея Bazel заключается в том, что ваша сборка — это чистая функция:

  • Исходники и зависимости вводятся.
  • Артефакт является выходом.
  • Побочных эффектов нет.

Для получения более подробной информации о концепции систем сборки на основе артефактов я рекомендую прочитать Главу 18 разработки программного обеспечения в Google.

{Быстро, правильно} выберите два

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

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

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

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

Основы Базеля

В этом руководстве мы создаем простое приложение Scala с помощью Bazel. Полный исходный код доступен здесь: https://github.com/tanishiking/bazel-tutorial-scala/tree/main/01_scala_tutorial

Структура проекта выглядит так:

|-- WORKSPACE
`-- src
    `-- main
        `-- scala
            |-- cmd
            |   |-- BUILD.bazel
            |   `-- Runner.scala
            `-- lib
                |-- BUILD.bazel
                `-- Greeting.scala

Файлы конфигурации Bazel — это файлы WORKSPACE и BUILD.bazel.

  • Файл WORKSPACE предназначен для добавления материалов из внешнего мира в ваш проект Bazel.
  • Файлы BUILD.bazel рассказывают о том, что происходит внутри вашего проекта Bazel.

Файл РАБОЧЕЙ ПРОСТРАНСТВА

Файл WORKSPACE содержит внешние зависимости (как для Bazel, так и для JVM). Например, мы загружаем rules_scala, расширение Bazel для компиляции Scala в файле WORKSPACE.

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") #Import a rule from the Bazel's "standard library"
 ...
http_archive( #This rule can download and import an archived repo
    name = "io_bazel_rules_scala", #The name that will be used to reference the repo
    sha256 = "77a3b9308a8780fff3f10cdbbe36d55164b85a48123033f5e970fdae262e8eb2",
    strip_prefix = "rules_scala-20220201", #Only files from this directory will be unpacked and imported
    type = "zip",
    url = "https://github.com/bazelbuild/rules_scala/releases/download/20220201/rules_scala-20220201.zip",
)

Подробнее читайте в README проекта.

Файлы BUILD.bazel

Чтобы определить сборку в Bazel, пишем BUILD.bazel files.

Прежде чем перейти к файлам BUILD.bazel, давайте кратко рассмотрим файлы Scala для сборки. Этот проект состоит из двух пакетов, каждый из которых содержит один файл Scala.

// src/main/scala/lib/Greeting.scala
package lib
object Greeting {
  def sayHi = println("Hi!")
}
// src/main/scala/cmd/Runner.scala
package cmd
import lib.Greeting
object Runner {
  def main(args: Array[String]) = {
    Greeting.sayHi
  }
}

Как видите, lib.Greeting — это библиотечный модуль, предоставляющий метод sayHi, а cmd.Runner зависит от lib. Приветствие.

Далее давайте посмотрим, как написать файлы BUILD.bazel для сборки этих исходников Scala.

scala_library

Чтобы собрать lib.Greeting в этом примере, мы помещаем файл BUILD.bazel рядом с Greeting.scala и определяем цель сборки с помощью правила scala_library, предоставленного rules_scala.

Правило в Bazel — это объявление набора инструкций по созданию или тестированию кода. Например, есть набор правил для создания Java-программ (который изначально поддерживается Bazel). rules_scala предоставляет набор правил для создания программ Scala.

scala_library компилирует предоставленные исходники Scala и создает JAR-файл.

# src/main/scala/lib/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library")
scala_library(
    # unique identifier of this target
    name = "greeting",
    # list of Scala files to build
    srcs = ["Greeting.scala"],
)
  • Оператор load импортирует правило scala_library в файл BUILD.bazel.
  • scala_library — одно из правил сборки в Bazel; мы описываем, что строить, используя правила.
  • Обязательными атрибутами scala_library являются name и srcs.

базал билд

Теперь у нас есть исходники Scala для сборки и конфигурация BUILD.bazel. Давайте создадим его с помощью командной строки bazel.

$ bazel build //src/main/scala/lib:greeting
...
INFO: Found 1 target...
Target //src/main/scala/lib:greeting up-to-date:
  bazel-bin/src/main/scala/lib/greeting.jar
INFO: Elapsed time: 0.152s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

Сборка прошла успешно 🎉, но подождите, что такое //src/main/scala/lib:greeting?

Этикетка

//src/main/scala/lib:greeting — это нечто, называемое меткой в Bazel, и оно указывает на приветствие в src/main/scala /lib/BUILD.bazel. В Bazel мы используем метку для уникальной идентификации цели сборки.

Этикетка состоит из 3-х компонентов. Например, в @myrepo//my/app/main:app_binary

  • @myrepo// указывает имя репозитория. Если мы опустим эту часть, метка будет относиться к репозиторию, содержащему этот файл BUILD.bazel. Поэтому мы можем опустить часть @myrepo при ссылке на метку, определенную в том же репозитории.
  • my/app/main представляет собой путь к пакету (файлу BUILD.bazel) относительно корня репозитория.
  • :app_binary – это целевое имя.

При этом //src/main/scala/lib:greeting указывает на цель, которая находится в той же рабочей области, определенной в файле BUILD.bazel, расположенном по адресу src/main/scala/lib, а целевое имя — greeting.

Зависимости

Затем создадим cmd.Runner, который зависит от lib.Greeting. На этот раз cmd.Runner зависит от lib.Greeting, поэтому мы вводим зависимость между целями с помощью атрибута deps.

# src/main/scala/cmd/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_binary")
scala_binary(
    name = "runner",
    main_class = "cmd.Runner",
    srcs = ["Runner.scala"],
    deps = ["//src/main/scala/lib:greeting"],
)

Отличия от предыдущего примера:

  • Мы используем scala_binary вместо scala_library:
  • scala_binary — это исполняемое правило. Исполняемые правила определяют, как создать исполняемый файл из исходных кодов. Этот процесс может содержать связывание зависимостей или перечисление путей к классам зависимостей.
  • Например, правило scala_binary создает исполняемый скрипт из исходников и зависимостей.
  • После создания исполняемого файла его можно запустить с помощью команды bazel run. Это запустит исполняемый файл.
  • Мы добавляем атрибут deps, чтобы перечислить все зависимости.
  • В этом примере мы добавляем метку //src/main/scala/lib:greeting, потому что cmd.Runner зависит от lib.Greeting.

Теперь мы должны быть в состоянии собрать приложение с помощью bazel build //…, но это не удается!

$ bazel build //src/main/scala/cmd:runner
ERROR: .../01_scala_tutorial/src/main/scala/cmd/BUILD.bazel:3:13:
in scala_binary rule //src/main/scala/cmd:runner:
target '//src/main/scala/lib:greeting' is not visible from
target '//src/main/scala/cmd:runner'.

Видимость

Базель имеет понятие видимость. По умолчанию видимость всех целей является частной, то есть только цели в одном пакете (например, в одном файле BUILD.bazel) могут получить доступ друг к другу.

Чтобы сделать lib:greeting видимым из cmd, добавьте атрибут visibility в greeting.

scala_library(
     name = "greeting",
     srcs = ["Greeting.scala"],
+    visibility = ["//src/main/scala/cmd:__pkg__"],
 )

//src/main/scala/cmd:__pkg__ — это спецификация видимости, которая предоставляет доступ к пакету //src/main/scala/cmd.

Теперь мы можем собрать приложение:

$ bazel build //src/main/scala/cmd:runner
...
INFO: Found 1 target...
Target //src/main/scala/cmd:runner up-to-date:
  bazel-bin/src/main/scala/cmd/runner.jar
  bazel-bin/src/main/scala/cmd/runner
INFO: Elapsed time: 0.146s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

Как видите, правило scala_binary создает еще один файл с именем runner в дополнение к runner.jar. Это скрипт-оболочка для runner.jar, и мы можем легко запустить JAR с помощью этого скрипта.

$ ./bazel-bin/src/main/scala/cmd/runner
Hi!
$ bazel run //src/main/scala/cmd:runner
Hi!

Совет: создавайте несколько целей

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

Ответ положительный. Мы можем использовать шаблон для выбора нескольких целей. Например, мы можем построить все цели с помощью $ bazel build //…

Итак, мы изучили основы Bazel, создав простое приложение на Scala, но как нам использовать сторонние библиотеки в Bazel?

Внешние зависимости JVM

Давайте теперь узнаем, как использовать сторонние библиотеки от Maven, создав простое приложение, которое анализирует программы Scala, используя scalameta, и красиво печатает AST, используя pprint.

В этом примере мы будем использовать rules_jvm_external, одно из стандартных правил, установленных для управления внешними зависимостями JVM.

Примечание. Мы можем загружать jar-файлы из репозиториев Maven, используя maven_jar, правило, поддерживаемое Bazel. Тем не менее, я рекомендую использовать rules_jvm_extenal, потому что он имеет ряд полезных функций по сравнению с maven_jar.

Полный пример кода доступен здесь: https://github.com/tanishiking/bazel-tutorial-scala/tree/main/02_scala_maven.

В этом проекте есть только одна программа Scala.

// src/main/scala/example/App.scala
package example
import scala.meta._
object App {
  def main(args: Array[String]) = {
    pprint.pprintln(parse(args.head))
  }
  private def parse(arg: String) = {
    arg.parse[Source].get
  }
}

Чтобы загрузить scalameta и pprint из репозиториев Maven, мы используем rules_jvm_external. Итак, мы должны сначала загрузить rules_jvm_external.

Чтобы загрузить rules_jvm_external, скопируйте и вставьте операторы настройки со страницы выпуска в свой файл WORKSPACE, например:

http_archive(
    name = "rules_jvm_external",
    strip_prefix = "rules_jvm_external-4.5",
    sha256 = "b17d7388feb9bfa7f2fa09031b32707df529f26c91ab9e5d909eb1676badd9a6",
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/refs/tags/4.5.zip",
)
...

Затем перечислите все зависимости в операторе maven_install, который также находится в файле WORKSPACE.

load("@rules_jvm_external//:defs.bzl", "maven_install")
maven_install(
    artifacts = [
        "org.scalameta:scalameta_2.13:4.5.13",
        "com.lihaoyi:pprint_2.13:0.7.3",
    ],
    repositories = [
        "https://repo1.maven.org/maven2",
    ],
)

Теперь Bazel может загружать зависимости, но как мы можем их использовать?

Чтобы использовать загруженные зависимости, нам нужно добавить их в атрибут deps правил сборки. rules_jvm_external автоматически создает цели для библиотек в репозитории @maven в следующем формате:

› Синтаксис метки по умолчанию для артефакта foo.bar:baz-qux:1.2.3: @maven//:foo_bar_baz_qux

https://github.com/bazelbuild/rules_jvm_external#usage

Поэтому мы можем ссылаться на com.lihaoyi:pprint_2.13:0.7.3 с меткой @maven//:com_lihaoyi_pprint_2_13. Итак, поместите следующий файл BUILD.bazel рядом с App.scala.

# src/main/scala/example/BUILD.bazel
scala_binary(
    name = "app",
    main_class = "example.App",
    srcs = ["App.scala"],
    deps = [
        "@maven//:com_lihaoyi_pprint_2_13",
        "@maven//:org_scalameta_scalameta_2_13",
    ],
)

И построить его!

$ bazel build //src/main/scala/example:app
...
INFO: Found 1 target...
Target //src/main/scala/example:app up-to-date:
  bazel-bin/src/main/scala/example/app.jar
  bazel-bin/src/main/scala/example/app
INFO: Elapsed time: 0.165s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
...
$ bazel-bin/src/main/scala/example/app "object main { println(1) }"
Source(
  stats = List(
    Defn.Object(
      ...
    )
  )
)

Хороший! 🎉 Вот как мы используем внешние зависимости JVM с rules_jvm_external.

Заключение

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

Как вы видели, Bazel требует от пользователей управления гораздо большим количеством конфигураций сборки, чем другие инструменты сборки, такие как sbt и Maven. Однако это приемлемый компромисс для очень крупных проектов. учитывая масштабируемую скорость сборки благодаря преимуществам Bazel, таким как воспроизводимые сборки и удаленный кеш.

Я надеюсь, что эта статья поможет вам сделать первые шаги в работе с Bazel.

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