Bazel コードベース

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 起動オプションを使用して変更できます。これは特に、任意のワークスペースで一度に 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. コマンドが制御を取得します。最も興味深いコマンドは、ビルドを実行するコマンド(ビルド、テスト、実行、カバレッジなど)です。この機能は 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 は、単一リポジトリ(ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリー)で動作していました。一方、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: 高速で、Skyframe を認識しないグローバー
  • SkyframeHybridGlobber: Skyframe を使用して、「Skyframe の再起動」(後述)を回避するために以前の globber に戻すバージョンです。

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

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

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

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

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

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

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

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

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

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

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

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

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

Skyframe

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

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 は強力ですが、同時に、時間や空間の複雑さが二次的(またはそれ以上)のコードを書いたり、Java 例外で Bazel サーバーをクラッシュさせたり、不変条件に違反したり(Options インスタンスを誤って変更したり、構成済みのターゲットを変更可能にしたり)するなど、Bad Things™ を起こしやすくなります。

構成されたターゲットの直接的な依存関係を決定するアルゴリズムは、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 で実装することもできます(こちらのドキュメントを参照)。

乗換案内情報プロバイダ

伝播情報プロバイダは、構成済みのターゲットが、そのターゲットに依存する他の構成済みターゲットについて情報を提供する方法(唯一の方法)です。名前に「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 は非推奨となっており(まだ削除できていません)、Starlark から TransitiveInfoProvider サブクラスにアクセスすることはできません。

構成済みのターゲット

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

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

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

Runfiles

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

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

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

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

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

  • 入力 runfiles マニフェスト。これは、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(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. ツールチェーンがサポートする実行制約とターゲット制約のセットを記述し、ツールチェーンの種類(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() を使用して実際に読み込まれ、リクエストした構成済みターゲットの実装で使用されます。

また、単一の「ホスト」構成があり、ターゲット構成が --cpu などのさまざまな構成フラグで表されるレガシー システムもあります。Google は上記のシステムに段階的に移行しています。以前の設定値に依存しているケースに対応するため、以前のフラグと新しいスタイルのプラットフォーム制約を変換するプラットフォーム マッピングを実装しました。コードは 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 のサブクラス(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)を定義します。前順序、後順序、トポロジ(ノードは常に祖先の後に来る)、および「指定しないが、毎回同じである必要があります」です。

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

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

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

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

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

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

注目すべき Artifact の種類は、仲介業者です。これらは、MiddlemanAction の出力である Artifact インスタンスによって示されます。特別なケースを処理するために使用されます。

  • 集約ミドルマンは、アーティファクトをグループ化するために使用されます。これは、多くのアクションが同じ大規模な入力セットを使用する場合、N × M の依存関係エッジではなく、N+M のみになるようにするためです(ネストされたセットに置き換えられます)。
  • 依存関係の仲介者をスケジュールすることで、あるアクションが別のアクションの前に実行されるようにします。主にリンティングに使用されますが、C++ コンパイルに使用されることもあります(詳細については CcCompilationContext.createMiddleman() をご覧ください)。
  • 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 はアクション グラフ(アクションとその入力アーティファクトと出力アーティファクトで構成される 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 つの方法しかありません)。

戦略は、他の戦略を呼び出して処理を行うことができます。これは、ローカルとリモートの両方でアクションを開始し、どちらが先に完了するかを判断する動的戦略で使用されます。

注目すべき戦略の 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 に精通していない)とアスペクトです。これらは、「同じ」出力ファイルを生成できるものの空間に別の次元を追加します。

現在のアプローチでは、構成のパスセグメントが <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 を参照)。

一部の言語ではオフライン計測が行われます(つまり、カバレッジ計測がコンパイル時に追加されます)。他の言語ではオンライン計測が行われます(つまり、カバレッジ計測が実行時に追加されます)。

もう一つのコアコンセプトはベースライン カバレッジです。これは、ライブラリ、バイナリ、またはテストのコードが実行されていない場合のカバレッジです。この問題を解決するには、バイナリのテストカバレッジを計算する場合、バイナリにテストにリンクされていないコードが含まれている可能性があるため、すべてのテストのカバレッジを統合するだけでは不十分です。そのため、すべてのバイナリに対してカバレッジ ファイルが出力されます。このファイルには、カバレッジ対象のファイルのみが含まれ、カバーされている行はありません。ターゲットのベースライン カバレッジ ファイルは 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 が提供する拡張ポイントのセットは、やや漠然としています。これを優れた設計原則の例として使用しないでください。

イベントバス

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

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

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

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

外部リポジトリ

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

WORKSPACE ファイル

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

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

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

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

リポジトリの取得

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

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

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

リポジトリの取得は非常に高コストになる可能性があるため、キャッシュにはさまざまなレイヤがあります。

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

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

マネージド ディレクトリ

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

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

リポジトリのマッピング

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

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

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

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

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

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

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

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

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