Bazel コードベース

このドキュメントでは、コードベースと Bazel の構造について説明します。これは、Bazel に貢献したいユーザーを対象としており、エンドユーザーを対象としていません。

はじめに

Bazel のコードベースは大きく(本番環境コードが約 35 万行、テストコードが約 26 万行)、全体像を把握している人はいません。誰もが自分の谷をよく知っていますが、あらゆる方向の丘の向こうに何があるかを知っている人はほとんどいません。

このドキュメントは、旅の途中で迷子にならないように、コードベースの概要を説明し、作業を始めやすくすることを目的としています。

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 実行可能ファイル(「クライアント」)が制御を取得します。適切なサーバー プロセスは、次の手順で設定されます。

  1. すでに抽出されているかどうかを確認します。そうでない場合は、その処理を行います。ここで、サーバーの実装が提供されます。
  2. 動作するアクティブなサーバー インスタンスがあるかどうかを確認します。実行中であること、正しい起動オプションが設定されていること、正しいワークスペース ディレクトリを使用していることを確認します。実行中のサーバーは、サーバーがリッスンしているポートを含むロックファイルがあるディレクトリ $OUTPUT_BASE/server を調べることで見つけます。
  3. 必要に応じて、古いサーバー プロセスを強制終了します
  4. 必要に応じて、新しいサーバー プロセスを起動します。

適切なサーバー プロセスが準備されると、実行する必要があるコマンドが 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 起動オプションを使用して変更できます。このオプションは、特定のワークスペースで同時に実行できる Bazel インスタンスが 1 つだけという制限を回避する場合などに役立ちます。

出力ディレクトリには、次のようなものが含まれます。

  • 取得された外部リポジトリ($OUTPUT_BASE/external)。
  • 実行ルート。現在のビルドのすべてのソースコードへのシンボリック リンクを含むディレクトリ。$OUTPUT_BASE/execroot にあります。ビルド中、作業ディレクトリは $EXECROOT/<name of main repository> です。これを $EXECROOT に変更する予定ですが、互換性のない変更であるため、長期的な計画となります。
  • ビルド中にビルドされたファイル。

コマンドの実行プロセス

Bazel サーバーが制御を取得し、実行する必要があるコマンドが通知されると、次の順序でイベントが発生します。

  1. BlazeCommandDispatcher に新しいリクエストが通知されます。コマンドの実行にワークスペースが必要かどうか(バージョンやヘルプなど、ソースコードに関係のないコマンドを除くほぼすべてのコマンド)と、別のコマンドが実行されているかどうかを判断します。

  2. 正しいコマンドが見つかります。各コマンドは BlazeCommand インターフェースを実装し、@Command アノテーションを持つ必要があります(これはアンチパターンです。コマンドに必要なすべてのメタデータが BlazeCommand のメソッドで記述されていると望ましいです)。

  3. コマンドライン オプションが解析されます。各コマンドには異なるコマンドライン オプションがあり、@Command アノテーションで説明されています。

  4. イベントバスが作成されます。イベントバスは、ビルド中に発生するイベントのストリームです。これらのうちのいくつかは、ビルドの進行状況を外部に伝えるために、Build Event Protocol の下で Bazel の外部にエクスポートされます。

  5. コマンドが制御を取得します。最も興味深いコマンドは、ビルドを実行するコマンド(build、test、run、coverage など)です。この機能は BuildTool によって実装されます。

  6. コマンドラインのターゲット パターンのセットが解析され、//pkg:all//pkg/... などのワイルドカードが解決されます。これは AnalysisPhaseRunner.evaluateTargetPatterns() で実装され、Skyframe で TargetPatternPhaseValue として具体化されます。

  7. 読み込み/分析フェーズが実行され、アクション グラフ(ビルドで実行する必要があるコマンドの有向非巡回グラフ)が生成されます。

  8. 実行フェーズが実行されます。つまり、リクエストされた最上位のターゲットをビルドするために必要なすべてのアクションが実行されます。

コマンドライン オプション

