Bzlmod で外部依存関係を管理する

Bzlmod は、Bazel 5.0 で導入された新しい外部依存関係システムのコードネームです。これは、段階的に修正することができなかった古いシステムのいくつかの課題に対処するために導入されました。詳細については、元の設計書の問題ステートメントのセクションをご覧ください。

Bazel 5.0 では、Bzlmod はデフォルトで有効になっていません。以下を有効にするには、--experimental_enable_bzlmod フラグを指定する必要があります。フラグ名が示すように、この機能は現在試験運用版です。機能が正式にリリースされるまで、API と動作が変更される場合があります。

プロジェクトを Bzlmod に移行するには、Bzlmod 移行ガイドをご覧ください。examples リポジトリに Bzlmod の使用例も掲載されています。

Bazel モジュール

古い WORKSPACE ベースの外部依存関係システムは、リポジトリ ルール(またはリポルール)を介して作成されるリポジトリ(またはリポジトリ)を中心としていました。新しいシステムでもリポジトリは重要なコンセプトですが、モジュールは依存関係の中核となるものです。

モジュールは基本的に、複数のバージョンを持つことができる Bazel プロジェクトです。各バージョンでは、依存する他のモジュールのメタデータを公開します。これは、Maven アーティファクト、npm パッケージ、Cargo クレート、Go モジュールなど、他の依存関係管理システムでおなじみのコンセプトに似ています。

モジュールは、WORKSPACE の特定の URL ではなく、nameversion のペアを使用して依存関係を指定します。依存関係は Bazel レジストリ(デフォルトでは Bazel セントラル レジストリ)でルックアップされます。ワークスペースでは、各モジュールが リポジトリに変換されます。

MODULE.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 ファイルでサポートされているディレクティブは次のとおりです。

バージョンの形式

Bazel には多様なエコシステムがあり、プロジェクトではさまざまなバージョニング スキームが使用されます。圧倒的に人気のあるのは SemVer ですが、その他にも、バージョンが日付ベースの Abseil20210324.2 など)のような、さまざまなスキームを使用する優れたプロジェクトがあります。

このため、Bzlmod では、より緩和された SemVer 仕様を採用しています。次の点が異なります。

  • SemVer では、バージョンの「release」部分が 3 つのセグメント MAJOR.MINOR.PATCH で構成しなければならないことを規定しています。Bazel ではこの要件が緩和され、セグメントの数を問いません。
  • SemVer では、「release」部分の各セグメントは数字のみにする必要があります。 Bazel では、文字も使用できるように拡張されています。また、比較セマンティクスは「プレリリース」部分の「識別子」と一致しています。
  • また、メジャー バージョン、マイナー バージョン、パッチ バージョンの増加のセマンティクスも適用されません。(ただし、下位互換性の表現について詳しくは、互換性レベルをご覧ください)。

有効な SemVer バージョンはすべて、有効な Bazel モジュール バージョンです。また、SemVer バージョン ab の 2 つでは、a < b を比較します(Bazel モジュール バージョンと比較した場合に同じ保留の場合)。

バージョンの解決

ダイヤモンド依存関係の問題は、バージョニングされた依存関係管理の要点です。次のような依存関係グラフがあるとします。

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

どのバージョンの D を使用すべきでしょうか?この問題を解決するために、Bzlmod は、Go モジュール システムで導入された最小バージョン選択(MVS)アルゴリズムを使用します。MVS は、モジュールのすべての新しいバージョンに下位互換性があると想定しているため、依存先プロバイダによって指定された最も高いバージョン(この例では D 1.1)を選択するだけです。「最小」と呼ばれるのは、D 1.1 が要件を満たす最小限のバージョンであるためです。D 1.2 以降が存在しても選択しません。これには、バージョンの選択が高忠実度かつ再現可能になるという利点もあります。

バージョンの解決は、レジストリではなく、ユーザーのマシン上でローカルで実行されます。

互換性レベル

なお、MVS では、下位互換性のないモジュールを単に別のモジュールとして扱うため、MVS の下位互換性の想定は可能です。SemVer に関しては、A 1.x と A 2.x は別々のモジュールとみなされ、解決された依存関係グラフ内で共存できます。これは、メジャー バージョンが Go のパッケージパスでエンコードされているという事実によって実現され、コンパイル時またはリンク時の競合は発生しません。

