日程の確定: BazelCon 2023 は、Google ミュンヘンで 10 月 24 ~ 25 日に開催されます。詳細

ツールチェーン

問題を報告する ソースを表示

このページでは、ツールチェーン フレームワークについて説明します。ツールチェーン フレームワークを使用すると、ルールの作成者は、プラットフォーム ベースのツール選択からルールロジックを分離できます。続行する前に、ルールプラットフォームのページを参照することをおすすめします。このページでは、ツールチェーンが必要な理由、ツールチェーンの定義方法と使用方法、プラットフォームの制約に基づいて Bazel が適切なツールチェーンを選択する方法について説明します。

目的

まず、ツールチェーンが解決するために設計された問題から見ていきましょう。「bar」プログラミング言語をサポートするルールを作成しているとします。bar_binary ルールは、barc コンパイラを使用して *.bar ファイルをコンパイルします。これは、ワークスペース自体で別のターゲットとしてビルドされるツールです。bar_binary ターゲットを作成するユーザーは、コンパイラへの依存関係を指定する必要がありません。そのため、プライベート依存関係としてルール定義に追加することで、暗黙の依存関係にすることができます。

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        "_compiler": attr.label(
            default = "//bar_tools:barc_linux",  # the compiler running on linux
            providers = [BarcInfo],
        ),
    },
)

//bar_tools:barc_linux がすべての bar_binary ターゲットの依存関係になったため、bar_binary ターゲットよりも前にビルドされます。この属性には、他の属性と同様にルールの実装関数からアクセスできます。

BarcInfo = provider(
    doc = "Information about how to invoke the barc compiler.",
    # In the real world, compiler_path and system_lib might hold File objects,
    # but for simplicity they are strings for this example. arch_flags is a list
    # of strings.
    fields = ["compiler_path", "system_lib", "arch_flags"],
)

def _bar_binary_impl(ctx):
    ...
    info = ctx.attr._compiler[BarcInfo]
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

ここでの問題は、コンパイラのラベルが bar_binary にハードコードされているにもかかわらず、ターゲットがビルドされるプラットフォームとビルドされるプラットフォーム(それぞれターゲット プラットフォームおよび実行プラットフォーム)によって異なるコンパイラが必要になる可能性があるということです。さらに、ルール作成者は利用可能なツールとプラットフォームをすべて知っているとは限らないため、ルールの定義でそれらをハードコードすることはできません。

理想的とは言えません。_compiler 属性を非公開にすることで、負担をユーザーに移すことができます。個別のターゲットをハードコードして、あるプラットフォームまたは別のプラットフォーム向けにビルドできます。

bar_binary(
    name = "myprog_on_linux",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_linux",
)

bar_binary(
    name = "myprog_on_windows",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_windows",
)

このソリューションを改善するには、select を使用して、プラットフォームに基づいて compiler を選択します。

config_setting(
    name = "on_linux",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

config_setting(
    name = "on_windows",
    constraint_values = [
        "@platforms//os:windows",
    ],
)

bar_binary(
    name = "myprog",
    srcs = ["mysrc.bar"],
    compiler = select({
        ":on_linux": "//bar_tools:barc_linux",
        ":on_windows": "//bar_tools:barc_windows",
    }),
)

しかし、これは手間がかかり、bar_binary ユーザー 1 人につき 1 度でも多くの質問が必要になります。このスタイルがワークスペース全体で一貫して使用されないと、単一のプラットフォームで問題なく機能しますが、マルチプラットフォームのシナリオに拡張するとビルドに失敗します。また、既存のルールやターゲットを変更せずに、新しいプラットフォームやコンパイラのサポートを追加するという問題に対処していません。

ツールチェーン フレームワークは、間接的レベルを追加することで、この問題を解決します。基本的に、ルール ファミリーの一部のターゲット(ツールチェーン タイプ)にルールの依存関係が宣言されているため、Bazel は該当するプラットフォーム制約に基づいて、自動的に特定のターゲット(ツールチェーン)に解決します。ルール作成者もターゲット作成者も、使用可能なプラットフォームとツールチェーンの完全なセットを知る必要はありません。

ツールチェーンを使用するルールの作成

ツールチェーン フレームワークでは、ルールがツールに直接依存するのではなく、ツールチェーン タイプに依存します。ツールチェーン タイプは、さまざまなプラットフォームで同じロールを提供するツールクラスを表すシンプルなターゲットです。たとえば、バーのコンパイラを表す型を宣言できます。

# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")

前のセクションのルール定義は、コンパイラを属性として受け取るのではなく、//bar_tools:toolchain_type ツールチェーンの使用を宣言するように変更されています。

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        # No `_compiler` attribute anymore.
    },
    toolchains = ["//bar_tools:toolchain_type"],
)