Bazel 呼び出しのコマンドライン オプションは OptionsParsingResult オブジェクトで記述されます。このオブジェクトには、オプションの「オプション クラス」から値へのマップが含まれます。「オプション クラス」は OptionsBase のサブクラスで、互いに関連するコマンドライン オプションをグループ化します。次に例を示します。

  1. プログラミング言語(CppOptions または JavaOptions)に関連するオプション。これらは FragmentOptions のサブクラスである必要があり、最終的に BuildOptions オブジェクトにラップされます。
  2. Bazel がアクションを実行する方法に関連するオプション(ExecutionOptions

これらのオプションは、分析フェーズで使用されるように設計されています(Java の RuleContext.getFragment() または Starlark の ctx.fragments を使用)。一部(C++ インクルード スキャンを行うかどうかなど)は実行フェーズで読み取られますが、その時点では BuildConfiguration が利用できないため、常に明示的な配管が必要です。詳細については、「構成」セクションをご覧ください。

警告: OptionsBase インスタンスは不変であると想定して、そのように使用することをおすすめします(SkyKeys の一部としてなど)。実際にはそうではなく、変更すると、デバッグが難しい微妙な方法で Bazel が壊れる可能性があります。残念ながら、実際に変更不可にするには大きな労力が必要です。(構築直後に FragmentOptions を変更し、他のユーザーが参照を保持する前に、また equals() または hashCode() が呼び出される前に変更するのは問題ありません)。

Bazel は、次の方法でオプション クラスを認識します。

  1. 一部は Bazel にハードワイヤードされています(CommonCommandOptions)。
  2. 各 Bazel コマンドの @Command アノテーションから
  3. ConfiguredRuleClassProvider(個々のプログラミング言語に関連するコマンドライン オプション)
  4. Starlark ルールでは、独自のオプションを定義することもできます(こちらを参照)。

各オプション(Starlark で定義されたオプションを除く)は、@Option アノテーションを持つ FragmentOptions サブクラスのメンバー変数です。このアノテーションは、コマンドライン オプションの名前と型、およびヘルプテキストを指定します。

コマンドライン オプションの値の Java 型は、通常は単純なもの(文字列、整数、ブール値、ラベルなど)です。ただし、より複雑な型のオプションもサポートしています。この場合、コマンドライン文字列からデータ型への変換は com.google.devtools.common.options.Converter の実装に委ねられます。

Bazel から見たソースツリー

Bazel はソフトウェアの構築を目的としており、ソースコードを読み取って解釈することでソフトウェアを構築します。Bazel が動作するソースコードの全体は「ワークスペース」と呼ばれ、リポジトリ、パッケージ、ルールに構造化されています。

リポジトリ

「リポジトリ」は、デベロッパーが作業するソースツリーです。通常は単一のプロジェクトを表します。Bazel の祖先である Blaze は、モノレポ(ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリー)で動作していました。一方、Bazel は、ソースコードが複数のリポジトリにまたがるプロジェクトをサポートしています。Bazel が呼び出されるリポジトリは「メイン リポジトリ」と呼ばれ、それ以外は「外部リポジトリ」と呼ばれます。

リポジトリは、ルート ディレクトリにある WORKSPACE(または WORKSPACE.bazel)というファイルでマークされます。このファイルには、ビルド全体に「グローバル」な情報(利用可能な外部リポジトリのセットなど)が含まれています。通常の Starlark ファイルと同様に機能するため、他の Starlark ファイルを load() できます。これは通常、明示的に参照されているリポジトリで必要なリポジトリをプルするために使用されます(これを「deps.bzl パターン」と呼びます)。

外部リポジトリのコードは、$OUTPUT_BASE/external の下にシンボリック リンクされるか、ダウンロードされます。

ビルドを実行するときに、ソースツリー全体を組み立てる必要があります。これは SymlinkForest によって行われます。SymlinkForest は、メイン リポジトリ内のすべてのパッケージを $EXECROOT に、すべての外部リポジトリを $EXECROOT/external または $EXECROOT/.. にシンボリック リンクします(もちろん、前者の場合、メイン リポジトリに external という名前のパッケージを含めることはできません。そのため、移行を進めています)。

パッケージ

すべてのリポジトリは、パッケージ、関連ファイルのコレクション、依存関係の仕様で構成されています。これらは、BUILD または BUILD.bazel というファイルで指定されます。両方が存在する場合、Bazel は BUILD.bazel を優先します。BUILD ファイルが引き続き受け入れられるのは、Bazel の祖先である Blaze がこのファイル名を使用していたためです。しかし、これは特に Windows でよく使用されるパス セグメントであることが判明しました。Windows ではファイル名の大文字と小文字は区別されません。

パッケージは互いに独立しています。パッケージの BUILD ファイルを変更しても、他のパッケージが変更されることはありません。BUILD ファイルの追加または削除は、他のパッケージを変更する可能性があります。これは、再帰的グロブがパッケージ境界で停止するため、BUILD ファイルの存在が再帰を停止するためです。

BUILD ファイルの評価は「パッケージの読み込み」と呼ばれます。これは PackageFactory クラスで実装され、Starlark インタープリタを呼び出すことで機能します。また、利用可能なルールクラスのセットに関する知識が必要です。パッケージの読み込みの結果は Package オブジェクトです。これは主に、文字列(ターゲットの名前)からターゲット自体へのマップです。

パッケージの読み込み時の複雑さの大部分はグロビングです。Bazel では、すべてのソースファイルを明示的にリストする必要はなく、代わりにグロブ(glob(["**/*.java"]) など)を実行できます。シェルとは異なり、サブディレクトリ(サブパッケージは除く)に移動する再帰的グロブをサポートしています。これにはファイル システムへのアクセスが必要ですが、アクセスが遅くなる可能性があるため、並列で可能な限り効率的に実行するためのさまざまな工夫を実装しています。

グロビングは次のクラスで実装されています。

  • LegacyGlobber、高速で Skyframe を意識しないグロバー
  • SkyframeHybridGlobber: Skyframe を使用し、「Skyframe の再起動」(後述)を回避するために以前の globber に戻るバージョン

Package クラス自体には、WORKSPACE ファイルの解析にのみ使用され、実際のパッケージでは意味のないメンバーが含まれています。これは設計上の欠陥です。通常のパッケージを記述するオブジェクトに、別のものを記述するフィールドを含めるべきではありません。例は以下のとおりです。

  • リポジトリ マッピング
  • 登録されたツールチェーン
  • 登録済みの実行プラットフォーム

理想的には、WORKSPACE ファイルの解析と通常のパッケージの解析を分離して、Package が両方のニーズに対応する必要がないようにします。残念ながら、この 2 つは深く絡み合っているため、分離は困難です。

ラベル、ターゲット、ルール

パッケージはターゲットで構成されます。ターゲットには次のタイプがあります。

  1. ファイル: ビルドの入力または出力のいずれかであるもの。Bazel の用語では、これらをアーティファクトと呼びます(別の場所で説明します)。ビルド中に作成されたすべてのファイルがターゲットになるわけではありません。Bazel の出力に関連付けられたラベルがないことはよくあります。
  2. ルール: 入力から出力を導出する手順を記述します。これらは通常、プログラミング言語(cc_libraryjava_librarypy_library など)に関連付けられますが、言語に依存しないもの(genrulefilegroup など)もあります。
  3. パッケージ グループ: 可視性のセクションで説明します。

ターゲットの名前はラベルと呼ばれます。ラベルの構文は @repo//pac/kage:name です。ここで、repo はラベルが存在するリポジトリの名前、pac/kageBUILD ファイルが存在するディレクトリ、name はパッケージのディレクトリを基準としたファイルのパス(ラベルがソースファイルを指す場合)です。コマンドラインでターゲットを参照する場合、ラベルの一部を省略できます。

  1. リポジトリが省略されている場合、ラベルはメイン リポジトリにあると見なされます。
  2. パッケージ部分が省略されている場合(name:name など)、ラベルは現在の作業ディレクトリのパッケージにあると見なされます(上位レベルの参照(..)を含む相対パスは許可されません)。

ルールの種類(「C++ ライブラリ」など)は「ルールクラス」と呼ばれます。ルールクラスは、Starlark(rule() 関数)または Java(「ネイティブ ルール」、型 RuleClass)で実装できます。長期的には、言語固有のルールはすべて Starlark で実装されますが、一部のレガシー ルール ファミリー(Java や C++ など)は、当面の間 Java で実装されます。

Starlark ルールクラスは load() ステートメントを使用して BUILD ファイルの先頭でインポートする必要がありますが、Java ルールクラスは ConfiguredRuleClassProvider に登録されているため、Bazel に「本質的に」認識されます。

ルールクラスには次のような情報が含まれます。

  1. 属性(srcsdeps など): 型、デフォルト値、制約など。
  2. 各属性に関連付けられた構成の移行とアスペクト(ある場合)
  3. ルールの実装
  4. 推移的情報プロバイダは、ルールが「通常」作成するものです

用語に関する注: コードベースでは、ルールクラスによって作成されたターゲットを意味する「ルール」という用語がよく使用されます。ただし、Starlark とユーザー向けドキュメントでは、「ルール」はルールクラス自体を指す場合にのみ使用する必要があります。ターゲットは単に「ターゲット」です。また、RuleClass の名前に「class」が含まれていますが、ルールクラスとそのタイプのターゲットの間に Java の継承関係はありません。

Skyframe

Bazel の基盤となる評価フレームワークは Skyframe と呼ばれます。このモデルでは、ビルド中にビルドする必要があるすべてのものが、任意のデータからその依存関係(つまり、ビルドするために知っておく必要がある他のデータ)を指すエッジを持つ有向非巡回グラフに整理されます。

グラフ内のノードは SkyValue と呼ばれ、その名前は SkyKey と呼ばれます。どちらも深く不変であり、不変オブジェクトのみがそれらから到達可能である必要があります。この不変条件はほぼ常に保持されます。保持されない場合(BuildConfigurationValue のメンバーである個々のオプション クラス BuildOptions とその SkyKey など)は、変更しないか、外部から観察できない方法でのみ変更するように努めます。このことから、Skyframe 内で計算されるもの(構成されたターゲットなど)もすべて不変である必要があります。

Skyframe グラフを観察する最も便利な方法は、bazel dump --skyframe=detailed を実行することです。これにより、グラフがダンプされ、1 行に 1 つの SkyValue が出力されます。かなり大きくなる可能性があるため、小さなビルドで行うことをおすすめします。

Skyframe は com.google.devtools.build.skyframe パッケージにあります。同様の名前のパッケージ com.google.devtools.build.lib.skyframe には、Skyframe 上の Bazel の実装が含まれています。Skyframe の詳細については、こちらをご覧ください。

指定された SkyKeySkyValue に評価するために、Skyframe はキーのタイプに対応する SkyFunction を呼び出します。関数の評価中に、SkyFunction.Environment.getValue() のさまざまなオーバーロードを呼び出すことで、Skyframe から他の依存関係をリクエストできます。これにより、これらの依存関係が Skyframe の内部グラフに登録されるという副作用が生じます。これにより、依存関係のいずれかが変更されたときに Skyframe が関数を再評価することがわかります。つまり、Skyframe のキャッシュ保存と増分計算は、SkyFunctionSkyValue の粒度で動作します。

SkyFunction が利用できない依存関係をリクエストすると、getValue() は null を返します。関数は、null を返すことで制御を Skyframe に戻す必要があります。後で、Skyframe は使用できない依存関係を評価し、関数を最初から再起動します。今回は getValue() 呼び出しが成功し、null 以外の結果が返されます。

このため、再起動前に SkyFunction 内で実行された計算はすべて繰り返す必要があります。ただし、キャッシュに保存されている依存関係 SkyValues の評価のために行われた作業は含まれません。そのため、この問題は通常、次の方法で回避します。

  1. 再起動の回数を制限するために、依存関係を一括で宣言する(getValuesAndExceptions() を使用)。
  2. SkyValue を異なる SkyFunction によって計算される個別の部分に分割し、それらを個別に計算してキャッシュに保存できるようにします。メモリ使用量が増加する可能性があるため、戦略的に行う必要があります。
  3. SkyFunction.Environment.getState() を使用するか、アドホックな静的キャッシュを「Skyframe の背後」に保持して、再起動間で状態を保存します。

基本的に、このような回避策が必要なのは、何十万もの Skyframe ノードが常に実行されており、Java が軽量スレッドをサポートしていないためです。

Starlark

Starlark は、Bazel の構成と拡張に使用されるドメイン固有の言語です。これは、型がはるかに少なく、制御フローに多くの制限があり、最も重要なのは、同時読み取りを可能にする強力な不変性保証がある、Python の制限付きサブセットとして考案されています。チューリング完全ではないため、一部の(すべてではない)ユーザーは、この言語内で一般的なプログラミング タスクを実行しようとしません。

Starlark は net.starlark.java パッケージで実装されています。独立した Go 実装はこちらにあります。Bazel で使用される Java 実装は、現在インタープリタです。

Starlark は、次のような複数のコンテキストで使用されます。

  1. BUILD 言語。ここで新しいルールを定義します。このコンテキストで実行される Starlark コードは、BUILD ファイル自体の内容と、それによって読み込まれた .bzl ファイルの内容にのみアクセスできます。
  2. ルールの定義。新しいルール(新しい言語のサポートなど)は、このように定義されます。このコンテキストで実行される Starlark コードは、直接の依存関係によって提供される構成とデータにアクセスできます(詳細については後述します)。
  3. WORKSPACE ファイル。ここでは、外部リポジトリ(メインのソースツリーにないコード)が定義されます。
  4. リポジトリ ルールの定義。ここで、新しい外部リポジトリ タイプを定義します。このコンテキストで実行される Starlark コードは、Bazel が実行されているマシンで任意のコードを実行し、ワークスペース外にアクセスできます。

BUILD ファイルと .bzl ファイルで使用できる言語は、表現する内容が異なるため、若干異なります。相違点の一覧については、こちらをご覧ください。

Starlark について詳しくは、こちらをご覧ください。

読み込み/分析フェーズ

読み込み/分析フェーズでは、Bazel が特定のルールをビルドするために必要なアクションを決定します。その基本単位は「構成済みターゲット」です。これは、当然ながら(ターゲット、構成)のペアです。

このフェーズは「読み込み/分析フェーズ」と呼ばれます。これは、2 つの異なる部分に分割できるためです。以前はシリアル化されていましたが、現在は時間的に重複させることができます。

  1. パッケージの読み込み(BUILD ファイルを、それらを表す Package オブジェクトに変換すること)
  2. 構成されたターゲットの分析(ルールの実装を実行してアクション グラフを生成する)

コマンドラインでリクエストされた構成済みターゲットの推移的閉包内の各構成済みターゲットは、ボトムアップで分析する必要があります。つまり、リーフノードから始まり、コマンドラインのターゲットまで分析します。単一の構成済みターゲットの分析への入力は次のとおりです。

  1. 構成。(ルールの作成方法。たとえば、ターゲット プラットフォームだけでなく、ユーザーが C++ コンパイラに渡したいコマンドライン オプションなど)
  2. 直接依存関係。推移的情報プロバイダは、分析対象のルールで使用できます。これらは、構成されたターゲットの推移閉包内の情報(クラスパス上のすべての .jar ファイルや、C++ バイナリにリンクする必要があるすべての .o ファイルなど)の「ロールアップ」を提供するので、このように呼ばれます。
  3. ターゲット自体。これは、ターゲットが存在するパッケージを読み込んだ結果です。ルールの場合、通常は属性が重要になります。
  4. 構成されたターゲットの実装。ルールの場合、これは Starlark または Java のいずれかになります。ルール以外の構成済みターゲットはすべて Java で実装されています。

構成されたターゲットの分析の出力は次のとおりです。

  1. 推移的な情報プロバイダは、それに依存するターゲットを構成した
  2. 作成できるアーティファクトと、それらを生成するアクション。

Java ルールに提供される API は RuleContext です。これは、Starlark ルールの ctx 引数に相当します。API はより強力ですが、同時に Bad Things™ を行うのも簡単です。たとえば、時間または空間の複雑さが 2 次(またはそれ以上)のコードを記述したり、Java 例外で Bazel サーバーをクラッシュさせたり、不変条件に違反したり(誤って Options インスタンスを変更したり、構成されたターゲットを可変にしたりするなど)する可能性があります。

構成されたターゲットの直接依存関係を決定するアルゴリズムは DependencyResolver.dependentNodeMap() にあります。

構成

構成は、ターゲットのビルド方法(どのプラットフォーム向けか、どのコマンドライン オプションを使用するかなど)です。

同じターゲットを同じビルド内の複数の構成用にビルドできます。これは、たとえば、ビルド中に実行されるツールとターゲット コードに同じコードが使用されていて、クロス コンパイルしている場合や、ファット Android アプリ(複数の CPU アーキテクチャのネイティブ コードを含むアプリ)をビルドしている場合に便利です。

概念的には、構成は BuildOptions インスタンスです。ただし、実際には BuildOptionsBuildConfiguration でラップされ、さまざまな追加機能が提供されます。依存関係グラフの上部から下部に伝播します。変更された場合は、ビルドを再分析する必要があります。

たとえば、リクエストされたテスト実行の数が変更された場合、テスト ターゲットにのみ影響するにもかかわらず、ビルド全体を再分析する必要があるなどの異常が発生します(このような事態を避けるために、構成を「トリミング」する計画がありますが、まだ準備が整っていません)。

ルール実装で構成の一部が必要な場合は、RuleClass.Builder.requiresConfigurationFragments() を使用して定義で宣言する必要があります。これは、ミス(Java フラグメントを使用する Python ルールなど)を回避するためと、Python オプションが変更された場合に C++ ターゲットを再分析する必要がないように構成のトリミングを容易にするためです。

ルールの構成は、その「親」ルールの構成と同じであるとは限りません。依存関係エッジの構成を変更するプロセスは、「構成の移行」と呼ばれます。このエラーは次の 2 か所で発生する可能性があります。

  1. 依存関係エッジ。これらの遷移は Attribute.Builder.cfg() で指定され、Rule(遷移が発生する場所)と BuildOptions(元の構成)から 1 つ以上の BuildOptions(出力構成)への関数です。
  2. 構成されたターゲットへの受信エッジ。これらは RuleClass.Builder.cfg() で指定されます。

関連するクラスは TransitionFactoryConfigurationTransition です。

構成の移行は次のように使用されます。

  1. 特定の依存関係がビルド中に使用され、実行アーキテクチャでビルドされることを宣言するには
  2. 特定の依存関係を複数のアーキテクチャ用にビルドする必要があることを宣言する(ファット Android APK のネイティブ コードなど)

構成の移行によって複数の構成が生じる場合、それは分割移行と呼ばれます。

構成の移行は Starlark でも実装できます(ドキュメントはこちら)。

推移的な情報プロバイダ

推移的情報プロバイダは、構成されたターゲットが、それに依存する他の構成済みターゲットについて伝えるための方法(唯一の方法)です。名前に「推移的」が含まれているのは、通常、構成されたターゲットの推移的閉包のロールアップであるためです。

通常、Java の推移的情報プロバイダと Starlark の推移的情報プロバイダは 1 対 1 で対応しています(例外は DefaultInfo です。この API は Java の直接的な音訳よりも Starlark らしいと判断されたため、FileProviderFilesToRunProviderRunfilesProvider の統合になっています)。キーは次のいずれかです。

  1. Java の Class オブジェクト。これは、Starlark からアクセスできないプロバイダでのみ使用できます。これらのプロバイダは TransitiveInfoProvider のサブクラスです。
  2. 文字列。これはレガシーであり、名前の衝突が発生しやすいため、強く推奨されません。このような推移的情報プロバイダは build.lib.packages.Info の直接サブクラスです。
  3. プロバイダのシンボル。これは provider() 関数を使用して Starlark から作成できます。新しいプロバイダを作成する際は、この方法が推奨されます。シンボルは Java の Provider.Key インスタンスで表されます。

Java で実装された新しいプロバイダは、BuiltinProvider を使用して実装する必要があります。NativeProvider は非推奨です(まだ削除する時間がありません)。また、TransitiveInfoProvider サブクラスは Starlark からアクセスできません。

構成済みのターゲット

構成されたターゲットは RuleConfiguredTargetFactory として実装されます。Java で実装されたルールクラスごとにサブクラスがあります。Starlark で構成されたターゲットは StarlarkRuleConfiguredTargetUtil.buildRule() を介して作成されます。

構成されたターゲット ファクトリは、RuleConfiguredTargetBuilder を使用して戻り値を構築する必要があります。これは次の要素で構成されています。

  1. filesToBuild。このルールが表すファイルのセットという曖昧なコンセプトです。これらは、構成されたターゲットがコマンドラインまたは genrule の srcs にある場合にビルドされるファイルです。
  2. 実行ファイル(通常とデータ)。
  3. 出力グループ。これらは、ルールがビルドできるさまざまな「他のファイルセット」です。これらは、BUILD の filegroup ルールの output_group 属性と、Java の OutputGroupInfo プロバイダを使用してアクセスできます。

Runfiles

一部のバイナリは実行にデータファイルを必要とします。入力ファイルが必要なテストがその代表的な例です。これは、Bazel では「runfiles」というコンセプトで表されます。「runfiles tree」は、特定のバイナリのデータファイルのディレクトリ ツリーです。ファイル システムに、出力ツリーのソースにあるファイルを指す個々のシンボリック リンクを含むシンボリック リンク ツリーとして作成されます。

ランファイル セットは Runfiles インスタンスとして表されます。概念的には、runfiles ツリー内のファイルのパスから、それを表す Artifact インスタンスへのマップです。単一の Map よりも少し複雑になる理由は 2 つあります。

  • ほとんどの場合、ファイルの runfiles パスは execpath と同じです。これは RAM を節約するために使用されます。
  • ランファイル ツリーにはさまざまな種類のレガシー エントリがあり、これらも表現する必要があります。

ランファイルは RunfilesProvider を使用して収集されます。このクラスのインスタンスは、構成されたターゲット(ライブラリなど)とその推移的閉包に必要なランファイルを表します。これらはネストされたセットのように収集されます(実際には、ネストされたセットを使用して実装されます)。各ターゲットは、依存関係のランファイルを統合し、独自のランファイルをいくつか追加して、結果のセットを依存関係グラフの上方向に送信します。RunfilesProvider インスタンスには 2 つの Runfiles インスタンスが含まれます。1 つは「data」属性を介してルールが依存している場合、もう 1 つはその他の種類の依存関係がある場合です。これは、ターゲットがデータ属性を介して依存している場合とそうでない場合で、異なるランファイルを表示することがあるためです。これは、まだ削除できていない望ましくない従来の動作です。

バイナリのランファイルは RunfilesSupport のインスタンスとして表されます。RunfilesSupport は実際にビルドできる機能があるため(マッピングのみの Runfiles とは異なります)、Runfiles とは異なります。このため、次の追加コンポーネントが必要になります。

  • 入力 runfiles マニフェスト。これは、runfiles ツリーのシリアル化された説明です。これは runfiles ツリーの内容のプロキシとして使用され、マニフェストの内容が変更された場合にのみ runfiles ツリーが変更されると Bazel は想定しています。
  • 出力ランファイル マニフェスト。これは、ランファイル ツリーを処理するランタイム ライブラリ(特に Windows)で使用されます。Windows では、シンボリック リンクがサポートされていない場合があります。
  • ランファイル ミドルマン。ランファイル ツリーが存在するためには、シンボリック リンク ツリーとシンボリック リンクが指すアーティファクトをビルドする必要があります。依存関係エッジの数を減らすために、ランファイル ミドルマンを使用してこれらすべてを表すことができます。
  • RunfilesSupport オブジェクトが表すランファイルを持つバイナリを実行するためのコマンドライン引数

アスペクト

アスペクトは、「依存関係グラフに沿って計算を伝播する」方法です。Bazel ユーザー向けの説明はこちらをご覧ください。プロトコル バッファは、良い動機付けの例です。proto_library ルールは特定の言語について知る必要はありませんが、任意のプログラミング言語でプロトコル バッファ メッセージ(プロトコル バッファの「基本単位」)の実装をビルドすることは、proto_library ルールに結合する必要があります。これにより、同じ言語の 2 つのターゲットが同じプロトコル バッファに依存している場合、そのプロトコル バッファは 1 回だけビルドされます。

構成済みターゲットと同様に、Skyframe では SkyValue として表されます。構成済みターゲットの構築方法と非常によく似ています。RuleContext にアクセスできる ConfiguredAspectFactory というファクトリ クラスがありますが、構成済みターゲット ファクトリとは異なり、アタッチされている構成済みターゲットとそのプロバイダについても認識しています。

依存関係グラフの下に伝播されるアスペクトのセットは、Attribute.Builder.aspects() 関数を使用して各属性に指定されます。プロセスに関与するクラスの中には、名前が紛らわしいものがいくつかあります。

  1. AspectClass はアスペクトの実装です。Java(この場合はサブクラス)または Starlark(この場合は StarlarkAspectClass のインスタンス)のいずれかで指定できます。RuleConfiguredTargetFactory に類似しています。
  2. AspectDefinition はアスペクトの定義です。必要なプロバイダ、提供するプロバイダが含まれ、適切な AspectClass インスタンスなどの実装への参照が含まれます。RuleClass に類似しています。
  3. AspectParameters は、依存関係グラフの下に伝播されるアスペクトをパラメータ化する方法です。現在は文字列間のマップです。プロトコル バッファは、その有用性を示す良い例です。言語に複数の API がある場合、プロトコル バッファをビルドする API に関する情報を依存関係グラフに伝播する必要があります。
  4. Aspect は、依存関係グラフを伝播するアスペクトの計算に必要なすべてのデータを表します。アスペクト クラス、その定義、パラメータで構成されます。
  5. 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 つの部分に分割されます。

  1. ツールチェーンがサポートする実行制約とターゲット制約のセットを記述し、ツールチェーンの種類(C++ や Java など)を示す toolchain() ルール(後者は toolchain_type() ルールで表されます)
  2. 実際のツールチェーン(cc_toolchain() など)を記述する言語固有のルール

このようにするのは、ツールチェーンの解決を行うためにすべてのツールチェーンの制約を知る必要があるためです。言語固有の *_toolchain() ルールにはそれよりもはるかに多くの情報が含まれているため、読み込みに時間がかかります。

実行プラットフォームは、次のいずれかの方法で指定します。

  1. WORKSPACE ファイルで register_execution_platforms() 関数を使用する
  2. コマンドラインで --extra_execution_platforms コマンドライン オプションを使用する

利用可能な実行プラットフォームのセットは、RegisteredExecutionPlatformsFunction で計算されます。

構成されたターゲットのターゲット プラットフォームは PlatformOptions.computeTargetPlatform() によって決定されます。最終的には複数のターゲット プラットフォームをサポートしたいと考えていますが、まだ実装されていません。

構成されたターゲットに使用されるツールチェーンのセットは、ToolchainResolutionFunction によって決定されます。これは、次の関数です。

  • 登録されたツールチェーンのセット(WORKSPACE ファイルと構成内)
  • 目的の実行プラットフォームとターゲット プラットフォーム(構成内)
  • 構成されたターゲット(UnloadedToolchainContextKey))に必要なツールチェーン タイプのセット
  • 構成されたターゲット(exec_compatible_with 属性)と構成(--experimental_add_exec_constraints_to_targets)の実行プラットフォーム制約のセット(UnloadedToolchainContextKey

結果は UnloadedToolchainContext です。これは、ツールチェーン タイプ(ToolchainTypeInfo インスタンスとして表される)から選択したツールチェーンのラベルへのマッピングです。ツールチェーン自体ではなくラベルのみが含まれているため、「unloaded」と呼ばれます。

その後、ツールチェーンは ResolvedToolchainContext.load() を使用して実際に読み込まれ、それらをリクエストした構成済みターゲットの実装で使用されます。

また、単一の「ホスト」構成と、--cpu などのさまざまな構成フラグで表されるターゲット構成に依存するレガシー システムもあります。上記のシステムへの移行は段階的に進めています。以前の構成値に依存しているケースに対応するため、以前のフラグと新しいスタイルのプラットフォーム制約の間で変換を行うプラットフォーム マッピングを実装しました。コードは PlatformMappingFunction にあり、Starlark 以外の「リトル言語」を使用しています。

制約

ターゲットを少数のプラットフォームのみに対応するように指定したい場合があります。Bazel には、この目的を達成するためのメカニズムが複数あります。

  • ルール固有の制約
  • environment_group() / environment()
  • プラットフォームの制約

ルール固有の制約は、主に Google 内で Java ルールに使用されます。この制約は廃止されつつあり、Bazel では使用できませんが、ソースコードに参照が含まれている場合があります。これを制御する属性は constraints= と呼ばれます。

environment_group() と environment()

これらのルールは以前のメカニズムであり、広く使用されていません。

すべてのビルドルールは、ビルド可能な「環境」を宣言できます。ここで、「環境」は environment() ルールのインスタンスです。

ルールでサポートされている環境を指定する方法はいくつかあります。

  1. restricted_to= 属性を使用します。これは最も直接的な仕様形式です。このグループでルールがサポートする環境の正確なセットを宣言します。
  2. compatible_with= 属性を使用します。これは、デフォルトでサポートされている「標準」環境に加えて、ルールがサポートする環境を宣言します。
  3. パッケージ レベルの属性 default_restricted_to=default_compatible_with= を使用します。
  4. environment_group() ルールのデフォルトの仕様。すべての環境は、テーマに関連するピアのグループ(「CPU アーキテクチャ」、「JDK バージョン」、「モバイル オペレーティング システム」など)に属しています。環境グループの定義には、restricted_to= / environment() 属性で指定されていない場合に「デフォルト」でサポートされる環境が含まれます。このような属性のないルールは、すべてのデフォルトを継承します。
  5. ルールクラスのデフォルト。これにより、指定されたルールクラスのすべてのインスタンスのグローバル デフォルトがオーバーライドされます。たとえば、この機能を使用すると、各インスタンスがこの機能を明示的に宣言しなくても、すべての *_test ルールをテスト可能にできます。

environment() は通常のルールとして実装されますが、environment_group()Target のサブクラスであり、RuleEnvironmentGroup)ではなく、Starlark(StarlarkLibrary.environmentGroup())からデフォルトで使用できる関数であり、最終的に同名のターゲットを作成します。これは、各環境が属する環境グループを宣言し、各環境グループがデフォルトの環境を宣言する必要があるために発生する循環依存関係を回避するためです。

