Bazel コードベース

問題を報告 ソースを表示

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

はじめに

Bazel のコードベースは大きく(本番環境用コードが 350 KLOC 以下、テストコードが 260 KLOC 程度)、全体像に精通している人はいません。誰もが自分の谷をよく知っていますが、あらゆる方向に丘陵地帯に何があるのかを知っている人はほんのわずかです。

旅の途中で、わかりやすい道が失われ、森の中の暗い中に身を置いてしまわないように、このドキュメントではコードベースの概要を説明します。

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

コントリビューションは、通常の GitHub pull リクエスト メカニズムで受け入れられます。Google 社員が内部ソースツリーに手動でインポートしてから、GitHub に再エクスポートします。

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

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

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

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

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

各サーバー インスタンスには 1 つのソースツリー(ワークスペース)が関連付けられており、通常、各ワークスペースにはアクティブなサーバー インスタンスが 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 接続のキャンセル呼び出しに変換され、できるだけ早くコマンドの終了を試みます。3 回目の Ctrl-C の後、クライアントは代わりに SIGKILL をサーバーに送信します。

クライアントのソースコードは src/main/cpp にあり、サーバーとの通信に使用されるプロトコルは src/main/protobuf/command_server.proto にあります。

サーバーのメインのエントリ ポイントは BlazeRuntime.main() で、クライアントからの gRPC 呼び出しは GrpcServerImpl.run() によって処理されます。

ディレクトリ レイアウト

Bazel では、ビルド中にやや複雑なディレクトリ セットが作成されます。詳しくは、出力ディレクトリ レイアウトをご覧ください。

「workspace」は Bazel が実行されるソースツリーです。通常は、ソース管理からチェックアウトしたものに対応しています。

Bazel は、すべてのデータを「出力ユーザールート」に配置します。通常は $HOME/.cache/bazel/_bazel_${USER} ですが、--output_user_root 起動オプションを使用してオーバーライドできます。

「インストール ベース」は、Bazel が抽出される場所です。これは自動的に行われ、各 Bazel バージョンには、インストール ベースでチェックサムに基づいてサブディレクトリが作成されます。デフォルトでは $OUTPUT_USER_ROOT/install にありますが、--install_base コマンドライン オプションを使用して変更できます。

「出力ベース」は、特定のワークスペースにアタッチされている Bazel インスタンスが書き込みを行う場所です。各出力ベースでは常に 1 つの Bazel サーバー インスタンスしか実行されません。いつもは $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. イベントバスが作成されます。イベントバスは、ビルド中に発生するイベントのストリームです。これらの一部は、ビルドの進捗状況を伝えるために、ビルド イベント プロトコルに基づいて Bazel の外部にエクスポートされます。

  5. コマンドが制御を取得します。最も興味深いコマンドはビルドを実行するコマンドです(ビルド、テスト、実行、カバレッジなど)。この機能は BuildTool によって実装されています。

  6. コマンドラインのターゲット パターンのセットが解析され、//pkg:all//pkg/... などのワイルドカードが解決されます。これは AnalysisPhaseRunner.evaluateTargetPatterns() に実装され、Skyframe で TargetPatternPhaseValue として具体化されます。

  7. 読み込み/分析フェーズを実行して、アクション グラフ(ビルドで実行する必要があるコマンドの有向非巡回グラフ)を生成します。

  8. 実行フェーズが実行されます。つまり、リクエストされたトップレベル ターゲットのビルドに必要なすべてのアクションが実行されます。

コマンドライン オプション