実装関数は、ツールチェーン タイプをキーとして使用して、ctx.attr ではなく ctx.toolchains の下でこの依存関係にアクセスするようになりました。

def _bar_binary_impl(ctx):
    ...
    info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
    # The rest is unchanged.
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

ctx.toolchains["//bar_tools:toolchain_type"] は、Bazel がツールチェーンの依存関係を解決したターゲットの ToolchainInfo プロバイダを返します。ToolchainInfo オブジェクトのフィールドは、基盤となるツールのルールによって設定されます。次のセクションでは、BarcInfo オブジェクトをラップする barcinfo フィールドがあるように、このルールが定義されます。

Bazel のツールチェーンをターゲットに解決する方法については、下記をご覧ください。実際に解決されたツールチェーン ターゲットのみが、bar_binary ターゲットの依存関係になります。候補ツールチェーンのスペース全体ではありません。

必須およびオプションのツールチェーン

デフォルトでは、ルールがベアラベルを使用してツールチェーン タイプの依存関係を表現している場合(上記を参照)、ツールチェーン タイプは必須とみなされます。必須のツールチェーンに対して Bazel で一致するツールチェーンが見つからない場合(下記のツールチェーンの解決を参照)、エラーが表示され、分析が停止します。

次のように、ツールチェーン タイプの依存関係を任意で宣言することもできます。

