Bazel コードベース

問題を報告する ソースを表示 ナイトリー · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

はじめに

Bazel のコードベースは大規模(約 350 KLOC の本番環境コード、約 260 KLOC のテストコード)で、全体像に精通している人は誰もいません。特定の渓谷については誰もがよく知っているものの、あらゆる方向の丘の上に何があるかを知っている人はほとんどいません。

旅の途中で、単純な道が失われ、暗闇の中で自分を見つけられないように、このドキュメントではコードベースの概要を説明し、作業を簡単に始められるようにします。

Bazel のソースコードのパブリック バージョンは、GitHub の github.com/bazelbuild/bazel にあります。これは「信頼できる情報源」ではなく、Google 外部では役に立たない追加機能を含む Google 内部ソースツリーから派生したものです。長期的な目標は、GitHub を信頼できる情報源にすることである。

貢献は通常の GitHub pull リクエスト メカニズムを通じて受け入れられ、Google 社員によって内部ソースツリーに手動でインポートされ、GitHub に再エクスポートされます。

クライアント/サーバー アーキテクチャ

Bazel の大部分は、ビルド間で RAM に残るサーバー プロセスに存在します。これにより、Bazel はビルド間で状態を維持できます。

そのため、Bazel コマンドラインには、起動とコマンドの 2 種類のオプションがあります。次のようなコマンドラインの場合:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

一部のオプション(--host_jvm_args=)は実行するコマンドの名前の前に、一部は後ろに配置されます(-c opt)。前者は「起動オプション」と呼ばれ、サーバー プロセス全体に影響します。後者は「コマンド オプション」と呼ばれ、1 つのコマンドにのみ影響します。

各サーバー インスタンスには 1 つのワークスペース(リポジトリと呼ばれるソースツリーのコレクション)が関連付けられており、通常、各ワークスペースには 1 つのアクティブなサーバー インスタンスがあります。これは、カスタム出力ベースを指定することによって回避できます(詳細については、ディレクトリ レイアウトのセクションをご覧ください)。

Bazel は、有効な .zip ファイルでもある単一の ELF 実行可能ファイルとして配布されます。bazel と入力すると、C++ で実装された上記の ELF 実行可能ファイル(「クライアント」)が制御を取得します。次の手順で適切なサーバー プロセスを設定します。

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

適切なサーバー プロセスの準備が整うと、実行する必要があるコマンドが gRPC インターフェースを介してサーバー プロセスに通信され、Bazel の出力がターミナルにパイプで送り返されます。同時に実行できるコマンドは 1 つだけです。これは、C++ の部分と Java の部分による複雑なロック メカニズムを使用して実装されます。bazel version を別のコマンドと並行して実行できないのは少し不便なので、複数のコマンドを並行して実行するためのインフラストラクチャが用意されています。主なブロック要因は、BlazeModule のライフサイクルと BlazeRuntime の一部状態です。

コマンドの終了時に、Bazel サーバーはクライアントが返す必要がある終了コードを送信します。興味深いのは bazel run の実装です。このコマンドのジョブは、Bazel でビルドしたものを実行することですが、ターミナルがないため、サーバー プロセスからは実行できません。代わりに、exec() するバイナリと引数をクライアントに指示します。

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 起動オプションを使用して変更できます。これは、ワークスペースで一度に実行できる 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. コマンドが制御を取得します。最も興味深いコマンドは、ビルドを実行するコマンド(ビルド、テスト、実行、カバレッジなど)です。この機能は 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 が呼び出されるリポジトリは「メイン リポジトリ」と呼ばれ、他のリポジトリは「外部リポジトリ」と呼ばれます。

リポジトリは、ルート ディレクトリにあるリポジトリ境界ファイル(MODULE.bazelREPO.bazel、または以前のコンテキストでは WORKSPACE または WORKSPACE.bazel)でマークされます。メイン リポジトリは、Bazel を呼び出すソースツリーです。外部リポジトリはさまざまな方法で定義されます。詳細については、外部依存関係の概要をご覧ください。

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

ビルドを実行する際は、ソースツリー全体をつなぎ合わせる必要があります。これは SymlinkForest によって行われます。これは、メイン リポジトリ内のすべてのパッケージを $EXECROOT に、外部リポジトリはすべて $EXECROOT/external または $EXECROOT/.. にシンボリック リンクします。

パッケージ

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

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

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

