Bazel コードベース

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

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

はじめに

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ctrl+C を押すと、クライアントはそれを gRPC 接続の Cancel 呼び出しに変換し、できるだけ早くコマンドを終了しようとします。3 回目の Ctrl-C の後、クライアントは代わりに SIGKILL をサーバーに送信します。

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

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

ディレクトリ レイアウト

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

「ワークスペース」は、Bazel が実行されるソースツリーです。通常、ソース コントロールからチェックアウトしたものに対応します。

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

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

「出力ベース」は、特定のワークスペースに接続された Bazel インスタンスが書き込む場所です。各出力ベースで実行されている Bazel サーバー インスタンスは 1 つだけです。通常は $OUTPUT_USER_ROOT/<checksum of the path to the workspace> にあります。これは、--output_base 起動オプションを使用して変更できます。これは、ワークスペースで一度に実行できる Bazel インスタンスが 1 つしかないという制限を回避する場合などに便利です。

出力ディレクトリには、次のようなファイルが含まれます。

  • フェッチされた外部リポジトリ($OUTPUT_BASE/external)。
  • 実行ルート。現在のビルドのすべてのソースコードへのシンボリック リンクを含むディレクトリ。$OUTPUT_BASE/execroot にあります。ビルド中、作業ディレクトリは $EXECROOT/<name of main repository> です。これを $EXECROOT に変更する予定ですが、互換性が非常に低い変更であるため、長期的な計画です。
  • ビルド中にビルドされたファイル。

コマンドの実行プロセス

Bazel サーバーが制御権を取得し、実行する必要があるコマンドについて通知されると、次の順序でイベントが発生します。

  1. BlazeCommandDispatcher に新しいリクエストが通知されます。コマンドが実行するワークスペースが必要かどうか(バージョンやヘルプなど、ソースコードと関係のないコマンドを除くほとんどのコマンド)、別のコマンドが実行されているかどうかを判断します。

  2. 正しいコマンドが見つかりました。各コマンドは BlazeCommand インターフェースを実装し、@Command アノテーションを付ける必要があります(これはアンチパターンです。コマンドで必要なすべてのメタデータが BlazeCommand のメソッドで記述されていると便利です)。

  3. コマンドライン オプションが解析されます。各コマンドには異なるコマンドライン オプションがあり、@Command アノテーションで説明されています。

  4. イベントバスが作成されます。イベントバスは、ビルド中に発生するイベントのストリーミングです。これらの一部は、ビルドの進行状況を通知するために、Build Event Protocol の傘下で Bazel の外部にエクスポートされます。

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

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

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

  8. 実行フェーズが実行されます。つまり、リクエストされた最上位のターゲットを構築するために必要なすべてのアクションが実行されます。

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

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

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

これらのオプションは、分析フェーズで使用するように設計されています(Java の RuleContext.getFragment() または Starlark の ctx.fragments を介して使用)。これらの一部(C++ インクルード スキャンを行うかどうかなど)は実行フェーズで読み取られますが、その場合は BuildConfiguration を使用できないため、明示的な配管が必要になります。詳細については、構成のセクションをご覧ください。

警告: OptionsBase インスタンスは不変であると見なして、そのように使用します(SkyKeys の一部など)。これは事実ではなく、変更すると、デバッグが難しい微妙な方法で Bazel が破損する可能性があります。残念ながら、実際に不変にすることは大きな作業です。(他の誰かが参照を保持する前に、また equals() または hashCode() が呼び出される前に、作成直後に FragmentOptions を変更することは問題ありません)。

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

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

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

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

Bazel が認識するソースツリー

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

リポジトリ

「リポジトリ」は、デベロッパーが作業するソースツリーです。通常は単一のプロジェクトを表します。Bazel の祖先である Blaze は、単一リポジトリ(ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリー)で動作していました。一方、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

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

構成されたターゲットの直接依存関係を決定するアルゴリズムは 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 プロバイダを使用してアクセスできます。

Runfiles

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

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

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

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

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

  • 入力 runfiles マニフェスト。これは、runfiles ツリーのシリアル化された説明です。これは、runfiles ツリーの内容のプロキシとして使用されます。Bazel は、マニフェストのコンテンツが変更された場合にのみ、runfiles ツリーが変更されると想定します。
  • 出力 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)を定義します。前順序、後順序、トポロジ(ノードは常にその祖先の後に来る)、および「無視、ただし毎回同じである必要があります」です。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

共有アクション

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

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

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

実行フェーズ

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

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

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

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

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

アクションの実行はコストが高いため、Skyframe の背後でヒットできるキャッシュのレイヤがいくつかあります。

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

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

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

