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

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

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

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

Bazel モジュール

古い WORKSPACE ベースの外部依存関係システムは、リポジトリ ルール(またはリポジトリ ルール)を介して作成されたリポジトリ(またはリポジトリ)を中心に構成されています。新しいシステムでもリポジトリは重要な概念ですが、依存関係のコア単位はモジュールです。

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

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

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 ですが、Abseil など、異なるスキームを使用する著名なプロジェクトもあります。Abseil のバージョンは日付ベースです(例: 20210324.2)。

このため、Bzlmod では SemVer 仕様のより緩いバージョンを採用しています。違いは次のとおりです。

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

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

バージョンの解決

ダイアモンド依存関係の問題は、バージョン管理された依存関係管理の分野では定番です。次の依存関係グラフがあるとします。

       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_skylib の両方が Bazel 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 ファイルを解析することで検出できるため、競合を簡単に検出して解決でき、他の依存関係に影響を与えることはありません。

厳密な依存関係

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

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

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

レジストリ

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

インデックス レジストリ

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

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

  • /bazel_registry.json: レジストリのメタデータを含む JSON ファイル。例:
    • mirrors。ソース アーカイブに使用するミラーのリストを指定します。
    • source.json ファイルで local_repository タイプのモジュールのベースパスを指定する module_base_path
  • /modules: このレジストリ内の各モジュールのサブディレクトリを含むディレクトリ。
  • /modules/$MODULE: このモジュールの各バージョンのサブディレクトリと、次のファイルを含むディレクトリ:
    • metadata.json: モジュールに関する情報を含む JSON ファイル。次のフィールドがあります。
      • homepage: プロジェクトのホームページの URL。
      • maintainers: JSON オブジェクトのリスト。各オブジェクトは、レジストリ内のモジュールのメンテナーの情報に対応しています。これは、プロジェクトの作成者と同じである必要はありません。
      • versions: このレジストリで見つかったこのモジュールのすべてのバージョンのリスト。
      • yanked_versions: このモジュールの yanked バージョンのリスト。これは現在 no-op ですが、将来的には、yank されたバージョンはスキップされるか、エラーが生成されます。
  • /modules/$MODULE/$VERSION: 次のファイルを含むディレクトリ:
    • MODULE.bazel: このモジュール バージョンの MODULE.bazel ファイル。
    • source.json: このモジュール バージョンのソースを取得する方法に関する情報を含む JSON ファイル。
      • デフォルトのタイプは「archive」で、次のフィールドがあります。
        • url: ソース アーカイブの URL。
        • integrity: アーカイブの Subresource Integrity チェックサム。
        • strip_prefix: ソース アーカイブを抽出するときに削除するディレクトリ プレフィックス。
        • patches: 文字列のリスト。各文字列は、抽出されたアーカイブに適用するパッチ ファイルの名前を示します。パッチファイルは /modules/$MODULE/$VERSION/patches ディレクトリにあります。
        • patch_strip: Unix パッチの --strip 引数と同じ。
      • これらのフィールドを使用して、ローカルパスを使用するようにタイプを変更できます。
        • type: local_path
        • path: リポジトリのローカルパス。次のように計算されます。
          • パスが絶対パスの場合は、そのまま使用されます。
          • パスが相対パスで、module_base_path が絶対パスの場合、パスは <module_base_path>/<path> に解決されます。
          • path と module_base_path の両方が相対パスの場合、path は <registry_path>/<module_base_path>/<path> に解決されます。レジストリはローカルでホストされ、--registry=file://<registry_path> で使用される必要があります。それ以外の場合は、Bazel はエラーをスローします。
    • patches/: パッチ ファイルを含むオプションのディレクトリ。source.json のタイプが「archive」の場合にのみ使用されます。

Bazel 中央レジストリ

Bazel Central Registry(BCR)は、bcr.bazel.build にあるインデックス レジストリです。その内容は GitHub リポジトリ bazelbuild/bazel-central-registry によってバックアップされます。

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

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

レジストリの選択

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

モジュール拡張機能

モジュール拡張機能を使用すると、依存関係グラフ全体でモジュールから入力データを読み取り、依存関係を解決するために必要なロジックを実行し、最後にリポジトリルールを呼び出してリポジトリを作成することで、モジュール システムを拡張できます。機能は現在の 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 ユーティリティ Coursier を呼び出して Maven に接続し、解決を実行します。最後に、解決結果を使用して、仮説の maven_single_jar リポジトリ ルールを使用して、多数のリポジトリを作成します。