パッケージの読み込み中に複雑になる主な原因はグロブです。Bazel では、すべてのソースファイルを明示的にリストする必要はなく、代わりにグロブ(glob(["**/*.java"]) など)を実行できます。シェルとは異なり、サブディレクトリに降りる再帰的なグロブをサポートしています(サブパッケージには対応していません)。これにはファイル システムへのアクセスが必要ですが、処理が遅くなる可能性があるため、並列かつ可能な限り効率的に実行できるように、あらゆる種類の手法を実装しています。

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

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

Package クラス自体には、「外部」パッケージ(外部依存関係に関連する)の解析に排他的に使用され、実際のパッケージでは意味をなさないメンバーがいくつか含まれています。これは設計上の欠陥です。通常のパッケージを記述するオブジェクトには、他のものを説明するフィールドを含めるべきではありません。次のようなアクセサリーが含まれます。

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

理想的には、「外部」パッケージの解析と通常のパッケージの解析とをより分離することで、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 継承関係はありません。

Skyframe

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

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

Skyframe グラフをモニタリングする最も簡単な方法は、bazel dump --skyframe=deps を実行することです。これにより、1 行に 1 つの SkyValue としてグラフがダンプされます。サイズがかなり大きくなるため、小さなビルドで行うことをおすすめします。

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

特定の 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 の背後」にアドホック静的キャッシュを保持する)。複雑な SkyFunctions では、再起動と再起動の間の状態管理が複雑になる可能性があるため、SkyFunction 内の階層計算を一時停止および再開するフックなど、論理同時実行に対する構造化されたアプローチのために StateMachine が導入されました。例: DependencyResolver#computeDependencies は、getState()StateMachine を使用して、構成されたターゲットの直接依存関係の膨大なセットを計算します。この処理を行わないと、再起動に時間がかかり、コストが増大する可能性があります。

基本的に、Bazel ではこのような回避策が必要です。これは、数十万ものフライト中の Skyframe ノードが一般的であり、Java による軽量スレッドのサポートが 2023 年時点で StateMachine 実装よりも優れていないためです。

Starlark

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

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

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

  1. BUILD ファイル。ここで新しいビルド ターゲットを定義します。このコンテキストで実行される Starlark コードは、BUILD ファイル自体と、そのファイルによって読み込まれた .bzl ファイルの内容にのみアクセスできます。
  2. MODULE.bazel ファイル。ここで外部依存関係を定義します。このコンテキストで実行される Starlark コードは、事前定義されたいくつかのディレクティブにのみ非常に限定的にアクセスできます。
  3. .bzl ファイル。ここでは、新しいビルドルール、リポジトリ ルール、モジュール拡張機能が定義されます。この Starlark コードは、新しい関数を定義し、他の .bzl ファイルから読み込むことができます。

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 はより強力ですが、同時に、たとえば、時間または空間の複雑さが 2 次(またはそれ以上)のコードを記述したり、Java 例外で Bazel サーバーをクラッシュさせたり、不変条件に違反する(Options インスタンスが誤って変更されたり、構成済みのターゲットが変更可能になったりするなど)、「バッド シングス TM」を簡単に行うことができます。

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

構成

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

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

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

これにより、たとえば、リクエストされたテスト実行回数が変更された場合、テストターゲットにのみ影響するにもかかわらず、ビルド全体を再分析しなければならないなどの異常が発生します(このようなことが起こらないように構成を「トリム」する予定ですが、まだ準備ができていません)。

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

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

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

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

構成遷移は、次のような場合に使用されます。

  1. 特定の依存関係がビルド中に使用され、実行アーキテクチャでビルドされる必要があることを宣言する
  2. 特定の依存関係を複数のアーキテクチャ用にビルドする必要があることを宣言する(FAT 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 は非推奨です(まだ削除されていません)。TransitiveInfoProvider サブクラスには Starlark からアクセスできません。

構成済みターゲット

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

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

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

実行ファイル

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

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

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

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

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

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

アスペクト

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

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

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

  1. AspectClass は、アスペクトの実装です。Java または Starlark のいずれかです(Java の場合はサブクラス、Starlark の場合は StarlarkAspectClass のインスタンス)。RuleConfiguredTargetFactory に類似しています。
  2. AspectDefinition は、アスペクトの定義です。必要なプロバイダ、提供するプロバイダが含まれ、適切な AspectClass インスタンスなどの実装への参照が含まれます。RuleClass に似ています。
  3. AspectParameters は、依存関係グラフに下方に伝播されるアスペクトをパラメータ化する方法です。現在は文字列と文字列のマッピングです。これが役立つ理由の好例は、プロトコル バッファです。1 つの言語に複数の 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. toolchain() ルール: ツールチェーンがサポートする実行とターゲットの制約のセットを記述し、ツールチェーンの種類(C++ や Java など)を指定します(後者は toolchain_type() ルールで表されます)。
  2. 実際のツールチェーンを記述する言語固有のルール(cc_toolchain() など)

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

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

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

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

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

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

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

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

その後、ツールチェーンは 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 のサブクラス(Rule ではない)であり、Starlark(StarlarkLibrary.environmentGroup())でデフォルトで使用できる関数でもあります。この関数は最終的に同名のターゲットを作成します。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 の既知の長年の不正確な問題の 1 つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM プロパティによって有効にされる、ある程度機能する実装があります)。

