Bazel コードベース

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

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

はじめに

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

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

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

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

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

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

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

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

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

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

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 は、単一リポジトリ(ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリー)で動作していました。一方、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(["**/*.java"]) など)を実行できます。シェルとは異なり、サブディレクトリに降りる再帰的なグロブをサポートしています(サブパッケージには対応していません)。これにはファイル システムへのアクセスが必要ですが、処理が遅くなる可能性があるため、並列かつ可能な限り効率的に実行できるように、あらゆる種類の手法を実装しています。

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

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

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

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

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

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

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

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

読み込み/分析フェーズ

読み込み / 分析フェーズでは、特定のルールのビルドに必要なアクションが Bazel によって決定されます。その基本単位は「構成されたターゲット」です。これはよく使われ、(ターゲットと構成)のペアになります。

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

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

コマンドラインでリクエストされた構成済みターゲットの推移閉包内の各構成済みターゲットは、下から上(まずリーフノード、次にコマンドライン上のノード)に分析する必要があります。構成された単一のターゲットの分析への入力は次のとおりです。

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

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

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

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

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

構成

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

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

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

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

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

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

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

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

次のような構成の遷移が使用される。

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

構成遷移の結果として複数の構成が作成される場合、それは分割遷移と呼ばれます。

構成の遷移は Starlark で実装することもできます(こちらのドキュメントを参照)。

乗換案内情報プロバイダ

伝播情報プロバイダは、構成済みのターゲットが、そのターゲットに依存する他の構成済みターゲットについて情報を提供する方法(唯一の方法)です。名前に「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 の filegroup ルールの output_group 属性と、Java の OutputGroupInfo プロバイダを使用してアクセスできます。

実行ファイル

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

一連の runfile は 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 などのさまざまな構成フラグで表されるレガシー システムもあります。Google は上記のシステムに段階的に移行しています。以前の設定値に依存しているケースに対応するため、以前のフラグと新しいスタイルのプラットフォーム制約を変換するプラットフォーム マッピングを実装しました。コードは PlatformMappingFunction にあり、Starlark 以外の「小さな言語」を使用しています。

制約

ターゲットが少数のプラットフォームとのみ互換性を持つように指定したい場合があります。Bazel には、この目的を達成するためのメカニズムが複数あります(残念ながら)。

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

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

environment_group() と environment()

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

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

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

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

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

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

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

プラットフォームの制約

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

公開設定

多くのデベロッパーが参加する大規模なコードベース(Google など)で作業する場合は、他のすべての人がコードに恣意的に依存しないように注意する必要があります。そうでない場合、Hyrum の法則に従い、実装の詳細と見なしていた動作にユーザーが依存するようになります。

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

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

  • RuleVisibility インターフェースは可視性の宣言を表します。定数(完全に公開または完全に非公開)またはラベルのリストにできます。
  • ラベルは、パッケージ グループ(パッケージの事前定義リスト)、パッケージ(//pkg:__pkg__)、パッケージのサブツリー(//pkg:__subpackages__)のいずれかを参照できます。これは、//pkg:* または //pkg/... を使用するコマンドライン構文とは異なります。
  • パッケージ グループは、独自のターゲット(PackageGroup)と構成済みターゲット(PackageGroupConfiguredTarget)として実装されます。必要に応じて、これらを単純なルールに置き換えることができます。ロジックは、PackageSpecification//pkg/... などの単一のパターンに対応)、PackageGroupContents(単一の package_grouppackages 属性に対応)、PackageSpecificationProviderpackage_group とその伝播 includes を集約)を使用して実装されます。
  • 公開設定ラベルリストから依存関係への変換は、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 の既知の長年の不正確な問題の 1 つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM プロパティによって有効にされる、ある程度機能する実装があります)。

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

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

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

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

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

最終的には、すべてを SpawnAction に移動しようとします。JavaCompileAction はかなり近いことですが、C++ は .d ファイルの解析とインクルード スキャンのため少し特殊なケースです。

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

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

共有アクション

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

これは誤動作と見なされますが、ソースファイルを何らかの方法で処理する必要があるときに、そのファイルが複数のルールによって参照される場合(handwave-handwave)は、実行時間が大幅に短縮されるため、この問題を解消するのは非常に困難です。ただし、共有アクションの各インスタンスはメモリに個別に保存されるため、RAM の使用量が増えます。

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

実行フェーズ

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

現在のアプローチでは、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++ など)では、コンパイル時にカバレッジ インストルメンテーションが追加される言語もあれば、オンラインでインストルメンテーションを行う言語もあります。つまり、実行時にカバレッジ インストルメンテーションが追加される言語もあります。

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

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

ルールごとにカバレッジ収集のために 2 つのファイル グループ(インストルメント化されたファイルのセットとインストルメンテーション メタデータ ファイルのセット)を追跡します。

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

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

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

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

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

クエリエンジン

Bazel には、さまざまなグラフに関するさまざまな質問に使用できる小さな言語があります。次のクエリの種類が用意されています。

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

これらはそれぞれ、AbstractBlazeQueryEnvironment をサブクラス化することで実装されます。QueryFunction のサブクラス化により、追加のクエリ関数を作成できます。クエリ結果のストリーミングを可能にするために、結果をデータ構造に収集するのではなく、query2.engine.CallbackQueryFunction に渡します。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 コードの読み込みに使用できます。この新しい Starlark コードは、新しいリポジトリ ルールの定義に使用できます。

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

リポジトリの取得

リポジトリのコードが 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// リポジトリを使用するように両方のリポジトリを再マッピングできます。

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

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

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

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

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

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