Bazel 呼び出しのコマンドライン オプションは、OptionsParsingResult オブジェクトに記述されます。このオブジェクトには、「オプション クラス」からオプションの値へのマップが含まれています。「オプション クラス」は OptionsBase のサブクラスであり、相互に関連するコマンドライン オプションをグループ化します。例:

  1. プログラミング言語に関連するオプション(CppOptions または JavaOptions)。FragmentOptions のサブクラスにする必要がありますが、最終的に BuildOptions オブジェクトにラップされます。
  2. Bazel によるアクションの実行方法に関連するオプション(ExecutionOptions

これらのオプションは、分析フェーズで使用するように設計されています(Java の RuleContext.getFragment() または Starlark の ctx.fragments を使用)。その一部(たとえば、C++ にスキャンを含めるかどうか)は実行フェーズで読み取られますが、BuildConfiguration はそのときに使用できないため、常に明示的なプラミングが必要です。詳細については、「構成」のセクションをご覧ください。

警告: OptionsBase インスタンスが不変であると仮定して、その方法(SkyKeys の一部など)で使用します。これは誤りです。デバッグが困難な方法で Bazel を破る方法を変更します。残念ながら、それらを実際に不変にするにはかなりの労力がかかります。(作成直後の、他のユーザーが参照を保持する機会を得る前、かつ equals() または hashCode() が呼び出される前に FragmentOptions を変更しても問題ありません)。

Bazel は、次の方法でオプション クラスを学習します。

  1. いくつかは Bazel(CommonCommandOptions)に有線接続されています。
  2. 各 Bazel コマンドの @Command アノテーションから
  3. ConfiguredRuleClassProvider から(個々のプログラミング言語に関連するコマンドライン オプション)
  4. Starlark のルールでは、独自のオプションを定義することもできます(こちらをご覧ください)。

各オプション(Starlark 定義のオプションを除く)は、@Option アノテーションを持つ FragmentOptions サブクラスのメンバー変数です。このアノテーションは、コマンドライン オプションの名前とタイプ、およびヘルプテキストを指定します。

コマンドライン オプションの値の Java 型は通常、単純なもの(文字列、整数、ブール値、ラベルなど)です。ただし、より複雑な型のオプションもサポートしています。この場合、コマンドライン文字列からデータ型に変換する処理は、com.google.devtools.common.options.Converter の実装になります。

Bazel で表示されるソースツリー

Bazel は、ソースコードの読み取りと解釈によってソフトウェアのビルドを行います。Bazel が動作するソースコード全体は「ワークスペース」と呼ばれ、リポジトリ、パッケージ、ルールから構成されます。

リポジトリ

「リポジトリ」はデベロッパーが作業するソースツリーで、通常は単一のプロジェクトを表します。Bazel の祖先である Blaze は、monorepo 上で動作します。つまり、ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリーです。一方、Bazel は、ソースコードが複数のリポジトリにまたがるプロジェクトをサポートしています。Bazel が呼び出されたリポジトリは「メイン リポジトリ」と呼ばれ、他のリポジトリは「外部リポジトリ」と呼ばれます。

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

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

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

パッケージ

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

パッケージは互いに独立しています。パッケージの BUILD ファイルを変更しても、他のパッケージは変更されません。BUILD ファイルを追加または削除すると、他のパッケージを変更できます。再帰的な glob はパッケージの境界で停止し、BUILD ファイルが存在すると再帰が停止するためです。

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

パッケージ読み込みの複雑さの大部分は、グローバル化の問題です。Bazel では、すべてのソースファイルを明示的にリストする必要はありません。代わりに、glob(glob(["**/*.java"]) など)を実行できます。シェルとは異なり、サブディレクトリに下がる再帰的な glob がサポートされています(ただし、サブパッケージには格納されません)。これにはファイル システムへのアクセスが必要ですが、速度が遅くなる可能性があるため、Google では、可能な限り効率的に並行して実行されるように、あらゆる種類の手法を実装しています。

グローバル化は次のクラスで実装されます。

  • LegacyGlobber さん、スカイフレームには気づかないグロバーです
  • 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 の継承関係はありません。

スカイフレーム

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

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

構成

構成とは、ターゲットをビルドする方法、つまり、どのプラットフォーム、どのコマンドライン オプションを使用したかなどを指します。

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

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

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

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

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

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

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

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

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

構成の移行によって複数の構成が作成される場合、これをスプリット遷移といいます。

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

推移的情報プロバイダ

推移的情報プロバイダは、構成済みターゲットがそれに依存する他の構成済みターゲットについて通知するための方法(および唯一)です。その名前が「推移的」である理由は、通常、これが構成されたターゲットの推移的クロージャの一種のロールアップであるためです。

一般的に、Java の推移的情報プロバイダと Starlark の情報プロバイダの間には 1 対 1 の対応があります(例外は DefaultInfo で、これは FileProviderFilesToRunProviderRunfilesProvider を集約したものです。これは、この API は Java の直接文字変換よりも Starlark 風であると判断されたためです)。それらのキーは次のいずれかです。

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

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

構成済みのターゲット

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

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

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

実行ファイル

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

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

  • ほとんどの場合、ファイルの runfiles パスは、その execpath と同じです。RAM を節約するために使用します。
  • ランファイル ツリーには古い種類のエントリがあり、それらも表す必要があります。

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

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

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

さまざまな側面

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

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

依存関係グラフに伝播される一連の要素は、Attribute.Builder.aspects() 関数を使用して属性ごとに指定されます。このプロセスに参加するクラスの中には、名前がわかりにくいものがいくつかあります。

  1. AspectClass はアスペクトの実装です。Java(この場合はサブクラス)または Starlark(この場合は StarlarkAspectClass のインスタンス)のいずれかです。これは RuleConfiguredTargetFactory に似ています。
  2. AspectDefinition はアスペクトの定義です。必要なプロバイダ、提供するプロバイダ、実装への参照(適切な AspectClass インスタンスなど)が含まれます。これは RuleClass と同じです。
  3. AspectParameters は、依存関係グラフに伝播される側面をパラメータ化する方法です。現在は文字列と文字列をマッピングしています。これが有用である理由の好例は、プロトコル バッファです。言語に複数の API がある場合、プロトコル バッファをビルドする API に関する情報を依存関係グラフに伝播する必要があります。
  4. Aspect は、依存関係グラフに伝播する側面を計算するために必要なすべてのデータを表します。これは、アスペクト クラス、その定義、パラメータで構成されます。
  5. RuleAspect は、特定のルールを伝播する要素を決定する関数です。Rule -> Aspect 関数です。

やや予想外に複雑になるのは、各側面が他の側面に関連している可能性があることです。たとえば、Java IDE のクラスパスを収集するある側面では、クラスパス上のすべての .jar ファイルを把握する必要があるかもしれませんが、その一部はプロトコル バッファです。その場合、IDE のアスペクトは、(proto_library ルール + Java proto アスペクト)ペアに関連付けられます。

アスペクトの複雑さは、クラス AspectCollection に取り込まれます。

プラットフォームとツールチェーン

Bazel はマルチプラットフォーム ビルドをサポートしています。つまり、ビルド アクションが実行される複数のアーキテクチャと、コードがビルドされる複数のアーキテクチャが存在するビルドです。Bazel の用語では、このようなアーキテクチャをプラットフォームと呼びます(詳細なドキュメントはこちら)。

プラットフォームは、制約設定(「CPU アーキテクチャ」のコンセプトなど)から制約値(x86_64 などの特定の CPU など)への Key-Value マッピングで記述されます。@platforms リポジトリには、よく使用される制約の設定と値の「辞書」があります。

ツールチェーンのコンセプトは、ビルドが実行されるプラットフォームとターゲット プラットフォームによっては、異なるコンパイラの使用が必要になる場合があります。たとえば、特定の C++ ツールチェーンを特定の OS で実行し、他の一部の OS をターゲットにできる場合があります。Bazel は、セット実行とターゲット プラットフォームに基づいて、使用する C++ コンパイラを決定する必要があります(ツールチェーンに関するドキュメントはこちら)。

このために、ツールチェーンには、サポートする実行とターゲット プラットフォームの制約のセットでアノテーションが付けられます。そのために、ツールチェーンの定義は以下の 2 つの部分に分かれています。

  1. 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 などのさまざまな構成フラグで表される 1 つの「ホスト」構成とターゲット構成に依存しているレガシー システムもあります。Google では現在、上記のシステムに段階的に移行しています。以前の構成値に依存しているケースに対処するために、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 のサブクラスであり、RuleEnvironmentGroup)ではなく、Starlark からデフォルトで使用できる関数(StarlarkLibrary.environmentGroup())で、最終的にこれは、各環境で所属する環境グループを宣言する必要があり、各環境グループでデフォルト環境を宣言する必要があるため、循環的な依存関係が発生しないようにするためです。

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

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