注目すべき Artifact の種類は、仲介業者です。これらは、MiddlemanAction の出力である Artifact インスタンスで示されます。これらは 1 つの特殊なケースで使用されます。

  • Runfiles ミドルマンは、出力マニフェストと runfiles ツリーによって参照されるすべてのアーティファクトに個別に依存しなくても済むように、runfiles ツリーの存在を確保するために使用されます。

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

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

Bazel に既知のコンテンツを含むファイルを書き込むなど、その他の特殊なケースもあります。AbstractAction のサブクラスです。ほとんどのアクションは SpawnAction または StarlarkAction です(同じで、別々のクラスにする必要はありません)。ただし、Java と C++ には独自のアクション タイプ(JavaCompileActionCppCompileActionCppLinkAction)があります。

最終的には、すべてを SpawnAction に移行したいと考えています。JavaCompileAction は非常に近いですが、C++ は .d ファイルの解析と include スキャンが原因で、少し特殊なケースです。

アクション グラフのほとんどは SkyFrame グラフに「埋め込まれています」。概念的には、アクションの実行は ActionExecutionFunction の呼び出しとして表されます。アクション グラフの依存関係エッジから Skyframe 依存関係エッジへのマッピングについては、ActionExecutionFunction.getInputDeps()Artifact.key() で説明されています。Skyframe エッジの数を少なくするための最適化がいくつかあります。

  • 派生アーティファクトには独自の SkyValue がありません。代わりに、Artifact.getGeneratingActionKey() を使用して、鍵を生成するアクションの鍵を検索します。
  • ネストされたセットには独自の Skyframe キーがあります。

共有アクション

一部のアクションは、複数の構成されたターゲットによって生成されます。Starlark ルールは、派生アクションを構成とパッケージによって決定されたディレクトリにのみ配置できるため、より制限されています(それでも、同じパッケージ内のルールが競合する可能性があります)。Java で実装されたルールは、派生アーティファクトを任意の場所に配置できます。

これは誤った機能とみなされますが、これを取り除くことは非常に困難です。たとえば、ソースファイルをなんらかの方法で処理する必要があり、そのファイルを複数のルール(handwave-handwave)で参照する必要がある場合に、実行時間を大幅に短縮できるからです。これには RAM がいくらかかかります。共有アクションの各インスタンスは、個別にメモリに保存する必要があります。

2 つのアクションが同じ出力ファイルを生成する場合は、完全に同じにする必要があります。つまり、同じ入力、同じ出力を持ち、同じコマンドラインを実行します。この同等関係は Actions.canBeShared() に実装されており、すべての Action を確認することで、分析フェーズと実行フェーズ間で検証されます。これは SkyframeActionExecutor.findAndStoreArtifactConflicts() で実装されており、ビルドの「グローバル」ビューを必要とする Bazel の唯一の場所です。

実行フェーズ

この時点で、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.0 という)ヘッダー ファイルのみをスキャンします#include
  • アクションの実行中に一部のファイルが使用されなかったことが判明することがあります。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 を使用して、使用するツールのチェックサムを計算することで決定されます。これは、アクションのどの入力がツールの一部を表し、どの入力が入力を表すかを把握している必要があります。これは、Action: 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 に精通していない)とアスペクトです。これらは、「同じ」出力ファイルを生成できるものの空間に別の次元を追加します。

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

テスト

Bazel は、テストの実行を豊富にサポートしています。以下がサポートされます。

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

テストは、テストの実行方法を説明する TestProvider を持つ通常の構成済みのターゲットです。

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

実行するテストを決定する

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

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

次に、コマンドライン オプションに従って、サイズ、タグ、タイムアウト、言語でテストがフィルタされます。これは TestFilter に実装され、ターゲットの解析中に TargetPatternPhaseFunction.determineTests() から呼び出され、結果が TargetPatternPhaseValue.getTestsToRunLabels() に渡されます。フィルタできるルール属性を構成できない理由は、分析フェーズの前に発生するため、構成を使用できないためです。