bar_binary = rule(
    ...
    toolchains = [
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

オプションのツールチェーン タイプを解決できない場合、分析は続行され、ctx.toolchains["//bar_tools:toolchain_type"] の結果は None になります。

config_common.toolchain_type 関数はデフォルトで必須です。

以下の形式を使用できます。

  • 必須のツールチェーン タイプ:
    • toolchains = ["//bar_tools:toolchain_type"]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
  • オプションのツールチェーン タイプ:
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
bar_binary = rule(
    ...
    toolchains = [
        "//foo_tools:toolchain_type",
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

同じルール内でフォームを組み合わせることもできます。ただし、同じツールチェーン タイプが複数回記載されている場合は、最も厳密なバージョンが適用されます。ここで、必須は任意よりも厳格です。

ツールチェーンを使用するコンテンツの記述

アスペクトは、ルールと同じツールチェーン API にアクセスできます。必要なツールチェーン タイプを定義し、コンテキストを介してツールチェーンにアクセスした後、ツールチェーンを使用して新しいアクションを生成できます。

bar_aspect = aspect(
    implementation = _bar_aspect_impl,
    attrs = {},
    toolchains = ['//bar_tools:toolchain_type'],
)

def _bar_aspect_impl(target, ctx):
  toolchain = ctx.toolchains['//bar_tools:toolchain_type']
  # Use the toolchain provider like in a rule.
  return []

ツールチェーンの定義

特定のツールチェーン タイプにツールチェーンを定義するには、次の 3 つが必要です。

  1. ツールまたはツール スイートの種類を表す言語固有のルール。慣例として、このルールの名前には「_toolchain」が付加されます。

    1. 注: \_toolchain ルールではビルド アクションを作成できません。代わりに、他のルールからアーティファクトを収集し、ツールチェーンを使用するルールに転送します。そのルールによって、すべてのビルド アクションが作成されます。
  2. このルールタイプの複数のターゲット。さまざまなプラットフォーム用のツールまたはツール スイートのバージョンを表します。

  3. このようなターゲットごとに、ツールチェーン toolchain ルールで使用されるメタデータを提供して、ツールチェーン フレームワークで使用されるメタデータを提供します。この toolchain ターゲットは、このツールチェーンに関連付けられた toolchain_type も参照します。つまり、特定の _toolchain ルールはどの toolchain_type にも関連付けることができます。これは、このルールが toolchain_type に関連付けられている _toolchain ルールを使用する toolchain インスタンスでのみ存在します。

実行例では、bar_toolchain ルールの定義を次に示します。この例ではコンパイラしかありませんが、リンカーなどのツールもグループ化できます。

def _bar_toolchain_impl(ctx):
    toolchain_info = platform_common.ToolchainInfo(
        barcinfo = BarcInfo(
            compiler_path = ctx.attr.compiler_path,
            system_lib = ctx.attr.system_lib,
            arch_flags = ctx.attr.arch_flags,
        ),
    )
    return [toolchain_info]

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler_path": attr.string(),
        "system_lib": attr.string(),
        "arch_flags": attr.string_list(),
    },
)

このルールは ToolchainInfo プロバイダを返す必要があります。このプロバイダは、消費ルールが ctx.toolchains とツールチェーン タイプのラベルを使用して取得するオブジェクトになります。ToolchainInfostruct など)は、任意のフィールドと値のペアを保持できます。ToolchainInfo に追加するフィールドの指定は、ツールチェーン タイプで明確に文書化する必要があります。この例では、値は上で定義したスキーマを再利用するために BarcInfo オブジェクトにラップされて返されます。このスタイルは、検証とコードの再利用に役立つことがあります。

特定の barc コンパイラのターゲットを定義できるようになりました。

bar_toolchain(
    name = "barc_linux",
    arch_flags = [
        "--arch=Linux",
        "--debug_everything",
    ],
    compiler_path = "/path/to/barc/on/linux",
    system_lib = "/usr/lib/libbarc.so",
)

bar_toolchain(
    name = "barc_windows",
    arch_flags = [
        "--arch=Windows",
        # Different flags, no debug support on windows.
    ],
    compiler_path = "C:\\path\\on\\windows\\barc.exe",
    system_lib = "C:\\path\\on\\windows\\barclib.dll",
)

最後に、2 つの bar_toolchain ターゲットの toolchain 定義を作成します。これらの定義は、言語固有のターゲットをツールチェーン タイプにリンクし、ツールチェーンが特定のプラットフォームに適しているかどうかを Bazel に知らせる制約情報を提供します。

toolchain(
    name = "barc_linux_toolchain",
    exec_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_linux",
    toolchain_type = ":toolchain_type",
)

toolchain(
    name = "barc_windows_toolchain",
    exec_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_windows",
    toolchain_type = ":toolchain_type",
)

上記の相対パス構文の使用は、これらの定義がすべて同じパッケージにあることを示していますが、ツールチェーン タイプ、言語固有のツールチェーン ターゲット、toolchain 定義ターゲットのすべてを個別のパッケージにできる理由はありません。

実際の例については、go_toolchain をご覧ください。

ツールチェーンと構成

ルールの作成者にとって重要な質問は、bar_toolchain ターゲットを分析するときに、どのような構成を認識し、どのような遷移を依存関係に使用するべきかということです。上の例では、文字列属性を使用していますが、Bazel リポジトリの他のターゲットに依存する複雑なツールチェーンの場合はどうなるでしょうか。

より複雑なバージョンの bar_toolchain を見てみましょう。

def _bar_toolchain_impl(ctx):
    # The implementation is mostly the same as above, so skipping.
    pass

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler": attr.label(
            executable = True,
            mandatory = True,
            cfg = "exec",
        ),
        "system_lib": attr.label(
            mandatory = True,
            cfg = "target",
        ),
        "arch_flags": attr.string_list(),
    },
)

attr.label の使用方法は標準のルールの場合と同じですが、cfg パラメータの意味はわずかに異なります。

ツールチェーンの解決を介してターゲット(親と呼ばれる)からツールチェーンへの依存関係は、「ツールチェーン遷移」と呼ばれる特別な構成遷移を使用します。ツールチェーンの移行では、実行プラットフォームが親と同じツールチェーンに同一になることを除けば、構成は変わりません(そうしないと、ツールチェーンのツールチェーンの解決で任意の実行プラットフォームが選択され、親の場合と同じであるとは限りません)。これにより、ツールチェーンの exec 依存関係を親のビルド アクションでも実行できるようになります。cfg = "target" を使用するツールチェーンの依存関係(または「target」がデフォルトであるため cfg を指定しない)は、親と同じターゲット プラットフォーム向けにビルドされます。これにより、ツールチェーン ルールで、それを必要とするビルドルールにライブラリ(上記の system_lib 属性)とツール(compiler 属性)の両方を提供できます。システム ライブラリは最終的なアーティファクトにリンクしているので、同じプラットフォーム用にビルドする必要があります。一方、コンパイラは、ビルド時に呼び出されるツールであり、実行プラットフォームで実行できる必要があります。

