Bazel コードベース

問題を報告 ソースを表示

このドキュメントでは、コードベースと 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 実行可能ファイル(「クライアント」)が制御を取得します。以下の手順に沿って、適切なサーバー プロセスを設定します。

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

適切なサーバー プロセスの準備が整うと、実行する必要があるコマンドが 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 サーバーが制御を取得し、実行する必要があるコマンドに関する通知を受けると、次の一連のイベントが発生します。

  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 が壊れてしまいます。残念ながら、それらを実際に不変にするのは多大な労力を要する作業です。(作成直後、他のユーザーが参照を保持できる前、equals() または hashCode() が呼び出される前に FragmentOptions を変更しても問題ありません)。

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 は、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 つは密接に関連しているため、残念ながらこれは困難です。

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

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

  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 とユーザー向けのドキュメントでは、「Rule」はルールクラス自体を指すためだけに使用する必要があります。ターゲットは単なる「ターゲット」です。また、RuleClass の名前に「class」が含まれているにもかかわらず、ルールクラスとその型のターゲットの間に Java 継承関係はありません。

スカイフレーム

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

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

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

スカイフレームは 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 は、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 つの構成済みターゲットの分析では、次の入力を行います。

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

構成されたターゲットの分析の出力は次のようになります。

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

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

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

構成

構成とは、どのプラットフォームに対して、どのコマンドライン オプションを使用するかなど、ターゲットをビルドする「方法」のことです。

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

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

その結果、たとえば、リクエストされたテスト実行の数がテスト ターゲットにのみ影響するにもかかわらず、ビルド全体を再分析しなければならないといった異常が発生します(Google では構成を「トリミング」する計画がありますが、まだ準備ができていません)。

ルールの実装が構成の一部を必要とする場合は、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 で、FileProviderFilesToRunProviderRunfilesProvider を結合したものです。この API は、Java の直接文字変換よりも Starlark 風にしていると見なされていたためです)。キーは次のいずれかです。

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

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

構成済みのターゲット

構成済みのターゲットは RuleConfiguredTargetFactory として実装されます。Java で実装される各ルールクラスにサブクラスがあります。Starlark の構成ターゲットは、StarlarkRuleConfiguredTargetUtil.buildRule() を使用して作成されます。

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

  1. filesToBuild: 「このルールが表すファイルのセット」という曖昧なコンセプト。これらは、構成されたターゲットがコマンドラインまたは genrule の srcs にある場合にビルドされるファイルです。
  2. 実行ファイル、通常のファイル、およびデータです。
  3. 出力グループ。これらは、ルールで作成できるさまざまな「その他のファイルセット」です。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() 関数を使用して属性ごとに指定されます。混同される名前のクラスがこのプロセスに参加しています。

  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) 内)で必要なツールチェーン タイプのセット
  • 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() ルールのインスタンスです。

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

  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 のような)多数の開発者とともに大規模なコードベースを扱っている場合は、他の開発者が自分のコードに勝手に依存することのないように注意する必要があります。そうしないと、Hyrum の法則に従い、実装の詳細とみなされた行動にユーザーが依存するようになります。

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

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

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

ネストされたセット

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

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

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

この問題を回避するために、NestedSet のコンセプトを考案しました。これは、他の NestedSet インスタンスと自身のインスタンスから構成されるデータ構造で、集合の有向非巡回グラフを形成します。これらは不変であり、そのメンバーは反復処理できます。複数のイテレーション順序(NestedSet.Order)を定義しています。preorder、postorder、トポロジ(ノードは常に祖先の後に来ます)、Don't Care ですが、毎回同じである必要があります。

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

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

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

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

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