プラットフォームの制約

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

公開設定

多数のデベロッパーがいる大規模なコードベースで作業している場合は(Google など)、他のユーザーが勝手にコードに依存しないように注意する必要があります。そうしないと、ヒラムの法則に従い、実装の詳細と見なされる動作に依存するようになります。

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

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

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

ネストされたセット

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

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

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

この問題を回避するために、NestedSet のコンセプトを考案しました。これは、他の NestedSet インスタンスとそのメンバーから構成されるデータ構造であり、セットの有向非巡回グラフを形成します。これらは不変であり、そのメンバーは反復処理が可能です。複数の反復順序(NestedSet.Order)を定義します。すなわち、preorder、postorder、トポロジ(ノードは常に祖先の後に来る)、「Don't Care, ただし、毎回同じであるべき」です。

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

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

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

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

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

ソース アーティファクトをツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできない根本的な理由はありません。まだ実装していないだけです(ただし、BUILD ファイル内のソース ディレクトリの参照は、Bazel で古くから知られているいくつかの既知の問題の一つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM プロパティで有効になるような実装があります)。

重要な種類の Artifact は仲介者です。これは、MiddlemanAction の出力である Artifact インスタンスによって示されます。次のように、特別なケースに使用されます。

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

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

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

また、Bazel に認識されているコンテンツを含むファイルを作成するなど、他にもいくつかの特殊なケースがあります。これらは AbstractAction のサブクラスです。Java と C++ には独自のアクション タイプ(JavaCompileActionCppCompileActionCppLinkAction)がありますが、ほとんどのアクションは SpawnAction または StarlarkAction です(これらも同じですが、間違いなく別々のクラスにしないでください)。

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

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

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

