Управляйте внешними зависимостями с помощью Bzlmod

Bzlmod — это кодовое имя новой внешней системы зависимостей , представленной в Bazel 5.0. Он был введен для устранения нескольких болевых точек старой системы, которые невозможно было исправить постепенно; см. раздел «Постановка задачи» оригинального проектного документа для получения более подробной информации.

В Bazel 5.0 Bzlmod по умолчанию не включен; флаг --experimental_enable_bzlmod должен быть указан, чтобы следующее вступило в силу. Как следует из названия флага, эта функция в настоящее время является экспериментальной ; API и поведение могут измениться до официального запуска функции.

Модули Базель

Старая система внешних зависимостей на основе WORKSPACE сосредоточена вокруг репозиториев (или репозиториев ), созданных с помощью правил репозитория (или правил репозиториев). Хотя репозитории по-прежнему являются важной концепцией в новой системе, модули являются основными единицами зависимости.

Модуль — это, по сути, проект Bazel, который может иметь несколько версий, каждая из которых публикует метаданные о других модулях, от которых он зависит. Это аналогично знакомым понятиям в других системах управления зависимостями: артефакт Maven, пакет npm, крейт Cargo, модуль Go и т. д.

Модуль просто указывает свои зависимости, используя пары name и version вместо конкретных URL-адресов в WORKSPACE . Затем зависимости просматриваются в реестре Bazel ; по умолчанию Центральный реестр Bazel . В вашей рабочей области каждый модуль затем превращается в репозиторий.

МОДУЛЬ.bazel

Каждая версия каждого модуля имеет файл MODULE.bazel , в котором объявляются его зависимости и другие метаданные. Вот простой пример:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

Файл MODULE.bazel должен находиться в корне каталога рабочей области (рядом с файлом WORKSPACE ). В отличие от файла WORKSPACE , вам не нужно указывать свои транзитивные зависимости; вместо этого вы должны указывать только прямые зависимости, а файлы MODULE.bazel ваших зависимостей обрабатываются для автоматического обнаружения транзитивных зависимостей.

Файл MODULE.bazel похож на файлы BUILD , поскольку он не поддерживает какую-либо форму потока управления; он дополнительно запрещает операторы load . Директивы, поддерживаемые файлами MODULE.bazel :

  • module , чтобы указать метаданные о текущем модуле, включая его имя, версию и т. д.;
  • bazel_dep , чтобы указать прямые зависимости от других модулей Bazel;
  • Переопределения, которые могут использоваться только корневым модулем (то есть не модулем, который используется в качестве зависимости) для настройки поведения определенной прямой или транзитивной зависимости:
  • Директивы, связанные с расширениями модуля :

Формат версии

Bazel имеет разнообразную экосистему, и проекты используют различные схемы управления версиями. Наиболее популярным на сегодняшний день является SemVer , но есть и известные проекты, использующие различные схемы, такие как Abseil , версии которого основаны на дате, например 20210324.2 ).

По этой причине Bzlmod принимает более расслабленную версию спецификации SemVer, в частности, разрешая любое количество последовательностей цифр в «выпускной» части версии (вместо ровно 3, как предписывает SemVer: MAJOR.MINOR.PATCH ). Кроме того, не применяется семантика основных, дополнительных и исправлений версий. (Однако см. уровень совместимости для получения подробной информации о том, как мы обозначаем обратную совместимость.) Другие части спецификации SemVer, такие как дефис, обозначающий предварительную версию, не изменяются.

Разрешение версии

Алмазная зависимость является одной из основных проблем в пространстве управления зависимостями версий. Предположим, у вас есть следующий граф зависимостей:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Какую версию D следует использовать? Чтобы решить этот вопрос, Bzlmod использует алгоритм выбора минимальной версии (MVS), представленный в модульной системе Go. MVS предполагает, что все новые версии модуля обратно совместимы, и поэтому просто выбирает самую старшую версию, указанную любым зависимым (в нашем примере D 1.1). Он называется «минимальным», потому что D 1.1 здесь является минимальной версией, которая может удовлетворить наши требования; даже если существует D 1.2 или новее, мы их не выбираем. Дополнительным преимуществом этого является то, что выбор версии является точным и воспроизводимым .

Разрешение версий выполняется локально на вашем компьютере, а не реестром.

Уровень совместимости

Обратите внимание, что предположение MVS об обратной совместимости возможно, поскольку оно просто рассматривает несовместимые с предыдущими версиями модуля как отдельный модуль. С точки зрения SemVer это означает, что A 1.x и A 2.x считаются отдельными модулями и могут сосуществовать в разрешенном графе зависимостей. Это, в свою очередь, стало возможным благодаря тому факту, что основная версия закодирована в пути к пакету в Go, поэтому не возникает никаких конфликтов во время компиляции или компоновки.

