このドキュメントでは、コードベースと 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 つのアクティブなサーバー インスタンスがあります。これは、カスタム出力ベースを指定することで回避できます(詳細については、「ディレクトリ レイアウト」セクションをご覧ください)。
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 接続での Cancel 呼び出しに変換し、できるだけ早くコマンドの終了を試みます。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
に新しいリクエストが通知されます。このコマンドにワークスペースが必要かどうか(バージョンやヘルプなどソースコードに関係のないコマンドを除くほとんどすべてのコマンド)と、別のコマンドが実行されているかどうかを判別します。正しいコマンドが見つかりました。各コマンドでインターフェース
BlazeCommand
を実装し、@Command
アノテーションを付ける必要があります(これは少しアンチパターンです。コマンドに必要なすべてのメタデータがBlazeCommand
のメソッドで記述されていると便利です)。コマンドライン オプションが解析されます。各コマンドには、
@Command
アノテーションで説明されているとおり、さまざまなコマンドライン オプションがあります。イベントバスが作成されます。イベントバスは、ビルド中に発生したイベントのストリームです。ビルドの進め方を世界に伝えるために、その一部は Build Event Protocol に基づいて Bazel の外部にエクスポートされます。
コマンドが制御を取得します。最も興味深いコマンドは、ビルドを実行するコマンド(build、test、run、coverage など)です。この機能は
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 が壊れてしまいます。残念ながら、それらを実際に不変にするのは多大な労力を要する作業です。(作成直後、他のユーザーが参照を保持できる前、equals()
または hashCode()
が呼び出される前に FragmentOptions
を変更しても問題ありません)。
Bazel は、次の方法でオプション クラスを学習します。
- Bazel に組み込まれているものもあります(
CommonCommandOptions
) - 各 Bazel コマンドの
@Command
アノテーションから ConfiguredRuleClassProvider
から(これらは、個々のプログラミング言語に関連するコマンドライン オプションです)- Starlark ルールでは、独自のオプションを定義することもできます(こちらを参照)。
各オプション(Starlark で定義されたオプションを除く)は、@Option
アノテーションを持つ FragmentOptions
サブクラスのメンバー変数です。このアノテーションは、コマンドライン オプションの名前と型、およびヘルプテキストを指定します。
通常、コマンドライン オプションの値の Java 型は単純なもの(文字列、整数、ブール値、ラベルなど)です。ただし、より複雑な型のオプションもサポートされています。この場合、コマンドライン文字列からデータ型への変換の役割は com.google.devtools.common.options.Converter
の実装です。
Bazel で表示されたソースツリー
Bazel はソフトウェアをビルドするビジネスで、ソースコードの読み取りと解釈によって行われます。Bazel が動作するソースコード全体は「ワークスペース」と呼ばれ、リポジトリ、パッケージ、ルールで構成されています。
リポジトリ
「リポジトリ」はデベロッパーが作業するソースツリーであり、通常は単一のプロジェクトを表します。Bazel の祖先である Blaze は、monorepo、つまりビルドの実行に使用されるすべてのソースコードを含む単一のソースツリーで動作していました。一方、Bazel は、ソースコードが複数のリポジトリにまたがっているプロジェクトをサポートしています。Bazel が呼び出されるリポジトリは「メイン リポジトリ」、その他は「外部リポジトリ」と呼ばれます。
リポジトリは、ルート ディレクトリにある WORKSPACE
(または WORKSPACE.bazel
)というファイルによってマークされます。このファイルには、使用可能な外部リポジトリのセットなど、ビルド全体に対して「グローバル」な情報が含まれています。これは通常の Starlark ファイルと同様に機能するため、他の Starlark ファイルを load()
できます。これは、明示的に参照されるリポジトリが必要とするリポジトリを pull するためによく使用されます(これを「deps.bzl
パターン」と呼びます)。
外部リポジトリのコードは、$OUTPUT_BASE/external
でシンボリック リンクされるか、ダウンロードされます。
ビルドを実行する際は、ソースツリー全体をつなぎ合わせる必要があります。これは SymlinkForest
によって行われます。これは、メイン リポジトリ内のすべてのパッケージを $EXECROOT
に、すべての外部リポジトリを $EXECROOT/external
または $EXECROOT/..
にシンボリック リンクします(当然、前者はメイン リポジトリに external
というパッケージを配置できません。そのため、ここから移行します)。
パッケージ
すべてのリポジトリは、パッケージ、関連ファイルのコレクション、依存関係の仕様で構成されています。これらは、BUILD
または BUILD.bazel
というファイルで指定されます。両方が存在する場合、Bazel は BUILD.bazel
を優先します。BUILD
ファイルをそのまま使用できる理由は、Bazel の祖先である Blaze がこのファイル名を使用しているためです。しかし、Windows では特にファイル名の大文字と小文字を区別しない、パスセグメントとしてよく使われることが判明しました。
パッケージは互いに独立しています。パッケージの BUILD
ファイルに変更を加えても、他のパッケージは変更されません。再帰 glob はパッケージ境界で停止し、BUILD
ファイルが存在すると再帰が停止するため、BUILD
ファイルを追加または削除すると、他のパッケージを変更できます。
BUILD
ファイルの評価は「パッケージの読み込み」と呼ばれます。これはクラス PackageFactory
に実装され、Starlark インタープリタを呼び出して動作します。また、使用可能なルールクラスのセットに関する知識が必要です。パッケージの読み込みの結果は Package
オブジェクトです。ほとんどの場合、文字列(ターゲットの名前)からターゲット自体へのマップです。
パッケージの読み込み時に複雑になるのは「グローブ」です。Bazel では、すべてのソースファイルを明示的に列挙する必要はなく、代わりに glob(glob(["**/*.java"])
など)を実行できます。シェルとは異なり、サブディレクトリに(ただしサブパッケージではない)再帰 glob をサポートしています。これにはファイル システムへのアクセスが必要ですが、処理が遅くなる可能性があるため、並列かつ可能な限り効率的に実行できるように、あらゆる種類の手法を実装しています。
グロビングは次のクラスに実装されています。
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 では「本質的に」認識されています。
ルールクラスには、以下のような情報が含まれます。
- 属性(
srcs
、deps
など): タイプ、デフォルト値、制約など。 - 各属性に関連付けられた構成の遷移とアスペクト(存在する場合)
- ルールの実装
- ルールで「通常」作成される推移情報プロバイダ
用語に関する注: コードベースでは、ルールクラスによって作成されたターゲットを意味するために「ルール」を使用することがよくあります。しかし、Starlark とユーザー向けドキュメントでは、「Rule」はルールクラス自体を参照するためにのみ使用する必要があり、ターゲットは単なる「ターゲット」です。また、RuleClass
の名前に「class」が含まれていますが、ルールクラスとその型のターゲットの間に Java 継承関係はありません。
スカイフレーム
Bazel の基盤となる評価フレームワークは Skyframe です。そのモデルは、ビルド中に構築する必要があるすべてのデータが、任意のデータからその依存関係、つまり、構築のために知っている必要がある他のデータを指すエッジを持つ有向非巡回グラフに編成されます。
グラフ内のノードは SkyValue
と呼ばれ、その名前は SkyKey
と呼ばれます。どちらも不変のオブジェクトです。不変のオブジェクトにのみ到達できるようにする必要があります。この不変条件はほとんどの場合に当てはまります。そうでない場合(BuildConfigurationValue
とその SkyKey
のメンバーである個々のオプション クラス BuildOptions
など)、変更しないようにしたり、外部から監視できない方法でのみ変更したりしないようにしています。したがって、Skyframe 内で計算されるすべて(構成されたターゲットなど)も不変である必要があります。
Skyframe グラフを観察する最も便利な方法は、bazel dump
--skyframe=deps
を実行することです。これにより、1 行に 1 つの SkyValue
がダンプされます。ビルドがかなり大きくなる可能性があるため、小規模なビルドでその方法をおすすめします。
スカイフレームは 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 は特定のルールのビルドに必要なアクションを判断します。その基本単位は「構成されたターゲット」です。これはよく使われ、(ターゲットと構成)のペアになります。
「読み込み/分析フェーズ」と呼ばれるのは、以前はシリアル化されていた 2 つの部分に分割できるため、これらは時間的に重なり合う可能性があります。
- パッケージを読み込む。つまり、
BUILD
ファイルをパッケージを表すPackage
オブジェクトに変換する。 - 構成されたターゲットを分析する、つまりルールの実装を実行してアクション グラフを生成する
コマンドラインでリクエストされた構成済みターゲットの推移的クロージャ内の構成済みターゲットは、ボトムアップで分析する必要があります。つまり、最初にリーフノードを使用してから、コマンドライン上のリーフノードまで分析する必要があります。1 つの構成済みターゲットの分析では、次の入力を行います。
- 構成。(そのルールをビルドする「方法」。たとえば、ターゲット プラットフォームだけでなく、ユーザーが C++ コンパイラに渡すコマンドライン オプションなど)
- 直接的な依存関係。分析対象のルールでは、推移的情報プロバイダを使用できます。このように呼び出されたのは、クラスパス上のすべての .jar ファイルや、C++ バイナリにリンクする必要があるすべての .o ファイルなど、構成されたターゲットの推移的クロージャに情報の「ロールアップ」が提供されるためです。
- ターゲット自体。これは、ターゲットが含まれているパッケージを読み込んだ結果です。ルールの場合、これには属性が含まれます。通常は重要になります。
- 構成されたターゲットの実装。ルールの場合 Starlark または Java を使用できます。ルール以外の構成ターゲットはすべて Java で実装されます。
構成されたターゲットの分析の出力は次のようになります。
- それに依存するターゲットを構成した推移的情報プロバイダは、
- 作成可能なアーティファクトと、それを生成するアクション。
Java ルールに提供される API は RuleContext
です。これは、Starlark ルールの ctx
引数に相当します。API はより強力ですが、同時に、たとえば、時間または空間の複雑さが 2 次的(またはそれ以上に悪い)コードを作成する、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
は非推奨となっており(まだ削除できていません)、Starlark から TransitiveInfoProvider
サブクラスにアクセスすることはできません。
構成済みのターゲット
構成済みのターゲットは RuleConfiguredTargetFactory
として実装されます。Java で実装される各ルールクラスにサブクラスがあります。Starlark の構成ターゲットは、StarlarkRuleConfiguredTargetUtil.buildRule()
を使用して作成されます。
構成済みのターゲット ファクトリでは、RuleConfiguredTargetBuilder
を使用して戻り値を構築する必要があります。以下の要素で構成されます。
filesToBuild
: 「このルールが表すファイルのセット」という曖昧なコンセプト。これらは、構成されたターゲットがコマンドラインまたは genrule の srcs にある場合にビルドされるファイルです。- 実行ファイル、通常のファイル、およびデータです。
- 出力グループ。これらは、ルールで作成できるさまざまな「その他のファイルセット」です。Build ではファイル グループ ルールの output_group 属性を使用し、Java では
OutputGroupInfo
プロバイダを使用してアクセスできます。
実行ファイル
一部のバイナリは、実行するためにデータファイルを必要とします。典型的な例は、入力ファイルを必要とするテストです。Bazel では、「runfiles」のコンセプトで表されます。「runfiles ツリー」は、特定のバイナリのデータファイルのディレクトリ ツリーです。出力ツリーのソース内のファイルを指す個々のシンボリック リンクを持つシンボリック リンク ツリーとしてファイル システムに作成されます。
実行ファイルのセットは Runfiles
インスタンスとして表されます。概念的には、runfiles ツリー内のファイルのパスから、それを表す Artifact
インスタンスへのマップです。これは、次の 2 つの理由で 1 つの Map
よりも少し複雑です。
- ほとんどの場合、ファイルの runfiles パスは execpath と同じです。 これを使用して RAM を節約します。
- 実行ファイル ツリーにはさまざまな種類のエントリがあり、それらも表現する必要があります。
実行ファイルは RunfilesProvider
を使用して収集されます。このクラスのインスタンスは、構成されたターゲット(ライブラリなど)とその推移的クロージャが必要なランファイルを表し、ネストされたセットのように収集されます(実際は、ネストされたセットを使用して実装されます)。各ターゲットは依存関係のランファイルを結合し、独自のランファイルをいくつか追加し、その結果セットを依存関係グラフで上に送信します。RunfilesProvider
インスタンスには 2 つの Runfiles
インスタンスが含まれます。1 つは「data」属性を介してルールが依存している場合に、もう 1 つはその他の受信依存関係用です。これは、データ属性を介してターゲットに依存した場合に、ターゲットが異なるランファイルを提示する場合があるためです。これはまだ削除されていない、望ましくない従来の動作です。
バイナリの実行ファイルは RunfilesSupport
のインスタンスとして表されます。これは Runfiles
とは異なります。RunfilesSupport
には実際にビルドできる点です(単なるマッピングである Runfiles
とは異なります)。これには、次の追加コンポーネントが必要です。
- 入力 runfile マニフェスト。これは runfiles ツリーをシリアル化した説明です。runfiles ツリーの内容のプロキシとして使用されます。Bazel は、マニフェストの内容が変更された場合にのみ runfiles ツリーが変更されると想定します。
- 出力の runfile マニフェスト。これは、実行ファイル ツリーを処理するランタイム ライブラリで使用されます。特に Windows では、シンボリック リンクがサポートされていない場合があります。
- runfiles 中間者。実行ファイル ツリーを作成するには、シンボリック リンク ツリーと、シンボリック リンクが指すアーティファクトをビルドする必要があります。依存関係エッジの数を減らすには、runfile ミドルマンを使用して、これらすべてを表現します。
RunfilesSupport
オブジェクトが表すランファイルを含むバイナリを実行するためのコマンドライン引数。
アスペクト
アスペクトは、「計算を依存関係グラフに伝播」する方法です。Bazel のユーザーについては、こちらをご覧ください。良い例として、プロトコル バッファがあります。proto_library
ルールは特定の言語を認識しませんが、任意のプログラミング言語でプロトコル バッファ メッセージ(プロトコル バッファの「基本単位」)の実装を proto_library
ルールと結合する必要があります。これにより、同じ言語の 2 つのターゲットが同じプロトコル バッファに依存していても、1 回だけビルドされます。
構成済みターゲットと同様に、Skyframe では SkyValue
として表され、その作成方法は構成済みターゲットのビルドとよく似ています。RuleContext
にアクセスできる ConfiguredAspectFactory
というファクトリー クラスがありますが、構成済みのターゲット ファクトリとは異なり、接続されている構成済みターゲットとそのプロバイダも認識しています。
依存関係グラフに伝播されるアスペクトのセットは、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 つの部分に分かれています。
- ツールチェーンがサポートする実行制約とターゲット制約のセットを記述し、ツールチェーンの種類(C++ や Java など)を伝える
toolchain()
ルール(後者は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()
を使用して読み込まれ、要求した構成済みのターゲットの実装で使用されます。
また、1 つの「ホスト」構成と、さまざまな構成フラグ(--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 のような)多数の開発者とともに大規模なコードベースを扱っている場合は、他の開発者が自分のコードに勝手に依存することのないように注意する必要があります。そうしないと、Hyrum の法則に従い、実装の詳細とみなされた行動にユーザーが依存するようになります。
Bazel は、「可視性」というメカニズムでこれをサポートしています。可視性属性を使用して、特定のターゲットにのみ依存できることを宣言できます。この属性は少し特殊です。ラベルのリストを保持しますが、これらのラベルは特定のターゲットへのポインタではなく、パッケージ名のパターンをエンコードできるためです。(はい、これは設計上の欠陥です)。
これは次の場所に実装されています。
RuleVisibility
インターフェースは可視性の宣言を表します。定数(完全に公開または完全に非公開)またはラベルのリストを指定できます。- ラベルは、パッケージ グループ(事前定義されたパッケージのリスト)、パッケージを直接(
//pkg:__pkg__
)、またはパッケージのサブツリー(//pkg:__subpackages__
)のいずれかを参照できます。これは、//pkg:*
または//pkg/...
を使用するコマンドライン構文とは異なります。 - パッケージ グループは、独自のターゲット(
PackageGroup
)と構成済みのターゲット(PackageGroupConfiguredTarget
)として実装されます。必要に応じて、単純なルールに置き換えることもできます。そのロジックは、//pkg/...
などの単一パターンに対応するPackageSpecification
、単一のpackage_group
のpackages
属性に対応するPackageGroupContents
、package_group
とその推移的なincludes
を集計するPackageSpecificationProvider
を利用して実装されます。 - 公開設定ラベルリストから依存関係への変換は、
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 ですが、毎回同じである必要があります。
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()
をご覧ください)。 - 実行ファイル中間者は、runfiles ツリーの存在を確認するために使用されます。これにより、出力マニフェストと、runfiles ツリーによって参照されるすべてのアーティファクトに個別に依存する必要がなくなります。
アクションは、実行する必要があるコマンド、必要な環境、生成される出力のセットとして最もよく理解されています。アクションの説明を構成する主な要素は次のとおりです。
- 実行する必要があるコマンドライン
- 必要な入力アーティファクト
- 設定する必要がある環境変数
- 実行する必要がある環境(プラットフォームなど)を記述するアノテーション
他にも、コンテンツが Bazel に認識されているファイルを書き込むなど、いくつかの特殊なケースがあります。AbstractAction
のサブクラスです。Java と C++ には独自のアクション型(JavaCompileAction
、CppCompileAction
、CppLinkAction
)がありますが、ほとんどのアクションは SpawnAction
または StarlarkAction
です(同じですが、間違いなく別々のクラスではありません)。
最終的には、すべてを SpawnAction
に移動しようとします。JavaCompileAction
はかなり近いことですが、C++ は .d ファイルの解析とインクルード スキャンのため少し特殊なケースです。
アクション グラフのほとんどは SkyFrame グラフに「埋め込まれています」。概念的には、アクションの実行は ActionExecutionFunction
の呼び出しとして表されます。アクション グラフの依存関係エッジから Skyframe 依存関係エッジへのマッピングについては、ActionExecutionFunction.getInputDeps()
と Artifact.key()
で説明されています。Skyframe エッジの数を少なくするための最適化がいくつかあります。
- 派生アーティファクトには、独自の
SkyValue
はありません。代わりに、Artifact.getGeneratingActionKey()
を使用して、それを生成するアクションのキーが検索されます。 - ネストされたセットには独自のスカイフレーム キーがあります。
共有操作
一部のアクションは、複数の構成されたターゲットによって生成されます。Starlark のルールでは、派生アクションをその構成とそのパッケージによって決定されるディレクトリに置くことが許可されるため、制限が厳しくなります(ただし、同じパッケージ内のルールは競合する可能性があります)。一方、Java で実装されたルールでは、派生アーティファクトがどこにでも配置される可能性があります。
これは誤った機能とみなされますが、これを取り除くことは非常に困難です。たとえば、ソースファイルをなんらかの方法で処理する必要があり、そのファイルを複数のルール(handwave-handwave)で参照する必要がある場合に、実行時間を大幅に短縮できるからです。これには RAM がいくらかかかります。共有アクションの各インスタンスは、個別にメモリに保存する必要があります。
2 つのアクションが同じ出力ファイルを生成する場合は、完全に同じである必要があります。つまり、同じ入力、同じ出力を持ち、同じコマンドラインを実行します。この等価関係は Actions.canBeShared()
に実装され、すべてのアクションを確認して分析フェーズと実行フェーズの間で検証されます。これは SkyframeActionExecutor.findAndStoreArtifactConflicts()
に実装されており、ビルドの「グローバル」ビューを必要とする Bazel 内の場所の 1 つです。
実行フェーズ
このタイミングで、Bazel が実際にビルド アクション(出力を生成するコマンドなど)の実行を開始します。
分析フェーズの後、Bazel が最初に行うのは、ビルドする必要があるアーティファクトを特定することです。このロジックは TopLevelArtifactHelper
でエンコードされます。大まかに言えば、コマンドラインで構成されたターゲットの filesToBuild
と、「このターゲットがコマンドライン上にある場合は、これらのアーティファクトをビルドする」ことを明示的に表現する特別な出力グループの内容です。
次のステップでは、実行ルートを作成します。Bazel にはファイル システムのさまざまな場所(--package_path
)からソース パッケージを読み取るオプションがあるため、完全なソースツリーを使用してローカルで実行されるアクションを提供する必要があります。これはクラス SymlinkForest
によって処理され、分析フェーズで使用されたすべてのターゲットをメモし、すべてのパッケージを実際の場所から使用されたターゲットとシンボリック リンクする単一のディレクトリ ツリーを構築します。または、コマンドに正しいパスを渡すこともできます(--package_path
を考慮します)。次の理由により、これは適切ではありません。
- パッケージがパッケージパス エントリから別のもの(以前はよく出現していた)に移動すると、アクション コマンドラインが変更されます。
- アクションをリモートで実行する場合とローカルで実行する場合とでは、コマンドラインが異なります。
- 使用するツールに固有のコマンドライン変換が必要である(Java クラスパスと C++ インクルード パスなどの違いを考慮)
- アクションのコマンドラインを変更すると、そのアクションのキャッシュ エントリが無効になる
--package_path
は段階的に非推奨に
次に、Bazel はアクション グラフ(アクションとその入力アーティファクトと出力アーティファクトで構成された二部有向グラフ)の走査と実行を開始します。各アクションの実行は、SkyValue
クラス ActionExecutionValue
のインスタンスによって表されます。
アクションの実行にはコストがかかるため、Skyframe の背後でヒットできるキャッシュ レイヤがいくつかあります。
ActionExecutionFunction.stateMap
には、ActionExecutionFunction
の Skyframe の再起動を安くするためのデータが含まれています。- ローカル アクション キャッシュには、ファイル システムの状態に関するデータが含まれています。
- リモート実行システムには通常、独自のキャッシュも含まれている
ローカル アクション キャッシュ
このキャッシュは、Skyframe の背後にある別のレイヤです。アクションが Skyframe で再実行されても、ローカル アクション キャッシュでヒットする可能性があります。ローカル ファイル システムの状態を表し、ディスクにシリアル化されます。つまり、新しい Bazel サーバーを起動すると、Skyframe グラフが空であっても、ローカル アクション キャッシュ ヒットが発生する可能性があります。
このキャッシュでは、メソッド ActionCacheChecker.getTokenIfNeedToExecute()
を使用してヒットがないかチェックされます。
その名前とは異なり、派生アーティファクトのパスから、そのアーティファクトを発行したアクションまでのマップです。このアクションは次のように記述されます。
- 入出力ファイルとそれらのチェックサムのセット
- その「アクションキー」は、通常は実行されたコマンドラインですが、一般的には、入力ファイルのチェックサムによってキャプチャされなかったすべてのものを表します(たとえば、
FileWriteAction
の場合は書き込まれたデータのチェックサムです)。
また、まだ開発中の非常に実験的な「トップダウン アクション キャッシュ」もあります。これは、推移的ハッシュを使用して、キャッシュに何度もアクセスすることを回避します。
入力検出と入力プルーニング
一部のアクションは、単に一連の入力を取得するよりも複雑な場合があります。アクションの入力セットに対する変更は、次の 2 つの形式で行われます。
- アクションは、実行前に新しい入力を発見したり、入力の一部が実際には必要でないと判断したりすることがあります。標準的な例は C++ です。ここでは、C++ ファイルがどのヘッダー ファイルを使用するかを推移的クロージャから推測し、知識に基づいて推測することをおすすめします。これにより、すべてのファイルをリモート エグゼキュータに送信しないようにできます。そのため、すべてのヘッダー ファイルを「入力」として登録するのではなく、ソースファイルをスキャンして推移的インクルードされたヘッダーを 8 通りに(8.0.0.0.0 または 8.1.0.0.0.0 といった)ヘッダーで
#include
- アクションの実行中に一部のファイルが使用されていないことに気付く場合があります。これは、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++ コンパイル アクションは再実行されません。
これまでのところ、この問題の原理的な解決方法は見つかっていませんが、構成トリミングの問題と類似しています。オプションの詳細については、こちらをご覧ください。主な問題となるのは、Starlark ルール(作成者は通常 Bazel に精通していない)とアスペクトです。これらのルールにより、「同じ」出力ファイルを生成できるものが空間に広がります。
現在のアプローチでは、構成のパスセグメントは <CPU>-<compilation mode>
にさまざまな接尾辞が追加されており、Java で実装された構成の移行によってアクションの競合が発生しません。さらに、ユーザーがアクションの競合が発生しないように、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
を使用してテストを実行します。排他テストはそれぞれ、個別の Skyframe 呼び出しによって実行され、「メイン」ビルドの後にテストの実行をリクエストします。これは SkyframeExecutor.runExclusiveTest()
に実装されています。
通常のアクションでは、アクションが完了するとターミナル出力がダンプされますが、ユーザーはテストの出力をストリーミングするようリクエストして、長時間実行テストの進捗状況について通知を受け取ることができます。これは、--test_output=streamed
コマンドライン オプションで指定され、異なるテストの出力が混在しないように排他的なテスト実行を意味します。
これは、適切に名前が付けられた StreamedTestOutput
クラスに実装され、該当するテストの test.log
ファイルに対する変更をポーリングし、Bazel ルールがあるターミナルに新しいバイトをダンプすることで機能します。
実行されたテストの結果は、さまざまなイベント(TestAttempt
、TestResult
、TestingCompleteEvent
など)を監視することで、イベントバスで利用できます。結果は Build Event Protocol にダンプされ、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 つのコアコンセプトは、ベースライン カバレッジです。これは、ライブラリ、バイナリ、またはテストでコードが実行されなかった場合のカバレッジです。このソリューションが解決する問題は、バイナリのテスト カバレッジを計算したい場合に、どのテストにもリンクされていないコードがバイナリに含まれている可能性があるため、すべてのテストのカバレッジをマージするだけでは不十分であるということです。そのため、すべてのバイナリに対してカバレッジ ファイルが出力されます。このファイルには、カバレッジを収集しているファイルのみが含まれ、カバーされている行はありません。ターゲットのベースライン カバレッジ ファイルは bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
にあります。--nobuild_tests_only
フラグを Bazel に渡すと、テストに加えて、バイナリとライブラリに対しても生成されます。
ベースライン カバレッジは現在、破損しています。
ルールごとにカバレッジ収集のために 2 つのファイル グループ(インストルメント化されたファイルのセットとインストルメンテーション メタデータ ファイルのセット)を追跡します。
インストゥルメント化されたファイルのセットは、計測するファイルのセットだけです。オンライン カバレッジ ランタイムの場合は、ランタイムにこれを使用して、計測するファイルを決定できます。ベースライン カバレッジの実装にも使用されます。
インストルメンテーション メタデータ ファイルのセットは、Bazel が必要とする LCOV ファイルを生成するために必要な追加ファイルのセットです。実際には、これはランタイム固有のファイルで構成されます。たとえば、gcc はコンパイル中に .gcno ファイルを出力します。カバレッジ モードが有効になっている場合、これらはテスト アクションの入力のセットに追加されます。
カバレッジが収集されているかどうかは BuildConfiguration
に保存されます。これは、このビットに応じてテスト アクションとアクション グラフを変更する簡単な方法であるため便利ですが、このビットを反転すると、すべてのターゲットを再分析する必要があることも意味します(C++ などの一部の言語では、カバレッジを収集できるコードを出力するために別のコンパイラ オプションが必要です。そのため、いずれにしても再分析が必要になるため、この問題はある程度軽減されます)。
カバレッジ サポート ファイルは、暗黙的な依存関係でラベルを介して依存するため、呼び出しポリシーでオーバーライドできます。これにより、Bazel の異なるバージョン間でファイルを区別できるようになります。こうした違いを取り除き、そのうちの 1 つを標準化するのが理想的です。
また、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
(Bazel が以前 Blaze と呼ばれていたときの歴史の遺物)をサブクラス化し、コマンドの実行中にさまざまなイベントに関する情報を取得する必要があります。
主に、Bazel の一部のバージョン(Google で使用しているものなど)でのみ必要となる、コア以外のさまざまな機能を実装するために使用されます。
- リモート実行システムへのインターフェース
- 次のコマンドを新しく導入しました。
BlazeModule
が提供する拡張ポイントのセットは、やや漠然としています。これを優れた設計原則の例として使用しないでください。
イベントバス
BlazeModule が他の Bazel と通信する主な方法は、イベントバス(EventBus
)を使用することです。ビルドごとに新しいインスタンスが作成され、Bazel のさまざまな部分がイベントをポストできます。モジュールは、関心のあるイベントのリスナーを登録できます。たとえば、以下がイベントとして表されます。
- ビルドするビルド ターゲットのリストが決まっています(
TargetParsingCompleteEvent
) - 最上位の構成が決定されました(
BuildConfigurationEvent
) - ターゲットは正常にビルドされましたが、正常にビルドされませんでした(
TargetCompleteEvent
) - テストが実行されました(
TestAttempt
、TestSummary
)
これらのイベントの一部は、Build Event Protocol で Bazel の外部で表されます(BuildEvent
です)。これにより、BlazeModule
だけでなく、Bazel プロセス外からもビルドを監視できるようになります。これらは、プロトコル メッセージを含むファイルとしてアクセスできます。また、Bazel は(Build Event Service と呼ばれる)サーバーに接続して、イベントをストリーミングすることもできます。
これは、Java パッケージ build.lib.buildeventservice
と build.lib.buildeventstream
に実装されています。
外部リポジトリ
Bazel は元々 monorepo(ビルドに必要なすべてを含む単一のソースツリー)で使用するように設計されていましたが、Bazel は必ずしもそうではない世界に存在します。「外部リポジトリ」は、この 2 つの世界を橋渡しするために使用される抽象化です。外部リポジトリは、ビルドには必要ですが、メインのソースツリーにないコードを表します。
WORKSPACE ファイル
外部リポジトリのセットは、WORKSPACE ファイルを解析することによって決定されます。たとえば、次のような宣言を行います。
local_repository(name="foo", path="/foo/bar")
@foo
という名前のリポジトリが利用可能になります。これが複雑になるのは、Starlark ファイルで新しいリポジトリ ルールを定義できることです。これにより、新しい Starlark コードを読み込み、新しいリポジトリ ルールなどを定義できます。
このケースを処理するため、WORKSPACE ファイル(WorkspaceFileFunction
内)の解析は、load()
ステートメントで区切られたチャンクに分割されます。チャンク インデックスは WorkspaceFileKey.getIndex()
で示され、X 番目の load()
ステートメントまでインデックス X を評価するまで 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//
が一方のバイナリのリポジトリにある 1 つの Guava リポジトリ(@guava1//
など)と、もう一方の Guava リポジトリ(@guava2//
など)がもう一方のリポジトリを参照できるようになります。
または、ダイヤモンドのjoinにも使用できます。リポジトリが @guava1//
に依存し、もう 1 つのリポジトリが @guava2//
に依存している場合、リポジトリ マッピングにより、一方が正規の @guava//
リポジトリを使用するように両方のリポジトリを再マッピングできます。
このマッピングは、WORKSPACE ファイルで個々のリポジトリ定義の repo_mapping
属性として指定されます。その後、WorkspaceFileValue
のメンバーとして Skyframe に表示され、次のように実装されます。
Package.Builder.repositoryMapping
。RuleClass.populateRuleAttributeValues()
によってパッケージ内のルールのラベル値属性を変換するために使用されます。Package.repositoryMapping
: 分析フェーズで使用します(読み込みフェーズで解析されない$(location)
などの解決に使用します)。- load() ステートメントのラベルを解決するための
BzlLoadFunction
JNI ビット
Bazel のサーバーは、ほとんど Java で記述されています。例外は、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
ファイルの書き込みに使用できるスクラッチ ファイル システムがあります。その後、さまざまなヘルパー メソッドで構成されたターゲットのリクエスト、構成の変更、分析結果に関するさまざまなアサートを行うことができます。