共有アクション

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

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

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

実行フェーズ

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

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

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

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

次に、Bazel はアクション グラフ(アクションとその入出力アーティファクトで構成される二部グラフ、有向グラフ)の走査とアクションの実行を開始します。各アクションの実行は、SkyValue クラスの ActionExecutionValue のインスタンスで表されます。

アクションの実行にはコストがかかるため、Skyframe の背後でヒットできるキャッシュ保存レイヤがいくつか用意されています。

  • ActionExecutionFunction.stateMap には、ActionExecutionFunction の Skyframe の再起動を低く抑えるためのデータが含まれています。
  • ローカル アクション キャッシュには、ファイル システムの状態に関するデータが含まれます。
  • 通常、リモート実行システムには独自のキャッシュも含まれている

ローカル アクションのキャッシュ

このキャッシュは、Skyframe の背後にある別のレイヤです。アクションが Skyframe で再実行されても、ローカル アクション キャッシュでヒットする可能性があります。ローカル ファイル システムの状態を表し、ディスクにシリアル化されています。つまり、新しい Bazel サーバーを起動すると、Skyframe グラフが空であってもローカル アクション キャッシュ ヒットが発生する可能性があります。

このキャッシュのヒットは、ActionCacheChecker.getTokenIfNeedToExecute() メソッドを使用してチェックされます。

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

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

まだ開発中の非常に試験運用版の「トップダウン アクション キャッシュ」もあります。これは、推移的ハッシュを使用して、キャッシュが何度もアクセスされないようにしています。

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