У нас в Базеле таких гарантий нет. Таким образом, нам нужен способ обозначить номер «основной версии», чтобы обнаруживать обратно несовместимые версии. Это число называется уровнем совместимости и указывается для каждой версии модуля в директиве module() . Имея эту информацию на руках, мы можем выдать ошибку, когда обнаружим, что в разрешенном графе зависимостей существуют версии одного и того же модуля с разными уровнями совместимости.

Имена репозиториев

В Bazel у каждой внешней зависимости есть имя репозитория. Иногда одна и та же зависимость может использоваться через разные имена репозиториев (например, и @io_bazel_skylib , и @bazel_skylib означают Bazel skylib ), или одно и то же имя репозитория может использоваться для разных зависимостей в разных проектах.

В Bzlmod репозитории могут генерироваться модулями Bazel и расширениями модулей . Чтобы разрешить конфликты имен репозиториев, мы внедряем механизм сопоставления репозиториев в новой системе. Вот два важных понятия:

  • Каноническое имя репозитория : глобально уникальное имя репозитория для каждого репозитория. Это будет имя каталога, в котором находится репозиторий.
    Он построен следующим образом ( Предупреждение : канонический формат имени не является API, на который вы должны полагаться, он может быть изменен в любое время):

    • Для репозиториев модулей Bazel: module_name . version
      ( Пример . @bazel_skylib.1.0.3 )
    • Для репозиториев расширения модуля: module_name . version . extension_name . repo_name
      ( Пример . @rules_cc.0.0.1.cc_configure.local_config_cc )
  • Имя локального репозитория: имя репозитория, которое будет использоваться в файлах BUILD и .bzl в репозитории. Одна и та же зависимость может иметь разные локальные имена для разных репозиториев.
    Он определяется следующим образом:

    • Для репозиториев модулей Bazel: module_name по умолчанию или имя, указанное атрибутом repo_name в bazel_dep .
    • Для репозиториев расширения модуля: имя репозитория вводится через use_repo .

Каждый репозиторий имеет словарь сопоставления репозитория его прямых зависимостей, который представляет собой сопоставление имени локального репозитория с каноническим именем репозитория. Мы используем отображение репозитория для разрешения имени репозитория при создании метки. Обратите внимание, что не существует конфликта имен канонических репозиториев, а использование имен локальных репозиториев можно обнаружить путем анализа файла MODULE.bazel , поэтому конфликты можно легко обнаружить и разрешить, не затрагивая другие зависимости.

Строгие депсы

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

Строгие зависимости реализованы на основе отображения репозитория . По сути, сопоставление репозитория для каждого репо содержит все его прямые зависимости , любой другой репозиторий не виден. Видимые зависимости для каждого репозитория определяются следующим образом:

  • Репозиторий модуля Bazel может видеть все репозитории, представленные в файле MODULE.bazel через bazel_dep и use_repo .
  • Репозиторий расширения модуля может видеть все видимые зависимости модуля, предоставляющего расширение, а также все другие репозитории, созданные тем же расширением модуля.

Реестры

Bzlmod обнаруживает зависимости, запрашивая их информацию из реестров Bazel. Реестр Bazel — это просто база данных модулей Bazel. Единственная поддерживаемая форма реестров — это индексный реестр , представляющий собой локальный каталог или статический HTTP-сервер в определенном формате. В будущем мы планируем добавить поддержку одномодульных реестров , которые представляют собой просто репозитории git, содержащие исходники и историю проекта.

Реестр индексов

Индексный реестр — это локальный каталог или статический HTTP-сервер, содержащий информацию о списке модулей, включая их домашнюю страницу, сопровождающих, файл MODULE.bazel каждой версии и способ получения исходного кода каждой версии. Примечательно, что ему не нужно обслуживать исходные архивы самостоятельно.

Реестр индекса должен соответствовать следующему формату:

  • /bazel_registry.json : файл JSON, содержащий метаданные реестра. В настоящее время у него есть только один ключ, mirrors , указывающий список зеркал, используемых для исходных архивов.
  • /modules : каталог, содержащий подкаталог для каждого модуля в этом реестре.
  • /modules/$MODULE : каталог, содержащий подкаталог для каждой версии этого модуля, а также следующий файл:
    • metadata.json : файл JSON, содержащий информацию о модуле, со следующими полями:
      • homepage : URL-адрес домашней страницы проекта.
      • maintainers : Список объектов JSON, каждый из которых соответствует информации мейнтейнера модуля в реестре . Обратите внимание, что это не обязательно то же самое, что и авторы проекта.
      • versions : список всех версий этого модуля, которые можно найти в этом реестре.
      • yanked_versions : Список скопированных версий этого модуля. В настоящее время это не работает, но в будущем скопированные версии будут пропущены или выдавать ошибку.
  • /modules/$MODULE/$VERSION : каталог, содержащий следующие файлы:
    • MODULE.bazel : файл MODULE.bazel этой версии модуля.
    • source.json : файл JSON, содержащий информацию о том, как получить исходный код этой версии модуля, со следующими полями:
      • url : URL-адрес исходного архива.
      • integrity : Контрольная сумма целостности подресурса архива.
      • strip_prefix : Префикс каталога для удаления при извлечении исходного архива.
      • patches : список строк, каждая из которых называет файл патча, применяемый к извлеченному архиву. Файлы исправлений находятся в каталоге /modules/$MODULE/$VERSION/patches .
      • patch_strip : То же, что и аргумент --strip патча Unix.
    • patches/ : необязательный каталог, содержащий файлы патчей.