--target_environment コマンドライン オプションを使用すると、ビルドを特定の環境に制限できます。

制約チェックの実装は RuleContextConstraintSemanticsTopLevelConstraintSemantics にあります。

プラットフォームの制約

ターゲットがどのプラットフォームと互換性があるかを記述する現在の「公式」の方法は、ツールチェーンとプラットフォームの記述に使用される制約と同じものを使用することです。pull リクエスト #10945 で審査中です。

公開設定

多くのデベロッパーが大規模なコードベースで作業している場合(Google など)、他のユーザーが自分のコードに勝手に依存しないように注意する必要があります。そうしないと、ハイラムの法則に従って、実装の詳細と見なした動作に依存するようになります。

Bazel は、公開設定というメカニズムでこれをサポートしています。公開設定属性を使用して、特定のターゲットが依存できるのは特定のターゲットのみであることを宣言できます。この属性は少し特殊です。ラベルのリストを保持しますが、これらのラベルは特定のターゲットへのポインタではなく、パッケージ名に対するパターンをエンコードする場合があります。(はい、これは設計上の欠陥です)。

これは次の場所で実装されています。

  • RuleVisibility インターフェースは、可視性宣言を表します。定数(完全に公開または完全に非公開)またはラベルのリストのいずれかになります。
  • ラベルは、パッケージ グループ(パッケージの事前定義リスト)、パッケージ(//pkg:__pkg__)、パッケージのサブツリー(//pkg:__subpackages__)のいずれかを参照できます。これは、//pkg:* または //pkg/... を使用するコマンドライン構文とは異なります。
  • パッケージ グループは独自のターゲット(PackageGroup)と構成済みターゲット(PackageGroupConfiguredTarget)として実装されています。必要に応じて、これらを単純なルールに置き換えることもできます。これらのロジックは、PackageSpecification//pkg/... などの単一のパターンに対応)、PackageGroupContents(単一の package_grouppackages 属性に対応)、PackageSpecificationProviderpackage_group とその推移的な includes を集計)を使用して実装されます。
  • 可視性ラベルのリストから依存関係への変換は、DependencyResolver.visitTargetVisibility とその他のいくつかの場所で行われます。
  • 実際のチェックは CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility() で行われます。

ネストされたセット

多くの場合、構成されたターゲットは依存関係から一連のファイルを統合し、独自のファイルを追加して、統合されたセットを推移的情報プロバイダにラップします。これにより、それに依存する構成済みターゲットも同じ処理を行うことができます。例:

  • ビルドに使用される C++ ヘッダー ファイル
  • cc_library の推移閉包を表すオブジェクト ファイル
  • Java ルールをコンパイルまたは実行するためにクラスパスに含める必要がある .jar ファイルのセット
  • Python ルールの推移閉包内の Python ファイルのセット

たとえば ListSet を使用して単純にこれを行うと、メモリ使用量が 2 次関数になります。N 個のルールのチェーンがあり、各ルールがファイルを追加する場合、コレクション メンバーは 1+2+...+N 個になります。

この問題を回避するために、NestedSet というコンセプトが考案されました。これは、他の NestedSet インスタンスと独自のメンバーで構成されるデータ構造であり、集合の有向非巡回グラフを形成します。これらは不変であり、メンバーを反復処理できます。複数の反復順序(NestedSet.Order)を定義します。先行順序、後行順序、トポロジカル(ノードは常にその祖先の後に来る)、および「気にしないが、毎回同じであるべき」です。

同じデータ構造は、Starlark では depset と呼ばれます。

アーティファクトとアクション

実際のビルドは、ユーザーが求める出力を生成するために実行する必要がある一連のコマンドで構成されます。コマンドは Action クラスのインスタンスとして表され、ファイルは Artifact クラスのインスタンスとして表されます。これらは、「アクション グラフ」と呼ばれる二部グラフ、有向グラフ、非巡回グラフで構成されています。

アーティファクトには、ソース アーティファクト(Bazel の実行前に使用可能なもの)と派生アーティファクト(ビルドが必要なもの)の 2 種類があります。派生アーティファクト自体は、複数の種類に分類できます。

  1. **通常のアーティファクト。**これらは、チェックサムを計算して最新の状態かどうかを確認します。mtime はショートカットとして使用されます。ctime が変更されていない場合は、ファイルのチェックサムは計算されません。
  2. 解決されていないシンボリック リンク アーティファクト。これらは、readlink() を呼び出すことで最新の状態であるかどうかがチェックされます。通常のアーティファクトとは異なり、これらはダングリング シンボリック リンクになる可能性があります。通常、ファイルを何らかのアーカイブにパックする場合に使用されます。
  3. ツリー アーティファクト。これらは単一のファイルではなく、ディレクトリ ツリーです。これらのファイルセットとその内容をチェックすることで、最新の状態かどうかが確認されます。これらは TreeArtifact として表されます。
  4. 定数メタデータ アーティファクト。これらのアーティファクトの変更は、再ビルドをトリガーしません。これはビルドスタンプ情報専用です。現在の時刻が変更されただけでリビルドしたくありません。

ソース アーティファクトがツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできない根本的な理由はありません。まだ実装されていないだけです(ただし、実装する必要があります。BUILD ファイルでソース ディレクトリを参照することは、Bazel の長年の既知の誤りの 1 つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM プロパティで有効になる、ある程度機能する実装があります)。

Artifact の代表的な例は仲介業者です。これらは、MiddlemanAction の出力である Artifact インスタンスで示されます。これらは、いくつかのことを特別に処理するために使用されます。

  • 集約ミドルウェアは、アーティファクトをグループ化するために使用されます。これは、多くのアクションが同じ大きな入力セットを使用する場合、N*M 個の依存関係エッジではなく、N+M 個の依存関係エッジのみが存在するようにするためです(ネストされたセットに置き換えられます)。
  • スケジューリング依存関係の仲介者は、あるアクションが別のアクションの前に実行されるようにします。これらは主に linting に使用されますが、C++ コンパイルにも使用されます(説明については CcCompilationContext.createMiddleman() を参照)。
  • Runfiles 中間ファイルは、runfiles ツリーの存在を保証するために使用されます。これにより、出力マニフェストと runfiles ツリーで参照されるすべてのアーティファクトに個別に依存する必要がなくなります。

アクションは、実行する必要があるコマンド、必要な環境、生成される出力のセットとして理解するのが最適です。アクションの説明の主なコンポーネントは次のとおりです。

  • 実行する必要があるコマンドライン
  • 必要な入力アーティファクト
  • 設定する必要がある環境変数
  • 実行に必要な環境(プラットフォームなど)を記述するアノテーション

Bazel がコンテンツを認識しているファイルの書き込みなど、他にも特別なケースがいくつかあります。これらは AbstractAction のサブクラスです。ほとんどのアクションは SpawnAction または StarlarkAction です(同じであり、別々のクラスにするべきではありません)。ただし、Java と C++ には独自のアクション タイプ(JavaCompileActionCppCompileActionCppLinkAction)があります。

最終的にはすべてを 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 の数少ない場所の 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() を使用してヒットがチェックされます。

名前とは異なり、これは派生アーティファクトのパスからそれを生成したアクションへのマップです。アクションは次のように記述します。

  1. 入力ファイルと出力ファイルのセットとそのチェックサム
  2. 「アクションキー」。通常は実行されたコマンドラインですが、一般的には、入力ファイルのチェックサムでキャプチャされないすべてのものを表します(たとえば、FileWriteAction の場合は、書き込まれたデータのチェックサムです)。

また、開発中の試験運用段階の「トップダウン アクション キャッシュ」もあります。これは、推移的ハッシュを使用して、キャッシュへのアクセス回数を減らします。

入力の検出と入力のプルーニング

一部のアクションは、単に入力セットがあるだけよりも複雑です。アクションの入力セットの変更には、次の 2 つの形式があります。

  • アクションは、実行前に新しい入力を検出したり、一部の入力が実際には必要ないと判断したりすることがあります。標準的な例は C++ です。C++ ファイルがその推移的閉包から使用するヘッダー ファイルを推測する方が、すべてのファイルをリモート実行プログラムに送信する必要がないため、より適切です。そのため、すべてのヘッダー ファイルを「入力」として登録しないオプションがあります。このオプションでは、ソースファイルをスキャンして推移的に含まれるヘッダーを検出し、#include ステートメントで言及されているヘッダー ファイルのみを入力としてマークします(完全な C プリプロセッサを実装する必要がないように、過大評価します)。このオプションは現在、Bazel で「false」にハードワイヤードされており、Google でのみ使用されています。
  • アクションは、実行中に一部のファイルが使用されなかったことを認識する場合があります。C++ では、これは「.d ファイル」と呼ばれます。コンパイラは、どのヘッダー ファイルが使用されたかを後から通知します。Make よりも増分性が低くなるのを避けるため、Bazel はこの事実を利用します。コンパイラに依存するため、インクルード スキャナよりも優れた推定値が得られます。

これらは Action のメソッドを使用して実装されます。

  1. Action.discoverInputs() が呼び出されます。必要なアーティファクトのネストされたセットが返されます。これらはソース アーティファクトである必要があります。これにより、構成されたターゲット グラフに同等のものがない依存関係エッジがアクション グラフに存在しなくなります。
  2. アクションは Action.execute() を呼び出すことで実行されます。
  3. Action.execute() の最後に、アクションは Action.updateInputs() を呼び出して、すべての入力が必要ではなかったことを Bazel に伝えることができます。使用された入力が未使用として報告されると、増分ビルドが正しく行われない可能性があります。

アクション キャッシュが新しい Action インスタンス(サーバーの再起動後に作成されたインスタンスなど)でヒットを返すと、Bazel は updateInputs() を呼び出して、入力セットに以前の入力検出とプルーニングの結果を反映させます。

Starlark アクションでは、ctx.actions.run()unused_inputs_list= 引数を使用して、一部の入力を未使用として宣言する機能を利用できます。

アクションを実行するさまざまな方法: Strategies/ActionContexts

一部のアクションはさまざまな方法で実行できます。たとえば、コマンドラインはローカルで実行することも、ローカルでさまざまな種類のサンドボックスで実行することも、リモートで実行することもできます。この概念を体現するものが ActionContext(または Strategy。名前変更が半分しか成功しなかったため)と呼ばれます。

アクション コンテキストのライフサイクルは次のとおりです。

  1. 実行フェーズが開始されると、BlazeModule インスタンスにアクション コンテキストが照会されます。これは ExecutionTool のコンストラクタで行われます。アクション コンテキスト タイプは、ActionContext のサブインターフェースを参照する Java Class インスタンスと、アクション コンテキストが実装する必要があるインターフェースによって識別されます。
  2. 利用可能なアクション コンテキストから適切なものが選択され、ActionExecutionContextBlazeExecutor に転送されます。
  3. アクションは ActionExecutionContext.getContext()BlazeExecutor.getStrategy() を使用してコンテキストをリクエストします(実際には 1 つの方法のみにする必要があります)。

戦略は、他の戦略を呼び出してジョブを実行できます。これは、ローカルとリモートの両方でアクションを開始し、最初に完了した方を使用する動的戦略などで使用されます。

注目すべき戦略の 1 つは、永続的なワーカー プロセス(WorkerSpawnStrategy)を実装する戦略です。この戦略のアイデアは、起動に時間がかかるツールがあるため、アクションごとに新しいツールを起動するのではなく、アクション間で再利用する必要があるというものです(Bazel は、個々のリクエスト間で観測可能な状態を保持しないというワーカー プロセスの約束に依存しているため、これは潜在的な正確性の問題を表しています)。

ツールが変更された場合は、ワーカー プロセスを再起動する必要があります。ワーカーを再利用できるかどうかは、WorkerFilesHash を使用して使用されたツールのチェックサムを計算することで判断されます。これは、アクションのどの入力がツールの一部を表し、どの入力が入力を表すかを知ることに依存します。これは、アクションの作成者によって決定されます。Spawn.getToolFiles()Spawn のランファイルは、ツールの一部としてカウントされます。

戦略(またはアクション コンテキスト)の詳細:

  • アクションを実行するためのさまざまな戦略については、こちらをご覧ください。
  • ローカルとリモートの両方でアクションを実行し、どちらが先に完了するかを確認する動的戦略に関する情報は、こちらで確認できます。
  • ローカルでアクションを実行する際の複雑さについては、こちらをご覧ください。

ローカル リソース マネージャー

Bazel は多くのアクションを並行して実行できます。並行して実行する必要があるローカル アクションの数はアクションによって異なります。アクションに必要なリソースが多いほど、ローカルマシンに過負荷がかからないように、同時に実行するインスタンスの数を減らす必要があります。

これは ResourceManager クラスで実装されています。各アクションには、ResourceSet インスタンス(CPU と RAM)の形式で、必要なローカル リソースの推定値をアノテーションする必要があります。アクション コンテキストがローカル リソースを必要とする処理を行う場合、ResourceManager.acquireResources() を呼び出し、必要なリソースが利用可能になるまでブロックされます。

ローカル リソース管理の詳細については、こちらをご覧ください。

出力ディレクトリの構造

各アクションには、出力を配置する出力ディレクトリ内の個別の場所が必要です。通常、派生アーティファクトの場所は次のとおりです。

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

特定の構成に関連付けられているディレクトリの名前はどのように決定されますか?望ましい 2 つのプロパティが競合しています。

  1. 同じビルドで 2 つの構成が発生する可能性がある場合は、両方が同じアクションの独自のバージョンを持つことができるように、異なるディレクトリが必要です。そうしないと、同じ出力ファイルを生成するアクションのコマンドラインなどについて 2 つの構成が一致しない場合、Bazel はどちらのアクションを選択すべきかわかりません(「アクションの競合」)。
  2. 2 つの構成が「ほぼ」同じものを表している場合は、同じ名前を付ける必要があります。これにより、コマンドラインが一致する場合に、一方の構成で実行されたアクションを他方の構成で再利用できます。たとえば、Java コンパイラのコマンドライン オプションの変更によって、C++ コンパイル アクションが再実行されることはありません。

これまでのところ、この問題を解決する原則的な方法は見つかっていません。この問題は、構成のトリミングの問題と類似しています。オプションの詳細については、こちらをご覧ください。主な問題領域は、Starlark ルール(通常、作成者は Bazel に精通していない)とアスペクトです。アスペクトは、「同じ」出力ファイルを生成できるものの空間に別の次元を追加します。

現在の方法では、構成のパス セグメントは <CPU>-<compilation mode> で、Java で実装された構成の移行でアクションの競合が発生しないように、さまざまな接尾辞が追加されています。また、ユーザーがアクションの競合を引き起こさないように、Starlark 構成遷移のセットのチェックサムが追加されます。完璧とは言えません。これは OutputDirectories.buildMnemonic() で実装されており、各構成フラグメントが独自のパートを出力ディレクトリの名前に追加することに依存しています。

テスト

Bazel は、テストの実行を幅広くサポートしています。サポートされているオプションは次のとおりです。

  • リモートでテストを実行する(リモート実行バックエンドが利用可能な場合)
  • テストを複数回並行して実行する(デフレークまたはタイミング データを収集する場合)
  • テストのシャーディング(同じテストのテストケースを複数のプロセスに分割して高速化)
  • 不安定なテストの再実行
  • テストをテストスイートにグループ化する

テストは、テストの実行方法を記述する TestProvider を持つ、構成された通常のターゲットです。

  • ビルドの結果としてテストが実行されるアーティファクト。これは、シリアル化された TestResultData メッセージを含む「キャッシュ ステータス」ファイルです。
  • テストを実行する回数
  • テストを分割するシャードの数
  • テストの実行方法に関するパラメータ(テストのタイムアウトなど)

実行するテストの決定

実行するテストを決定するプロセスは複雑です。

まず、ターゲット パターンの解析中に、テストスイートが再帰的に展開されます。拡張は TestsForTargetPatternFunction に実装されています。やや意外なのは、テストスイートでテストが宣言されていない場合、パッケージ内のすべてのテストが参照されることです。これは、Package.beforeBuild() で、$implicit_tests という暗黙的な属性をテストスイートのルールに追加することで実装されます。

次に、コマンドライン オプションに従って、サイズ、タグ、タイムアウト、言語でテストをフィルタします。これは 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 ルールが適用されるターミナルに新しいバイトをダンプすることで機能します。

実行されたテストの結果は、さまざまなイベント(TestAttemptTestResultTestingCompleteEvent など)をモニタリングすることで、イベントバスで確認できます。結果は Build Event Protocol にダンプされ、AggregatingTestListener によってコンソールに出力されます。

カバレッジの収集

カバレッジは、bazel-testlogs/$PACKAGE/$TARGET/coverage.dat ファイルの LCOV 形式でテストによって報告されます。

カバレッジを収集するために、各テスト実行は collect_coverage.sh というスクリプトでラップされます。

このスクリプトは、カバレッジ収集を有効にし、カバレッジ ランタイムがカバレッジ ファイルを書き込む場所を特定するために、テストの環境を設定します。その後、テストを実行します。テスト自体が複数のサブプロセスを実行し、複数の異なるプログラミング言語で記述された部分で構成されている場合があります(カバレッジ収集ランタイムは別々です)。ラッパー スクリプトは、必要に応じて結果のファイルを LCOV 形式に変換し、単一のファイルにマージします。

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 つのファイルグループを追跡します。

計測対象のファイルセットは、計測対象のファイルのセットです。オンライン カバレッジ ランタイムの場合、これはランタイム時に使用して、どのファイルを計測するかを決定できます。ベースライン カバレッジの実装にも使用されます。

計測メタデータ ファイルのセットは、テストで LCOV ファイルを生成するために必要な追加ファイルのセットです。実際には、これはランタイム固有のファイルで構成されます。たとえば、gcc はコンパイル中に .gcno ファイルを出力します。これらは、カバレッジ モードが有効になっている場合、テスト アクションの入力セットに追加されます。

カバレッジが収集されているかどうかは BuildConfiguration に保存されます。このビットに応じてテスト アクションとアクション グラフを簡単に変更できるため便利ですが、このビットが反転すると、すべてのターゲットを再分析する必要があります(C++ などの一部の言語では、カバレッジを収集できるコードを生成するために異なるコンパイラ オプションが必要になるため、再分析が必要になるため、この問題は多少軽減されます)。

カバレッジ サポート ファイルは、暗黙的な依存関係のラベルを介して依存しているため、呼び出しポリシーでオーバーライドできます。これにより、Bazel のバージョンごとに異なるファイルを使用できます。理想的には、これらの違いは解消され、いずれかに標準化されるでしょう。

また、Bazel 呼び出しのすべてのテストで収集されたカバレッジを統合する「カバレッジ レポート」も生成します。これは CoverageReportActionFactory によって処理され、BuildView.createResult() から呼び出されます。実行される最初のテストの :coverage_report_generator 属性を参照して、必要なツールにアクセスします。

クエリエンジン

Bazel には、さまざまなグラフについてさまざまなことを尋ねるために使用される小さな言語があります。次のクエリの種類が用意されています。

  • bazel query はターゲット グラフの調査に使用されます
  • bazel cquery は、構成されたターゲット グラフを調査するために使用されます。
  • bazel aquery はアクション グラフの調査に使用されます

これらはそれぞれ AbstractBlazeQueryEnvironment をサブクラス化することで実装されます。QueryFunction をサブクラス化することで、追加のクエリ関数を追加できます。ストリーミング クエリの結果を許可するために、結果を何らかのデータ構造に収集するのではなく、query2.engine.CallbackQueryFunction に渡され、QueryFunction は返したい結果に対して query2.engine.Callback を呼び出します。

クエリの結果は、ラベル、ラベルとルールクラス、XML、protobuf など、さまざまな方法で出力できます。これらは OutputFormatter のサブクラスとして実装されます。

一部のクエリ出力形式(proto など)の微妙な要件は、Bazel がパッケージ読み込みで提供されるすべての情報を出力する必要があることです。これにより、出力を比較して、特定のターゲットが変更されたかどうかを判断できます。そのため、属性値はシリアル化可能である必要があります。複雑な Starlark 値を持つ属性がない属性型が少ないのはそのためです。一般的な回避策は、ラベルを使用して、そのラベルで複雑な情報をルールに関連付けることです。この回避策はあまり満足のいくものではありません。この要件を解除できると非常に助かります。

モジュール システム

Bazel は、モジュールを追加することで拡張できます。各モジュールは BlazeModule をサブクラス化する必要があります(この名前は、Bazel が Blaze と呼ばれていた頃の名残です)。コマンドの実行中にさまざまなイベントに関する情報を取得します。

これらは主に、Bazel の一部のバージョン(Google で使用しているバージョンなど)でのみ必要となる「コア以外の」さまざまな機能を実装するために使用されます。

  • リモート実行システムへのインターフェース
  • 次のコマンドを新しく導入しました。

拡張ポイント BlazeModule のセットはやや無秩序です。優れた設計原則の例として使用しないでください。

イベントバス

BlazeModules が Bazel の他の部分と通信する主な方法は、イベントバス(EventBus)を使用することです。ビルドごとに新しいインスタンスが作成され、Bazel のさまざまな部分がイベントを投稿し、モジュールが関心のあるイベントのリスナーを登録できます。たとえば、次のものはイベントとして表されます。

  • ビルドするビルド ターゲットのリストが決定されました(TargetParsingCompleteEvent
  • 最上位の構成が決定されました(BuildConfigurationEvent
  • ターゲットがビルドされた(成功または失敗)(TargetCompleteEvent
  • テストが実行されました(TestAttemptTestSummary

これらのイベントの一部は、Bazel の外部で Build Event Protocol で表されます(BuildEvent です)。これにより、BlazeModule だけでなく、Bazel プロセス外の要素もビルドを監視できます。これらは、プロトコル メッセージを含むファイルとしてアクセスするか、Bazel がサーバー(ビルド イベント サービス)に接続してイベントをストリーミングできます。

これは、build.lib.buildeventservicebuild.lib.buildeventstream の Java パッケージで実装されています。

外部リポジトリ

Bazel は元々、モノレポ(ビルドに必要なすべてを含む単一のソースツリー)で使用されるように設計されましたが、Bazel は必ずしもそうではない世界に存在します。「外部リポジトリ」は、この 2 つの世界をつなぐために使用される抽象化です。ビルドに必要なコードを表しますが、メインのソースツリーにはありません。

WORKSPACE ファイル

外部リポジトリのセットは、WORKSPACE ファイルを解析することで決定されます。たとえば、次のような宣言があるとします。

    local_repository(name="foo", path="/foo/bar")

これにより、@foo というリポジトリの結果が使用可能になります。複雑なのは、Starlark ファイルで新しいリポジトリ ルールを定義できることです。このルールを使用して新しい Starlark コードを読み込み、新しいリポジトリ ルールを定義できます。

このケースを処理するため、WORKSPACE ファイル(WorkspaceFileFunction 内)の解析は、load() ステートメントで区切られたチャンクに分割されます。チャンク インデックスは WorkspaceFileKey.getIndex() で示されます。インデックス X まで WorkspaceFileFunction を計算することは、X 番目の load() ステートメントまで評価することを意味します。

リポジトリの取得

リポジトリのコードを Bazel で使用できるようにするには、フェッチする必要があります。これにより、Bazel は $OUTPUT_BASE/external/<repository name> の下にディレクトリを作成します。

リポジトリの取得は、次の手順で行われます。

  1. PackageLookupFunction はリポジトリが必要であることを認識し、SkyKey として RepositoryName を作成して RepositoryLoaderFunction を呼び出す
  2. RepositoryLoaderFunction は、不明な理由でリクエストを RepositoryDelegatorFunction に転送します(コードでは、Skyframe の再起動時に再ダウンロードを回避するためとされていますが、あまり確実な理由ではありません)。
  3. RepositoryDelegatorFunction は、リクエストされたリポジトリが見つかるまで WORKSPACE ファイルのチャンクを反復処理して、取得をリクエストされたリポジトリルールを特定します。
  4. リポジトリの取得を実装する適切な RepositoryFunction が見つかります。これは、リポジトリの Starlark 実装か、Java で実装されたリポジトリのハードコードされたマップのいずれかです。

リポジトリの取得は非常にコストがかかるため、キャッシュ保存のレイヤは複数あります。

  1. ダウンロードされたファイルには、チェックサム(RepositoryCache)をキーとするキャッシュがあります。これには、WORKSPACE ファイルでチェックサムが利用可能であることが必要ですが、これは密閉性にとっても良いことです。これは、実行中のワークスペースや出力ベースに関係なく、同じワークステーション上のすべての Bazel サーバー インスタンスで共有されます。
  2. $OUTPUT_BASE/external の各リポジトリに「マーカー ファイル」が書き込まれます。このファイルには、リポジトリの取得に使用されたルールのチェックサムが含まれています。Bazel サーバーが再起動してもチェックサムが変更されない場合、再取得されません。これは RepositoryDelegatorFunction.DigestWriter で実装されています。
  3. --distdir コマンドライン オプションは、ダウンロードするアーティファクトの検索に使用される別のキャッシュを指定します。これは、Bazel がインターネットからランダムなものを取得しないようにする必要があるエンタープライズ設定で役立ちます。これは DownloadManager によって実装されます。

リポジトリがダウンロードされると、そのリポジトリ内のアーティファクトはソース アーティファクトとして扱われます。通常、Bazel はソース アーティファクトに対して stat() を呼び出して、その最新性をチェックします。また、これらのアーティファクトは、それらが存在するリポジトリの定義が変更されたときにも無効になります。したがって、外部リポジトリ内のアーティファクトの FileStateValue は、その外部リポジトリに依存する必要があります。これは ExternalFilesHelper によって処理されます。

マネージド ディレクトリ

外部リポジトリがワークスペース ルートのファイル(ソースツリーのサブディレクトリにダウンロードしたパッケージを格納するパッケージ マネージャーなど)を変更する必要がある場合があります。これは、ソースファイルはユーザーによってのみ変更され、Bazel 自身によって変更されないという Bazel の前提と矛盾しており、パッケージがワークスペース ルートの下のすべてのディレクトリを参照できるようになります。このような外部リポジトリを機能させるために、Bazel は次の 2 つの処理を行います。

  1. ユーザーが、Bazel がアクセスできないワークスペースのサブディレクトリを指定できるようにします。これらは .bazelignore というファイルにリストされ、機能は BlacklistedPackagePrefixesFunction で実装されています。
  2. ワークスペースのサブディレクトリから、そのサブディレクトリを処理する外部リポジトリへのマッピングを ManagedDirectoriesKnowledge にエンコードし、それらを参照する FileStateValue を通常の外部リポジトリの場合と同じように処理します。

リポジトリのマッピング

複数のリポジトリが同じリポジトリに依存しているが、バージョンが異なる場合があります(これは「ダイヤモンド依存関係の問題」の一例です)。たとえば、ビルド内の別々のリポジトリにある 2 つのバイナリが Guava に依存する場合、両方とも @guava// で始まるラベルで Guava を参照し、それが異なるバージョンを意味することを想定します。

そのため、Bazel では外部リポジトリ ラベルを再マッピングして、文字列 @guava// が 1 つのバイナリのリポジトリ内の 1 つの Guava リポジトリ(@guava1// など)と、もう 1 つのバイナリのリポジトリ内の別の Guava リポジトリ(@guava2// など)を参照できるようにします。

また、この機能はダイヤモンドを結合するためにも使用できます。あるリポジトリが @guava1// に依存し、別のリポジトリが @guava2// に依存している場合、リポジトリ マッピングを使用すると、両方のリポジトリを正規の @guava// リポジトリを使用するように再マッピングできます。

マッピングは、個々のリポジトリ定義の repo_mapping 属性として WORKSPACE ファイルで指定されます。その後、Skyframe に WorkspaceFileValue のメンバーとして表示され、次の場所に接続されます。

  • Package.Builder.repositoryMapping。パッケージ内のルールのラベル値属性を RuleClass.populateRuleAttributeValues() で変換するために使用されます。
  • Package.repositoryMapping(分析フェーズで使用されます。読み込みフェーズで解析されない $(location) などの解決に使用されます)
  • load() ステートメントのラベルを解決するための BzlLoadFunction

JNI ビット

Bazel のサーバーは主に Java で記述されています。ただし、Java が単独で実行できない部分、または実装時に単独で実行できなかった部分は例外です。これは主に、ファイル システム、プロセス制御、その他のさまざまな低レベルの処理に限定されます。

C++ コードは src/main/native にあり、ネイティブ メソッドを含む Java クラスは次のとおりです。

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

コンソール出力

コンソール出力の生成は単純なことのように思えますが、複数のプロセス(リモートの場合もある)の実行、きめ細かいキャッシュ保存、見やすくカラフルなターミナル出力の実現、長時間実行されるサーバーの存在などが重なり、簡単ではありません。

クライアントから RPC 呼び出しが届くとすぐに、2 つの RpcOutputStream インスタンス(stdout と stderr 用)が作成され、それらに出力されたデータがクライアントに転送されます。これらは OutErr((stdout, stderr) ペア)にラップされます。コンソールに出力する必要があるものはすべて、これらのストリームを通過します。これらのストリームは BlazeCommandDispatcher.execExclusively() に渡されます。

出力はデフォルトで ANSI エスケープ シーケンスで出力されます。これらが不要な場合(--color=no)、AnsiStrippingOutputStream によって削除されます。また、System.outSystem.err はこれらの出力ストリームにリダイレクトされます。これは、System.err.println() を使用してデバッグ情報を出力しても、クライアントのターミナル出力(サーバーのターミナル出力とは異なる)に表示されるようにするためです。プロセスがバイナリ出力(bazel query --output=proto など)を生成する場合、stdout のマングリングは行われません。

短いメッセージ(エラー、警告など)は EventHandler インターフェースを介して表現されます。なお、これらは EventBus に投稿するものとは異なります(混乱を招く可能性があります)。各 Event には EventKind(エラー、警告、情報など)があり、Location(イベントが発生した原因となったソースコード内の場所)が含まれる場合もあります。

一部の EventHandler 実装では、受信したイベントを保存します。これは、さまざまな種類のキャッシュ保存された処理(キャッシュ保存された構成済みターゲットによって出力された警告など)によって UI に返される情報を再生するために使用されます。

一部の EventHandler は、最終的にイベントバスに到達するイベントの投稿も許可します(通常の Event はイベントバスに表示されません)。これらは ExtendedEventHandler の実装であり、主な用途はキャッシュに保存された EventBus イベントの再生です。これらの EventBus イベントはすべて Postable を実装していますが、EventBus に投稿されるすべてのものがこのインターフェースを実装しているわけではありません。ExtendedEventHandler によってキャッシュに保存されるものだけです(ほとんどのものがそうであると望ましいですが、強制ではありません)。

ターミナル出力は、Bazel が行うすべての高度な出力形式と進行状況レポートを担当する UiEventHandler を介してほとんど出力されます。入力は次の 2 つです。

  • イベントバス
  • Reporter を介してパイプされたイベント ストリーム

コマンド実行機構(Bazel の残りの部分など)がクライアントへの RPC ストリームに直接接続するのは Reporter.getOutErr() を介してのみです。これにより、これらのストリームに直接アクセスできます。これは、コマンドで大量のバイナリ データ(bazel query など)をダンプする必要がある場合にのみ使用されます。

Bazel のプロファイリング

Bazel は高速です。Bazel も遅いです。ビルドは、許容できる限界まで大きくなる傾向があるためです。このため、Bazel には、ビルドと Bazel 自体のプロファイリングに使用できるプロファイラが含まれています。これは、Profiler という名前のクラスに実装されています。デフォルトでオンになっていますが、オーバーヘッドが許容範囲になるように、要約されたデータのみを記録します。コマンドライン --record_full_profiler_data を使用すると、可能な限りすべてのデータを記録します。

Chrome プロファイラ形式でプロファイルを出力します。Chrome で表示するのが最適です。データモデルはタスク スタックのモデルです。タスクを開始して終了できます。タスクは互いにネストされることが想定されています。各 Java スレッドには独自のタスク スタックが割り当てられます。TODO: アクションと継続渡しスタイルではどのように機能しますか?

Profiler は BlazeRuntime.initProfiler()BlazeRuntime.afterCommand() でそれぞれ開始と停止が行われ、可能な限り長くライブ状態を維持して、すべてをプロファイリングできるようにします。プロファイルに何かを追加するには、Profiler.instance().profile() を呼び出します。タスクの終了を表すクロージャを含む Closeable を返します。try-with-resources ステートメントで使用するのが最適です。

また、MemoryProfiler で基本的なメモリ プロファイリングも行います。また、常にオンになっており、主に最大ヒープサイズと GC の動作を記録します。

Bazel のテスト

Bazel には、Bazel を「ブラック ボックス」として観察するテストと、分析フェーズのみを実行するテストの 2 種類があります。前者を「統合テスト」、後者を「単体テスト」と呼びますが、後者は統合テストというよりは、統合度が低い統合テストのようなものです。また、必要に応じて実際の単体テストもいくつかあります。

統合テストには次の 2 種類があります。

  1. src/test/shell の非常に精巧な bash テスト フレームワークを使用して実装されたもの
  2. Java で実装されたもの。これらは BuildIntegrationTestCase のサブクラスとして実装されます。

BuildIntegrationTestCase は、ほとんどのテストシナリオに対応しているため、推奨される統合テスト フレームワークです。Java フレームワークであるため、デバッグが可能で、多くの一般的な開発ツールとシームレスに統合できます。Bazel リポジトリには、BuildIntegrationTestCase クラスの例が多数あります。

分析テストは BuildViewTestCase のサブクラスとして実装されます。BUILD ファイルの書き込みに使用できるスクラッチ ファイル システムがあります。さまざまなヘルパー メソッドで、構成されたターゲットをリクエストしたり、構成を変更したり、分析結果に関するさまざまなことをアサートしたりできます。