このドキュメントでは、コードベースと Bazel の構造について説明します。これは、エンドユーザーではなく、Bazel に寄与する意思があるユーザーを対象としています。
はじめに
Bazel のコードベースは大きく(約 350 KLOC の本番環境コードと約 260 KLOC テストコード)、誰もこの環境全体に精通していません。誰もが特定の谷についてよく知っていますが、あらゆる方向で丘の中腹にあるものはほとんどわかりません。
道のりが途中で失われて森の中の暗い中にいることがないように、このドキュメントではコードベースの概要を説明し、作業を簡単に開始できるようにしています。
Bazel のソースコードの公開バージョンは、GitHub の github.com/bazelbuild/bazel にあります。これは「信頼できる情報源」ではありません。Google 内部のソースツリーから派生したものであり、Google の外部では有用でない追加機能が含まれています。長期的な目標は、GitHub を信頼できる情報源にすることです。
投稿は、通常の GitHub pull リクエスト メカニズムで受け入れられます。Google 社員が内部ソースツリーに手動でインポートしてから、GitHub に再エクスポートされます。
クライアント/サーバー アーキテクチャ
Bazel は大部分が、ビルドとビルドの間で RAM を離れるサーバー プロセス内に存在します。これにより、Bazel はビルド間で状態を維持できます。
このため、Bazel コマンドラインには起動とコマンドの 2 種類のオプションがあります。コマンドラインでは次のようになります。
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
オプションには、実行するコマンドの名前の前にあるオプション(--host_jvm_args=
)と、実行するコマンドの名前の前にあるオプション(-c opt
)があります。前者の種類は「起動オプション」と呼ばれ、サーバー プロセス全体に影響します。後者の場合は、1 つのコマンドにのみ影響します。
各サーバー インスタンスには 1 つのソースツリー(ワークスペース)が関連付けられ、各ワークスペースには通常、1 つのアクティブなサーバー インスタンスがあります。これを回避するには、カスタム出力ベースを指定します(詳しくは、「ディレクトリ レイアウト」セクションをご覧ください)。
Bazel は、有効な .zip ファイルである単一の ELF 実行可能ファイルとして配布されます。「bazel
」と入力すると、C++ で実装された上記の ELF 実行可能ファイル(「クライアント」)が制御を取得します。次の手順で適切なサーバー プロセスをセットアップします。
- すでに自身を抽出しているかどうかを確認します。それ以外の場合は、この処理を行います。サーバーの実装はここから行われます。
- 正常に動作するアクティブなサーバー インスタンスが存在するかどうかを確認します。アクティブであること、起動オプションが適切で、適切なワークスペース ディレクトリを使用していること。サーバーがリッスンしているポートを含むロックファイルがあるディレクトリ
$OUTPUT_BASE/server
から、実行中のサーバーを探します。 - 必要に応じて、古いサーバー プロセスを強制終了します。
- 必要に応じて、新しいサーバー プロセスを起動する
適切なサーバー プロセスの準備ができたら、実行する必要のあるコマンドが gRPC インターフェースを介してそのサーバー プロセスに通知され、Bazel の出力がターミナルにパイプで戻されます。同時に実行できるコマンドは 1 つのみです。これは、C++ のパートと Java のパートによる複雑なロック メカニズムを使用して実装されています。bazel version
を別のコマンドと並行して実行できないのはわかりにくいため、複数のコマンドを並列実行するためのインフラストラクチャが存在します。主な阻害要因は、BlazeModule
のライフサイクルと BlazeRuntime
の状態にあります。
コマンドの最後に、Bazel サーバーはクライアントが返す終了コードを送信します。注意すべき点は bazel run
の実装です。このコマンドの役割は、Bazel がビルドしたばかりのものを実行することですが、ターミナルがないためサーバー プロセスから実行できません。代わりに、ujexec() に必要なバイナリと引数をクライアントに伝えます。
Ctrl+C キーを押すと、クライアントはそれを gRPC 接続でのキャンセル呼び出しに変換し、できるだけ早くコマンドの終了を試みます。3 回目の Ctrl+C の後に、クライアントは SIGKILL をサーバーに送信します。
クライアントのソースコードは src/main/cpp
にあり、サーバーとの通信に使用されるプロトコルは src/main/protobuf/command_server.proto
にあります。
サーバーの主なエントリ ポイントは BlazeRuntime.main()
で、クライアントからの gRPC 呼び出しは GrpcServerImpl.run()
によって処理されます。
ディレクトリ レイアウト
Bazel では、ビルド時にやや複雑なディレクトリのセットが作成されます。詳細については、出力ディレクトリ レイアウトをご覧ください。
「ワークスペース」は、Bazel が実行されているソースツリーです。通常は、ソース管理からチェックアウトしたものに対応します。
Bazel では、すべてのデータが「出力ユーザールート」に配置されます。通常は $HOME/.cache/bazel/_bazel_${USER}
ですが、--output_user_root
起動オプションでオーバーライドできます。
「インストール ベース」は Bazel が抽出される場所です。この処理は自動的に行われ、各 Bazel のバージョンは、インストール ベースでのチェックサムに基づいてサブディレクトリを取得します。デフォルトは $OUTPUT_USER_ROOT/install
ですが、--install_base
コマンドライン オプションで変更できます。
「出力ベース」は、特定のワークスペースに接続されている Bazel インスタンスが書き込みを行う場所です。各出力ベースでいつでも実行される Bazel サーバー インスタンスは最大 1 つです。たいてい$OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
の時間帯です。この設定は、--output_base
起動オプションを使用して変更できます。特に、任意のワークスペースで任意の時点で 1 つの Bazel インスタンスしか実行できないという制限を回避するのに役立ちます。
出力ディレクトリには、次のようなものが含まれます。
$OUTPUT_BASE/external
で取得された外部リポジトリ。- 実行可能ファイルのルート。現在のビルドのすべてのソースコードへのシンボリック リンクを含むディレクトリ。
$OUTPUT_BASE/execroot
にあります。ビルド中の作業ディレクトリは$EXECROOT/<name of main repository>
です。$EXECROOT
に変更する予定ですが、互換性が非常にないため長期的な計画です。 - ビルド中にビルドされたファイル。
コマンドの実行プロセス
Bazel サーバーが制御を取得し、実行する必要があるコマンドについて通知を受けると、次の一連のイベントが発生します。
BlazeCommandDispatcher
に新しいリクエストが通知されます。コマンドの実行にワークスペースが必要かどうか(version や help など、ソースコードとは無関係のコマンドを除くほぼすべてのコマンド)と、別のコマンドを実行しているかどうかが決定されます。正しいコマンドが見つかります。各コマンドはインターフェース
BlazeCommand
を実装し、@Command
アノテーションを付ける必要があります(これは少しアンチパターンであり、コマンドに必要なすべてのメタデータをBlazeCommand
のメソッドで記述すると便利です)。コマンドライン オプションが解析されます。各コマンドには、
@Command
アノテーションで説明されているさまざまなコマンドライン オプションがあります。イベントバスが作成されます。イベントバスは、ビルドで発生するイベントのストリームです。その一部がビルドイベント プロトコルに基づいて Bazel の外部にエクスポートされ、ビルドの進捗状況が共有されます。
コマンドで制御できるようになります。最も興味深いコマンドは、ビルド、テスト、実行、カバレッジなどを実行するコマンドです。この機能は
BuildTool
によって実装されます。コマンドラインのターゲット パターンのセットが解析され、
//pkg:all
や//pkg/...
などのワイルドカードは解決されます。これはAnalysisPhaseRunner.evaluateTargetPatterns()
に実装され、SkyFrame でTargetPatternPhaseValue
として再現されます。読み込み/分析フェーズが実行され、アクション グラフ(ビルドで実行する必要があるコマンドの有向非巡回グラフ)が生成されます。
実行フェーズが実行されます。つまり、リクエストされた最上位ターゲットのビルドに必要なすべてのアクションが実行されます。
コマンドライン オプション
Bazel 呼び出しのコマンドライン オプションは、OptionsParsingResult
オブジェクトで記述します。このオブジェクトには、「オプション クラス」からオプションの値へのマップが含まれています。「オプション クラス」は OptionsBase
のサブクラスであり、互いに関連するコマンドライン オプションをグループ化します。例:
- プログラミング言語(
CppOptions
またはJavaOptions
)に関連するオプション。FragmentOptions
のサブクラスにする必要があります。最終的にはBuildOptions
オブジェクトにラップされます。 - Bazel がアクションを実行する方法に関連するオプション(
ExecutionOptions
)
これらのオプションは、分析フェーズ(Java の RuleContext.getFragment()
または Starlark の ctx.fragments
)で使用するように設計されています。その一部(C++ にスキャンを含めるかどうかなど)は実行フェーズで読み取られますが、その時点で BuildConfiguration
は利用できないため、常に明示的なプラム化が必要になります。詳細については、「構成」セクションをご覧ください。
警告: OptionsBase
インスタンスが不変であるように見せて、その方法(SkyKeys
の一部など)で使用します。そういった場合、インスタンスを変更することは、デバッグが難しい方法で Bazel を細かく破壊する良い方法です。残念ながら、これらを実際に不変にするには多大な努力が必要です。
(作成後すぐに FragmentOptions
を変更しても、誰もそれへの参照を保持でき、equals()
または hashCode()
が呼び出される前に変更しても問題ありません)。
Bazel は、次の方法でオプション クラスを学習します。
- いくつかは Bazel(
CommonCommandOptions
)に有線接続されています。 - 各 Bazel コマンドの @Command アノテーションから
ConfiguredRuleClassProvider
から(個々のプログラミング言語に関連するコマンドライン オプション)- Starlark ルールは独自のオプションを定義することもできます(こちらをご覧ください)。
各オプション(Starlark 定義のオプションを除く)は、@Option
アノテーションを持つ FragmentOptions
サブクラスのメンバー変数です。このアノテーションでは、コマンドライン オプションの名前と型、およびヘルプテキストを指定します。
通常、コマンドライン オプションの値の Java 型は単純なものです(文字列、整数、ブール値、ラベルなど)。ただし、より複雑な型のオプションもサポートされています。この場合、コマンドライン文字列からデータ型に変換する処理は、com.google.devtools.common.options.Converter
の実装になります。
Bazel で表示されるソースツリー
Bazel は、ソースコードを読み取り、解釈することでソフトウェアのビルドを行います。Bazel が動作するソースコード全体は「ワークスペース」と呼ばれ、リポジトリ、パッケージ、ルールで構成されています。
リポジトリ
「リポジトリ」はデベロッパーが扱うソースツリーで、通常は 1 つのプロジェクトを表します。Bazel の祖先である Blaze は、monorepo 上で動作しています。つまり、ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリーで動作していました。対照的に、Bazel は、ソースコードが複数のリポジトリにまたがっているプロジェクトをサポートしています。Bazel の呼び出し元のリポジトリは「メイン リポジトリ」と呼ばれ、他のリポジトリは「外部リポジトリ」と呼ばれます。
リポジトリは、ルート ディレクトリにある WORKSPACE
(または WORKSPACE.bazel
)というファイルでマークされます。このファイルには、利用可能な外部リポジトリのセットなど、ビルド全体にとって「グローバル」な情報が含まれています。これは通常の Starlark ファイルと同様に機能し、他の Starlark ファイルに対して load()
を行うことができます。通常は、明示的に参照されているリポジトリで必要なリポジトリ(deps.bzl
パターンと呼びます)を pull するために使用されます。
外部リポジトリのコードは、$OUTPUT_BASE/external
でシンボリック リンクまたはダウンロードされます。
ビルドを実行する際は、ソースツリー全体をつなぎ合わせる必要があります。SymlinkForest によって、メイン リポジトリ内のすべてのパッケージが $EXECROOT
にシンボリック リンクされ、すべての外部リポジトリが $EXECROOT/external
または $EXECROOT/..
にシンボリック リンクされます(前者では、当然ながら、メイン リポジトリに external
というパッケージを配置することはできません。そのため、そこから移行しています)。
パッケージ
すべてのリポジトリは、パッケージ、関連するファイルのコレクション、依存関係の仕様で構成されます。これらは、BUILD
または BUILD.bazel
というファイルで指定されます。両方が存在する場合、Bazel では BUILD.bazel
が優先されます。BUILD
ファイルが引き続き受け入れられる理由は、Bazel の祖先である Blaze がこのファイル名を使用していたためです。ただし、特に Windows ではファイル名の大文字と小文字を区別しないパスセグメントは、よく使われることがわかっています。
パッケージは互いに独立しています。パッケージの BUILD
ファイルを変更しても、他のパッケージは変更されません。BUILD
ファイルの追加や削除により、他のパッケージを変更できます。再帰的な glob がパッケージの境界で停止し、BUILD
ファイルが存在すると再帰が停止してしまうためです。
BUILD
ファイルの評価を「パッケージの読み込み」と呼びます。クラス PackageFactory
に実装され、Starlark インタープリタを呼び出すことによって機能します。また、使用可能なルールクラスのセットに関する知識が必要です。パッケージ読み込みの結果は、Package
オブジェクトになります。ほとんどの場合、文字列(ターゲットの名前)からターゲット自体へのマップです。
パッケージの読み込み中に複雑になるのは、グロビングの問題です。Bazel では、すべてのソースファイルを明示的にリストする必要はありません。代わりに glob(glob(["**/*.java"])
など)を実行できます。シェルとは異なり、(サブパッケージではなく)サブディレクトリに下がる再帰的な glob がサポートされます。これにはファイル システムへのアクセスが必要です。速度が遅くなる可能性があるため、Google では、可能な限り効率的に並行して実行できるように、あらゆる種類の手法を導入しています。
グロビングは次のクラスで実装されます。
LegacyGlobber
さん、スカイフレームを意識しない高速で快適な地球儀ですSkyframeHybridGlobber
: Skyframe を使用し、「Skyframe の再起動」を回避するために以前の globber に戻すバージョン(後述)
Package
クラス自体には、WORKSPACE ファイルの解析にのみ使用されるメンバーが含まれており、実際のパッケージでは意味をなしません。これは設計上の欠陥です。通常のパッケージを記述するオブジェクトには、他の何かを記述するフィールドを含めてはいけません。たとえば
- リポジトリ マッピング
- 登録済みのツールチェーン
- 登録済みの実行プラットフォーム
理想的には、WORKSPACE ファイルの解析と通常のパッケージの解析を分離することで、Package
が両方のニーズに対応する必要がないようにします。この 2 つは非常に深く絡み合っているため、残念ながらこれは困難です。
ラベル、ターゲット、ルール
パッケージはターゲットで構成されます。ターゲットには次のタイプがあります。
- ファイル: ビルドの入力または出力のもの。Bazel の用語では、これらをアーティファクトと呼びます(他の部分で説明します)。ビルド中に作成されたすべてのファイルがターゲットというわけではありません。Bazel の出力に関連付けられたラベルがないのは一般的です。
- ルール: 入力から出力を導出するステップを記述します。通常はプログラミング言語(
cc_library
、java_library
、py_library
など)に関連付けられますが、言語に依存しないもの(genrule
、filegroup
など)もあります。 - パッケージ グループ: 公開設定のセクションをご覧ください。
ターゲットの名前はラベルと呼ばれます。ラベルの構文は @repo//pac/kage:name
です。ここで、repo
はラベルが含まれるリポジトリの名前、pac/kage
は BUILD
ファイルがあるディレクトリ、name
はパッケージのディレクトリを基準とするファイルのパス(ラベルがソースファイルを参照している場合)です。コマンドラインでターゲットを参照する場合は、ラベルの一部を省略できます。
- リポジトリを省略すると、ラベルはメイン リポジトリに設定されます。
- パッケージ部分が省略されている場合(
name
や:name
など)、ラベルは現在作業ディレクトリのパッケージ内にあるものと見なされます(アップレベルの参照(..)を含む相対パスは使用できません)。
ルールの種類(「C++ ライブラリ」など)は「ルールクラス」と呼ばれます。ルールクラスは、Starlark(rule()
関数)または Java(いわゆる「ネイティブ ルール」型 RuleClass
)のいずれかで実装できます。長期的には、すべての言語固有のルールは Starlark に実装されますが、以前のルール ファミリーの一部(Java や C++ など)は当面 Java のままです。
Starlark ルールクラスは、load()
ステートメントを使用して BUILD
ファイルの先頭でインポートする必要がありますが、Java ルールクラスは ConfiguredRuleClassProvider
に登録されているため、Bazel によって「自然」に認識されます。
Rule クラスには、次のような情報が含まれます。
- その属性(
srcs
、deps
など): 型、デフォルト値、制約など - 各属性に適用されている構成の遷移と側面(ある場合)
- ルールの実装
- このルールで「通常」作成される推移的情報プロバイダ
用語に関する注意事項: コードベースでは、通常、「ルール」はルールクラスによって作成されるターゲットを意味します。しかし、Starlark とユーザー向けのドキュメントでは、「Rule」はルールクラス自体を参照するためにのみ使用する必要があります。ターゲットは単に「ターゲット」です。また、RuleClass
の名前に「クラス」が含まれているにもかかわらず、ルールクラスとその型のターゲットの間に Java 継承関係はありません。
スカイフレーム
Bazel の基盤となる評価フレームワークは Skyframe と呼ばれます。このモデルでは、ビルド中にビルドする必要があるものがすべて有向非巡回グラフに編成され、エッジが任意のデータとその依存関係(つまり、構築するために把握する必要がある他のデータ)を指しています。
グラフ内のノードは SkyValue
と呼ばれ、ノードの名前は SkyKey
と呼ばれます。どちらも非常に不変です。不変のオブジェクトにのみ到達する必要があります。この不変条件はほぼ常に当てはまり、うまくいかない場合(BuildConfigurationValue
とその SkyKey
のメンバーである個々のオプション クラス BuildOptions
など)は、変更しないようにするか、外部から確認できない方法でのみ変更するようにします。つまり、Skyframe 内で計算されるもの(構成されたターゲットなど)もすべて不変でなければならないということです。
Skyframe グラフを確認する最も便利な方法は、bazel dump
--skyframe=detailed
を実行してグラフを 1 行に 1 つの SkyValue
でダンプすることです。ビルドはかなり大きくなる可能性があるため、小規模なビルドに適しています。
SkyFrame は com.google.devtools.build.skyframe
パッケージに含まれています。同じような名前のパッケージ com.google.devtools.build.lib.skyframe
には、Skyframe 上の Bazel の実装が含まれています。Skyframe の詳細についてはこちらをご覧ください。
指定された SkyKey
を SkyValue
に評価するために、Skyframe はキーの型に対応する SkyFunction
を呼び出します。関数の評価中に、SkyFunction.Environment.getValue()
のさまざまなオーバーロードを呼び出して、Skyframe に他の依存関係をリクエストできます。これには、それらの依存関係を Skyframe の内部グラフに登録するという副作用があります。これにより、依存関係のいずれかが変更されたときに Skyframe が関数を再評価するようにします。つまり、Skyframe のキャッシュと増分計算は、SkyFunction
と SkyValue
の粒度で行われます。
利用できない依存関係を SkyFunction
がリクエストすると、getValue()
は null を返します。この関数は自身で null を返すことにより、Skyframe に制御を戻します。しばらくすると、Skyframe は使用できない依存関係を評価し、関数を最初からやり直します。今回初めて、getValue()
呼び出しは null 以外の結果で成功します。
そのため、再起動前に SkyFunction
内で実行された計算はすべて繰り返す必要があります。ただし、これにはキャッシュされている依存関係 SkyValues
を評価するための作業は含まれません。したがって、通常は次の方法でこの問題を回避します。
- 再起動の数を制限するために(
getValuesAndExceptions()
を使用して)依存関係をバッチで宣言する。 SkyValue
を、異なるSkyFunction
によって計算された別々の部分に分割して、個別に計算してキャッシュに保存できるようにする。メモリ使用量が増える可能性があるため、この設定は戦略的に行う必要があります。- 再起動の間で状態を保存する。
SkyFunction.Environment.getState()
を使用するか、アドホックの静的キャッシュを「Skyframe の背後で」保持します。
通常、処理中の Skyframe ノードは数十万に上り、Java は軽量スレッドをサポートしていないため、このような回避策が必要になります。
スターラーク
Starlark は、Bazel の構成と拡張に使用されるドメイン固有の言語です。これは、型がはるかに少なく、制御フローの制限が多い Python の制限されたサブセットとして考えられており、最も重要な点として、同時読み取りを可能にする強力な不変性が保証されています。これはチューリング完了ではないため、一部のユーザーは(ただし、すべてではない)ユーザーがその言語内で一般的なプログラミング タスクを行うのを思いとどまらせてしまいます。
Starlark は、net.starlark.java
パッケージに実装されています。独立した Go の実装もこちらにあります。現在、Bazel で使用されている Java 実装はインタープリタです。
Starlark は、次のようなさまざまな状況で使用されます。
BUILD
言語。ここで新しいルールを定義します。このコンテキストで実行される Starlark コードは、BUILD
ファイル自体とそれによって読み込まれる.bzl
ファイルのコンテンツにのみアクセスできます。- ルールの定義。これにより、新しいルール(新しい言語のサポートなど)が定義されます。このコンテキストで実行される Starlark コードは、直接的な依存関係で提供される構成とデータにアクセスできます(詳しくは後ほど)。
- WORKSPACE ファイル。ここで、外部リポジトリ(メインのソースツリーにないコード)が定義されます。
- リポジトリ ルールの定義。ここで新しい外部リポジトリ タイプが定義されます。このコンテキストで実行される Starlark コードは、Bazel が実行されているマシンで任意のコードを実行し、ワークスペースの外部に到達できます。
BUILD
ファイルと .bzl
ファイルで使用できる言語は、表現が異なるため若干異なります。相違点のリストについては、こちらをご覧ください。
Starlark の詳細については、こちらをご覧ください。
読み込み/分析フェーズ
読み込み/分析フェーズでは、Bazel が特定のルールの作成に必要なアクションを判断します。その基本単位は「構成済みターゲット」です。これは、実際には(ターゲットと構成)のペアです。
これを「読み込み/分析フェーズ」と呼びます。これは、以前はシリアル化されていたが、時間が重なって行われるようになったためです。
- パッケージの読み込み。つまり、
BUILD
ファイルを、パッケージを表すPackage
オブジェクトに変換する - 構成されたターゲットの分析、つまり、ルールの実装を実行してアクション グラフを作成する
コマンドラインでリクエストされた構成済みターゲットの推移的クロージャで構成された各ターゲットは、ボトムアップで分析する必要があります。つまり、まずリーフノードから、コマンドラインで最大まで分析する必要があります。構成された単一のターゲットの分析への入力は次のとおりです。
- 構成。(そのルールをビルドする方法。たとえば、ターゲット プラットフォームのほか、C++ コンパイラに渡すコマンドライン オプションなど)
- 直接的な依存関係。推移的情報プロバイダは、分析対象のルールで使用できます。クラスパス上のすべての .jar ファイルや、C++ バイナリにリンクする必要があるすべての .o ファイルなど、構成されたターゲットの推移的クロージャで情報の「ロールアップ」を提供するため、このような名前が付けられています。
- ターゲット自体。これは、ターゲットが含まれるパッケージを読み込んだ結果です。ルールの場合、これには属性が含まれますが、通常はこれが重要です。
- 構成されたターゲットの実装。ルールに関しては、Starlark または Java のいずれかで記述できます。ルールで構成されていないターゲットはすべて Java で実装されます。
構成されたターゲットの分析出力は次のとおりです。
- それに依存するターゲットを構成した推移的情報プロバイダは、プロジェクトにアクセスできます。
- 作成できるアーティファクトと、アーティファクトを生成するアクションです。
Java ルールで提供されている API は RuleContext
です。これは、Starlark ルールの ctx
引数に相当します。この API はより強力ですが、それと同時に、時間または空間の複雑さが二次的(または悪い)のコードを記述する、Java 例外で Bazel サーバーがクラッシュする、不変条件に違反する(Options
インスタンスを誤って変更する、構成したターゲットを可変にするなど)場合に、Bad ThingsTM を実行する方が簡単です。
構成されたターゲットの直接的な依存関係を決定するアルゴリズムは、DependencyResolver.dependentNodeMap()
にあります。
構成
構成とは、ターゲットをビルドする「方法」、つまり、どのプラットフォームに、どのコマンドライン オプションを使用して作成するかなどを指します。
同じビルド内の複数の構成に対して同じターゲットをビルドできます。これは、ビルド中で実行されるツールとターゲット コードに同じコードを使用していて、クロスコンパイルを行う場合や、太い Android アプリ(複数の CPU アーキテクチャに対応したネイティブ コードを含むアプリ)をビルドする場合に便利です。
概念的には、この構成は BuildOptions
インスタンスです。ただし実際には、BuildOptions
は BuildConfiguration
でラップされ、追加の機能が提供されます。これは、依存関係グラフの上部から下部に伝播されます。変更した場合は
ビルドを再分析する必要があります
その結果、テスト ターゲットにのみ影響するにもかかわらず、リクエストされたテスト実行の数が変更された場合、ビルド全体を再分析しなければならないなどの異常が生じます(Google では、構成を「トリミング」して、そうではないものの、まだ準備ができていない状態になる予定です)。
ルール実装が構成の一部を必要とする場合は、定義内で RuleClass.Builder.requiresConfigurationFragments()
を使用して宣言する必要があります。これは、間違い(Java フラグメントを使用する Python ルールなど)を回避するためと、Python オプションが変更された場合に C++ ターゲットを再分析する必要がないように構成のトリミングを容易にするためです。
ルールの構成は、その「親」ルールの構成と必ずしも同じではありません。依存関係エッジで構成を変更するプロセスは、「構成の遷移」と呼ばれます。これは、次の 2 つの場所で発生します。
- 依存関係のエッジにある。これらの遷移は
Attribute.Builder.cfg()
で指定され、Rule
(移行が行われる場所)とBuildOptions
(元の構成)から 1 つ以上のBuildOptions
(出力構成)への関数です。 - 構成されたターゲットへの任意の受信エッジ。これらは
RuleClass.Builder.cfg()
で指定します。
関連するクラスは TransitionFactory
と ConfigurationTransition
です。
構成の遷移は、次のように使用されます。
- ビルド時に特定の依存関係が使用され、実行アーキテクチャでビルドする必要があることを宣言する場合
- 特定の依存関係を複数のアーキテクチャ(ファットした Android APK のネイティブ コードなど)でビルドする必要があることを宣言する場合
構成の移行で複数の構成が生じる場合は、スプリット移行といいます。
構成の移行は Starlark で実装することもできます(こちらのドキュメント)。
推移的情報プロバイダ
推移的情報プロバイダは、構成済みのターゲットから、それに依存する他の構成済みのターゲットについて通知するための手段(および唯一)です。「推移的」という名前が使われているのは、通常、これは構成済みのターゲットの推移的クロージャの一種のロールアップであるためです。
通常、Java の推移的な情報プロバイダと Starlark の情報プロバイダとの間には 1 対 1 の対応関係があります(例外は DefaultInfo
で、これは FileProvider
、FilesToRunProvider
、RunfilesProvider
を合体したものです。これは、この API は Java の直接的な文字変換よりも Starlark 風であると考えられていたためです)。鍵は次のいずれかです。
- Java クラス オブジェクト。これは、Starlark からアクセスできないプロバイダでのみ使用できます。これらのプロバイダは
TransitiveInfoProvider
のサブクラスです。 - 文字列。これは従来の方法であり、名前の競合が発生する可能性があるため、推奨されません。このような推移的情報プロバイダは、
build.lib.packages.Info
の直接サブクラスです。 - プロバイダ シンボル。これは、
provider()
関数を使用して Starlark から作成できます。新しいプロバイダを作成する場合、この方法をおすすめします。このシンボルは、Java ではProvider.Key
インスタンスで表されます。
Java で実装される新しいプロバイダは、BuiltinProvider
を使用して実装する必要があります。NativeProvider
は非推奨となっており(まだ削除されていません)、TransitiveInfoProvider
サブクラスには Starlark からアクセスできません。
構成済みのターゲット
構成されたターゲットは RuleConfiguredTargetFactory
として実装されます。Java で実装されたルールクラスごとにサブクラスがあります。Starlark が構成したターゲットは、StarlarkRuleConfiguredTargetUtil.buildRule()
を通じて作成されます。
構成済みのターゲット ファクトリでは、RuleConfiguredTargetBuilder
を使用して戻り値を構築する必要があります。次の要素で構成されています。
- 「このルールが表すファイルのセット」という曖昧なコンセプトである
filesToBuild
。これらは、構成されたターゲットがコマンドラインまたは genrule の srcs にあるときにビルドされるファイルです。 - ランファイル、レギュラー、データ。
- それらの出力グループ。ルールでビルドできる、さまざまな「その他のファイルセット」です。BUILD ではファイル グループ ルールの output_group 属性を使用し、Java では
OutputGroupInfo
プロバイダを使用してアクセスできます。
実行ファイル
一部のバイナリを実行するにはデータファイルが必要です。わかりやすい例としては、入力ファイルが必要なテストがあります。Bazel では、「runfile」というコンセプトでこれを表現します。「runfiles ツリー」は特定のバイナリのデータファイルのディレクトリ ツリーです。ファイル システム内に、出力ツリーのソース内のファイルを参照する個々のシンボリック リンクを持つシンボリック リンク ツリーとして作成されます。
runfile のセットは Runfiles
インスタンスとして表されます。概念的には、runfiles ツリー内のファイルのパスから、それを表す Artifact
インスタンスまでのマップです。これは、次の 2 つの理由から単一の Map
よりもやや複雑です。
- ほとんどの場合、ファイルの runfiles パスは execpath と同じです。メモリを節約するために使用しています。
- ランファイル ツリーにはさまざまな従来のエントリがありますが、それらも表す必要があります。
ランファイルは RunfilesProvider
を使用して収集されます。このクラスのインスタンスは、構成済みのターゲット(ライブラリなど)とその推移的クロージャの必要性を表し、ネストされたセットのように収集されます(実際には、カバーの下にあるネストされたセットを使用して実装されます)。各ターゲットは依存関係のランファイルを結合し、独自の実行ファイルを追加し、結果セットを依存関係グラフの上方に送信します。RunfilesProvider
インスタンスには 2 つの Runfiles
インスタンスが含まれます。1 つは「data」属性を介してルールが依存する場合用で、もう 1 つは他の種類の受信依存関係用です。これは、ターゲットが、データ属性を介して依存している場合、それとは異なるランファイルを提示する場合があるためです。これはまだ削除されていない、望ましくない従来の動作です。
バイナリの実行ファイルは RunfilesSupport
のインスタンスとして表されます。これは Runfiles
とは異なります。RunfilesSupport
は実際にビルドできるためです(単なるマッピングである Runfiles
とは異なります)。そのためには、次の追加コンポーネントが必要になります。
- 入力 runfiles マニフェスト。これは、runfile ツリーのシリアル化された説明です。runfiles ツリーの内容のプロキシとして使用されます。Bazel は、マニフェストの内容が変更された場合にのみ、runfiles ツリーが変更されると想定します。
- 出力の runfiles マニフェスト。これは、特に Windows では、シンボリック リンクをサポートしていない場合があります。実行ファイル ツリーを処理するランタイム ライブラリで使用されます。
- runfiles 仲介者。runfiles ツリーが存在するには、シンボリック リンク ツリーと、シンボリック リンクが指すアーティファクトをビルドする必要があります。依存関係のエッジを減らすために、runfiles 中間者を使用してこれらをすべて表すことができます。
RunfilesSupport
オブジェクトが表す Runfile のバイナリを実行するためのコマンドライン引数。
さまざまな側面
アスペクトは、「計算を依存関係グラフの下に伝播する」方法です。Bazel のユーザーについては、こちらで説明しています。動機付けの良い例としては、プロトコル バッファがあります。proto_library
ルールは特定の言語を認識すべきではありませんが、任意のプログラミング言語でプロトコル バッファ メッセージ(プロトコル バッファの「基本単位」)の実装を構築する場合は、同じ言語の 2 つのターゲットが同じプロトコル バッファに依存している場合に、一度だけビルドされるように、proto_library
ルールに関連付ける必要があります。
構成済みターゲットと同様に、これらのターゲットも Skyframe で SkyValue
として表現され、その構築方法は構成済みターゲットの構築方法と非常によく似ています。ConfiguredAspectFactory
というファクトリ クラスがあり、RuleContext
にアクセスできますが、構成済みのターゲット ファクトリとは異なり、構成済みのターゲットもそのプロバイダに接続され、構成済みのターゲットも認識されます。
依存関係グラフに伝播される一連の要素は、Attribute.Builder.aspects()
関数を使用して、各属性に対して指定されます。紛らわしい名前のクラスもこのプロセスに参加し、いくつかあります。
AspectClass
はアスペクトの実装です。これは、Java(この場合はサブクラス)または Starlark(この場合はStarlarkAspectClass
のインスタンス)のいずれかです。これはRuleConfiguredTargetFactory
に似ています。AspectDefinition
はアスペクトの定義です。必要なプロバイダ、それが提供するプロバイダ、実装への参照(適切なAspectClass
インスタンスなど)が含まれます。これはRuleClass
に似ています。AspectParameters
は、依存関係グラフに伝播される側面をパラメータ化する方法です。現在は、文字列から文字列にマッピングされています。これが有効である理由の好例は、プロトコル バッファです。言語に複数の API がある場合は、プロトコル バッファをビルドする必要がある API に関する情報を依存関係グラフに伝播する必要があります。Aspect
は、依存関係グラフに伝播する側面の計算に必要なすべてのデータを表します。これはアスペクト クラス、その定義、パラメータで構成されます。RuleAspect
は、特定のルールを伝播する要素を決定する関数です。Rule
->Aspect
関数です。
やや予想外に複雑になるのは、アスペクトが他のアスペクトと関連付けられる可能性があることです。たとえば、Java IDE 用のクラスパスを収集するアスペクトでは、クラスパス上のすべての .jar ファイルを知る必要があるかもしれませんが、その一部はプロトコル バッファです。その場合、IDE アスペクトは(proto_library
ルール + Java proto アスペクト)ペアにアタッチします。
アスペクトに関するアスペクトの複雑さは、クラス AspectCollection
にキャプチャされます。
プラットフォームとツールチェーン
Bazel は、マルチプラットフォーム ビルドをサポートしています。つまり、ビルド アクションを実行する複数のアーキテクチャと、コードがビルドされる複数のアーキテクチャが存在するビルドをサポートしています。Bazel では、これらのアーキテクチャを「プラットフォーム」と呼びます(詳細なドキュメントはこちら)。
プラットフォームは、制約設定(CPU アーキテクチャのコンセプトなど)から制約値(x86_64 などの特定の CPU など)への Key-Value マッピングによって記述されます。@platforms
リポジトリには、最もよく使用される制約の設定と値をまとめた「辞書」があります。
ツールチェーンのコンセプトは、ビルドが実行されているプラットフォームとターゲットのプラットフォームによっては、異なるコンパイラを使用しなければならない場合があるという事実に起因しています。たとえば、特定の C++ ツールチェーンは特定の OS で実行されていて、他の OS をターゲットにできる場合があります。Bazel は、設定の実行とターゲット プラットフォームに基づいて、使用する C++ コンパイラを決定する必要があります(ツールチェーンのドキュメントはこちらをご覧ください)。
そのために、ツールチェーンでサポートされている実行とターゲット プラットフォームの制約のセットを使用して、ツールチェーンにアノテーションを付けることができます。そのために、ツールチェーンの定義は次の 2 つの部分に分かれています。
toolchain()
ルールは、ツールチェーンがサポートする実行制約とターゲット制約を記述し、ツールチェーンの種類(C++ や Java など)を指示するものです(後者はtoolchain_type()
ルールで表されます)。- 実際のツールチェーンを記述する言語固有のルール(
cc_toolchain()
など)
このように行われるのは、ツールチェーンを解決するためにすべてのツールチェーンの制約を知る必要があるためです。また、言語固有の *_toolchain()
ルールにはそれよりも多くの情報が含まれており、読み込みに時間がかかります。
実行プラットフォームは、次のいずれかの方法で指定します。
- WORKSPACE ファイルで
register_execution_platforms()
関数を使用する - コマンドラインで --extra_execution_platforms コマンドライン オプションを使用する
使用可能な実行プラットフォームのセットは、RegisteredExecutionPlatformsFunction
で計算されます。
構成されたターゲットのターゲット プラットフォームは PlatformOptions.computeTargetPlatform()
によって決まります。最終的には複数のターゲット プラットフォームをサポートすることになるため、プラットフォームのリストになりますが、まだ実装されていません。
構成されたターゲットに使用されるツールチェーンのセットは、ToolchainResolutionFunction
によって決定されます。次の機能によって実現されます。
- 登録済みのツールチェーンのセット(WORKSPACE ファイルと構成内)
- 目的の実行およびターゲット プラットフォーム(構成内)
- 構成されたターゲットに必要なツールチェーン タイプのセット(
UnloadedToolchainContextKey)
内) UnloadedToolchainContextKey
に構成されたターゲット(exec_compatible_with
属性)と構成(--experimental_add_exec_constraints_to_targets
)の実行プラットフォーム制約のセット
その結果は UnloadedToolchainContext
になります。これは基本的に、ツールチェーン タイプ(ToolchainTypeInfo
インスタンスとして表される)から選択したツールチェーンのラベルへのマップです。「アンロード」と呼ばれているのは、ツールチェーン自体ではなく、ラベルだけであるためです。
その後、ツールチェーンは ResolvedToolchainContext.load()
を使用して実際に読み込まれ、それらをリクエストした構成済みターゲットの実装で使用されます。
また、--cpu
などのさまざまな構成フラグで表される単一の「ホスト」構成とターゲット構成に依存しているレガシー システムもあります。現在、上記のシステムに段階的に移行しています。以前の構成値に依存しているケースに対処するために、以前のフラグと新しいスタイルのプラットフォーム制約を変換するプラットフォーム マッピングを実装しています。そのコードは PlatformMappingFunction
にあり、Starlark 以外の「小さな言語」を使用しています。
制約
少数のプラットフォームとのみ互換性があるターゲットを指定したい場合もあります。残念ながら、Bazel にはこれを実現するために複数のメカニズムがあります。
- ルール固有の制約
environment_group()
/environment()
- プラットフォームの制約
ルール固有の制約は主に Google for Java ルール内で使用されます。この制約は廃止され、Bazel では使用できませんが、ソースコードにはその制約への参照が含まれている場合があります。これを制御する属性は constraints=
です。
environment_group() と environment()
これらのルールは以前のメカニズムであり、広く使用されていません。
すべてのビルドルールは、どの「環境」向けにビルドできるかを宣言できます。ここで、「環境」は environment()
ルールのインスタンスです。
サポートされている環境は、さまざまな方法でルールに指定できます。
restricted_to=
属性を使用する。これは最も直接的な仕様であり、このグループに対してルールがサポートする環境の正確なセットを宣言します。compatible_with=
属性を使用する。これにより、デフォルトでサポートされている「スタンダード環境」環境に加えて、ルールがサポートする環境を宣言します。- パッケージ レベルの属性
default_restricted_to=
とdefault_compatible_with=
を使用する。 environment_group()
ルールのデフォルトの仕様によって、どの環境も、テーマに関連するピア(「CPU アーキテクチャ」、「JDK バージョン」、「モバイル オペレーティング システム」など)のグループに属する。環境グループの定義には、restricted_to=
/environment()
属性で特に指定されていなければ、「デフォルト」でサポートする必要がある環境が指定されます。このような属性のないルールは、すべてのデフォルトを継承します。- ルールクラスのデフォルトを使用する。これは、指定されたルールクラスのすべてのインスタンスのグローバル デフォルトをオーバーライドします。これを使用すると、たとえば、各インスタンスでこの機能を明示的に宣言することなく、すべての
*_test
ルールをテスト可能にできます。
environment()
は通常のルールとして実装されるのに対し、environment_group()
は Target
のサブクラスであり、Rule
(EnvironmentGroup
)ではなく、また、最終的に匿名のターゲットを作成する Starlark(StarlarkLibrary.environmentGroup()
)からデフォルトで使用できる関数でもあります。これは、各環境でそれが属する環境グループを宣言する必要があり、各環境グループでそのデフォルト環境を宣言する必要があるため、循環的な依存関係が発生しないようにするためです。
--target_environment
コマンドライン オプションを使用すると、ビルドを特定の環境に制限できます。
制約チェックの実装は、RuleContextConstraintSemantics
と TopLevelConstraintSemantics
にあります。
プラットフォームの制約
ターゲットと互換性があるプラットフォームを記述する現在の「公式」な方法は、ツールチェーンとプラットフォームを記述するのと同じ制約を使用することです。これは pull リクエスト #10945 で審査中です。
公開設定
多数の開発者がいる大規模なコードベースを扱っている場合(Google など)、他の誰かが自分のコードに無作為に頼らないよう注意する必要があります。そうしないと、ヒラムの法則に従って、ユーザーは実装の詳細とみなされた行動に依存するようになります。
Bazel は、visibility というメカニズムによってこれをサポートしています。visibility 属性の使用だけが、特定のターゲットに依存できることを宣言できます。この属性は少し特殊です。ラベルのリストを保持しますが、これらのラベルが特定のターゲットへのポインタではなく、パッケージ名のパターンをエンコードする場合があるためです。(設計上の欠陥です)。
これは、次の場所に実装されます。
RuleVisibility
インターフェースは可視性の宣言を表します。定数(完全な公開または完全な非公開)またはラベルのリストのいずれかを指定できます。- ラベルは、パッケージ グループ(事前定義されたパッケージのリスト)、パッケージを直接参照する(
//pkg:__pkg__
)、パッケージのサブツリー(//pkg:__subpackages__
)のいずれかを参照できます。これは、//pkg:*
または//pkg/...
を使用するコマンドライン構文とは異なります。 - パッケージ グループは、独自のターゲット(
PackageGroup
)と構成済みのターゲット(PackageGroupConfiguredTarget
)として実装されます。必要に応じて、単純なルールに置き換えることもできます。これらのロジックは、PackageSpecification
(//pkg/...
のような 1 つのパターンに対応)、PackageGroupContents
(単一のpackage_group
のpackages
属性に対応します)、PackageSpecificationProvider
(package_group
とその推移的なincludes
で集計)を使用して実装されます。 - 公開設定ラベルリストから依存関係への変換は、
DependencyResolver.visitTargetVisibility
などで行われます。 - 実際のチェックは
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
で行われます。
ネストされたセット
多くの場合、構成済みのターゲットは、依存関係から一連のファイルを集約して自身のファイルを追加し、その集計セットを推移的情報プロバイダにラップします。これにより、構成済みのターゲットは依存関係に依存します。例:
- ビルドに使用される C++ ヘッダー ファイル
cc_library
の推移的クロージャを表すオブジェクト ファイル- Java ルールをコンパイルまたは実行する際にクラスパス上に存在する必要がある .jar ファイルのセット
- Python ルールの推移的クロージャ内の Python ファイルのセット
たとえば List
や Set
を使用して単純なやり方でこれを行うと、二次的なメモリ使用量が発生します。N 個のルールのチェーンがあり、各ルールがファイルを追加すると、1+2+...+N 個のコレクション メンバーになります。
この問題を回避するために、NestedSet
というコンセプトを考案しました。これは、他の NestedSet
インスタンスと独自のメンバーで構成されるデータ構造であり、集合の有向非巡回グラフを形成します。これらは不変であり、そのメンバーは反復処理が可能です。反復処理の順序(NestedSet.Order
)は、preorder、postorder、トポロジ(ノードは常に祖先の後に来る)、「don't care, but it should be a different time」という複数の反復順序を定義します。
Starlark では、同じデータ構造を depset
といいます。
アーティファクトとアクション
実際のビルドは、ユーザーが求める出力を生成するために実行する必要がある一連のコマンドで構成されています。コマンドは Action
クラスのインスタンスとして表され、ファイルは Artifact
クラスのインスタンスとして表されます。これらのグラフは、「アクション グラフ」と呼ばれる二パートの有向非巡回グラフに配置されます。
アーティファクトには、ソース アーティファクト(Bazel の実行前に使用できるもの)と派生アーティファクト(ビルドが必要なアーティファクト)の 2 種類があります。派生アーティファクト自体が複数の種類に:
- **通常のアーティファクト。**これらは、ショートカットとして mtime を使用してチェックサムを計算することで、最新かどうかを確認しています。ctime が変更されていない場合、ファイルのチェックサムは行いません。
- 未解決のシンボリック リンク アーティファクト。readlink() を呼び出すことにより、最新性がチェックされます。通常のアーティファクトとは異なり、シンボリック リンクがぶら下がっている場合があります。通常は、いくつかのファイルをなんらかのアーカイブにまとめる場合に使用されます。
- ツリー アーティファクト。これらは単一ファイルではなく、ディレクトリ ツリーです。ファイルが最新であるかどうかは、ファイル内のファイルセットとその内容をチェックすることでチェックされます。
TreeArtifact
として表されます。 - 定数のメタデータ アーティファクト。これらのアーティファクトに変更を加えても、再ビルドはトリガーされません。これはビルド スタンプ情報にのみ使用されます。現在時刻が変更されたからといって再ビルドは行われていません。
ソース アーティファクトをツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできない根本的な理由はありません。単に、まだ実装していないだけです(ただし、BUILD
ファイルでソース ディレクトリの参照は、Bazel に以前から知られているいくつかの誤りの問題の一つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM プロパティによって有効にできる実装があります)。
Artifact
の顕著な種類がミドルマンです。これらは、MiddlemanAction
の出力である Artifact
インスタンスによって示されます。これらは、次のことを特殊化するために使用されます。
- 集約仲介は、アーティファクトをグループ化するために使用されます。したがって、多くのアクションが同じ大規模な入力セットを使用する場合、N×M の依存関係エッジは存在せず、N+M のみ(ネストされたセットに置き換えられます)
- 依存関係の中間処理をスケジュールすることで、アクションが別のアクションの前に実行されるようにします。主に lint チェックに使用されますが、C++ コンパイルにも使用されます(詳しくは
CcCompilationContext.createMiddleman()
をご覧ください)。 - ランファイル ミドルマンは、runfile ツリーの存在を確保するために使用され、出力マニフェストと、runfile ツリーによって参照されるすべてのアーティファクトに個別に依存する必要をなくします。
アクションは、実行する必要があるコマンド、必要な環境、それが生成する出力のセットとして理解することをおすすめします。アクションの説明の主な構成要素は次のとおりです。
- 実行する必要があるコマンドライン
- 必要な入力アーティファクト
- 設定が必要な環境変数
- \ で実行する必要がある環境(プラットフォームなど)を記述するアノテーション
他にもいくつかの特殊なケースがあります。たとえば、Bazel で内容が認識されているファイルの作成などです。これらは AbstractAction
のサブクラスです。Java と C++ には独自のアクション型(JavaCompileAction
、CppCompileAction
、CppLinkAction
)がありますが、ほとんどのアクションは SpawnAction
または StarlarkAction
です(同じですが、ほぼ別々のクラスにすべきではありません)。
最終的には、すべてを SpawnAction
に移行する必要があります。JavaCompileAction
はかなり似ていますが、C++ は .d ファイルの解析とスキャンが含まれているため、少々特殊なケースです。
アクション グラフは、主に Skyframe グラフに「埋め込まれています」。概念的には、アクションの実行は ActionExecutionFunction
の呼び出しとして表されます。アクション グラフの依存関係エッジから Skyframe の依存関係エッジへのマッピングについては、ActionExecutionFunction.getInputDeps()
と Artifact.key()
で説明していますが、Skyframe エッジの数を抑えるために、いくつかの最適化があります。
- 派生アーティファクトには独自の
SkyValue
がありません。代わりに、Artifact.getGeneratingActionKey()
を使用して、このキーを生成するアクションのキーが検索されます。 - ネストされたセットには独自の SkyFrame キーがあります。
共有操作
一部のアクションは複数の構成済みターゲットによって生成されます。Starlark ルールは、構成とそのパッケージによって決定されるディレクトリに派生アクションを配置できるだけであるため(ただし、同じパッケージ内のルールは競合する可能性があります)、Java で実装されたルールでは派生アーティファクトをどこにでも配置できます。
これは誤った機能と見なされますが、削除することは、実行時間を大幅に短縮できるため、非常に困難です。たとえば、なんらかの方法でソースファイルを処理する必要があり、そのファイルが複数のルール(ハンドウェーブ)で参照されている場合などです。これにはある程度の RAM が必要です。共有アクションの各インスタンスは個別にメモリに保存する必要があります。
2 つのアクションが同じ出力ファイルを生成する場合、それらはまったく同じである必要があります。つまり、同じ入力、同じ出力を持ち、同じコマンドラインを実行する必要があります。この等価関係は Actions.canBeShared()
に実装されており、すべてのアクションを確認することで、分析フェーズと実行フェーズの間で検証されます。これは SkyframeActionExecutor.findAndStoreArtifactConflicts()
に実装されており、ビルドの「グローバル」ビューを必要とする Bazel の数少ない場所の一つです。
実行フェーズ
このときに、Bazel がビルド アクション(出力を生成するコマンドなど)の実行を開始します。
分析フェーズの後に Bazel が最初に行うことは、ビルドする必要があるアーティファクトを特定することです。このロジックは TopLevelArtifactHelper
でエンコードされます。大まかに言うと、「このターゲットがコマンドライン上にある場合は、これらのアーティファクトをビルドします」と明示する明示的な目的は、コマンドラインに設定されたターゲットの filesToBuild
と特別な出力グループの内容です。
次のステップは、実行ルートの作成です。Bazel には、ファイル システム(--package_path
)のさまざまな場所からソース パッケージを読み取るオプションがあるため、完全なソースツリーを使用して、ローカルで実行されたアクションを提供する必要があります。これは SymlinkForest
クラスによって処理され、分析フェーズで使用されるすべてのターゲットをメモし、1 つのディレクトリ ツリーを構築して、実際の場所から使用ターゲットにすべてのパッケージをシンボリック リンクします。別の方法として、正しいパスをコマンドに渡すこともできます(--package_path
を考慮)。これは、次の理由で望ましくありません。
- パッケージがパッケージ パス エントリから別のエントリに移動すると、アクション コマンドラインが変更されます(これはよく使用されます)。
- その結果、アクションをローカルで実行する場合とリモートで実行した場合、コマンドラインは異なります。
- 使用するツールに固有のコマンドライン変換が必要です(Java クラスパスと C++ インクルードパスなどの違いを考慮してください)。
- アクションのコマンドラインを変更すると、そのアクションのキャッシュ エントリが無効になる
--package_path
のサポートは徐々に終了しています。
次に、Bazel はアクション グラフ(アクションとその入出力アーティファクトで構成される二部グラフ)の走査とアクションの実行を開始します。各アクションの実行は、SkyValue
クラスの ActionExecutionValue
のインスタンスで表されます。
アクションの実行にはコストがかかるため、Skyframe の背後でヒットできるキャッシュ レイヤがいくつかあります。
ActionExecutionFunction.stateMap
には、ActionExecutionFunction
の SkyFrame の再起動を低コストで行えるようにするデータが含まれています。- ローカル アクション キャッシュには、ファイル システムの状態に関するデータが含まれます。
- 通常、リモート実行システムには独自のキャッシュが
ローカル アクションのキャッシュ
このキャッシュは、Skyframe の背後にある別のレイヤです。Skyframe でアクションが再実行されても、ローカル アクション キャッシュでヒットする可能性があります。これはローカル ファイル システムの状態を表し、ディスクにシリアル化されています。つまり、新しい Bazel サーバーを起動すると、Skyframe グラフが空であっても、ローカル アクション キャッシュ ヒットを取得できます。
このキャッシュは、ActionCacheChecker.getTokenIfNeedToExecute()
メソッドを使用してヒットがあるかどうかチェックされます。
これは、その名前とは異なり、派生アーティファクトのパスと、アーティファクトを出力したアクションへのマップです。アクションは次のように記述されます。
- 入出力ファイルとチェックサムのセット
- アクション キー(通常は実行されたコマンドライン)は、入力ファイルのチェックサムでキャプチャされないすべてのものを表します(
FileWriteAction
の場合は書き込まれたデータのチェックサムなど)。
また、きわめて試験運用版の「トップダウン アクション キャッシュ」もまだ開発中です。これは、推移的ハッシュを使用してキャッシュを何度も参照するのを回避します。
入力検出と入力プルーニング
アクションの中には、単なる入力セットよりも複雑なものもあります。アクションの入力セットの変更には、次の 2 つの形式があります。
- アクションでは、実行前に新しい入力を発見したり、入力の一部が実際には必要でないと判断したりすることがあります。この標準的な例は C++ です。C++ ファイルがその推移的クロージャからどのヘッダー ファイルを使用するかを知識に基づいて推測することをおすすめします。これにより、すべてのファイルをリモート エグゼキュータに送信することを考慮する必要はありません。そのため、すべてのヘッダー ファイルを「入力」として登録せず、ソース ファイルをスキャンして、
#include
で過多に組み込まれたヘッダー オプションのみにヘッダー オプションを false として実装します。 - アクションによって、実行中に一部のファイルが使用されていなかったことが判明することがあります。C++ では、これを「.d ファイル」と呼びます。コンパイラは、どのヘッダー ファイルが後で使用されたかを示します。また、Make よりもインクリメンタリティが悪いという懸念を避けるために、Bazel ではこの事実を利用します。この方法はコンパイラに依存するため、インクルード スキャナよりも推定で優れています。
これらは Action のメソッドを使用して実装されます。
Action.discoverInputs()
が呼び出される。ネストが必要と判断されたアーティファクトのセットが返されます。これらは、構成されたターゲット グラフに同等のものがない依存関係エッジがアクション グラフ内に存在しないように、ソース アーティファクトである必要があります。- アクションは
Action.execute()
を呼び出して実行します。 Action.execute()
の最後で、アクションはAction.updateInputs()
を呼び出して、すべての入力が必要ではないことを Bazel に知らせます。そのため、使用されている入力が未使用と報告されると、誤った増分ビルドが行われる可能性があります。
アクション キャッシュが新しいアクション インスタンス(サーバーの再起動後に作成された場合など)でヒットを返すと、Bazel は updateInputs()
自体を呼び出して、以前に行われた入力検出とプルーニングの結果が入力のセットに反映されます。
Starlark アクションでは、この機能を利用して、ctx.actions.run()
の unused_inputs_list=
引数を使用して一部の入力を未使用として宣言できます。
アクションを実行するさまざまな方法: Strategies/ActionContexts
一部のアクションはさまざまな方法で実行できます。たとえば、コマンドラインはローカル、ローカル、さまざまな種類のサンドボックスで、またはリモートで実行できます。これを具体化する概念は ActionContext
と呼ばれます(または、名前変更の半分に成功したため、Strategy
と呼ばれます)。
アクション コンテキストのライフサイクルは次のとおりです。
- 実行フェーズが開始されると、
BlazeModule
インスタンスにどのようなアクション コンテキストがあるかを尋ねられます。これは、ExecutionTool
のコンストラクタで行われます。アクション コンテキスト タイプは、ActionContext
のサブインターフェースと、アクション コンテキストが実装する必要があるインターフェースを参照する JavaClass
インスタンスによって識別されます。 - 使用可能なアクション コンテキストの中から適切なアクション コンテキストが選択され、
ActionExecutionContext
とBlazeExecutor
に転送されます。 - アクションは、
ActionExecutionContext.getContext()
とBlazeExecutor.getStrategy()
を使用してコンテキストをリクエストします(これを行う方法は 1 つのみです)。
ストラテジストは、そのジョブを実行するために他のストラテジを自由に呼び出すことができます。これは、たとえば、ローカルとリモートの両方でアクションを開始し、最初に終了した方を使用する動的戦略で使用されます。
注目すべき戦略の一つは、永続的なワーカー プロセス(WorkerSpawnStrategy
)を実装する方法です。一部のツールでは起動に時間がかかるため、アクションごとに新たに開始するのではなく、アクション間で再利用する必要があります(Bazel は、ワーカー プロセスが個々のリクエスト間で監視可能な状態を保持しないことを前提としているため、正確性の問題につながる可能性があります)。
ツールが変更された場合は、ワーカー プロセスを再起動する必要があります。ワーカーを再利用できるかどうかは、WorkerFilesHash
を使用して使用するツールのチェックサムを計算することで決まります。これは、アクションのどの入力がツールの一部を表し、どの入力が入力を表すかを把握したうえで、アクションの作成者によって決定されます。つまり、Spawn.getToolFiles()
の実行ファイルがツールの一部としてカウントされます。Spawn
戦略(またはアクションのコンテキスト)についての詳細情報:
- アクションを実行するためのさまざまな戦略については、こちらをご覧ください。
- ローカルとリモートの両方でアクションを実行し、最初に完了した終了を確認する動的戦略に関する情報は、こちらをご覧ください。
- アクションをローカルで実行する場合の詳細については、こちらをご覧ください。
ローカル リソース マネージャー
Bazel は、多くのアクションを同時に実行できます。同時に実行するローカル アクションの数はアクションによって異なります。アクションに必要なリソースが多いほど、ローカルマシンに過剰な負荷がかからないようにするために、同時に実行するインスタンスが少なくなります。
これは ResourceManager
クラスに実装されます。各アクションには、ResourceSet
インスタンスの形式(CPU と RAM)の形式で、必要なローカル リソースの推定値を示す必要があります。アクション コンテキストは、ローカル リソースを必要とする操作を行うと、ResourceManager.acquireResources()
を呼び出し、必要なリソースが使用可能になるまでブロックされます。
ローカル リソース管理の詳細については、こちらをご覧ください。
出力ディレクトリの構造
アクションごとに、出力を保存する出力ディレクトリ内に別々の場所が必要です。派生アーティファクトの場所は通常、次のようになります。
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
特定の構成に関連付けられているディレクトリの名前はどのように決定されますか。次の 2 つの望ましいプロパティが相反しています。
- 同じビルドで 2 つの構成が発生する可能性がある場合は、別々のディレクトリを作成して、どちらも同じアクションの独自のバージョンを持つ必要があります。そうしないと、同じ出力ファイルを生成するアクションのコマンドラインなど、2 つの構成に相違がある場合、Bazel はどのアクションを選択すべきか判断できません(「アクションの競合」)
- 2 つの構成が「ほぼ」同じことを表している場合は、一方の構成で実行されるアクションを他方の構成で再利用できるように、構成を同じにする必要があります。たとえば、Java コンパイラでコマンドライン オプションを変更しても、C++ コンパイル アクションは再実行されないようにします。
これまでのところ、この問題を解決する原則的な方法は思いつきません。この方法は、構成トリミングの問題と類似しています。オプションの詳細については、こちらをご覧ください。主な問題となるのは、作成者は通常 Bazel になじみがない Starlark ルールと、「同じ」出力ファイルを生成できる領域に別の側面を追加する要素です。
現在のアプローチでは、構成のパスセグメントは、Java で実装された構成遷移でアクションの競合が発生しないように、さまざまな接尾辞が追加された <CPU>-<compilation mode>
です。さらに、一連の Starlark 構成遷移のチェックサムが追加され、ユーザーがアクションの競合を引き起こさないようにします。完璧とはほど遠いものです。これは OutputDirectories.buildMnemonic()
に実装され、各構成フラグメントが出力ディレクトリの名前に独自の部分を追加することに依存します。
テスト
Bazel は、テストを実行するための豊富なサポートを備えています。サポート対象:
- リモートでのテストの実行(リモート実行バックエンドが利用可能な場合)
- テストを並行して複数回実行する(タイミング データをデフレークまたは収集するため)
- テストのシャーディング(同じテスト内のテストケースを複数のプロセスに分割して高速化する)
- 不安定なテストの再実行
- テストをテストスイートにグループ化する
テストは、テストの実行方法を記述した TestProvider を含む、通常の構成ターゲットです。
- ビルドによってテストが実行されるようになったアーティファクト。これは、シリアル化された
TestResultData
メッセージを含む「キャッシュ ステータス」ファイルです。 - テストを実行する回数
- テストを分割するシャードの数
- テストの実行方法に関する一部のパラメータ(テストのタイムアウトなど)
実行するテストの決定
実行するテストの決定は、複雑なプロセスです。
まず、ターゲット パターンの解析中に、テストスイートが再帰的に展開されます。拡張は TestsForTargetPatternFunction
で実装されています。いくつか驚くべき点がありますが、テストスイートでテストを宣言しない場合、パッケージ内のすべてのテストが参照されます。これは、$implicit_tests
という暗黙的な属性をテストスイート ルールに追加することで、Package.beforeBuild()
で実装されます。
次に、コマンドライン オプションに従って、テストをサイズ、タグ、タイムアウト、言語でフィルタします。これは TestFilter
に実装され、ターゲットの解析中に TargetPatternPhaseFunction.determineTests()
から呼び出され、結果が TargetPatternPhaseValue.getTestsToRunLabels()
に格納されます。フィルタリング可能なルール属性を構成できない理由は、これが分析フェーズの前に発生し、構成が利用できないためです。
その後、BuildView.createResult()
でさらに処理されます。分析に失敗したターゲットは除外され、テストは排他テストと非排他テストに分割されます。その後、この情報は AnalysisResult
に挿入されます。これにより、ExecutionTool
は実行するテストを認識できます。
この複雑なプロセスにある程度の透明性を提供するために、コマンドラインで特定のターゲットが指定されているときに実行されるテストを示す tests()
クエリ演算子(TestsFunction
に実装されています)を使用できます。残念ながら再実装であるため、さまざまな微妙な点で上記から逸脱している可能性があります。
テストの実行
テストを実行するには、キャッシュ ステータスのアーティファクトをリクエストします。続いて TestRunnerAction
が実行されます。最終的には、--test_strategy
コマンドライン オプションで選択した TestActionContext
が呼び出され、リクエストされた方法でテストが実行されます。
テストは、環境変数を使用してテストの内容を伝える複雑なプロトコルに従って実行されます。Bazel で想定されるテストと、Bazel で想定されるテストの詳細については、こちらをご覧ください。簡単に言うと、終了コード 0 は成功、それ以外は失敗です。
キャッシュ ステータス ファイルに加えて、各テストプロセスでは他のいくつかのファイルも出力されます。これらは「テストログ ディレクトリ」に配置されます。これは、ターゲット構成の出力ディレクトリの testlogs
というサブディレクトリです。
test.xml
: テストシャード内の個々のテストケースの詳細を示す JUnit スタイルの XML ファイルtest.log
: テストのコンソール出力。stdout と stderr は分離されません。test.outputs
: 「未宣言の出力ディレクトリ」。ターミナルに出力される内容に加えてファイルを出力するテストで使用されます。
テスト実行中は、通常のターゲットのビルドでは実行できない事象として、排他的なテスト実行と出力ストリーミングの 2 つがあります。
一部のテストは、他のテストと並行して実行しないなど、排他モードで実行する必要があります。この情報を引き出すには、テストルールに tags=["exclusive"]
を追加するか、--test_strategy=exclusive
を使用してテストを実行します。各排他テストは、「main」ビルドの後にテストの実行をリクエストする個別の Skyframe 呼び出しによって実行されます。これは SkyframeExecutor.runExclusiveTest()
に実装されています。
通常のアクションは、アクションの完了時にターミナル出力がダンプされます。これとは異なり、ユーザーはテストの出力のストリーミングをリクエストして、長時間実行テストの進行状況について通知を受け取ることができます。これは --test_output=streamed
コマンドライン オプションで指定され、異なるテストの出力が混在しないように、排他的なテスト実行を示します。
これは、適切な名前の StreamedTestOutput
クラスに実装されており、対象のテストの test.log
ファイルに対する変更をポーリングし、Bazel がルールするターミナルに新しいバイトをダンプします。
実行されたテストの結果は、さまざまなイベント(TestAttempt
、TestResult
、TestingCompleteEvent
など)を監視することで、イベントバスで確認できます。これらはビルドイベント プロトコルにダンプされ、AggregatingTestListener
によってコンソールに出力されます。
カバレッジの収集
カバレッジは、bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
ファイルの LCOV 形式のテストによって報告されます。
カバレッジを収集するため、各テスト実行は collect_coverage.sh
というスクリプトにラップされています。
このスクリプトは、テスト環境をセットアップしてカバレッジの収集を有効にし、カバレッジ ランタイムによってカバレッジ ファイルを書き込む場所を決定します。その後、テストを実行します。テスト自体で複数のサブプロセスを実行し、複数の異なるプログラミング言語で記述された部分(別々のカバレッジ収集ランタイムを含む)で構成されることもあります。ラッパー スクリプトは、必要に応じて結果のファイルを LCOV 形式に変換し、1 つのファイルにマージします。
collect_coverage.sh
の挿入はテスト戦略によって行われ、テストの入力に collect_coverage.sh
が必要です。これは、暗黙的な属性 :coverage_support
によって実現されます。この属性は、構成フラグ --coverage_support
の値に解決されます(TestConfiguration.TestOptions.coverageSupport
を参照)。
オフライン インストルメンテーションを行う言語もあれば、コンパイル時にカバレッジ インストルメンテーションが追加される言語(C++ など)もあれば、オンライン インストルメンテーションを追加する言語もあります。その場合、カバレッジ インストルメンテーションは実行時に追加されます。
もう 1 つの重要なコンセプトは、ベースライン カバレッジです。ライブラリ、バイナリ、またはコードが実行されなかった場合のテストの範囲です。これにより解決される問題は、バイナリのテスト カバレッジを計算する場合、すべてのテストのカバレッジをマージするだけでは不十分であることです。バイナリには、どのテストにもリンクされていないコードが存在する可能性があるためです。したがって、Google は、すべてのバイナリに対してカバレッジ ファイルを出力します。そこには、カバーされる行のないカバレッジを収集するファイルのみが含まれます。ターゲットのベースライン カバレッジ ファイルは bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
にあります。--nobuild_tests_only
フラグを Bazel に渡した場合のテストに加えて、バイナリとライブラリに対しても生成されます。
現在、ベースライン カバレッジは切れています。
ルールごとにカバレッジを収集するために、インストルメンテーション ファイルのセットとインストルメンテーション メタデータ ファイルのセットの 2 つのファイル グループを追跡します。
インストゥルメント化されたファイルのセットとは、単にインストゥルメント化するファイルのセットのことです。オンライン カバレッジ ランタイムの場合は、ランタイム時にこれを使用して、計測するファイルを決定できます。また、ベースライン カバレッジの実装にも使用されます。
インストルメンテーション メタデータ ファイルのセットは、Bazel で必要な LCOV ファイルを生成するために必要な追加ファイルのセットです。実際には、これはランタイム固有のファイルで構成されます。たとえば、gcc はコンパイル時に .gcno ファイルを出力します。カバレッジ モードが有効になっている場合、これらはテスト アクションの入力セットに追加されます。
カバレッジが収集されているかどうかは BuildConfiguration
に格納されます。このビットに応じてテスト アクションとアクション グラフを簡単に変更できるため便利ですが、このビットを反転すると、すべてのターゲットを再分析する必要があります(C++ などの一部の言語では、カバレッジを収集できるコードを出力するために異なるコンパイラ オプションが必要になるため、とにかく再分析が必要になるため、この問題はある程度緩和されます)。
カバレッジ サポート ファイルは、暗黙的な依存関係でラベルに依存しているため、呼び出しポリシーでオーバーライドできます。これにより、Bazel のバージョン間で異なる場合があります。こうした違いをなくし、そのうちの一つに基づいて標準化するのが理想的です。
また、Bazel の呼び出し内のすべてのテストで収集されたカバレッジを統合した「カバレッジ レポート」も生成します。これは CoverageReportActionFactory
によって処理され、BuildView.createResult()
から呼び出されます。最初に実行されるテストの :coverage_report_generator
属性を確認することで、必要なツールにアクセスできます。
クエリエンジン
Bazel には、さまざまなグラフについてさまざまなことを尋ねるために使用される小さな言語があります。次のクエリの種類が用意されています。
bazel query
を使用してターゲット グラフを調査するbazel cquery
は、構成されたターゲット グラフの調査に使用されます。bazel aquery
を使用してアクション グラフを調査する
これらはそれぞれ、AbstractBlazeQueryEnvironment
をサブクラス化することで実装されます。QueryFunction
をサブクラス化することで、追加のクエリ関数を実行できます。クエリ結果をストリーミングできるようにするため、なんらかのデータ構造に収集するのではなく、query2.engine.Callback
が QueryFunction
に渡され、返される結果を得るために呼び出されます。
クエリの結果は、ラベル、ラベル、ルールクラス、XML、protobuf など、さまざまな方法で出力できます。これらは OutputFormatter
のサブクラスとして実装されます。
一部のクエリ出力形式(proto であることは確かです)の微妙な要件は、出力の差分を比較して特定のターゲットが変更されたかどうかを判断できるように、Bazel がパッケージ読み込みが提供するすべての情報を出力する必要があることです。そのため、属性値をシリアル化する必要があります。複雑な Starlark 値を持つ属性を持たない属性タイプは、わずかです。通常の回避策は、ラベルを使用し、そのラベルを持つルールに複雑な情報を付加することです。これはかなり満足できる回避策ではなく、この要件を解除した方が良いでしょう。
モジュール システム
Bazel は、モジュールを追加することで拡張できます。各モジュールは BlazeModule
をサブクラス化する必要があり(この名前は、かつて Blaze と呼ばれていたときの Bazel の歴史の遺物です)、コマンドの実行中にさまざまなイベントに関する情報を取得します。
これらは主に、Bazel の一部のバージョン(Google で使用しているバージョンなど)で必要な、次のような「コアでない」機能を実装するために使用されます。
- リモート実行システムへのインターフェース
- 次のコマンドを新しく導入しました。
BlazeModule
が提供する拡張ポイントのセットは、やや無理です。優れた設計原則の例としてこの説明を使用しないでください。
イベントバス
BlazeModules は Bazel の残りの部分と主に通信するためにイベントバス(EventBus
)を使用します。ビルドごとに新しいインスタンスが作成され、Bazel のさまざまな部分がイベントをそのインスタンスにポストし、モジュールは関心のあるイベントのリスナーを登録できます。たとえば、以下はイベントとして表されます。
- ビルドされるビルド ターゲットのリストが決定済み(
TargetParsingCompleteEvent
) - トップレベル構成が確定しました(
BuildConfigurationEvent
) - ターゲットがビルドされました(成功または失敗)(
TargetCompleteEvent
) - テストが実行されました(
TestAttempt
、TestSummary
)
これらのイベントの一部は、ビルドイベント プロトコルの Bazel 外(BuildEvent
)で表されます。これにより、BlazeModule
だけでなく、Bazel プロセス外でもビルドを監視できます。プロトコル メッセージを含むファイルとしてアクセスすることも、Bazel がサーバー(ビルドイベント サービス)に接続してイベントをストリーミングすることもできます。
これは、build.lib.buildeventservice
と build.lib.buildeventstream
の Java パッケージに実装されます。
外部リポジトリ
Bazel は元々、monorepo(ビルドに必要なものをすべて含む単一のソースツリー)で使用するように設計されましたが、この環境では必ずしもそうとは限りません。「外部リポジトリ」は、この 2 つの世界を橋渡しするために使用される抽象化です。ビルドには必要ですが、メインのソースツリーにはないコードを表します。
WORKSPACE ファイル
外部リポジトリのセットは、WORKSPACE ファイルの解析によって決定されます。たとえば、次のように宣言します。
local_repository(name="foo", path="/foo/bar")
@foo
というリポジトリが利用可能になります。これが複雑になる点は、Starlark ファイルで新しいリポジトリ ルールを定義できることです。これにより、新しい Starlark コードの読み込みが可能になり、新しいリポジトリ ルールなどを定義することができます。
この場合、WORKSPACE ファイル(WorkspaceFileFunction
内)の解析を、load()
ステートメントで区切られたチャンクに分割します。チャンク インデックスは WorkspaceFileKey.getIndex()
で示され、インデックス X が X 番目の load()
ステートメントまでチャンク インデックスを評価するまで WorkspaceFileFunction
を計算します。
リポジトリの取得
リポジトリのコードを Bazel で使用できるようにする前に、fetchedする必要があります。これにより、Bazel が $OUTPUT_BASE/external/<repository name>
の下にディレクトリを作成します。
リポジトリの取得は次の手順で行われます。
PackageLookupFunction
はリポジトリが必要であることを認識し、RepositoryName
をSkyKey
として作成してRepositoryLoaderFunction
を呼び出します。RepositoryLoaderFunction
は、不明確な理由でリクエストをRepositoryDelegatorFunction
に転送します(コードは Skyframe の再起動時に再ダウンロードを回避するように指示していますが、これはかなり確実な理由ではありません)。RepositoryDelegatorFunction
は、リクエストされたリポジトリが見つかるまで WORKSPACE ファイルのチャンクを反復処理することにより、取得が求められているリポジトリ ルールを見つけます。- リポジトリの取得を実装する適切な
RepositoryFunction
が見つかる。リポジトリの Starlark 実装か、Java で実装されたリポジトリのハードコードされたマップのいずれかです。
リポジトリの取得は非常にコストがかかる可能性があるため、さまざまなキャッシュ保存レイヤがあります。
- チェックサム(
RepositoryCache
)をキーとするダウンロード済みファイル用のキャッシュがあります。このためには、WORKSPACE ファイルでチェックサムを使用できる必要がありますが、密閉性に優れています。これは、実行されているワークスペースや出力ベースに関係なく、同じワークステーション上のすべての Bazel サーバー インスタンスによって共有されます。 $OUTPUT_BASE/external
の下に、リポジトリごとに「マーカー ファイル」が書き込まれます。ここには、そのファイルの取得に使用されたルールのチェックサムが格納されています。Bazel サーバーが再起動されてもチェックサムが変更されない場合、再取得されません。これはRepositoryDelegatorFunction.DigestWriter
に実装されます。--distdir
コマンドライン オプションは、ダウンロードするアーティファクトの検索に使用される別のキャッシュを指定します。これは、Bazel がインターネットからランダムな情報を取得しないエンタープライズ設定に便利です。これはDownloadManager
によって実装されます。
リポジトリがダウンロードされると、そのリポジトリのアーティファクトはソース アーティファクトとして扱われます。通常、Bazel はソース アーティファクトに対して stat() を呼び出して最新性をチェックするため、問題が発生します。これらのアーティファクトは、含まれているリポジトリの定義が変更されると無効になります。したがって、外部リポジトリ内のアーティファクトの FileStateValue
は、外部リポジトリに依存する必要があります。これは ExternalFilesHelper
によって処理されます。
マネージド ディレクトリ
外部リポジトリは、ワークスペースのルート下にあるファイルの変更が必要になることがあります(ダウンロードしたパッケージをソースツリーのサブディレクトリに格納するパッケージ マネージャーなど)。これは、Bazel がソースファイルを単独で変更するのではなくユーザーによってのみ変更し、パッケージがワークスペースのルート下にあるすべてのディレクトリを参照できるようにするという仮定とは矛盾しています。このような外部リポジトリを機能させるために、Bazel は次の 2 つのことを行います。
- Bazel によるアクセスが許可されていないワークスペースのサブディレクトリをユーザーが指定できるようにします。これらは
.bazelignore
というファイルにリストされ、機能はBlacklistedPackagePrefixesFunction
に実装されます。 - ワークスペースのサブディレクトリから外部リポジトリへのマッピングは
ManagedDirectoriesKnowledge
にエンコードされ、それらを参照するFileStateValue
が通常の外部リポジトリと同じ方法で処理されます。
リポジトリのマッピング
複数のリポジトリが同じリポジトリに、異なるバージョンで依存しようとすることがあります(これは「ダイヤモンド依存関係の問題」のインスタンスです)。たとえば、ビルド内の別々のリポジトリにある 2 つのバイナリが Guava に依存している場合、どちらも @guava//
で始まるラベルで Guava を参照し、異なるバージョンを意味すると考えられます。
したがって、Bazel では、一方のバイナリのリポジトリで文字列 @guava//
が一方の Guava リポジトリ(@guava1//
など)を参照し、もう一方の Guava リポジトリ(@guava2//
など)がもう一方のリポジトリを参照できるように、一方が外部リポジトリのラベルを再マッピングできます。
または、これはダイヤモンドをjoinするためにも使用できます。リポジトリが @guava1//
に依存し、別のリポジトリが @guava2//
に依存している場合、リポジトリ マッピングにより、一方は正規の @guava//
リポジトリを使用するように両方のリポジトリを再マッピングできます。
マッピングは、WORKSPACE ファイルで、個々のリポジトリ定義の repo_mapping
属性として指定します。その後、WorkspaceFileValue
のメンバーとして Skyframe に表示され、次のようにパイプライン化されます。
Package.Builder.repositoryMapping
: パッケージ内のルールのラベル値属性をRuleClass.populateRuleAttributeValues()
によって変換するために使用されます。Package.repositoryMapping
: 分析フェーズで使用します(読み込みフェーズで解析されない$(location)
などの解決に使用します)。BzlLoadFunction
: load() ステートメントのラベルを解決
JNI ビット
Bazel のサーバーは、ほとんどが Java で記述されています。例外は、Java だけでは実行できない部分と、実装時に Java だけでは実行できない部分です。これは主に、ファイル システム、プロセス制御、その他のさまざまな下位レベルの操作に限定されます。
C++ コードは src/main/native にあり、ネイティブ メソッドを持つ Java クラスは次のとおりです。
NativePosixFiles
、NativePosixFileSystem
ProcessUtils
WindowsFileOperations
、WindowsFileProcesses
com.google.devtools.build.lib.platform
コンソール出力
コンソール出力の出力は単純なように思えますが、複数のプロセス(場合によってはリモートで)の実行、きめ細かいキャッシュ保存、見やすくカラフルなターミナル出力、長時間稼働するサーバーの使用が合わさることで、簡単なことではありません。
RPC 呼び出しがクライアントから到着した直後に、2 つの RpcOutputStream
インスタンス(stdout と stderr 用)が作成され、出力されたデータがクライアントに転送されます。その後、OutErr
((stdout、stderr)ペア)にラップされます。コンソールに出力する必要があるものはすべて、これらのストリームを通過します。その後、これらのストリームは BlazeCommandDispatcher.execExclusively()
に引き継がれます。
デフォルトでは、出力は ANSI エスケープ シーケンスとともに出力されます。これらが望ましくない場合(--color=no
)は、AnsiStrippingOutputStream
によって削除されます。さらに、System.out
と System.err
はこれらの出力ストリームにリダイレクトされます。これは、System.err.println()
を使用してデバッグ情報を出力した後でも、クライアントのターミナル出力(サーバーの出力とは異なります)に表示されるようにするためです。プロセスがバイナリ出力(bazel query --output=proto
など)を生成する場合、stdout のミュージングは行われません。
短いメッセージ(エラー、警告など)は、EventHandler
インターフェースを通じて表現されます。特に、EventBus
に送信する内容とは異なります(混乱します)。各 Event
には EventKind
(エラー、警告、情報など)があり、Location
(イベントを発生させたソースコード内の場所)が含まれる場合があります。
一部の EventHandler
実装は、受け取ったイベントを保存します。これは、さまざまな種類のキャッシュされた処理によって発生した情報を UI にリプレイするために使用されます。たとえば、キャッシュに保存された構成済みのターゲットによって出力された警告などです。
一部の EventHandler
では、イベントバスへのルートに到達するイベントの投稿も許可されています(通常の Event
はそこには表示されません)。これらは ExtendedEventHandler
の実装であり、主にキャッシュされた EventBus
イベントの再生に使用されます。これらの EventBus
イベントはすべて Postable
を実装しますが、EventBus
に送信されるすべてが必ずしもこのインターフェースを実装するわけではありません。ExtendedEventHandler
によってキャッシュに保存されるイベントのみを実装します(これは適切な処理であり、ほとんどの処理は行われますが、強制されません)。
ターミナル出力は主に UiEventHandler
を介して出力されます。これは、Bazel による高度な出力フォーマットと進行状況レポートすべてを担当します。入力は 2 つあります。
- イベントバス
- Reporter を通じてパイプされたイベント ストリーム
コマンド実行マシン(Bazel の残りの部分など)がクライアントへの RPC ストリームに直接接続する必要があるのは、Reporter.getOutErr()
のみです。これにより、これらのストリームに直接アクセスできます。コマンドで大量のバイナリデータをダンプする必要がある場合にのみ使用されます(bazel query
など)。
Bazel のプロファイリング
Bazel は高速です。Bazel も低速です。これは、ビルドが許容範囲の端まで増加する傾向があるためです。このため、Bazel には、ビルドと Bazel 自体のプロファイリングに使用できるプロファイラが含まれています。これは、Profiler
という名前のクラスに実装されます。このオプションはデフォルトでオンになっていますが、オーバーヘッドが許容されるように要約データのみを記録します。コマンドライン --record_full_profiler_data
を使用すると、できるすべてのものを記録できます。
プロファイルは Chrome プロファイラ形式で出力されます。Chrome での表示に適しています。 タスクスタックのデータモデルです。タスクの開始と終了ができ、互いにきちんとネストされる必要があります。各 Java スレッドは独自のタスクスタックを取得します。TODO: アクションと連続受け渡しスタイルではどのように機能しますか?
プロファイラは、それぞれ BlazeRuntime.initProfiler()
と BlazeRuntime.afterCommand()
で開始および停止され、すべてをプロファイリングできるように、可能な限り長く稼働しようとします。プロファイルに何かを追加するには、Profiler.instance().profile()
を呼び出します。Closeable
を返します。このクロージャがタスクの終了を表します。try-with-resources ステートメントで使用することをおすすめします。
MemoryProfiler
では、基本的なメモリのプロファイリングも行います。また、この機能は常にオンになっており、ほとんどの場合、最大ヒープサイズと GC 動作が記録されます。
Bazel のテスト
Bazel には、主に 2 種類のテストがあります。Bazel を「ブラック ボックス」として確認するテストと、分析フェーズのみを実行するテストです。前者を「統合テスト」、後者を「単体テスト」と呼んでいますが、これらはどちらかというと、統合度が低い統合テストです。必要な場合は、実際の単体テストも用意されています。
統合テストには次の 2 種類があります。
src/test/shell
の下にある非常に複雑な bash テスト フレームワークを使用して実装されています。- Java で実装されます。これらは、
BuildIntegrationTestCase
のサブクラスとして実装されます。
BuildIntegrationTestCase
は、ほとんどのテストシナリオに対応可能なため、統合テスト フレームワークとして推奨されます。Java フレームワークであるため、多くの一般的な開発ツールとのデバッグ可能性とシームレスな統合が実現します。Bazel リポジトリには、BuildIntegrationTestCase
クラスの例が多数あります。
分析テストは、BuildViewTestCase
のサブクラスとして実装されます。BUILD
ファイルの書き込みに使用できるスクラッチ ファイル システムがあり、さまざまなヘルパー メソッドで、構成されたターゲットのリクエスト、構成の変更、分析結果に関するさまざまなアサートを行うことができます。