Bazel コードベース

問題を報告 ソースを表示 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

はじめに

Bazel のコードベースは大きく(約 350,000 行の本番環境コードと約 260,000 行のテストコード)、全体を把握している人はいません。誰もが自分の担当領域をよく理解していますが、どの方向にも山の向こうにあるものを把握している人はほとんどいません。

途中で迷子になって、まっすぐな道を失うことがないように、このドキュメントでは、コードベースの概要を説明して、作業を開始しやすくしています。

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 でビルドしたものを実行することですが、ターミナルがないため、サーバー プロセスからは実行できません。代わりに、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 起動オプションを使用して変更できます。これは、ワークスペースで一度に実行できる 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 が呼び出されるリポジトリは「メイン リポジトリ」と呼ばれ、それ以外は「外部リポジトリ」と呼ばれます。

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

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

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

パッケージ

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

パッケージは互いに独立しています。あるパッケージの BUILD ファイルの変更によって、他のパッケージが変更されることはありません。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 継承関係はありません。

Skyframe

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

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

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

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

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

  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 インスタンスです。ただし実際には、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 で実装することもできます(ドキュメントはこちら)。

乗換案内情報プロバイダ

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

通常、Java の伝播情報プロバイダと Starlark の情報プロバイダは 1 対 1 で対応しています(例外は DefaultInfo です。これは FileProviderFilesToRunProviderRunfilesProvider の統合であり、Java の直接の翻字よりも Starlark に近い API と見なされています)。キーは次のいずれかです。

  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 ではファイル グループ ルールの output_group 属性を使用し、Java では OutputGroupInfo プロバイダを使用してアクセスできます。

実行ファイル

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

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

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

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

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

  • 入力 runfile マニフェスト。これは、runfiles ツリーのシリアル化された説明です。runfiles ツリーの内容のプロキシとして使用されます。Bazel は、マニフェストの内容が変更された場合にのみ runfiles ツリーが変更されると想定します。
  • 出力 runfiles マニフェスト。これは、実行ファイル ツリーを処理するランタイム ライブラリで使用されます。特に Windows では、シンボリック リンクがサポートされていない場合があります。
  • runfiles 中間者。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 は、依存関係グラフに下方に伝播されるアスペクトをパラメータ化する方法です。現在は文字列から文字列へのマップです。これが有用である理由の良い例として、プロトコル バッファがあります。言語に複数の 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. 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() を使用して読み込まれ、要求した構成済みのターゲットの実装で使用されます。

また、単一の「ホスト」構成があり、ターゲット構成が --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 のような)多数の開発者が関与する大規模なコードベースで作業している場合は、他の開発者が自分のコードに勝手に依存しないように気をつける必要があります。そうしないと、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 のインスタンスとして表されます。これらは、「アクション グラフ」と呼ばれる 2 分割の有向非巡回グラフに配置されます。

アーティファクトには、ソース アーティファクト(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 のみになるようにするためです(ネストされたセットに置き換えられます)。
  • 依存関係の仲介者をスケジュールすることで、あるアクションが別のアクションの前に実行されるようにします。主にリンティングに使用されますが、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 の唯一の場所です。

実行フェーズ

このタイミングで、Bazel が実際にビルド アクション(出力を生成するコマンドなど)の実行を開始します。

分析フェーズ後に Bazel が最初に行うことは、ビルドする必要があるアーティファクトを決定することです。このロジックは TopLevelArtifactHelper でエンコードされます。大まかに言えば、コマンドラインで構成されたターゲットの filesToBuild と、「このターゲットがコマンドライン上にある場合は、これらのアーティファクトをビルドする」ことを明示的に表現する特別な出力グループの内容です。

次のステップでは、実行ルートを作成します。Bazel には、ファイル システム内のさまざまな場所(--package_path)からソース パッケージを読み取るオプションがあるため、ローカルで実行されるアクションに完全なソースツリーを提供する必要があります。これは SymlinkForest クラスによって処理され、分析フェーズで使用されるすべてのターゲットをメモし、使用されたターゲットがある実際の場所からすべてのパッケージをシンボリック リンクする単一のディレクトリ ツリーを構築します。または、コマンドに正しいパスを渡すこともできます(--package_path を考慮します)。次の理由により、これは適切ではありません。

  • パッケージがパッケージパス エントリから別のもの(以前はよく出現していた)に移動すると、アクション コマンドラインが変更されます。
  • アクションをリモートで実行する場合とローカルで実行する場合とでは、コマンドラインが異なります。
  • 使用しているツールに固有のコマンドライン変換が必要です(Java クラスパスと C++ インクルードパスの違いなど)。
  • アクションのコマンドラインを変更すると、アクション キャッシュ エントリが無効になる
  • --package_path は段階的に非推奨に

次に、Bazel はアクション グラフ(アクションとその入力アーティファクトと出力アーティファクトで構成される 2 分割の有向グラフ)の走査とアクションの実行を開始します。各アクションの実行は、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 つの方法しかありません)。

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

注目すべき戦略の一つは、永続ワーカー プロセス(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 に精通していない)とアスペクトです。これらのルールにより、「同じ」出力ファイルを生成できるものが空間に広がります。

現在のアプローチでは、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 の異なるバージョン間でファイルを区別できるようになります。こうした違いを取り除き、そのうちの 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 が提供する拡張ポイントのセットは、やや漠然としています。優れた設計原則の例として使用しないでください。

イベントバス

BlazeModules が 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 はもともと、単一リポジトリ(ビルドに必要なものがすべて含まれる単一ソースツリー)で使用するように設計されていましたが、必ずしもそうではない世界で存在しています。「外部リポジトリ」は、この 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 つの Guava リポジトリ(@guava1// など)と、もう一方の Guava リポジトリ(@guava2// など)がもう一方のリポジトリを参照できるようになります。

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

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

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

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 ファイルを書き込むのに使用できるスクラッチ ファイル システムがあります。さまざまなヘルパー メソッドを使用して、構成されたターゲットをリクエストし、構成を変更し、分析結果についてさまざまなことをアサートできます。