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

問題を報告 ソースを表示 Nightly · 7.4 .

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

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

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

Bazel モジュール

従来の WORKSPACE ベースの外部依存関係システムは、リポジトリ(リポジトリ)を中心に構成され、リポジトリ ルール(リポジトリ ルール)を使用して作成されます。リポジトリは新しいシステムでも重要なコンセプトですが、モジュールが依存関係のコア ユニットです。

モジュールは、基本的に複数のバージョンを持つことができる Bazel プロジェクトです。各バージョンは、依存する他のモジュールに関するメタデータを公開します。これは、Maven のアーティファクト、npm のパッケージ、Cargo のcrate、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 など、日付ベースのバージョン(20210324.2 など)を使用する有名なプロジェクトもあります。

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

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

有効な SemVer バージョンは、有効な Bazel モジュール バージョンです。また、2 つの SemVer バージョン aba < 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 では、モジュールの下位互換性のないバージョンを別のモジュールとして単純に扱うため、下位互換性に関する想定は現実的です。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: ソース アーカイブに使用するミラーのリストを指定します。
    • module_base_path: source.json ファイルで local_repository タイプのモジュールのベースパスを指定します。
  • /modules: このレジストリ内の各モジュールのサブディレクトリを含むディレクトリ。
  • /modules/$MODULE: このモジュールの各バージョンのサブディレクトリと、次のファイルを含むディレクトリ。
    • metadata.json: モジュールに関する情報を含む JSON ファイル。次のフィールドがあります。
      • homepage: プロジェクトのホームページの URL。
      • maintainers: JSON オブジェクトのリスト。各オブジェクトは、レジストリ内のモジュールの管理者の情報に対応しています。これは、プロジェクトの作成者と同じである必要はありません。
      • versions: このレジストリにあるこのモジュールのすべてのバージョンのリスト。
      • yanked_versions: このモジュールの削除されたバージョンのリスト。これは現在は no-op ですが、今後は、削除されたバージョンはスキップされるか、エラーが発生します。
  • /modules/$MODULE/$VERSION: 次のファイルを含むディレクトリ。
    • MODULE.bazel: このモジュール バージョンの MODULE.bazel ファイル。
    • source.json: このモジュール バージョンのソースを取得する方法に関する情報が含まれる JSON ファイル。
      • デフォルトのタイプは「archive」で、次のフィールドがあります。
        • url: ソース アーカイブの URL。
        • integrity: アーカイブのサブリソースの完全性チェックサム。
        • strip_prefix: ソース アーカイブの抽出時に削除するディレクトリ接頭辞。
        • patches: 文字列のリスト。それぞれが、抽出されたアーカイブに適用するパッチファイルの名前です。パッチファイルは /modules/$MODULE/$VERSION/patches ディレクトリにあります。
        • patch_strip: Unix パッチの --strip 引数と同じです。
      • 次のフィールドを使用してローカルパスを使用するようにタイプを変更できます。
        • type: local_path
        • path: リポジトリのローカルパス。次のように計算されます。
          • パスが絶対パスの場合は、そのまま使用されます。
          • path が相対パスで、module_base_path が絶対パスの場合、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 Central Registry

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

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

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

レジストリの選択

再利用可能な 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 ユーティリティ Coursier を呼び出して Maven に接続し、解決を実行します。最後に、解決結果を使用して、仮想の maven_single_jar リポジトリ ルールを使用して複数のリポジトリを作成します。