このページでは、プロジェクトのビルド方法をカスタマイズするための Bazel の API である Starlark 構成の利点と基本的な使用法について説明します。ビルド設定を定義する方法と例についても説明します。
これにより、以下のことが可能になります。
- プロジェクトのカスタムフラグを定義する。
--define
は必要ない。 - Transitions を作成して、親とは異なる構成(
--compilation_mode=opt
や--cpu=arm
など)で依存関係を構成します。 - ルールにデフォルトをより適切にベイクする(たとえば、指定された SDK で
//my:android_app
を自動的にビルドする)
などを .bzl ファイルからすべて参照できます(Bazel のリリースは不要)。例については、bazelbuild/examples
リポジトリをご覧ください。
ユーザー定義のビルド設定
ビルド設定は、単一の構成情報です。構成は Key-Value マップと考えることができます。--cpu=ppc
と --copt="-DFoo"
を設定すると、{cpu: ppc, copt: "-DFoo"}
のような構成が生成されます。各エントリはビルド設定です。
cpu
や copt
のような従来のフラグはネイティブ設定です。キーは定義され、その値はネイティブの Bazel Java コード内で設定します。
Bazel のユーザーは、コマンドラインと、ネイティブに維持されている他の API を介してのみ、読み取りと書き込みを行うことができます。ネイティブ フラグと、そのフラグを公開する API を変更するには、Bazel リリースが必要です。ユーザー定義のビルド設定は .bzl
ファイルで定義されます(したがって、変更を登録するために Bazel のリリースは必要ありません)。コマンドラインで設定することもできますが(flags
と指定されている場合は下記参照)、ユーザー定義の移行で設定することもできます。
ビルド設定の定義
build_setting
rule()
パラメータ
ビルド設定は、他のルールと同様に、Starlark の rule()
関数の build_setting
属性を使用して区別されます。
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
build_setting
属性は、ビルド設定のタイプを指定する関数を受け取ります。この型は、bool
や string
などの基本的な Starlark 型のセットに限定されています。詳細については、config
モジュールのドキュメントをご覧ください。ルールの実装関数では、より複雑な入力を行うことができます。詳しくは下記をご覧ください。
config
モジュールの関数は、オプションのブール値パラメータ flag
を受け取ります。このパラメータはデフォルトで false に設定されています。flag
が true に設定されている場合、ビルド設定は、コマンドラインおよびユーザーによって、およびデフォルト値と遷移を介してルールライターによって設定されます。ユーザーがすべての設定を行えるようにすべきではありません。たとえば、ルールライターがテストルール内で有効にするデバッグモードを使用している場合、テスト以外のルール内でこの機能を無差別に有効にする機能をユーザーに許可したくはありません。
ctx.build_setting_value の使用
すべてのルールと同様に、ビルド設定ルールには実装関数があります。ビルド設定の基本的な Starlark タイプの値には、ctx.build_setting_value
メソッドを使用してアクセスできます。このメソッドは、ビルド設定ルールの ctx
オブジェクトでのみ使用できます。これらの実装メソッドは、ビルド設定値を直接転送することや、型チェックやより複雑な構造体の作成などの追加処理を行うことができます。enum
型のビルド設定を実装する手順は次のとおりです。
# example/buildsettings/build_settings.bzl
TemperatureProvider = provider(fields = ['type'])
temperatures = ["HOT", "LUKEWARM", "ICED"]
def _impl(ctx):
raw_temperature = ctx.build_setting_value
if raw_temperature not in temperatures:
fail(str(ctx.label) + " build setting allowed to take values {"
+ ", ".join(temperatures) + "} but was set to unallowed value "
+ raw_temperature)
return TemperatureProvider(type = raw_temperature)
temperature = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
マルチセット文字列フラグの定義
文字列の設定には allow_multiple
パラメータが追加されています。これにより、コマンドラインまたは bazelrcs でフラグを複数回設定できます。デフォルト値は引き続き文字列型の属性で設定されています。
# example/buildsettings/build_settings.bzl
allow_multiple_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "allow_multiple_flag")
allow_multiple_flag(
name = "roasts",
build_setting_default = "medium"
)
フラグの各設定は、単一の値として扱われます。
$ bazel build //my/target --//example:roasts=blonde \
--//example:roasts=medium,dark
上記は {"//example:roasts": ["blonde", "medium,dark"]}
に解析され、ctx.build_setting_value
はリスト ["blonde", "medium,dark"]
を返します。
ビルド設定のインスタンス化
build_setting
パラメータで定義されたルールには、暗黙的な必須の build_setting_default
属性があります。この属性の型は、build_setting
パラメータで宣言されたものと同じです。
# example/buildsettings/build_settings.bzl
FlavorProvider = provider(fields = ['type'])
def _impl(ctx):
return FlavorProvider(type = ctx.build_setting_value)
flavor = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
事前定義の設定
Skylib ライブラリには、カスタム Starlark を作成せずにインスタンス化できる一連の事前定義設定が含まれています。
たとえば、限られた文字列値を受け入れる設定を定義するには、次のようにします。
# example/BUILD
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
string_flag(
name = "myflag",
values = ["a", "b", "c"],
build_setting_default = "a",
)
完全なリストについては、一般的なビルド設定ルールをご覧ください。
ビルド設定の使用
ビルド設定に応じて
ターゲットが構成情報を読み取る場合、通常の属性依存関係を介してビルド設定に直接依存できます。
# example/rules.bzl
load("//example/buildsettings:build_settings.bzl", "FlavorProvider")
def _rule_impl(ctx):
if ctx.attr.flavor[FlavorProvider].type == "ORANGE":
...
drink_rule = rule(
implementation = _rule_impl,
attrs = {
"flavor": attr.label()
}
)
# example/BUILD
load("//example:rules.bzl", "drink_rule")
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
drink_rule(
name = "my_drink",
flavor = ":favorite_flavor",
)
言語では、その言語のすべてのルールが依存する標準的なビルド設定のセットが作成される場合があります。fragments
のネイティブの概念は Starlark の構成世界ではハードコードされたオブジェクトとして存在しませんが、この概念を変換するには、共通の暗黙的な属性セットを使用します。たとえば、以下の場合です。
# kotlin/rules.bzl
_KOTLIN_CONFIG = {
"_compiler": attr.label(default = "//kotlin/config:compiler-flag"),
"_mode": attr.label(default = "//kotlin/config:mode-flag"),
...
}
...
kotlin_library = rule(
implementation = _rule_impl,
attrs = dicts.add({
"library-attr": attr.string()
}, _KOTLIN_CONFIG)
)
kotlin_binary = rule(
implementation = _binary_impl,
attrs = dicts.add({
"binary-attr": attr.label()
}, _KOTLIN_CONFIG)
コマンドラインでビルド設定を使用する
ほとんどのネイティブ フラグと同様に、コマンドラインを使用してフラグとしてマークされているビルド設定を設定できます。ビルド設定の名前は、name=value
構文を使用した完全なターゲット パスです。
$ bazel build //my/target --//example:string_flag=some-value # allowed
$ bazel build //my/target --//example:string_flag some-value # not allowed
特殊なブール値の構文がサポートされています。
$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag
ビルド設定のエイリアスの使用
ビルド設定のターゲット パスにエイリアスを設定すると、コマンドラインで簡単に読み込めるようになります。エイリアスはネイティブ フラグと同様に機能し、二重ダッシュ オプションの構文も使用します。
エイリアスを設定するには、--flag_alias=ALIAS_NAME=TARGET_PATH
を .bazelrc
に追加します。たとえば、エイリアスを coffee
に設定するには、次のようにします。
# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp
ベスト プラクティス: エイリアスを複数回設定すると、最新のエイリアスが優先されます。予期しない解析結果を避けるため、一意のエイリアス名を使用してください。
エイリアスを使用するには、ビルド設定のターゲットパスの代わりにエイリアスを入力します。
上の例の coffee
をユーザーの .bazelrc
で設定します。
$ bazel build //my/target --coffee=ICED
これは、以下を置き換えるものです。
$ bazel build //my/target --//experimental/user/starlark_configurations/basic_build_setting:coffee-temp=ICED
おすすめの方法: コマンドラインでエイリアスを設定することは可能ですが、.bazelrc
に残すことでコマンドラインが整理されます。
ラベル付きビルドの設定
他のビルド設定とは異なり、build_setting
ルール パラメータを使用してラベル付きの設定を定義することはできません。代わりに、bazel には label_flag
と label_setting
の 2 つの組み込みルールがあります。これらのルールは、ビルド設定が設定されている実際のターゲットのプロバイダを転送します。label_flag
と label_setting
は遷移によって読み書きでき、他の build_setting
ルールと同様に label_flag
もユーザーが設定できます。唯一の違いは、手動で定義できない点です。
ラベル形式の設定により、最終的にデフォルトの遅延機能の機能が置き換えられます。レイトバインドされたデフォルトの属性はラベル型の属性で、構成によって最終値が影響を受ける可能性があります。Starlark では、これは configuration_field
API に代わるものです。
# example/rules.bzl
MyProvider = provider(fields = ["my_field"])
def _dep_impl(ctx):
return MyProvider(my_field = "yeehaw")
dep_rule = rule(
implementation = _dep_impl
)
def _parent_impl(ctx):
if ctx.attr.my_field_provider[MyProvider].my_field == "cowabunga":
...
parent_rule = rule(
implementation = _parent_impl,
attrs = { "my_field_provider": attr.label() }
)
# example/BUILD
load("//example:rules.bzl", "dep_rule", "parent_rule")
dep_rule(name = "dep")
parent_rule(name = "parent", my_field_provider = ":my_field_provider")
label_flag(
name = "my_field_provider",
build_setting_default = ":dep"
)
ビルド設定と select()
ユーザーは、select()
を使用してビルド設定に属性を設定できます。ビルド設定ターゲットは、config_setting
の flag_values
属性に渡すことができます。構成と一致する値は String
として渡され、照合のためにビルド設定のタイプに解析されます。
config_setting(
name = "my_config",
flag_values = {
"//example:favorite_flavor": "MANGO"
}
)
ユーザー定義の移行
構成遷移は、ビルドグラフ内で構成されたターゲット間の変換をマッピングします。
ルールを設定するときに、特別な属性を含める必要があります。
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist"
)
遷移を追加すると、ビルドグラフのサイズが急増します。これにより、このルールのターゲットを作成できるパッケージに許可リストが設定されます。上記のコードブロックのデフォルト値は、すべて許可リストに登録されています。ただし、ルールを使用するユーザーを制限する場合は、この属性を使用して独自のカスタム許可リストを指定できます。移行がビルドのパフォーマンスに与える影響についてアドバイスやサポートが必要な場合は、bazel-discuss@googlegroups.com にお問い合わせください。
定義
遷移は、ルール間の構成変更を定義します。たとえば、「親とは異なる CPU の依存関係をコンパイルする」などのリクエストは、遷移によって処理されます。
正式には、遷移は入力構成から 1 つ以上の出力構成への関数です。ほとんどの遷移は 1 対 1 です(例: 「--cpu=ppc
で入力構成をオーバーライドする」など)。1 対 2 の遷移も存在できますが、特別な制限があります。
Starlark では、遷移はルールと同じように transition()
関数と実装関数を定義します。
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//example:favorite_flavor" : "MINT"}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
transition()
関数は、実装関数、読み取るビルド設定のセット(inputs
)、書き込みを行うビルド設定のセット(outputs
)を取ります。実装関数には、settings
と attr
の 2 つのパラメータがあります。settings
は、inputs
パラメータで transition()
に対して宣言されているすべての設定の辞書 {String
:Object
} です。
attr
は、移行先のルールの属性と値の辞書です。送信エッジ遷移として接続する場合、これらの属性の値はすべて、select-() 解決後に構成されます。受信エッジ遷移として接続されている場合、attr
には、セレクタを使用して値を解決する属性は含まれません。--foo
の着信エッジ遷移で属性 bar
が読み取られ、--foo
で属性 bar
も設定されている場合、受信エッジ遷移で遷移の bar
の誤った値が読み取られる可能性があります。
実装関数は、適用する新しいビルド設定値の辞書(または複数の出力構成を持つ遷移の場合は辞書のリスト)を返す必要があります。返される辞書キーセットには、遷移関数の outputs
パラメータに渡されるビルド設定のセットが正確に含まれている必要があります。これは、移行中にビルド設定が実際に変更されなかった場合も同様です。元の値は、返されたディクショナリで明示的に渡す必要があります。
1 対 2 の遷移の定義
送信エッジ遷移は、1 つの入力構成を 2 つ以上の出力構成にマッピングできます。これは、マルチ アーキテクチャ コードをバンドルするルールを定義するのに役立ちます。
1 対 2 の遷移は、遷移実装関数で辞書のリストを返すことで定義されます。
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return [
{"//example:favorite_flavor" : "LATTE"},
{"//example:favorite_flavor" : "MOCHA"},
]
coffee_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
また、ルール実装関数が個々の依存関係を読み取るために使用するカスタムキーを設定することもできます。
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
遷移のアタッチ
遷移は、受信エッジと出力エッジの 2 つの場所にアタッチできます。つまり、実質的には、ルールは独自の構成を移行(受信エッジ移行)し、依存関係の構成を移行(送信エッジ移行)できます。
注: 現在のところ、Starlark の遷移をネイティブ ルールに関連付ける方法はありません。その場合、回避策を探すのに bazel-discuss@googlegroups.com に連絡してください。
受信エッジの遷移
受信エッジの遷移は、transition
オブジェクト(transition()
によって作成されたもの)を rule()
の cfg
パラメータにアタッチすることで有効になります。
# example/rules.bzl
load("example/transitions:transitions.bzl", "hot_chocolate_transition")
drink_rule = rule(
implementation = _impl,
cfg = hot_chocolate_transition,
...
受信エッジ遷移は 1 対 1 の遷移である必要があります。
送信エッジ遷移
送信エッジ遷移は、属性の cfg
パラメータに transition
オブジェクト(transition()
によって作成されたもの)をアタッチすることで有効になります。
# example/rules.bzl
load("example/transitions:transitions.bzl", "coffee_transition")
drink_rule = rule(
implementation = _impl,
attrs = { "dep": attr.label(cfg = coffee_transition)}
...
送信エッジエッジは、1 対 1 または 1 対 2 で遷移します。
これらのキーを読み取る方法については、遷移を使用した属性へのアクセスをご覧ください。
ネイティブ オプションへの移行
Starlark 遷移は、オプション名の特別な接頭辞を使用して、ネイティブ ビルド構成オプションの読み取りと書き込みを宣言することもできます。
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//command_line_option:cpu": "k8"}
cpu_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
サポートされていないネイティブ オプション
Bazel は、"//command_line_option:define"
による --define
での移行をサポートしていません。代わりに、カスタムのビルド設定を使用してください。一般に、--define
を新しく使用することは推奨されず、ビルド設定が優先されます。
Bazel は --config
での移行をサポートしていません。これは、--config
が他のフラグに展開される「拡張」フラグであるためです。
--config
には、--spawn_strategy
など、ビルド構成に影響しないフラグを含めることもできます。Bazel はこのようなフラグを個々のターゲットにバインドできません。つまり、遷移に一貫した方法を適用することはできません。
この問題を回避するには、移行で構成の一部であるフラグを明示的に項目化します。そのためには、--config
の拡張を 2 か所で管理する必要があります。これは既知の UI の不調です。
複数のビルド設定を許可する際の遷移
複数の値を許可するビルド設定を設定する場合は、その値のリストを指定する必要があります。
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "string_flag")
string_flag(name = "roasts", build_setting_default = "medium")
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
# Using a value of just "dark" here will throw an error
return {"//example:roasts" : ["dark"]},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:roasts"]
)
NoOps の遷移
遷移が {}
、[]
、または None
を返す場合、これはすべての設定を元の値に維持するための省略形です。これは、各出力をそれ自体に明示的に設定するよりも便利です。
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (attr)
if settings["//example:already_chosen"] is True:
return {}
return {
"//example:favorite_flavor": "dark chocolate",
"//example:include_marshmallows": "yes",
"//example:desired_temperature": "38C",
}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = ["//example:already_chosen"],
outputs = [
"//example:favorite_flavor",
"//example:include_marshmallows",
"//example:desired_temperature",
]
)
遷移による属性へのアクセス
送信エッジに遷移をアタッチする場合(1 対 1 または 1 対 2 の遷移であるかどうかは問いません)、ctx.attr
は、まだリストでなければ、強制的にリストになります。このリストに含まれる要素の順序は指定されていません。
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
return {"//example:favorite_flavor" : "LATTE"},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
def _rule_impl(ctx):
# Note: List access even though "dep" is not declared as list
transitioned_dep = ctx.attr.dep[0]
# Note: Access doesn't change, other_deps was already a list
for other dep in ctx.attr.other_deps:
# ...
coffee_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = coffee_transition)
"other_deps": attr.label_list(cfg = coffee_transition)
})
遷移が 1:2+
で、カスタムキーを設定している場合は、ctx.split_attr
を使用してキーごとに依存関係を個別に読み取ることができます。
# example/transitions/rules.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
def _rule_impl(ctx):
apple_dep = ctx.split_attr.dep["Apple deps"]
linux_dep = ctx.split_attr.dep["Linux deps"]
# ctx.attr has a list of all deps for all keys. Order is not guaranteed.
all_deps = ctx.attr.dep
multi_arch_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = multi_arch_transition)
})
完全なサンプルをご覧ください。
プラットフォームとツールチェーンとの統合
--cpu
や --crosstool_top
など、現在のネイティブ フラグの多くは、ツールチェーンの解決に関連したものです。将来的には、これらのタイプのフラグの明示的な移行は、ターゲット プラットフォームでの移行によって置き換えられる可能性があります。
メモリとパフォーマンスに関する考慮事項
ビルドに遷移を追加し、新しい構成を追加すると、ビルドグラフが大きくなります。また、ビルドグラフがわかりにくくなり、ビルドに時間がかかります。ビルドルールで遷移を使用することを検討する場合は、これらのコストを考慮することをおすすめします。以下の例は、遷移によってビルドグラフが指数関数的に増加する場合を示しています。
不正な動作のビルド: ケーススタディ
図 1. 最上位のターゲットとその依存関係を示すスケーラビリティ グラフ。
このグラフは、トップレベルのターゲットである //pkg:app を示します。これは 2 つのターゲット(//pkg:1_0 と //pkg:1_1)に依存します。どちらのターゲットも、//pkg:2_0 と //pkg:2_1 の 2 つのターゲットに依存します。どちらのターゲットも、//pkg:3_0 と //pkg:3_1 の 2 つのターゲットに依存します。 これは //pkg:n_0 と //pkg:n_1 まで続き、どちらも単一のターゲット //pkg:dep に依存します。
//pkg:app
を作成するには、次の \(2n+2\) ターゲットが必要です。
//pkg:app
//pkg:dep
//pkg:i_0
と//pkg:i_1
は \(i\) \([1..n]\)
フラグ --//foo:owner=<STRING>
と //pkg:i_b
が実装されるとします。
depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"
つまり、//pkg:i_b
はすべての依存関係の --owner
の古い値に b
を追加します。
これにより、次の構成されたターゲットが生成されます。
//pkg:app //foo:owner=""
//pkg:1_0 //foo:owner=""
//pkg:1_1 //foo:owner=""
//pkg:2_0 (via //pkg:1_0) //foo:owner="0"
//pkg:2_0 (via //pkg:1_1) //foo:owner="1"
//pkg:2_1 (via //pkg:1_0) //foo:owner="0"
//pkg:2_1 (via //pkg:1_1) //foo:owner="1"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_0) //foo:owner="00"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_1) //foo:owner="01"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_0) //foo:owner="10"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_1) //foo:owner="11"
...
//pkg:dep
は \(2^n\) 構成されたターゲットをconfig.owner=
"\(b_0b_1...b_n\)" \(b_i\) すべて \(\{0,1\}\)生成します。
これにより、ビルドグラフはターゲット グラフよりも指数関数的に拡大し、対応するメモリとパフォーマンスに影響します。
TODO: これらの問題を測定し、軽減するための戦略を追加する。
参考資料
ビルド構成の変更の詳細については、以下をご覧ください。
- Starlark ビルド構成
- Bazel 構成のロードマップ
- エンドツーエンドの例の完全なセット