アクションの中には、単なる入力セットよりも複雑なものもあります。アクションの入力セットの変更には、次の 2 つの形式があります。

  • アクションは、実行前に新しい入力を発見することや、一部の入力が実際に不要であると判断することがあります。C の標準的な例は C++ です。C++ ファイルがその推移的なクロージャからどのヘッダー ファイルを使用するかについて知識に基づいて推測することをおすすめします。これにより、すべてのファイルをリモート エグゼキュータに送信することを考慮する必要はありません。そのため、すべてのヘッダー ファイルを「入力」として登録するのではなく、ソースファイルをスキャンして、ヘッダー ファイル全体を「#include」で過大評価する必要があります。
  • アクションの実行中に、一部のファイルが使用されなかったことが判明することがあります。C++ ではこれを「.d ファイル」と呼びます。コンパイラは、どのヘッダー ファイルが後で使用されたかを伝えます。また、Make よりもインクリメンタリティが悪くするという問題を避けるため、Bazel はこの事実を利用します。この方法はコンパイラに依存するため、インクルード スキャナよりも適切な見積もりが得られます。

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

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

アクション キャッシュが新しいアクション インスタンスでヒットを返すと(サーバーの再起動後に作成された場合など)、Bazel は updateInputs() 自体を呼び出すため、入力の検出とプルーニングの結果が入力のセットに反映されます。

Starlark アクションでは、この機能を利用して、ctx.actions.run()unused_inputs_list= 引数を使用して一部の入力を未使用として宣言できます。

アクションを実行するさまざまな方法: Strategies/ActionContexts

一部のアクションはさまざまな方法で実行できます。たとえば、コマンドラインはローカルに実行できますが、さまざまな種類のサンドボックスにおいて、またはリモートで実行できます。これを具体化するコンセプトは、ActionContext(または、名前の変更の半分までしか行っていないため、Strategy)と呼ばれます。

アクション コンテキストのライフサイクルは次のとおりです。

  1. 実行フェーズが開始されると、BlazeModule インスタンスは、どのようなアクション コンテキストを持っているかを尋ねられます。これは ExecutionTool のコンストラクタで行われます。アクション コンテキスト タイプは、ActionContext のサブインターフェースとアクション コンテキストが実装する必要があるインターフェースを参照する Java Class インスタンスによって識別されます。
  2. 使用可能なアクション コンテキストから適切なアクション コンテキストが選択され、ActionExecutionContextBlazeExecutor に転送されます。
  3. アクションは、ActionExecutionContext.getContext()BlazeExecutor.getStrategy() を使用してコンテキストをリクエストします(これを行う方法は 1 つのみです)。

戦略は、他の戦略を呼び出してジョブを実行することもできます。たとえば、ローカルとリモートの両方でアクションを開始し、先に終了した方を使用する動的戦略で使用されます。

注目すべき戦略の 1 つが永続的なワーカー プロセス(WorkerSpawnStrategy)の実装です。一部のツールでは起動時間が長いため、アクションごとに新たに開始するのではなく、アクション間で再利用する必要があります(Bazel は、個々のリクエスト間で監視可能な状態を保持しないというワーカー プロセスの約束に依存しているため、これは正確性の問題の可能性になります)。

ツールが変更された場合は、ワーカー プロセスを再起動する必要があります。ワーカーを再利用できるかどうかは、WorkerFilesHash を使用して使用するツールのチェックサムを計算することによって決定されます。これは、アクションのどの入力がツールの一部を表し、どの入力が入力を表すかに依存します。これはアクションの作成者が決定し、Spawn の実行ファイルはツールの一部としてカウントされます。Spawn.getToolFiles()

戦略(またはアクション コンテキスト)について:

  • アクションを実行するためのさまざまな戦略については、こちらをご覧ください。
  • ローカルとリモートの両方でアクションを実行し、最初に完了する方を確認する動的戦略については、こちらをご覧ください。
  • ローカルで実行するアクションの詳細については、こちらをご覧ください。

ローカル リソース マネージャー

Bazel は、多数のアクションを並行して実行できます。並行して実行する必要があるローカル アクションの数はアクションごとに異なります。つまり、アクションに必要なリソースが多いほど、ローカルマシンの過負荷を避けるために、同時に実行するインスタンスの数は少なくなります。

これは ResourceManager クラスに実装されています。各アクションには、必要なローカル リソースの推定値(ResourceSet インスタンス(CPU と RAM))を付ける必要があります。次に、アクション コンテキストがローカル リソースを必要とする操作を行うと、ResourceManager.acquireResources() が呼び出され、必要なリソースが使用可能になるまでブロックされます。