ツールチェーンでの登録とビルド

この時点で、すべてのビルディング ブロックが組み立てられています。ツールチェーンの Bazel による解決手順を利用できるようにするだけです。これを行うには、register_toolchains() を使用して WORKSPACE ファイルでツールチェーンを登録するか、--extra_toolchains フラグを使用してツールチェーンのラベルをコマンドラインに渡します。

register_toolchains(
    "//bar_tools:barc_linux_toolchain",
    "//bar_tools:barc_windows_toolchain",
    # Target patterns are also permitted, so you could have also written:
    # "//bar_tools:all",
)

ツールチェーン タイプに依存するターゲットをビルドすると、ターゲット プラットフォームと実行プラットフォームに基づいて適切なツールチェーンが選択されます。

# my_pkg/BUILD

platform(
    name = "my_target_platform",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

bar_binary(
    name = "my_bar_binary",
    ...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform

Bazel は、//my_pkg:my_bar_binary@platforms//os:linux を持つプラットフォームでビルドされているため、//bar_tools:barc_linux_toolchain への //bar_tools:toolchain_type 参照を解決します。これにより、//bar_tools:barc_linux がビルドされますが、//bar_tools:barc_windows はビルドされません。

ツールチェーンの解決

ツールチェーンを使用するターゲットごとに、Bazel のツールチェーンの解決手順によってターゲットの具体的なツールチェーンの依存関係が決定されます。この手順では、一連の必要なツールチェーン タイプ、ターゲット プラットフォーム、使用可能な実行プラットフォームのリスト、使用可能なツールチェーンのリストを入力として使用します。出力は、ツールチェーン タイプごとに選択されたツールチェーンと、現在のターゲットに選択された実行プラットフォームです。

使用可能な実行プラットフォームとツールチェーンは、register_execution_platformsregister_toolchains を介して WORKSPACE ファイルから収集されます。その他の実行プラットフォームとツールチェーンは、--extra_execution_platforms--extra_toolchains を介してコマンドラインでも指定できます。 使用可能な実行プラットフォームとして、ホスト プラットフォームが自動的に含まれます。利用可能なプラットフォームとツールチェーンは、前のアイテムを優先して、決定論のために順序付きリストとして追跡されます。

解決手順は次のとおりです。

  1. target_compatible_with 句または exec_compatible_with 句は、リスト内の constraint_value ごとに(明示的またはデフォルトとして) constraint_value を持つ場合に、プラットフォームと一致します

    句で参照されていない constraint_settingconstraint_value がある場合、これらは一致には影響しません。

  2. ビルド対象のターゲットが exec_compatible_with 属性を指定している場合(またはルールの定義で exec_compatible_with 引数を指定している場合)、使用可能な実行プラットフォームのリストがフィルタリングされ、実行制約に一致しないものはすべて削除されます。

  3. 実行可能なプラットフォームごとに、各ツールチェーン タイプを、この実行プラットフォームとターゲット プラットフォームと互換性のある最初の利用可能なツールチェーン(存在する場合)に関連付けます。

  4. いずれかのツールチェーン タイプに対応する互換性のあるツールチェーンを見つけることができなかった実行プラットフォームは除外されます。残りのプラットフォームのうち、最初のプラットフォームが現在のターゲットの実行プラットフォームになり、関連するツールチェーン(存在する場合)はターゲットの依存関係になります。

選択した実行プラットフォームは、ターゲットが生成するすべてのアクションの実行に使用されます。

同じビルド内で(同じ CPU などに複数の)同じターゲットをビルドできる場合、解決手順はターゲットの各バージョンに対して独立して適用されます。

ルールで実行グループが使用されている場合、各実行グループは個別にツールチェーンの解決を実行し、それぞれに独自の実行プラットフォームとツールチェーンがあります。

ツールチェーンのデバッグ

既存のルールにツールチェーンのサポートを追加する場合は、--toolchain_resolution_debug=regex フラグを使用します。ツールチェーンの解決中に、このフラグは、正規表現変数に一致するツールチェーンのタイプまたはターゲット名の詳細出力を提供します。.* を使用すると、すべての情報を出力できます。Bazel は、解決プロセスでチェックしてスキップするツールチェーンの名前を出力します。

ツールチェーン解決からの cquery 依存関係を確認するには、cquery--transitions フラグを使用します。

# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)

# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
  [toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211