ソース アーティファクトがツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできない根本的な理由はありません。ただ、まだ実装していないというだけです(ただし、そうすべきです。ただし、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++ には独自のアクション型(JavaCompileActionCppCompileActionCppLinkAction)がありますが、ほとんどのアクションは 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() を使用してヒットがないかチェックされます。

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

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

また、まだ開発中の非常に実験的な「トップダウン アクション キャッシュ」もあります。これは、推移的ハッシュを使用して、キャッシュに何度もアクセスすることを回避します。

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

一部のアクションは、単に一連の入力を取得するよりも複雑な場合があります。アクションの入力セットに対する変更は、次の 2 つの形式で行われます。

  • アクションは、実行前に新しい入力を発見したり、入力の一部が実際には必要でないと判断したりすることがあります。標準的な例は C++ です。ここでは、C++ ファイルがどのヘッダー ファイルを使用するかを推移的クロージャから推測し、知識に基づいて推測することをおすすめします。これにより、すべてのファイルをリモート エグゼキュータに送信しないようにできます。そのため、すべてのヘッダー ファイルを「入力」として登録するのではなく、ソースファイルをスキャンして推移的インクルードされたヘッダーを 8 通りに(8.0.0.0.0 または 8.1.0.0.0.0 といった)ヘッダーで#include
  • アクションの実行中に一部のファイルが使用されていないことに気付く場合があります。これは、C++ では「.d ファイル」と呼ばれます。コンパイラは、どのヘッダー ファイルが事後に使用されたかを示します。Make よりもインクリメンタリティが悪くなるという恥ずかしがらないために、Bazel はこの事実を利用します。インクルード スキャナはコンパイラに依存するため、インクルード スキャナよりも正確に推定できます。

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

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

アクション キャッシュが新しいアクション インスタンス(サーバーの再起動後に作成されたものなど)でヒットを返すと、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 つだけです)。

ストラテジーは、他のストラテジーを自由に呼び出してジョブを実行できます。たとえば、ローカルとリモートの両方でアクションを開始し、終了した方のアクションを使用するダイナミック ストラテジーで使用されます。

注目すべき戦略の一つは、永続ワーカー プロセス(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 に実装されます。少し驚くべき点として、テストスイートでテストが宣言されていない場合、パッケージ内のすべてのテストが参照されることになります。これは、テストスイートのルールに $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 ルールがあるターミナルに新しいバイトをダンプすることで機能します。

実行されたテストの結果は、さまざまなイベント(TestAttemptTestResultTestingCompleteEvent など)を監視することで、イベントバスで利用できます。結果は 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.CallbackQueryFunction に渡されます。これにより、返される結果に対してこれを呼び出します。

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

一部のクエリ出力形式(proto は当然)の微妙な要件は、パッケージ読み込みが提供する情報を Bazel が出力する必要があることです。これにより、出力の差分を確認し、特定のターゲットが変更されたかどうかを判断できます。そのため、属性値はシリアル化可能である必要があります。そのため、複雑な Starlark 値を持つ属性を持たない属性タイプはごくわずかしかありません。通常は、ラベルを使用して、そのラベルの付いたルールに複雑な情報を追加します。これは満足のいく回避策とはいえず この要件を解除するのは有用です

モジュール システム

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

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

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

BlazeModule が提供する拡張ポイントのセットは、やや漠然としています。これを優れた設計原則の例として使用しないでください。

イベントバス

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

  • ビルドするビルド ターゲットのリストが決まっています(TargetParsingCompleteEvent
  • 最上位の構成が決定されました(BuildConfigurationEvent
  • ターゲットは正常にビルドされましたが、正常にビルドされませんでした(TargetCompleteEvent
  • テストが実行されました(TestAttemptTestSummary

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

これは、Java パッケージ build.lib.buildeventservicebuild.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> の下にディレクトリを作成します。

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

  1. PackageLookupFunction はリポジトリが必要であることを認識し、RepositoryNameSkyKey として作成します。これにより、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 は次の 2 つのことを行います。

  1. Bazel によるアクセスが許可されていないワークスペースのサブディレクトリをユーザーが指定できるようにします。これらは .bazelignore というファイルにリストされており、機能は BlacklistedPackagePrefixesFunction に実装されています。
  2. ワークスペースのサブディレクトリからそれを処理する外部リポジトリへのマッピングを 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.repositoryMappingRuleClass.populateRuleAttributeValues() によってパッケージ内のルールのラベル値属性を変換するために使用されます。
  • Package.repositoryMapping: 分析フェーズで使用します(読み込みフェーズで解析されない $(location) などの解決に使用します)。
  • load() ステートメントのラベルを解決するための BzlLoadFunction

JNI ビット

Bazel のサーバーは、ほとんど Java で記述されています。例外は、Java を実装したときに 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 によってキャッシュに保存されるイベントだけが実装されます(ただし、ほとんどの場合は推奨されますが、強制ではありません)。

ターミナルの出力は、ほとんど 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 種類があります。

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

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

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