ローカル リソース管理の詳細については、こちらをご覧ください。

出力ディレクトリの構造

各アクションは、出力を配置する出力ディレクトリ内に個別の場所を必要とします。通常、派生アーティファクトの場所は次のとおりです。

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

特定の構成に関連付けられているディレクトリの名前はどのように決定されますか。2 つの望ましいプロパティが競合しています。

  1. 同じビルドで 2 つの構成が発生する可能性がある場合は、別々のディレクトリを作成して、どちらも同じアクションの独自のバージョンを用意する必要があります。同じ出力ファイルを生成するアクションのコマンドラインなどで 2 つの構成が一致しない場合、Bazel はどちらのアクションを選択するべきか判断できません(「アクションの競合」)
  2. 2 つの構成が「ほぼ」同じものを表す場合は、どちらかで実行されたアクションを他方で再利用できるように、同じ名前を付ける必要があります。たとえば、Java コンパイラのコマンドライン オプションを変更しても、C++ コンパイル アクションは再実行されないようにします。

これまでのところ、この問題を解決するための原則的な方法は考えていません。これは、構成トリミングの問題と類似しています。オプションの詳細については、こちらをご覧ください。主な問題となるのは、作成者は通常 Bazel に詳しくない Starlark ルールと、「同じ」出力ファイルを生成する可能性のある領域に別の側面を追加する要素です。

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

テスト

Bazel は、テスト実行を豊かにサポートしています。サポート対象:

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

テストは通常の構成のターゲットであり、テストの実行方法を記述した TestProvider があります。

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

実行するテストの決定

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

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

次に、コマンドライン オプションに従って、サイズ、タグ、タイムアウト、言語でテストがフィルタリングされます。これは 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: 「宣言されていない出力ディレクトリ」。ターミナルに出力する内容に加えて、ファイルを出力するテストで使用されます。

テスト実行中は、通常のターゲットのビルドでは実行できないことがあります。1 つは排他的テスト実行、もう 1 つは出力ストリーミングです。

一部のテストは、他のテストと並行して実行しないなど、排他モードで実行する必要があります。これは、tags=["exclusive"] をテストルールに追加するか、--test_strategy=exclusive を使用してテストを実行することで導出できます。各排他テストは、「main」ビルドの後にテストの実行をリクエストする個別の Skyframe 呼び出しによって実行されます。これは SkyframeExecutor.runExclusiveTest() に実装されています。

アクションの終了時にターミナル出力がダンプされる通常のアクションとは異なり、ユーザーはテストの出力をストリーミングするようにリクエストできます。これにより、長時間実行テストの進捗状況が通知されます。これは --test_output=streamed コマンドライン オプションで指定され、異なるテストの出力が混在しないように、排他的なテスト実行を示唆します。

これは StreamedTestOutput クラスに実装されており、対象のテストの test.log ファイルに対する変更をポーリングし、Bazel がルールに基づいているターミナルに新しいバイトをダンプします。

実行されたテストの結果は、さまざまなイベント(TestAttemptTestResultTestingCompleteEvent など)を監視することで、イベントバスで確認できます。ビルドイベント プロトコルにダンプされ、AggregatingTestListener によってコンソールに出力されます。

カバレッジの収集

カバレッジは、テストによって LCOV 形式のファイルで報告されます。bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

カバレッジを収集するため、各テスト実行は collect_coverage.sh というスクリプトでラップされています。

このスクリプトは、テスト環境をセットアップしてカバレッジの収集を有効にし、カバレッジ ランタイムによってカバレッジ ファイルを書き込む場所を決定します。その後、テストを実行します。テスト自体が複数のサブプロセスを実行し、複数の異なるプログラミング言語で記述された部分(別のカバレッジ収集ランタイム)で構成されることもあります。ラッパー スクリプトは、必要に応じて結果のファイルを LCOV 形式に変換して 1 つのファイルにマージします。