Bazel では、そのような保証はありません。したがって、下位互換性のないバージョンを検出するためには、「メジャー バージョン」の番号を示す方法が必要です。この数値は「互換性レベル」と呼ばれ、module() ディレクティブで各モジュール バージョンに指定されます。この情報を用意しておくと、同じモジュールの互換性レベルが異なるバージョンが、解決済みの依存関係グラフに存在することを検出したときにエラーをスローできます。

リポジトリ名

Bazel では、すべての外部依存関係にリポジトリ名があります。異なるリポジトリ名で同じ依存関係が使用されている場合もあります(たとえば、@io_bazel_skylib@bazel_skylibBazel skylib を意味します)。また、異なるプロジェクトの異なる依存関係に同じリポジトリ名が使用されている場合もあります。

Bzlmod では、Bazel モジュールとモジュール拡張機能によってリポジトリを生成できます。リポジトリ名の競合を解決するため、新しいシステムではリポジトリ マッピング メカニズムを採用しています。ここでは、重要なコンセプトを 2 つ説明します。

  • 正規リポジトリ名: 各リポジトリのグローバルに一意のリポジトリ名。これは、リポジトリが存在するディレクトリ名になります。
    これは次のように構成されています(警告: 正規の名前の形式は依存する必要がある 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、または bazel_deprepo_name 属性で指定された名前。
    • モジュール拡張機能リポジトリの場合: use_repo で導入されたリポジトリ名。

すべてのリポジトリには、直接的な依存関係のリポジトリ マッピング辞書があります。これは、見かけのリポジトリ名から正規リポジトリ名へのマップです。ラベルを作成する際には、リポジトリ マッピングを使用してリポジトリ名を解決します。正規のリポジトリ名の競合はなく、MODULE.bazel ファイルを解析することで明らかなリポジトリ名の使用を検出できるため、他の依存関係に影響を与えることなく競合を簡単に捕捉して解決できます。

Strict の依存関係

新しい依存関係指定形式を使用すると、より厳密なチェックを実行できます。具体的には、モジュールはその直接的な依存関係から作成されたリポジトリのみを使用できるようになりました。これにより、推移的な依存関係グラフ内の何かが変更されたときに、偶発的な破損やデバッグ困難な破損を防ぐことができます。

厳密な依存関係は、リポジトリ マッピングに基づいて実装されます。基本的に、各リポジトリのリポジトリ マッピングには直接的な依存関係がすべて含まれ、他のリポジトリは表示されません。各リポジトリに表示される依存関係は、次のように決定されます。

  • Bazel モジュール リポジトリは、bazel_depuse_repo を介して MODULE.bazel ファイルに導入されたすべてのリポジトリを参照できます。
  • モジュール拡張機能リポジトリは、拡張機能を提供するモジュールのすべての表示可能な依存関係と、同じモジュール拡張機能によって生成された他のすべてのリポジトリを表示できます。

レジストリ

Bzlmod は、Bazel レジストリから依存関係の情報をリクエストして、依存関係を検出します。Bazel レジストリは、単に Bazel モジュールのデータベースです。サポートされているレジストリの形式はインデックス レジストリのみです。インデックス レジストリは、ローカル ディレクトリまたは特定の形式に従った静的 HTTP サーバーです。将来的には、単一モジュール レジストリのサポートも追加される予定です。単一モジュール レジストリとは、プロジェクトのソースと履歴を格納する Git リポジトリのことです。

インデックス レジストリ

インデックス レジストリは、モジュール リスト(ホームページ、メンテナンス担当者、各バージョンの MODULE.bazel ファイル、各バージョンのソースの取得方法など)に関する情報が含まれるローカル ディレクトリまたは静的 HTTP サーバーです。特に、ソース アーカイブ自体を提供する必要はありません

インデックス レジストリは、次の形式に従う必要があります。

  • /bazel_registry.json: 次のようなレジストリのメタデータを含む JSON ファイル。
    • mirrors: ソース アーカイブに使用するミラーのリストを指定します。
    • module_base_path: source.json ファイル内の local_repository 型のモジュールのベースパスを指定します。
  • /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: Unix パッチの --strip 引数と同じです。
      • 型を変更して、次のフィールドでローカルパスを使用できます。
        • type: local_path
        • path: リポジトリのローカルパス。次のように計算されます。
          • パスが絶対パスの場合は、そのまま使用されます。
          • パスが相対パスで、module_base_path が絶対パスの場合、パスは <module_base_path>/<path> に解決されます。
          • パスと module_base_path の両方が相対パスの場合、パスは <registry_path>/<module_base_path>/<path> に解決されます。レジストリはローカルでホストされ、--registry=file://<registry_path> で使用される必要があります。そうしないと、Bazel はエラーをスローします。
    • patches/: パッチファイルを含むオプションのディレクトリ。source.json が「アーカイブ済み」の場合にのみ使用されます。

Bazel セントラル レジストリ

Bazel Central Registry(BCR)は、bcr.bazel.build にあるインデックス レジストリです。その内容は、GitHub リポジトリ bazelbuild/bazel-central-registry を基盤としています。

BCR は Bazel コミュニティによって管理されています。投稿者は pull リクエストを送信できます。Bazel セントラル レジストリのポリシーと手順をご覧ください。

BCR では、通常のインデックス レジストリの形式に加えて、各モジュール バージョン(/modules/$MODULE/$VERSION/presubmit.yml)の presubmit.yml ファイルが必要です。このファイルは、このモジュール バージョンのサニティ チェックに使用できるいくつかの重要なビルドおよびテスト ターゲットを指定します。また、BCR の CI パイプラインで、BCR 内のモジュール間の相互運用性を確保するために使用されます。

レジストリの選択

繰り返し可能な Bazel フラグ --registry を使用して、モジュールのリクエスト元であるレジストリのリストを指定できます。これにより、サードパーティ レジストリまたは内部レジストリから依存関係を取得するようにプロジェクトを設定できます。以前のレジストリが優先されます。便宜上、プロジェクトの .bazelrc ファイルに --registry フラグのリストを指定できます。

モジュールの拡張機能

モジュール拡張機能を使用すると、依存関係グラフ全体のモジュールから入力データを読み取り、依存関係を解決するために必要なロジックを実行して、最終的にリポジトリルールを呼び出してリポジトリを作成することで、モジュール システムを拡張できます。機能面は現在の WORKSPACE マクロと似ていますが、モジュールや推移的依存関係を使用する方に適しています。

モジュール拡張機能は、リポジトリ ルールや WORKSPACE マクロと同様に、.bzl ファイルで定義されます。これらは直接呼び出されるのではなく、各モジュールが、拡張機能が読み取るためのタグと呼ばれるデータを指定できます。モジュール バージョンの解決後、モジュール拡張機能が実行されます。各拡張機能は、モジュールの解決後(実際にビルドが行われる前)に 1 回実行され、依存関係グラフ全体でその拡張機能に属するタグをすべて読み取ります。

          [ 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.1B 1.2 などが Bazel モジュールです。これらはそれぞれ MODULE.bazel ファイルと考えることができます。各モジュールでは、モジュール拡張用のタグを指定できます。ここでは、拡張機能「maven」に指定されたタグと、「cargo」に指定されたタグがあります。この依存関係グラフがファイナライズされると(たとえば、B 1.2 は実際には D 1.3bazel_dep があり、C が原因で D 1.4 にアップグレードされた場合)、拡張機能「maven」が実行され、すべての maven.* タグを読み取って、作成するリポジトリを決定します。「cargo」拡張機能も同様です。

拡張機能の使用状況

拡張機能は Bazel モジュール自体でホストされているため、モジュールで拡張機能を使用するには、まずそのモジュールに bazel_dep を追加してから、use_extension 組み込み関数を呼び出してスコープに含める必要があります。次の例は、rules_jvm_external モジュールで定義された架空の「maven」拡張機能を使用する MODULE.bazel ファイルのスニペットです。

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 があり、それぞれに複数の属性があります。タグクラスは、この拡張機能で使用されるタグのスキーマを定義します。上記の仮の「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 ユーティリティ コーシャを呼び出して Maven に接続し、解決を行います。最後に、解決結果で、架空の maven_single_jar リポジトリ ルールを使用して複数のリポジトリを作成します。