このキャッシュは、メソッド ActionCacheChecker.getTokenIfNeedToExecute() を使用してヒットを確認します。

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

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

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

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

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

  • アクションは、実行前に新しい入力を発見したり、一部の入力が実際には必要ないと判断したりすることがあります。典型的な例は C++ です。C++ ファイルがその推移閉包から使用するヘッダー ファイルを推測して、すべてのファイルをリモート エグゼキュータに送信しないようにすることをおすすめします。そのため、すべてのヘッダー ファイルを「入力」として登録せず、ソースファイルをスキャンして、推移的に含まれるヘッダーのみを #include ステートメントで言及されている入力としてマークできます(完全な C プリプロセッサを実装しなくても済むように、過大評価しています)。このオプションは現在、Bazel で「false」にハードコードされており、Google でのみ使用されます。
  • アクションの実行中に一部のファイルが使用されなかったことが判明することがあります。C++ では、これは「.d ファイル」と呼ばれます。コンパイラは、どのヘッダー ファイルが使用されたかを事後に通知します。Make よりも増分性が低いという不利を回避するため、Bazel はこの事実を利用します。これはコンパイラに依存するため、インクルード スキャナよりも正確な推定値が得られます。

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

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

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

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

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

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

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

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

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

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

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

戦略(またはアクション コンテキスト)の詳細:

  • アクションの実行に関するさまざまな戦略については、こちらをご覧ください。
  • ローカルとリモートの両方でアクションを実行し、どちらが先に完了するかを確認する動的戦略については、こちらをご覧ください。
  • ローカルでアクションを実行する際の複雑さについては、こちらをご覧ください。

ローカル リソース管理者

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

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

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

出力ディレクトリの構造

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

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

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

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

これまでのところ、この問題を解決するための原則的な方法は見つかっておらず、これは構成のトリミングの問題と類似しています。オプションについて詳しくは、こちらをご覧ください。主な問題領域は、Starlark ルール(通常、作成者は Bazel に精通していない)とアスペクトです。これらは、「同じ」出力ファイルを生成できるものの空間に別の次元を追加します。

現在のアプローチでは、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 形式に変換し、単一のファイルに統合します。

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

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

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

ベースライン カバレッジは現在機能していません。

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

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

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

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

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

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

クエリエンジン

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

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

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

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

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

モジュール システム

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

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

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

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

イベントバス

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

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

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

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

外部リポジトリ

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

WORKSPACE ファイル

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

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

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

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

リポジトリの取得

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

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

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

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

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

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

マネージド ディレクトリ

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

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

リポジトリのマッピング

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

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

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

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

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

JNI ビット

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

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

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

コンソール出力

コンソール出力の出力は単純な作業のように思えますが、複数のプロセス(場合によってはリモート)の実行、きめ細かいキャッシュ、美しくカラフルなターミナル出力、長時間実行サーバーの要件が重なり、単純なものではありません。

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

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

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

一部の EventHandler 実装では、受信したイベントが保存されます。これは、キャッシュに保存されたさまざまな処理(キャッシュに保存された構成済みターゲットから出力された警告など)によって発生した情報を UI に再現するために使用されます。

一部の EventHandler では、最終的にイベントバスに到達するイベントの投稿も許可されます(通常の Event はそこに表示されません)。これらは ExtendedEventHandler の実装であり、キャッシュに保存された EventBus イベントを再生するために主に使用されます。これらの EventBus イベントはすべて Postable を実装していますが、EventBus に投稿されるすべてのイベントがこのインターフェースを実装しているわけではありません。ExtendedEventHandler によってキャッシュに保存されるイベントのみが実装します(ほとんどのイベントが実装していますが、強制ではありません)。

ターミナル出力は、UiEventHandler から出力されます。これは、Bazel が行うすべての高度な出力形式と進捗状況レポートを担当します。ほとんど次の 2 つの入力があります。

  • イベントバス
  • Reporter を介してパイプされたイベント ストリーム

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

Bazel のプロファイリング

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

Chrome プロファイラ形式でプロファイルを出力します。Chrome で表示することをおすすめします。データモデルはタスクスタックです。タスクの開始と終了が可能で、タスクは互いにきちんとネストされている必要があります。各 Java スレッドには独自のタスクスタックが割り当てられます。TODO: アクションと継続渡しスタイルとの関係

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

また、MemoryProfiler では基本的なメモリ プロファイリングも行います。また、常にオンであり、主に最大ヒープサイズと GC の動作を記録します。

Bazel のテスト

Bazel には、Bazel を「ブラックボックス」として観察するテストと、分析フェーズのみを実行するテストの 2 種類があります。前者は「統合テスト」、後者は「単体テスト」と呼びますが、統合テストに近い単体テストです。必要に応じて、実際の単体テストも用意しています。

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

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

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

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