collect_coverage.sh の挿入はテスト戦略によって行われ、テストの入力に collect_coverage.sh が必要です。この処理は、構成フラグ --coverage_support の値に解決される暗黙的な属性 :coverage_support によって実現されます(TestConfiguration.TestOptions.coverageSupport を参照)。

オフライン インストルメンテーションを行う言語もあります。つまり、カバレッジ インストルメンテーションはコンパイル時に追加されます(C++ など)。その他の言語はオンライン インストルメンテーションを追加します。つまり、カバレッジ インストルメンテーションは実行時に追加されます。

もう 1 つの基本コンセプトは、ベースライン カバレッジです。これは、コードが実行されなかった場合のライブラリ、バイナリ、またはテストのカバレッジです。これによって解決される問題は、バイナリのテスト カバレッジを計算する際に、すべてのテストのカバレッジをマージするだけでは不十分であることです。バイナリには、どのテストにもリンクされていないコードが存在する可能性があるためです。したがって、Google は、カバレッジを収集するファイルのみを含み、カバーされた行がないすべてのバイナリについて、カバレッジ ファイルを出力します。ターゲットのベースライン カバレッジ ファイルは 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 に渡され、返される結果に対して呼び出されます。

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

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

モジュール システム

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

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

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

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

イベントバス

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

  • ビルドするビルド ターゲットのリストが確定しました(TargetParsingCompleteEvent
  • トップレベル構成が確定しました(BuildConfigurationEvent
  • ターゲットのビルドが成功または失敗した(TargetCompleteEvent
  • テストが実行されました(TestAttemptTestSummary

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

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

外部リポジトリ

Bazel は元々、monorepo(ビルドに必要なすべてを含む単一のソースツリー)で使用するように設計されていましたが、Bazel は必ずしもそうなるとは限りません。「外部リポジトリ」は、これら 2 つの領域をつなぐために使用される抽象化機能です。ビルドには必要だが、メインのソースツリーにはないコードを表します。

WORKSPACE ファイル

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

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

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

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

リポジトリの取得

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

リポジトリを取得する手順は次のとおりです。

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

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

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

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

マネージド ディレクトリ

外部リポジトリは、ワークスペースのルート下にあるファイルを変更しなければならない場合があります(ダウンロードしたパッケージがソースツリーのサブディレクトリに格納されるパッケージ マネージャーなど)。これは、Bazel が、ソースファイルをユーザーによってのみ変更し、それ自体では変更せず、パッケージがワークスペース ルートの下のすべてのディレクトリを参照できるようにするという仮定とは相反します。このような外部リポジトリを機能させるために、Bazel は 2 つのことを行います。

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

リポジトリのマッピング

複数のリポジトリが同じリポジトリに、異なるバージョンで依存しようとする場合があります(これは「ダイヤモンド依存関係の問題」のインスタンスです)。たとえば、ビルド内の別々のリポジトリにある 2 つのバイナリが Guava に依存する必要がある場合、どちらも @guava// で始まるラベルで Guava を参照し、異なるバージョンを意味すると想定します。

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

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

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

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

JNI ビット

Bazel のサーバーは、ほとんど Java で記述されています。例外は、Java だけでは実行できない部分、または実装時に Java 単独では実行できない部分です。これは主に、ファイル システム、プロセス制御、その他のさまざまな下位レベルの要素とのやり取りに限定されます。

C++ コードは src/main/native にあります。ネイティブ メソッドを持つ Java クラスは次のとおりです。

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

コンソール出力

コンソール出力の出力は簡単そうに思えますが、複数のプロセス(場合によってはリモートで)の実行、きめ細かいキャッシュ保存、色鮮やかなターミナル出力を行いたい、長時間稼働するサーバーを用意したいといった要素が重なれば、簡単なことではありません。

クライアントから RPC 呼び出しが到着した直後に、2 つの RpcOutputStream インスタンス(stdout と stderr 用)が作成され、そこに出力されたデータがクライアントに転送されます。これらは OutErr((stdout、stderr)ペア)にラップされます。コンソールに出力する必要があるすべてのものは、これらのストリームを通過します。その後、これらのストリームは BlazeCommandDispatcher.execExclusively() に引き継がれます。

デフォルトでは、出力は ANSI エスケープ シーケンスで出力されます。これらが望ましくない場合(--color=no)、AnsiStrippingOutputStream によって除去されます。さらに、System.outSystem.err はこれらの出力ストリームにリダイレクトされます。これは、System.err.println() を使用してデバッグ情報を出力しても、最終的にはクライアントのターミナル出力(サーバーの出力とは異なります)に表示されるためです。プロセスがバイナリ出力(bazel query --output=proto など)を生成する場合、stdout の変更は行われません。

短いメッセージ(エラー、警告など)は、EventHandler インターフェースを通じて表現されます。これらは、EventBus に投稿する内容とは異なります(混乱を招くものです)。各 Event には EventKind(エラー、警告、情報など)があり、Location(ソースコード内でイベントを発生させた場所)が含まれている場合があります。

一部の EventHandler 実装では、受け取ったイベントが保存されます。これは、さまざまな種類のキャッシュされた処理によって引き起こされた情報を UI にリプレイするために使用されます。たとえば、キャッシュに保存された構成済みのターゲットによって出力された警告などです。

一部の EventHandler では、最終的にイベントバスに向かうイベントを送信することができます(通常の Event は表示されません)。これらは ExtendedEventHandler の実装であり、主に、キャッシュに保存された EventBus イベントを再生するために使用されます。これらの EventBus イベントはすべて Postable を実装しますが、EventBus に送信されたすべてが必ずしもこのインターフェースを実装しているわけではありません。ExtendedEventHandler によってキャッシュに保存されたイベントのみを実装します(これは素晴らしい処理であり、ほとんどの処理が行われますが、強制適用されません)。

ターミナル出力は主に UiEventHandler を介して出力されます。これは、Bazel による高度な出力フォーマットと進行状況レポートを担当します。入力は 2 つあります。

  • イベントバス
  • Reporter からパイプで渡されるイベント ストリーム

コマンド実行マシン(Bazel の他の部分など)がクライアントへの RPC ストリームに直接接続するのは、Reporter.getOutErr() 経由のみです。これにより、これらのストリームに直接アクセスできます。コマンドで大量のバイナリデータをダンプする必要がある場合にのみ使用されます(bazel query など)。

Bazel のプロファイリング

Bazel は高速です。Bazel も、ビルドが許容範囲の端まで大きくなる傾向があるため、速度が遅くなります。このため、Bazel には、ビルドと Bazel 自体のプロファイリングに使用できるプロファイラが含まれています。これは、Profiler という名前がちょうどよいクラスに実装されています。デフォルトで有効になっていますが、オーバーヘッドが許容されるように要約データのみが記録されます。コマンドライン --record_full_profiler_data を使用すると、できる限りすべてを記録できます。

プロファイルが Chrome プロファイラ形式で出力されます。Chrome での表示に適しています。 このデータモデルはタスクスタックのものです。タスクの開始と終了ができ、それらは相互に整然とネストされています。各 Java スレッドは独自のタスクスタックを取得します。TODO: アクションや連続受け渡しスタイルではどのように機能しますか?

プロファイラは、それぞれ BlazeRuntime.initProfiler()BlazeRuntime.afterCommand() で開始および停止され、すべてをプロファイリングできるように、可能な限り長く稼働しようとします。プロファイルに何かを追加するには、Profiler.instance().profile() を呼び出します。Closeable を返します。このクロージャはタスクの終了を表します。try-with-resources ステートメントで使用することをおすすめします。

MemoryProfiler では、基本的なメモリ プロファイリングも行います。また、常にオンになっており、ほとんどの場合、最大ヒープサイズと GC 動作が記録されます。

Bazel のテスト

Bazel には、主に 2 種類のテストがあります。1 つは Bazel を「ブラック ボックス」として確認するテストで、もう 1 つは分析フェーズのみを実行するテストです。前者を「統合テスト」、後者を「単体テスト」と呼んでいますが、どちらかというと統合テストのようなものです。実際の単体テストも用意されています。

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

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

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

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