これはさらに BuildView.createResult() で処理されます。分析に失敗したターゲットは除外され、テストは排他的テストと非排他的テストに分割されます。その後、AnalysisResult に格納されます。これにより、ExecutionTool は実行するテストを認識します。

この複雑なプロセスに透明性を持たせるために、tests() クエリ演算子(TestsFunction に実装)を使用して、コマンドラインで特定のターゲットが指定されたときに実行されるテストを指定できます。残念ながら再実装であるため、上記から微妙に逸脱している可能性があります。

テストの実行

テストを実行するには、キャッシュ ステータスのアーティファクトをリクエストします。これにより TestRunnerAction が実行され、最終的に --test_strategy コマンドライン オプションで選択された TestActionContext が呼び出され、リクエストされた方法でテストが実行されます。

テストは、環境変数を使用してテストに期待される内容を伝える、精巧なプロトコルに従って実行されます。Bazel によるテストと Bazel によるテスト内容について詳しくは、こちらをご覧ください。最も単純な場合、終了コード 0 は成功、それ以外は失敗を表します。

各テストプロセスは、キャッシュ ステータス ファイルに加えて、他の多くのファイルを出力します。これらは、「テストログ ディレクトリ」に配置されます。これは、ターゲット構成の出力ディレクトリの testlogs というサブディレクトリです。

  • test.xml: テストシャード内の個々のテストケースを詳細に記述した JUnit スタイルの XML ファイル
  • test.log: テストのコンソール出力。stdout と stderr は分離されていません。
  • test.outputs: 「未宣言の出力ディレクトリ」。ターミナルに出力するものに加えてファイルを出力するテストで使用されます。

テストの実行中に発生する可能性があるが、通常のターゲットのビルド中に発生しないことが 2 つあります。それは、排他的テスト実行と出力ストリーミングです。

一部のテストは、他のテストと並行して実行しないなど、排他的モードで実行する必要があります。テストルールに tags=["exclusive"] を追加するか、--test_strategy=exclusive を使用してテストを実行します。各排他的テストは、メインビルドの後にテストの実行をリクエストする個別の 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++ など)では、コンパイル時にカバレッジ インストルメンテーションが追加される言語もあれば、オンラインでインストルメンテーションを行う言語もあります。つまり、実行時にカバレッジ インストルメンテーションが追加される言語もあります。

もう一つのコアコンセプトはベースライン カバレッジです。これは、ライブラリ、バイナリ、またはテストでコードが実行されなかった場合のカバレッジです。この問題を解決するには、バイナリのテストカバレッジを計算する場合、バイナリにテストにリンクされていないコードが含まれている可能性があるため、すべてのテストのカバレッジを統合するだけでは不十分です。そのため、カバレッジを収集するファイルのみが含まれ、カバレッジ対象の行が含まれていないバイナリごとにカバレッジ ファイルを出力します。ターゲットのベースライン カバレッジ ファイルは bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat にあります。--nobuild_tests_only フラグを Bazel に渡すと、テストだけでなくバイナリとライブラリにも生成されます。

ベースライン カバレッジは現在、破損しています。

カバレッジ収集では、ルールごとに 2 つのファイル グループ(計測対象ファイルのセットと計測メタデータ ファイルのセット)を追跡します。

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

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

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

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

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

クエリエンジン

Bazel には、さまざまなグラフについてさまざまな質問を行うための小さな言語があります。次のクエリの種類が用意されています。

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

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

クエリの結果は、ラベル、ラベルとルールクラス、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 と呼ばれる)サーバーに接続して、イベントをストリーミングすることもできます。

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

外部リポジトリ

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

WORKSPACE ファイル

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

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

@foo という名前のリポジトリ内の利用可能な結果。複雑なのは、Starlark ファイルで新しいリポジトリ ルールを定義できることです。この新しいリポジトリ ルールは、新しい 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 によって処理されます。

リポジトリのマッピング

複数のリポジトリが同じリポジトリに依存する場合がありますが、バージョンが異なる場合があります(これは「ダイアモンド依存関係の問題」の例です)。たとえば、ビルド内の別々のリポジトリにある 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) などの解決に使用します)。
  • BzlLoadFunction: load() ステートメントでラベルを解決

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 によってキャッシュに保存されるイベントのみが実装します(ほとんどのイベントが実装していますが、強制ではありません)。

ターミナル出力は、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 には、Bazel を「ブラックボックス」として観察するテストと、分析フェーズのみを実行するテストの 2 種類があります。前者を「統合テスト」、後者を「単体テスト」と呼びますが、統合テストに近いものの、統合性が低いです。必要に応じて、実際の単体テストも用意しています。

インテグレーション テストには次の 2 種類があります。

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

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

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