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 では、バージョンの「リリース」部分は MAJOR.MINOR.PATCH の 3 つのセグメントで構成する必要があります。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 は 最小バージョン選択 (MVS)アルゴリズムを Go モジュール システムで導入しました。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 デフォルトでは 、またはrepo_name 属性で指定された名前 bazel_dep
    • モジュール拡張機能リポジトリの場合: use_repo を介して導入されたリポジトリ名。

すべてのリポジトリには、直接的な依存関係のリポジトリ マッピング ディクショナリがあります。これは、表示リポジトリ名から正規リポジトリ名へのマッピングです。 ラベルを作成するときに、リポジトリ マッピングを使用してリポジトリ名を解決します。正規リポジトリ名の競合はなく、表示リポジトリ名の使用状況は MODULE.bazel ファイルを解析することで検出できるため、他の依存関係に影響を与えることなく、競合を簡単に検出して解決できます。

厳密な依存関係

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

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

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

レジストリ

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: アーカイブの Subresource 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 のタイプが「archive」の場合にのみ使用されます。

Bazel Central Registry

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 を使用して、モジュールをリクエストするレジストリのリストを指定できます。これにより、サードパーティまたは内部レジストリから依存関係を取得するようにプロジェクトを設定できます。以前のレジストリが優先されます。便宜上、プロジェクトの .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 ディレクティブを使用して宣言します 。これは、厳密な依存関係の条件を満たし、ローカル リポジトリ名の競合を回避するためです。

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_classesがあり、それぞれに 多数の属性があります。タグクラスは、この拡張機能で使用されるタグのスキーマを定義します。上記の仮想の「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 リポジトリルールを使用して、解決結果から複数のリポジトリを作成します。