Центральный реестр Базеля

Центральный реестр Bazel (BCR) — это индексный реестр, расположенный в реестре.bazel.build . Его содержимое поддерживается репозиторием GitHub bazelbuild/bazel-central-registry .

BCR поддерживается сообществом Bazel; участники могут отправлять запросы на включение. См. Политики и процедуры центрального реестра Bazel .

В дополнение к формату обычного реестра индексов, для BCR требуется файл presubmit.yml для каждой версии модуля ( /modules/$MODULE/$VERSION/presubmit.yml ). В этом файле указаны несколько основных целей сборки и тестирования, которые можно использовать для проверки работоспособности этой версии модуля, и он используется конвейерами CI BCR для обеспечения взаимодействия между модулями в BCR.

Выбор реестров

Повторяемый флаг --registry можно использовать для указания списка реестров, из которых запрашиваются модули, чтобы вы могли настроить свой проект для получения зависимостей из стороннего или внутреннего реестра. Более ранние реестры имеют приоритет. Для удобства вы можете поместить список флагов --registry в файл .bazelrc вашего проекта.

Расширения модуля

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

Расширения модулей определяются в файлах .bzl , как и правила репозитория или макросы WORKSPACE . Они не вызываются напрямую; скорее, каждый модуль может указывать части данных, называемые тегами , для чтения расширениями. Затем, после разрешения версии модуля, запускаются расширения модуля. Каждое расширение запускается один раз после разрешения модуля (еще до фактической сборки) и считывает все принадлежащие ему теги по всему графу зависимостей.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

В приведенном выше примере графа зависимостей A 1.1 и B 1.2 и т. д. являются модулями Bazel; вы можете думать о каждом из них как о файле MODULE.bazel . Каждый модуль может указывать некоторые теги для расширения модуля; здесь некоторые указаны для расширения «maven», а некоторые — для «cargo». Когда этот граф зависимостей завершен (например, может быть, B 1.2 на самом деле имеет bazel_dep в D 1.3 , но был обновлен до D 1.4 из-за C ), запускается расширение «maven», и он получает возможность читать все теги maven.* , используя информацию в нем, чтобы решить, какие репозитории создавать. Аналогично для «грузового» расширения.

Использование расширения

Расширения размещаются в самих модулях Bazel, поэтому, чтобы использовать расширение в вашем модуле, вам нужно сначала добавить bazel_dep в этот модуль, а затем вызвать встроенную функцию use_extension , чтобы включить его в область действия. Рассмотрим следующий пример, фрагмент из файла MODULE.bazel для использования гипотетического расширения «maven», определенного в модуле rules_jvm_external :

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

После внесения расширения в область действия вы можете использовать точечный синтаксис, чтобы указать для него теги. Обратите внимание, что теги должны следовать схеме, определенной соответствующими классами тегов (см. определение расширения ниже). Вот пример с указанием некоторых maven.dep и maven.pom .

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

Если расширение создает репозитории, которые вы хотите использовать в своем модуле, используйте директиву use_repo для их объявления. Это сделано для того, чтобы удовлетворить строгому условию deps и избежать конфликта имен локального репо.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

Репозитории, созданные расширением, являются частью его API, поэтому из указанных вами тегов вы должны знать, что расширение «maven» будет генерировать репозиторий с именем «org_junit_junit» и один с именем «com_google_guava_guava». С помощью use_repo вы можете при желании переименовать их в рамках вашего модуля, например, здесь «guava».

Определение расширения

Расширения модуля определяются аналогично правилам репо с использованием функции module_extension . Оба имеют функцию реализации; но в то время как правила репо имеют ряд атрибутов, расширения модулей имеют ряд tag_class es , каждый из которых имеет ряд атрибутов. Классы тегов определяют схемы для тегов, используемых этим расширением. Продолжая наш пример гипотетического расширения «maven» выше:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

Эти объявления ясно дают понять, что maven.dep и maven.pom могут быть указаны с использованием схемы атрибутов, определенной выше.

Функция реализации аналогична WORKSPACE , за исключением того, что она получает объект module_ctx , который предоставляет доступ к графу зависимостей и всем соответствующим тегам. Затем функция реализации должна вызывать правила репозитория для создания репозиториев:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

В приведенном выше примере мы просматриваем все модули в графе зависимостей ( ctx.modules ), каждый из которых является объектом bazel_module , поле tags которого предоставляет все теги maven.* в модуле. Затем мы вызываем утилиту CLI Coursier, чтобы связаться с Maven и выполнить разрешение. Наконец, мы используем результат разрешения для создания ряда репозиториев, используя гипотетическое правило maven_single_jar .