このドキュメントでは、コードベースと 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 実行可能ファイル(「クライアント」)が制御を取得します。次の手順で適切なサーバー プロセスを設定します。
- すでに解凍されているかどうかを確認します。そうでない場合は、それが実行されます。これがサーバーの実装の由来です。
- 動作しているアクティブなサーバー インスタンスがあるかどうかを確認します。実行中であり、正しい起動オプションがあり、正しいワークスペース ディレクトリを使用しています。実行中のサーバーは、サーバーがリッスンしているポートのロックファイルがあるディレクトリ
$OUTPUT_BASE/server
を調べて見つけます。 - 必要に応じて、古いサーバー プロセスを終了します。
- 必要に応じて、新しいサーバー プロセスを起動する
適切なサーバー プロセスの準備が整うと、実行する必要があるコマンドが 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 サーバーが制御権を取得し、実行する必要があるコマンドについて通知されると、次の順序でイベントが発生します。
BlazeCommandDispatcher
に新しいリクエストが通知されます。コマンドが実行するワークスペースが必要かどうか(バージョンやヘルプなど、ソースコードと関係のないコマンドを除くほとんどのコマンド)、別のコマンドが実行されているかどうかを判断します。正しいコマンドが見つかりました。各コマンドは
BlazeCommand
インターフェースを実装し、@Command
アノテーションを付ける必要があります(これはアンチパターンです。コマンドで必要なすべてのメタデータがBlazeCommand
のメソッドで記述されていると便利です)。コマンドライン オプションが解析されます。各コマンドには異なるコマンドライン オプションがあり、
@Command
アノテーションで説明されています。イベントバスが作成されます。イベントバスは、ビルド中に発生するイベントのストリーミングです。ビルドの進め方を世界に伝えるために、その一部は Build Event Protocol に基づいて Bazel の外部にエクスポートされます。
コマンドが制御を取得します。最も興味深いコマンドは、ビルドを実行するコマンド(build、test、run、coverage など)です。この機能は
BuildTool
によって実装されます。コマンドラインのターゲット パターンのセットが解析され、
//pkg:all
や//pkg/...
などのワイルドカードが解決されます。これはAnalysisPhaseRunner.evaluateTargetPatterns()
で実装され、Skyframe でTargetPatternPhaseValue
として再実体化されます。読み込み / 分析フェーズが実行され、アクション グラフ(ビルドのために実行する必要があるコマンドの有向非巡回グラフ)が生成されます。
実行フェーズが実行されます。つまり、リクエストされたトップレベル ターゲットのビルドに必要なすべてのアクションが実行されます。
コマンドライン オプション
Bazel 呼び出しのコマンドライン オプションは OptionsParsingResult
オブジェクトで記述されます。このオブジェクトには、「オプションクラス」からオプションの値へのマップが含まれています。「オプションクラス」は OptionsBase
のサブクラスであり、相互に関連するコマンドライン オプションをグループ化します。例:
- プログラミング言語(
CppOptions
またはJavaOptions
)に関連するオプション。これらはFragmentOptions
のサブクラスである必要があり、最終的にはBuildOptions
オブジェクトにラップされます。 - Bazel によるアクションの実行方法に関連するオプション(
ExecutionOptions
)
これらのオプションは、分析フェーズで使用するように設計されています(Java の RuleContext.getFragment()
または Starlark の ctx.fragments
を介して使用)。これらの一部(C++ インクルード スキャンを行うかどうかなど)は実行フェーズで読み取られますが、その場合は BuildConfiguration
を使用できないため、明示的な配管が必要になります。詳細については、構成のセクションをご覧ください。
警告: OptionsBase
インスタンスは不変であると見なして使用するのが一般的です(SkyKeys
の一部など)。しかし、これは事実ではなく、変更すると、デバッグが難しい微妙な方法で Bazel が破損する可能性があります。残念ながら、実際に不変にすることは大きな作業です。(他の誰かが参照を保持する前に、また equals()
または hashCode()
が呼び出される前に、作成直後に FragmentOptions
を変更することは問題ありません)。
Bazel は、次の方法でオプションクラスを学習します。
- 一部は Bazel にハードコードされています(
CommonCommandOptions
) - 各 Bazel コマンドの
@Command
アノテーションから ConfiguredRuleClassProvider
(個々のプログラミング言語に関連するコマンドライン オプション)- Starlark ルールでは、独自のオプションを定義することもできます(こちらを参照)。
各オプション(Starlark で定義されたオプションを除く)は、@Option
アノテーションを持つ FragmentOptions
サブクラスのメンバー変数です。このアノテーションは、コマンドライン オプションの名前と型、およびヘルプテキストを指定します。
コマンドライン オプションの値の Java 型は通常、単純な型(文字列、整数、ブール値、ラベルなど)です。ただし、より複雑な型のオプションもサポートしています。この場合、コマンドライン文字列からデータ型への変換の役割は com.google.devtools.common.options.Converter
の実装です。
Bazel が認識するソースツリー
Bazel はソフトウェアをビルドするビジネスで、ソースコードの読み取りと解釈によって行われます。Bazel が動作するソースコード全体は「ワークスペース」と呼ばれ、リポジトリ、パッケージ、ルールで構成されています。
リポジトリ
「リポジトリ」はデベロッパーが作業するソースツリーであり、通常は単一のプロジェクトを表します。Bazel の祖先である Blaze は、単一リポジトリ(ビルドの実行に使用されるすべてのソースコードを含む単一のソースツリー)で動作していました。一方、Bazel は、ソースコードが複数のリポジトリにまたがるプロジェクトをサポートしています。Bazel が呼び出されるリポジトリは「メイン リポジトリ」と呼ばれ、他のリポジトリは「外部リポジトリ」と呼ばれます。
リポジトリは、ルート ディレクトリにあるリポジトリ境界ファイル(MODULE.bazel
、REPO.bazel
、または以前のコンテキストでは WORKSPACE
または WORKSPACE.bazel
)でマークされます。メイン リポジトリは、Bazel を呼び出すソースツリーです。外部リポジトリはさまざまな方法で定義されます。詳細については、外部依存関係の概要をご覧ください。
外部リポジトリのコードは、$OUTPUT_BASE/external
にシンボリック リンクまたはダウンロードされます。
ビルドを実行する際は、ソースツリー全体をつなぎ合わせる必要があります。これは SymlinkForest
によって行われます。これは、メイン リポジトリ内のすべてのパッケージを $EXECROOT
に、外部リポジトリはすべて $EXECROOT/external
または $EXECROOT/..
にシンボリック リンクします。
パッケージ
すべてのリポジトリは、パッケージ、関連ファイルのコレクション、依存関係の仕様で構成されています。これらは、BUILD
または BUILD.bazel
というファイルで指定されます。両方が存在する場合、Bazel は BUILD.bazel
を優先します。BUILD
ファイルをそのまま使用できる理由は、Bazel の祖先である Blaze がこのファイル名を使用しているためです。しかし、これは、特にファイル名が大文字と小文字を区別しない Windows で、よく使用されるパス セグメントであることがわかりました。
パッケージは互いに独立しています。あるパッケージの BUILD
ファイルの変更によって、他のパッケージが変更されることはありません。BUILD
ファイルの追加または削除によって他のパッケージが変更される可能性があります。これは、再帰的なグロブがパッケージ境界で停止するため、BUILD
ファイルが存在すると再帰が停止するためです。
BUILD
ファイルの評価は「パッケージの読み込み」と呼ばれます。これは PackageFactory
クラスに実装されており、Starlark インタープリタを呼び出して動作します。利用可能なルールクラスのセットに関する知識が必要です。パッケージの読み込みの結果は Package
オブジェクトです。ほとんどの場合、文字列(ターゲットの名前)からターゲット自体へのマップです。
パッケージの読み込み中に複雑になる主な原因はグロブです。Bazel では、すべてのソースファイルを明示的にリストする必要はなく、代わりにグロブ(glob(["**/*.java"])
など)を実行できます。シェルとは異なり、サブディレクトリに降りる再帰的なグロブをサポートしています(サブパッケージには対応していません)。これにはファイル システムへのアクセスが必要ですが、処理が遅くなる可能性があるため、並列かつ可能な限り効率的に実行できるように、あらゆる種類の手法を実装しています。
グロビングは次のクラスに実装されています。
LegacyGlobber
は、スカイフレームをまったく知らない、高速で快適な地球儀です。SkyframeHybridGlobber
: Skyframe を使用し、「Skyframe の再起動」を回避するために以前のグローバーに戻すバージョン(後述)
Package
クラス自体には、(外部依存関係に関連する)「外部」パッケージの解析にのみ使用され、実際のパッケージには意味のないメンバーがいくつか含まれています。これは設計上の欠陥です。通常のパッケージを記述するオブジェクトに、他のものを記述するフィールドを含めるべきではありません。次のようなアクセサリーが含まれます。
- リポジトリのマッピング
- 登録されたツールチェーン
- 登録された実行プラットフォーム
理想的には、「外部」パッケージの解析と通常のパッケージの解析とをより分離することで、Package
が両方のニーズに対応する必要がなくなります。残念ながら、この 2 つは非常に深く絡み合っているため、分離することは困難です。
ラベル、ターゲット、ルール
パッケージはターゲットで構成されます。ターゲットには次のタイプがあります。
- ファイル: ビルドの入力または出力となるもの。Bazel の用語では、これらをアーティファクト(別の場所で説明)と呼びます。ビルド中に作成されるファイルはすべてターゲットではありません。Bazel の出力にラベルが関連付けられていないことはよくあります。
- ルール: 入力から出力を導出する手順を記述します。通常、プログラミング言語(
cc_library
、java_library
、py_library
など)に関連付けられますが、言語に依存しないものもあります(genrule
、filegroup
など)。 - パッケージ グループ: 公開設定セクションで説明します。
ターゲットの名前はラベルと呼ばれます。ラベルの構文は @repo//pac/kage:name
です。ここで、repo
はラベルがあるリポジトリの名前、pac/kage
は BUILD
ファイルがあるディレクトリ、name
はパッケージのディレクトリからの相対パス(ラベルがソースファイルを参照する場合)です。コマンドラインでターゲットを参照する場合は、ラベルの一部を省略できます。
- リポジトリを省略すると、ラベルはメイン リポジトリにあると見なされます。
- パッケージ部分(
name
や:name
など)が省略されている場合、ラベルは現在の作業ディレクトリのパッケージ内にあると見なされます(アップレベル参照(..)を含む相対パスは使用できません)。
ルールの一種(「C++ ライブラリ」など)は「ルールクラス」と呼ばれます。ルールクラスは、Starlark(rule()
関数)または Java(いわゆる「ネイティブ ルール」、型 RuleClass
)のいずれかで実装できます。長期的には、すべての言語固有のルールが Starlark で実装されますが、一部のレガシー ルール ファミリー(Java や C++ など)は、当面は引き続き Java で実装されます。
Starlark ルールクラスは、load()
ステートメントを使用して BUILD
ファイルの先頭でインポートする必要がありますが、Java ルールクラスは ConfiguredRuleClassProvider
に登録されているため、Bazel によって「本質的に」認識されます。
ルールクラスには、以下のような情報が含まれます。
- 属性(
srcs
、deps
など): 型、デフォルト値、制約など。 - 各属性に関連付けられた構成の遷移とアスペクト(存在する場合)
- ルールの実装
- ルールが「通常」作成する参照情報プロバイダ
用語に関する注: コードベースでは、ルールクラスによって作成されたターゲットを表すために「ルール」という用語がよく使用されます。しかし、Starlark とユーザー向けドキュメントでは、「Rule」はルールクラス自体を参照するためにのみ使用する必要があり、ターゲットは単なる「ターゲット」です。また、RuleClass
の名前に「class」が含まれていても、ルールクラスとそのタイプのターゲットの間に Java 継承関係はありません。
Skyframe
Bazel の基盤となる評価フレームワークは Skyframe です。このモデルでは、ビルド中にビルドする必要があるすべてのものが、任意のデータからその依存関係(つまり、その構築に必要な他のデータ)に向くエッジを持つ有向非循環グラフに編成されます。
グラフ内のノードは SkyValue
と呼ばれ、その名前は SkyKey
と呼ばれます。どちらも完全に不変です。これらのオブジェクトからアクセスできるのは、不変のオブジェクトのみです。この不変性はほとんどの場合保持されます。保持されない場合(BuildConfigurationValue
とその SkyKey
のメンバーである個々のオプション クラス BuildOptions
など)は、変更しないように、または外部から検出できない方法でのみ変更するようにしています。したがって、Skyframe 内で計算されるすべてのもの(構成されたターゲットなど)も不変である必要があります。
Skyframe グラフをモニタリングする最も簡単な方法は、bazel dump
--skyframe=deps
を実行することです。これにより、1 行に 1 つの SkyValue
としてグラフがダンプされます。サイズがかなり大きくなるため、小さなビルドで行うことをおすすめします。
スカイフレームは com.google.devtools.build.skyframe
パッケージにあります。同様の名前のパッケージ com.google.devtools.build.lib.skyframe
には、Skyframe 上に Bazel の実装が含まれています。Skyframe の詳細については、こちらをご覧ください。
特定の SkyKey
を SkyValue
に評価するために、Skyframe はキーの型に対応する SkyFunction
を呼び出します。関数の評価中に、SkyFunction.Environment.getValue()
のさまざまなオーバーロードを呼び出して、Skyframe から他の依存関係をリクエストすることがあります。これにより、これらの依存関係が Skyframe の内部グラフに登録されるという副作用があります。これにより、依存関係のいずれかが変更されたときに、Skyframe は関数を再評価することを認識します。つまり、Skyframe のキャッシュと増分計算は、SkyFunction
と SkyValue
の粒度で動作します。
利用できない依存関係を SkyFunction
がリクエストした場合、getValue()
は常に null を返します。関数は、自身が null を返すことで、制御を Skyframe に戻す必要があります。しばらくすると、Skyframe は使用できない依存関係を評価し、関数を最初から再開します。このときのみ、getValue()
呼び出しが null 以外の結果で成功します。
その結果、再起動前に SkyFunction
内で実行された計算を繰り返す必要があります。ただし、依存関係 SkyValues
の評価に行われた作業は含まれません。これはキャッシュに保存されます。そのため、通常は以下の方法でこの問題を回避します。
getValuesAndExceptions()
を使用して依存関係を一括で宣言し、再起動回数を制限する。SkyValue
を異なるSkyFunction
によって計算される個別の部分に分割し、個別に計算してキャッシュに保存できるようにします。メモリ使用量が増加する可能性があるため、戦略的に行う必要があります。- 再起動間での状態の保存(
SkyFunction.Environment.getState()
を使用するか、「Skyframe の背後」にアドホック静的キャッシュを保持する)。
通常、何十万もの処理中の Skyframe ノードがあり、Java は軽量スレッドをサポートしていないため、このような回避策が必要です。
Starlark
Starlark は、Bazel の構成と拡張に使用されるドメイン固有の言語です。これは、タイプがはるかに少なく、制御フローに制限が加えられ、最も重要なことに、強力な不変性が保証され、同時読み取りを可能にする Python の制限付きサブセットとして設計されています。チューリング完全ではないため、一部のユーザー(全員ではない)は、この言語で一般的なプログラミング タスクを実行することをためらう場合があります。
Starlark は net.starlark.java
パッケージに実装されています。こちらにも独立した Go 実装があります。Bazel で使用されている Java 実装は現在、インタープリタです。
Starlark は、次のような複数のコンテキストで使用されます。
BUILD
ファイル。ここで新しいビルド ターゲットを定義します。このコンテキストで実行される Starlark コードは、BUILD
ファイル自体と、そのファイルによって読み込まれた.bzl
ファイルの内容にのみアクセスできます。MODULE.bazel
ファイル。ここで外部依存関係を定義します。このコンテキストで実行される Starlark コードは、事前定義されたいくつかのディレクティブにのみ非常に限定的にアクセスできます。.bzl
ファイル。ここで、新しいビルドルール、リポジトリ ルール、モジュール拡張機能を定義します。この Starlark コードでは、新しい関数を定義し、他の.bzl
ファイルから読み込むことができます。
BUILD
ファイルと .bzl
ファイルで使用できる方言は、表現内容が異なるため若干異なります。相違点の一覧については、こちらをご覧ください。
Starlark について詳しくは、こちらをご覧ください。
読み込み/分析フェーズ
読み込み / 分析フェーズでは、特定のルールのビルドに必要なアクションが Bazel によって決定されます。基本単位は「構成済みターゲット」です。これは、(ターゲット、構成)ペアです。
「読み込み/分析フェーズ」と呼ばれるのは、以前はシリアル化されていた 2 つの部分に分割できるため、これらは時間的に重なり合う可能性があります。
- パッケージを読み込む。つまり、
BUILD
ファイルをパッケージを表すPackage
オブジェクトに変換する。 - 構成されたターゲットを分析する、つまりルールの実装を実行してアクション グラフを生成する
コマンドラインでリクエストされた構成済みターゲットの推移閉包内の各構成済みターゲットは、下から上(まずリーフノード、次にコマンドライン上のノード)に分析する必要があります。構成された単一のターゲットの分析への入力は次のとおりです。
- 構成。(そのルールをビルドする「方法」。たとえば、ターゲット プラットフォームだけでなく、ユーザーが C++ コンパイラに渡すコマンドライン オプションなど)
- 直接的な依存関係。分析対象のルールで、その参照情報プロバイダを使用できます。このように呼ばれるのは、構成されたターゲットの推移閉包内の情報を「ロールアップ」するためです(クラスパス上のすべての .jar ファイルや、C++ バイナリにリンクする必要があるすべての .o ファイルなど)。
- ターゲット自体。これは、ターゲットが含まれているパッケージを読み込んだ結果です。ルールの場合、これには属性が含まれます。通常は重要になります。
- 構成されたターゲットの実装。ルールの場合は、Starlark または Java のいずれかです。ルール以外の構成済みターゲットはすべて Java で実装されています。
構成されたターゲットの分析の出力は次のようになります。
- これに依存するターゲットを構成した伝播情報プロバイダは、
- 作成できるアーティファクトと、それらを生成するアクション。
Java ルールに提供される API は RuleContext
です。これは、Starlark ルールの ctx
引数と同等です。API は強力ですが、同時に、時間や空間の複雑さが二次的(またはそれ以上)のコードを書いたり、Java 例外で Bazel サーバーをクラッシュさせたり、不変条件に違反したり(Options
インスタンスを誤って変更したり、構成済みのターゲットを変更可能にしたり)するなど、Bad Things™ を起こしやすくなります。
構成されたターゲットの直接依存関係を決定するアルゴリズムは DependencyResolver.dependentNodeMap()
にあります。
構成
構成とは、どのプラットフォームに対して、どのコマンドライン オプションを使用するのかなど、ターゲットをビルドする「方法」のことです。
同じターゲットを、同じビルド内の複数の構成に対してビルドできます。これは、ビルド中に実行されるツールとターゲット コードに同じコードが使用され、クロスコンパイルを行う場合や、大規模な Android アプリ(複数の CPU アーキテクチャのネイティブ コードを含むアプリ)をビルドする場合などに便利です。
概念的には、構成は BuildOptions
インスタンスです。ただし、実際には、BuildOptions
は、さまざまな追加機能を提供する BuildConfiguration
でラップされます。依存関係グラフの上部から下部に伝播されます。変更された場合は、ビルドを再分析する必要があります。
その結果、たとえば、リクエストされたテスト実行の数がテスト ターゲットにのみ影響する場合でも、ビルド全体を再分析しなければならなくなるといった異常が発生します(Google では構成を「トリミング」する計画がありますが、まだ準備ができていません)。
ルールの実装で構成の一部が必要な場合は、RuleClass.Builder.requiresConfigurationFragments()
を使用して定義で宣言する必要があります。これは、(Java フラグメントを使用する Python ルールなど)間違いを回避するためと、構成のトリミングを容易にするためです。これにより、Python オプションが変更された場合でも、C++ ターゲットを再分析する必要がなくなります。
ルールの構成は、必ずしも「親」ルールの構成と同じではありません。依存関係エッジで構成を変更するプロセスは「構成遷移」と呼ばれます。次の 2 か所で発生する可能性があります。
- 依存関係エッジ。これらの遷移は
Attribute.Builder.cfg()
で指定され、Rule
(遷移が発生する場所)とBuildOptions
(元の構成)から 1 つ以上のBuildOptions
(出力構成)への関数です。 - 構成されたターゲットへの任意の受信エッジ。これらは
RuleClass.Builder.cfg()
で指定されます。
関連するクラスは TransitionFactory
と ConfigurationTransition
です。
構成遷移は、次のような場合に使用されます。
- 特定の依存関係がビルド中に使用され、実行アーキテクチャでビルドされる必要があることを宣言する
- 特定の依存関係を複数のアーキテクチャ用にビルドする必要があることを宣言する(ファットな Android APK のネイティブ コードなど)
構成遷移の結果として複数の構成が作成される場合、それは分割遷移と呼ばれます。
構成の遷移は Starlark で実装することもできます(こちらのドキュメントを参照)。
乗換案内情報プロバイダ
伝播情報プロバイダは、構成済みのターゲットが、そのターゲットに依存する他の構成済みターゲットについて情報を提供する方法(唯一の方法)です。名前に「transitive」が含まれているのは、通常、構成されたターゲットの推移閉包の一種のロールアップであるためです。
通常、Java の推移的情報プロバイダと Starlark の間では 1 対 1 の対応関係があります(例外は DefaultInfo
で、FileProvider
、FilesToRunProvider
、RunfilesProvider
を結合したものです。この API は Java の直接文字変換よりも Starlark 風にしていると見なされていたためです)。キーは次のいずれかです。
- Java クラス オブジェクト。これは、Starlark からアクセスできないプロバイダでのみ使用できます。これらのプロバイダは
TransitiveInfoProvider
のサブクラスです。 - 文字列。これは古い方法であり、名前が競合しやすいため、この方法はおすすめできません。このような伝播情報プロバイダは、
build.lib.packages.Info
の直接サブクラスです。 - プロバイダのシンボル。これは、
provider()
関数を使用して Starlark から作成できます。新しいプロバイダを作成する場合には、この方法をおすすめします。このシンボルは、Java ではProvider.Key
インスタンスで表されます。
Java で実装する新しいプロバイダは、BuiltinProvider
を使用して実装する必要があります。NativeProvider
は非推奨です(まだ削除されていません)。TransitiveInfoProvider
サブクラスには Starlark からアクセスできません。
構成済みのターゲット
構成されたターゲットは RuleConfiguredTargetFactory
として実装されます。Java で実装されたルールクラスごとにサブクラスがあります。Starlark で構成されたターゲットは StarlarkRuleConfiguredTargetUtil.buildRule()
で作成されます。
構成されたターゲット ファクトリーは、RuleConfiguredTargetBuilder
を使用して戻り値を構築する必要があります。構成要素は次のとおりです。
filesToBuild
は、「このルールが表すファイルのセット」というあいまいな概念です。これらは、構成されたターゲットがコマンドラインまたは genrule の srcs にある場合にビルドされるファイルです。- ランファイル(通常とデータ)。
- 出力グループ。これらは、ルールがビルドできるさまざまな「他のファイルセット」です。これらのファイルには、BUILD の filegroup ルールの output_group 属性と、Java の
OutputGroupInfo
プロバイダを使用してアクセスできます。
実行ファイル
一部のバイナリでは、実行するためにデータファイルが必要です。入力ファイルを必要とするテストはその一例です。これは Bazel では「runfiles」のコンセプトで表されます。「runfiles ツリー」は、特定のバイナリのデータファイルのディレクトリ ツリーです。出力ツリーのソースにあるファイルを指す個々のシンボリック リンクを含むシンボリック ツリーとして、ファイル システムに作成されます。
実行ファイルのセットは Runfiles
インスタンスとして表されます。概念的には、runfiles ツリー内のファイルのパスから、それを表す Artifact
インスタンスへのマップです。次の 2 つの理由から、単一の Map
よりも複雑になります。
- ほとんどの場合、ファイルの runfiles パスは execpath と同じです。これにより、RAM を節約できます。
- runfiles ツリーにはさまざまなレガシー エントリがあり、それらも表現する必要があります。
実行ファイルは RunfilesProvider
を使用して収集されます。このクラスのインスタンスは、構成されたターゲット(ライブラリなど)とその推移的クロージャが必要なランファイルを表し、ネストされたセットのように収集されます(実際は、ネストされたセットを使用して実装されます)。各ターゲットは依存関係のランファイルを結合し、独自のランファイルをいくつか追加し、その結果セットを依存関係グラフで上に送信します。RunfilesProvider
インスタンスには 2 つの Runfiles
インスタンスが含まれます。1 つは「data」属性を介してルールが依存している場合に、もう 1 つはその他の受信依存関係用です。これは、データ属性を介して依存している場合とそうでない場合で、ターゲットが異なるランファイルを提示することがあるためです。これは望ましくない以前の動作であり、まだ削除されていません。
バイナリのランファイルは RunfilesSupport
のインスタンスとして表されます。これは Runfiles
とは異なります。RunfilesSupport
は実際にビルドできる機能があるためです(Runfiles
は単なるマッピングです)。これには、次の追加コンポーネントが必要です。
- 入力 runfile マニフェスト。これは、runfiles ツリーのシリアル化された説明です。runfiles ツリーの内容のプロキシとして使用されます。Bazel は、マニフェストの内容が変更された場合にのみ runfiles ツリーが変更されると想定します。
- 出力 runfiles マニフェスト。これは、ランファイル ツリーを処理するランタイム ライブラリで使用されます。特に、シンボリック リンクをサポートしていない場合がある Windows で使用されます。
- runfiles ミドルマン。runfiles ツリーを存在させるには、シンボリック リンク ツリーと、シンボリック リンクが参照するアーティファクトをビルドする必要があります。依存関係エッジの数を減らすには、runfiles ミドルマンを使用してこれらをすべて表すことができます。
RunfilesSupport
オブジェクトが表す実行ファイルを実行するためのコマンドライン引数。
アスペクト
アスペクトは、「計算を依存関係グラフに伝播」する方法です。Bazel のユーザー向けの説明については、こちらをご覧ください。プロトコル バッファがその一例です。proto_library
ルールは特定の言語を認識する必要はありませんが、任意のプログラミング言語でプロトコル バッファ メッセージ(プロトコル バッファの「基本単位」)の実装をビルドする場合は、proto_library
ルールに関連付ける必要があります。これにより、同じ言語の 2 つのターゲットが同じプロトコル バッファに依存している場合、そのプロトコル バッファは 1 回だけビルドされます。
構成済みターゲットと同様に、Skyframe では SkyValue
として表され、構成方法は構成済みターゲットのビルド方法と非常によく似ています。RuleContext
にアクセスできる ConfiguredAspectFactory
というファクトリクラスがありますが、構成済みターゲット ファクトリとは異なり、接続されている構成済みターゲットとそのプロバイダも認識します。
依存関係グラフに伝播される一連のアスペクトは、Attribute.Builder.aspects()
関数を使用して各属性に指定します。このプロセスには、名前が紛らわしいクラスがいくつかあります。
AspectClass
は、アスペクトの実装です。Java または Starlark のいずれかです(Java の場合はサブクラス、Starlark の場合はStarlarkAspectClass
のインスタンス)。RuleConfiguredTargetFactory
に類似しています。AspectDefinition
は、アスペクトの定義です。必要なプロバイダ、提供するプロバイダが含まれ、適切なAspectClass
インスタンスなどの実装への参照が含まれます。RuleClass
に似ています。AspectParameters
は、依存関係グラフに下方に伝播されるアスペクトをパラメータ化する方法です。現在は文字列と文字列のマッピングです。これが有用である理由の良い例として、プロトコル バッファがあります。言語に複数の API がある場合、プロトコル バッファをビルドする API に関する情報は、依存関係グラフに伝播する必要があります。Aspect
は、依存関係グラフに伝播するアスペクトの計算に必要なすべてのデータを表します。アスペクト クラス、その定義、パラメータで構成されます。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 つの部分に分かれています。
toolchain()
ルール: ツールチェーンがサポートする実行とターゲットの制約のセットを記述し、ツールチェーンの種類(C++ や Java など)を指定します(後者はtoolchain_type()
ルールで表されます)。- 実際のツールチェーンを記述する言語固有のルール(
cc_toolchain()
など)
このように処理されるのは、ツールチェーンの解決を行うにはすべてのツールチェーンの制約を知る必要があるためです。言語固有の *_toolchain()
ルールにはそれよりもはるかに多くの情報が含まれているため、読み込みに時間がかかります。
実行プラットフォームは、次のいずれかの方法で指定します。
- MODULE.bazel ファイルで
register_execution_platforms()
関数を使用する - コマンドラインで --extra_execution_platforms コマンドライン オプションを使用する
使用可能な実行プラットフォームのセットは RegisteredExecutionPlatformsFunction
で計算されます。
構成されたターゲットのターゲット プラットフォームは、PlatformOptions.computeTargetPlatform()
によって決まります。最終的には複数のターゲット プラットフォームをサポートする予定ですが、まだ実装されていません。
構成されたターゲットに使用されるツールチェーンのセットは、ToolchainResolutionFunction
によって決まります。次の関数です。
- 登録されたツールチェーンのセット(MODULE.bazel ファイルと構成内)
- 目的の実行プラットフォームとターゲット プラットフォーム(構成内)
- 構成されたターゲット(
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()
ルールのインスタンスです。
ルールでサポートされている環境を指定するには、次の方法があります。
restricted_to=
属性を使用する。これは最も直接的な形式の仕様で、このグループでルールがサポートする環境の正確なセットを宣言します。compatible_with=
属性を使用する。これにより、デフォルトでサポートされている「標準」環境に加えて、ルールがサポートする環境を宣言します。- パッケージ レベルの属性
default_restricted_to=
とdefault_compatible_with=
を使用して。 environment_group()
ルールのデフォルト仕様を使用します。すべての環境は、テーマ別に関連するピアのグループ(「CPU アーキテクチャ」、「JDK バージョン」、「モバイル オペレーティング システム」など)に属しています。環境グループの定義には、restricted_to=
/environment()
属性で指定されていない場合、これらの環境のどれが「デフォルト」でサポートされるかが含まれます。このような属性のないルールは、すべてのデフォルトを継承します。- ルールクラスのデフォルト。これは、指定されたルールクラスのすべてのインスタンスのグローバル デフォルト値をオーバーライドします。これは、各インスタンスでこの機能を明示的に宣言しなくても、すべての
*_test
ルールをテスト可能にするために使用できます。
environment()
は通常のルールとして実装されますが、environment_group()
は Target
のサブクラスであり Rule
(EnvironmentGroup
)ではありません。また、最終的に同名のターゲットを作成する Starlark(StarlarkLibrary.environmentGroup()
)からデフォルトで使用できる関数でもあります。これは、各環境が属する環境グループを宣言し、各環境グループがデフォルトの環境を宣言する必要があるために発生する循環依存関係を回避するためです。
ビルドを特定の環境に制限するには、--target_environment
コマンドライン オプションを使用します。
制約チェックの実装は RuleContextConstraintSemantics
と TopLevelConstraintSemantics
にあります。
プラットフォームの制約
ターゲットと互換性があるプラットフォームを表現する現行の「公式」な方法は、ツールチェーンとプラットフォームの記述と同じ制約を使用することです。pull リクエスト #10945 で審査中です。
公開設定
(Google のような)多数の開発者が関与する大規模なコードベースで作業している場合は、他の開発者が自分のコードに勝手に依存しないように気をつける必要があります。そうでない場合、Hyrum の法則に従い、実装の詳細と見なしていた動作にユーザーが依存するようになります。
Bazel では、公開設定というメカニズムでこれをサポートしています。公開設定属性を使用して、特定のターゲットに依存できるのは特定のターゲットのみであることを宣言できます。この属性は、ラベルのリストを保持しますが、これらのラベルは特定のターゲットへのポインタではなく、パッケージ名のパターンをエンコードする可能性があるため、少し特殊です。(これは設計上の欠陥です)。
これは、次の場所で実装されています。
RuleVisibility
インターフェースは可視性の宣言を表します。定数(完全に公開または完全に非公開)またはラベルのリストにできます。- ラベルは、パッケージ グループ(パッケージの事前定義リスト)、パッケージ(
//pkg:__pkg__
)、パッケージのサブツリー(//pkg:__subpackages__
)のいずれかを参照できます。これは、//pkg:*
または//pkg/...
を使用するコマンドライン構文とは異なります。 - パッケージ グループは、独自のターゲット(
PackageGroup
)と構成済みターゲット(PackageGroupConfiguredTarget
)として実装されます。必要に応じて、これらを単純なルールに置き換えることができます。ロジックは、PackageSpecification
(//pkg/...
などの単一のパターンに対応)、PackageGroupContents
(単一のpackage_group
のpackages
属性に対応)、PackageSpecificationProvider
(package_group
とその伝播includes
を集約)を使用して実装されます。 - 公開設定ラベルリストから依存関係への変換は、
DependencyResolver.visitTargetVisibility
とその他のいくつかの場所で行われます。 - 実際のチェックは
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
で行われます。
ネストされたセット
多くの場合、構成済みのターゲットは、依存関係から一連のファイルを集約し、独自のファイルを追加して、集約セットを伝播情報プロバイダにラップします。これにより、それに依存する構成済みのターゲットも同じことができます。例:
- ビルドに使用される C++ ヘッダー ファイル
cc_library
の推移閉包を表すオブジェクト ファイル- Java ルールをコンパイルまたは実行するためにクラスパスに存在する必要がある .jar ファイルのセット
- Python ルールの推移閉包内の Python ファイルのセット
これを List
や Set
などを使用して単純に行うと、二次的なメモリ使用量が発生します。N 個のルールのチェーンがあり、各ルールがファイルを追加すると、1+2+...+N のコレクション メンバーになります。
この問題を回避するために、NestedSet
のコンセプトが考案されました。これは、他の NestedSet
インスタンスと自身のインスタンスから構成されるデータ構造で、集合の有向非巡回グラフを形成します。不変であり、メンバーを反復処理できます。複数の反復順序(NestedSet.Order
)を定義します。前順序、後順序、トポロジ(ノードは常に祖先の後に来る)、および「指定しないが、毎回同じである必要があります」です。
Starlark では、同じデータ構造が depset
と呼ばれます。
アーティファクトとアクション
実際のビルドは、ユーザーが求める出力を生成するために実行する必要がある一連のコマンドで構成されます。コマンドはクラス Action
のインスタンスとして表され、ファイルはクラス Artifact
のインスタンスとして表されます。これらは、「アクション グラフ」と呼ばれる 2 分割の有向非巡回グラフに配置されます。
アーティファクトには、ソース アーティファクト(Bazel の実行前に使用できるアーティファクト)と派生アーティファクト(ビルドが必要なアーティファクト)の 2 種類があります。派生アーティファクト自体は、複数の種類があります。
- **通常のアーティファクト。**これらは、ショートカットとして mtime を使用してチェックサムを計算することによって最新性がチェックされます。ctime が変更されていない場合、ファイルのチェックサムはチェックされません。
- 未解決のシンボリック リンク アーティファクト。これらは readlink() を呼び出して最新性をチェックします。通常のアーティファクトとは異なり、ダングリング シンボリック リンクである可能性があります。通常、一部のファイルをなんらかのアーカイブに圧縮する場合に使用されます。
- ツリー アーティファクト。これらは単一のファイルではなく、ディレクトリ ツリーです。その中のファイルのセットとその内容がチェックされ、最新性が確認されます。これらは
TreeArtifact
として表されます。 - 継続的なメタデータ アーティファクト。これらのアーティファクトの変更は再ビルドをトリガーしません。これはビルドスタンプ情報専用です。現在の時刻が変更されたという理由だけで再ビルドを行いません。
ソース アーティファクトをツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできない根本的な理由はありません。まだ実装されていないだけです(実装すべきです。BUILD
ファイルでソース ディレクトリを参照することは、Bazel の既知の長年の不正確な問題の 1 つです。BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM プロパティによって有効にされる、ある程度機能する実装があります)。
注目すべき Artifact
の種類は、仲介業者です。これらは、MiddlemanAction
の出力である Artifact
インスタンスで示されます。特別なケースを処理するために使用されます。
- 集約ミドルマンは、アーティファクトをグループ化するために使用されます。これは、多くのアクションが同じ大規模な入力セットを使用する場合、N × M の依存関係エッジではなく、N+M のみになるようにするためです(ネストされたセットに置き換えられます)。
- 依存関係ミドルマンのスケジュールにより、あるアクションが別のアクションより先に実行されます。主にリンティングに使用されますが、C++ コンパイルに使用されることもあります(詳細については
CcCompilationContext.createMiddleman()
をご覧ください)。 - 実行ファイル中間者は、runfiles ツリーの存在を確認するために使用されます。これにより、出力マニフェストおよび runfiles ツリーによって参照されるすべてのアーティファクトに個別に依存する必要がなくなります。
アクションは、実行する必要があるコマンド、必要な環境、生成される出力のセットとして理解するのが最も適切です。アクションの説明の主なコンポーネントは次のとおりです。
- 実行する必要があるコマンドライン
- 必要な入力アーティファクト
- 設定が必要な環境変数
- 実行する必要がある環境(プラットフォームなど)を記述するアノテーション
Bazel に既知のコンテンツを含むファイルを書き込むなど、その他の特殊なケースもあります。これらは AbstractAction
のサブクラスです。ほとんどのアクションは SpawnAction
または StarlarkAction
です(同じで、別々のクラスにする必要はありません)。ただし、Java と C++ には独自のアクション タイプ(JavaCompileAction
、CppCompileAction
、CppLinkAction
)があります。
最終的には、すべてを SpawnAction
に移動しようとします。JavaCompileAction
はかなり近いことですが、C++ は .d ファイルの解析とインクルード スキャンのため少し特殊なケースです。
アクション グラフは、ほとんどが Skyframe グラフに「埋め込まれています」。概念的には、アクションの実行は ActionExecutionFunction
の呼び出しとして表されます。アクショングラフの依存関係エッジから Skyframe の依存関係エッジへのマッピングは、ActionExecutionFunction.getInputDeps()
と Artifact.key()
で説明されています。Skyframe エッジの数を少なくするために、いくつかの最適化が行われています。
- 派生アーティファクトには、独自の
SkyValue
はありません。代わりに、Artifact.getGeneratingActionKey()
を使用して、鍵を生成するアクションの鍵を検索します。 - ネストされたセットには独自の Skyframe キーがあります。
共有アクション
一部のアクションは、複数の構成されたターゲットによって生成されます。Starlark ルールは、派生アクションを構成とパッケージによって決定されたディレクトリにのみ配置できるため、より制限されています(それでも、同じパッケージ内のルールが競合する可能性があります)。Java で実装されたルールは、派生アーティファクトを任意の場所に配置できます。
これは誤動作と見なされますが、ソースファイルを何らかの方法で処理する必要があるときに、そのファイルが複数のルールによって参照される場合(handwave-handwave)は、実行時間が大幅に短縮されるため、この問題を解消するのは非常に困難です。これには RAM がいくらかかかります。共有アクションの各インスタンスは、個別にメモリに保存する必要があります。
2 つのアクションが同じ出力ファイルを生成する場合、それらは完全に同じである必要があります。つまり、同じ入力、同じ出力、同じコマンドラインを持つ必要があります。この同等関係は Actions.canBeShared()
に実装されており、すべての Action を確認することで、分析フェーズと実行フェーズ間で検証されます。これは SkyframeActionExecutor.findAndStoreArtifactConflicts()
で実装されており、ビルドの「グローバル」ビューを必要とする Bazel の唯一の場所です。
実行フェーズ
このタイミングで、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()
を使用してヒットを確認します。
その名前とは異なり、派生アーティファクトのパスから、そのアーティファクトを出力したアクションまでのマップです。アクションは次のように記述されます。
- 入出力ファイルとそれらのチェックサムのセット
- 「アクションキー」。通常は実行されたコマンドラインですが、一般的には入力ファイルのチェックサムでキャプチャされないすべてのものを表します(
FileWriteAction
の場合、書き込まれたデータのチェックサムです)。
また、まだ開発中の高度な試験運用版の「トップダウン アクション キャッシュ」もあります。これは、伝播ハッシュを使用して、キャッシュに何度もアクセスしないようにします。
入力の検出と入力のプルーニング
一部のアクションは、単なる入力セットよりも複雑です。アクションの入力セットの変更には、次の 2 つの形式があります。
- アクションは、実行前に新しい入力を発見したり、入力の一部が実際には必要でないと判断したりすることがあります。標準的な例は C++ です。ここでは、C++ ファイルがどのヘッダー ファイルを使用するかを推移的クロージャから推測し、知識に基づいて推測することをおすすめします。これにより、すべてのファイルをリモート エグゼキュータに送信しないようにできます。そのため、すべてのヘッダー ファイルを「入力」として登録するのではなく、ソースファイルをスキャンして推移的インクルードされたヘッダーを 8 つにしすぎない(8.0.0.0.0.0 という)ヘッダー ファイルのみをスキャンします
#include
- アクションの実行中に一部のファイルが使用されなかったことが判明することがあります。C++ では、これは「.d ファイル」と呼ばれます。コンパイラは、どのヘッダー ファイルが使用されたかを事後に通知します。Make よりも増分性が低いという不利を回避するために、Bazel はこの事実を利用します。これはコンパイラに依存するため、インクルード スキャナよりも正確な見積もりが得られます。
これらは、Action のメソッドを使用して実装されます。
Action.discoverInputs()
が呼び出されます。必須と判断されたアーティファクトのネストされたセットが返されます。アクション グラフには、構成されたターゲット グラフと同等ではない依存関係エッジがないように、これらはソース アーティファクトである必要があります。- アクションは
Action.execute()
を呼び出して実行されます。 Action.execute()
の終了時に、アクションはAction.updateInputs()
を呼び出して、入力の一部が不要であることを Bazel に通知できます。使用済みの入力が未使用として報告されると、増分ビルドが正しく行われない可能性があります。
アクション キャッシュが新しいアクション インスタンス(サーバーの再起動後に作成されたものなど)でヒットを返すと、Bazel は updateInputs()
自体を呼び出して、入力のセットに以前に行った入力の検出とプルーニングの結果を反映します。
Starlark アクションでは、ctx.actions.run()
の unused_inputs_list=
引数を使用して、一部の入力を未使用として宣言できます。
アクションを実行するさまざまな方法: Strategies / ActionContexts
一部のアクションは、さまざまな方法で実行できます。たとえば、コマンドラインはローカルで、ローカルのさまざまなサンドボックスで、またはリモートで実行できます。これを具現化するコンセプトは ActionContext
と呼ばれます(名前変更は半分しか成功しなかったため、Strategy
とも呼ばれます)。
アクション コンテキストのライフサイクルは次のとおりです。
- 実行フェーズが開始されると、
BlazeModule
インスタンスにアクション コンテキストが尋ねられます。これはExecutionTool
のコンストラクタで発生します。アクション コンテキストのタイプは、ActionContext
のサブインターフェースを参照する JavaClass
インスタンスによって識別されます。このインターフェースは、アクション コンテキストが実装する必要があります。 - 使用可能なアクション コンテキストから適切なアクション コンテキストが選択され、
ActionExecutionContext
とBlazeExecutor
に転送されます。 - アクションは
ActionExecutionContext.getContext()
とBlazeExecutor.getStrategy()
を使用してコンテキストをリクエストします(実際には 1 つの方法しかありません)。
戦略は、他の戦略を呼び出して処理を行うことができます。これは、ローカルとリモートの両方でアクションを開始し、どちらが先に完了するかを判断する動的戦略で使用されます。
注目すべき戦略の 1 つは、永続的なワーカー プロセス(WorkerSpawnStrategy
)を実装する方法です。この方法では、起動時間が長いツールは、アクションごとに新たに起動するのではなく、アクション間で再利用します(Bazel は、個々のリクエスト間で観測可能な状態を保持しないというワーカー プロセスの約束に依存しているため、これは潜在的な正確性の問題を表します)。
ツールが変更された場合は、ワーカー プロセスを再起動する必要があります。ワーカーを再利用できるかどうかは、WorkerFilesHash
を使用して使用したツールのチェックサムを計算することで決まります。アクションの入力のうちどれがツールの一部を表し、どの入力が入力を表すかを知る必要があります。これはアクションの作成者が決定します。Spawn.getToolFiles()
と Spawn
の実行ファイルはツールの一部としてカウントされます。
戦略(またはアクションのコンテキスト)についての詳細:
- アクションの実行に関するさまざまな戦略については、こちらをご覧ください。
- ローカルとリモートの両方でアクションを実行し、先に終了した方を表示する動的戦略については、こちらをご覧ください。
- ローカルでアクションを実行する際の複雑さについては、こちらをご覧ください。
ローカル リソース管理者
Bazel では、多くのアクションを並列に実行できます。並行して実行するローカル アクションの数はアクションによって異なります。アクションに必要なリソースが多いほど、ローカルマシンの過負荷を避けるために、同時に実行するインスタンスの数が少なくなります。
これはクラス ResourceManager
で実装されています。各アクションには、ResourceSet
インスタンス(CPU と RAM)の形式で、必要なローカル リソースの推定値をアノテーションを付ける必要があります。その後、アクション コンテキストがローカル リソースを必要とする操作を行うと、ResourceManager.acquireResources()
を呼び出し、必要なリソースが使用可能になるまでブロックされます。
ローカル リソース管理の詳細については、こちらをご覧ください。
出力ディレクトリの構造
各アクションでは、出力を配置する出力ディレクトリに別々の場所が必要です。通常、派生アーティファクトの場所は次のとおりです。
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
特定の構成に関連付けられているディレクトリの名前はどのように決定されますか?望ましいプロパティには相反するものが 2 つあります。
- 同じビルドで 2 つの構成が発生する場合は、両方の構成で同じアクションの独自のバージョンを使用できるように、異なるディレクトリを指定する必要があります。そうしないと、同じ出力ファイルを生成するアクションのコマンドラインなど、2 つの構成が一致しない場合、Bazel はどのアクションを選択すればよいかわかりません(「アクションの競合」)。
- 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 がルールを設定するターミナルに新しいバイトをダンプすることで機能します。
実行されたテストの結果は、さまざまなイベント(TestAttempt
、TestResult
、TestingCompleteEvent
など)を監視することで、イベントバスで利用できます。これらの結果は Build Event Protocol にダンプされ、AggregatingTestListener
によってコンソールに出力されます。
カバレッジの収集
カバレッジは、ファイル bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
の LCOV 形式でテストによって報告されます。
カバレッジを収集するため、各テスト実行は collect_coverage.sh
というスクリプトにラップされています。
このスクリプトは、カバレッジの収集を有効にして、カバレッジ ランタイムによってカバレッジ ファイルが書き込まれる場所を特定するように、テストの環境を設定します。その後、テストが実行されます。テスト自体が複数のサブプロセスを実行し、複数の異なるプログラミング言語で記述された部分(個別のカバレッジ収集ランタイムを持つ)で構成されている場合があります。ラッパー スクリプトは、必要に応じて結果ファイルを LCOV 形式に変換し、1 つのファイルに統合します。
collect_coverage.sh
の挿入はテスト戦略によって行われ、collect_coverage.sh
がテストの入力にある必要があります。これは、構成フラグ --coverage_support
の値に解決される暗黙的な属性 :coverage_support
によって実現されます(TestConfiguration.TestOptions.coverageSupport
を参照)。
オフライン インストルメンテーションを行う言語(C++ など)では、コンパイル時にカバレッジ インストルメンテーションが追加される言語もあれば、オンラインでインストルメンテーションを行う言語もあります。つまり、実行時にカバレッジ インストルメンテーションが追加される言語もあります。
もう一つのコアコンセプトはベースライン カバレッジです。これは、ライブラリ、バイナリ、またはテストでコードが実行されなかった場合のカバレッジです。このソリューションが解決する問題は、バイナリのテスト カバレッジを計算したい場合、どのテストにもリンクされていないコードがバイナリに含まれている可能性があるため、すべてのテストのカバレッジをマージするだけでは不十分であるということです。そのため、カバレッジを収集するファイルのみが含まれ、カバレッジ対象の行が含まれていないバイナリごとにカバレッジ ファイルを出力します。ターゲットのベースライン カバレッジ ファイルは bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
にあります。--nobuild_tests_only
フラグを Bazel に渡すと、テストに加えて、バイナリとライブラリに対しても生成されます。
ベースライン カバレッジは現在機能していません。
カバレッジ収集では、ルールごとに 2 つのファイル グループ(計測対象ファイルのセットと計測メタデータ ファイルのセット)を追跡します。
計測対象のファイルセットは、計測するファイルのセットです。オンライン カバレッジ ランタイムの場合は、ランタイムにこれを使用して、計測するファイルを決定できます。ベースライン カバレッジの実装にも使用されます。
計測メタデータ ファイルのセットとは、Bazel が必要な LCOV ファイルをテストから生成するためにテストが必要とする追加ファイルのセットです。実際には、これはランタイム固有のファイルで構成されます。たとえば、gcc はコンパイル中に .gcno ファイルを出力します。カバレッジ モードが有効になっている場合、これらはテストアクションの入力セットに追加されます。
カバレッジが収集されているかどうかは BuildConfiguration
に保存されます。このビットに基づいてテスト アクションとアクション グラフを簡単に変更できるため便利ですが、このビットが反転すると、すべてのターゲットを再分析する必要があります(C++ などの一部の言語では、カバレッジを収集できるコードを出力するために異なるコンパイラ オプションが必要になります。この場合、いずれにせよ再分析が必要になるため、この問題は軽減されます)。
カバレッジ サポート ファイルは、暗黙的な依存関係のラベルを介して依存しているため、呼び出しポリシーによってオーバーライドできます。これにより、Bazel のバージョンによって異なることができます。理想的には、これらの違いを排除し、いずれかに標準化する必要があります。
また、「カバレッジ レポート」も生成されます。これは、Bazel 呼び出しのすべてのテストで収集されたカバレッジを統合したものです。これは CoverageReportActionFactory
によって処理され、BuildView.createResult()
から呼び出されます。実行される最初のテストの :coverage_report_generator
属性を調べて、必要なツールにアクセスします。
クエリエンジン
Bazel には、さまざまなグラフに関するさまざまな質問に使用できる小さな言語があります。次のクエリの種類が用意されています。
bazel query
は、ターゲット グラフの調査に使用されます。bazel cquery
は、構成されたターゲット グラフを調査するために使用されます。bazel aquery
はアクション グラフの調査に使用されます。
これらはそれぞれ、AbstractBlazeQueryEnvironment
のサブクラス化によって実装されます。QueryFunction
のサブクラス化により、追加のクエリ関数を作成できます。クエリ結果のストリーミングを可能にするために、結果をデータ構造に収集するのではなく、query2.engine.Callback
を QueryFunction
に渡します。QueryFunction
は、返す結果を呼び出します。
クエリの結果は、ラベル、ラベルとルールクラス、XML、protobuf など、さまざまな方法で出力できます。これらは OutputFormatter
のサブクラスとして実装されます。
一部のクエリ出力形式(proto など)には、出力を比較して特定のターゲットが変更されたかどうかを判断できるように、Bazel がパッケージ読み込みによって提供されるすべての情報を出力する必要があるという微妙な要件があります。そのため、属性値はシリアル化可能である必要があります。そのため、複雑な Starlark 値を持つ属性のない属性タイプはごくわずかです。通常は、ラベルを使用して、そのラベルの付いたルールに複雑な情報を追加します。あまり満足のいく回避策ではなく、この要件を解除できれば幸いです。
モジュール システム
Bazel は、モジュールを追加することで拡張できます。各モジュールは BlazeModule
(Bazel が以前 Blaze と呼ばれていたときの歴史の遺物)をサブクラス化し、コマンドの実行中にさまざまなイベントに関する情報を取得する必要があります。
主に、一部のバージョンの Bazel(Google で使用しているバージョンなど)でのみ必要なさまざまな「コア以外の」機能を実装するために使用されます。
- リモート実行システムへのインターフェース
- 次のコマンドを新しく導入しました。
BlazeModule
が提供する拡張ポイントのセットは、やや無秩序です。これを優れた設計原則の例として使用しないでください。
イベントバス
BlazeModules が Bazel の他の部分と通信する主な方法は、イベントバス(EventBus
)です。ビルドごとに新しいインスタンスが作成され、Bazel のさまざまな部分がイベントを投稿できます。また、モジュールは関心のあるイベントのリスナーを登録できます。たとえば、以下がイベントとして表されます。
- ビルドするビルド ターゲットのリストが決定されました(
TargetParsingCompleteEvent
) - 最上位の構成が決定されている(
BuildConfigurationEvent
) - ターゲットは正常にビルドされましたが、正常にビルドされませんでした(
TargetCompleteEvent
) - テストが実行されました(
TestAttempt
、TestSummary
)
これらのイベントの一部は、Build Event Protocol で Bazel の外部で表されます(BuildEvent
です)。これにより、BlazeModule
だけでなく、Bazel プロセス外のオブジェクトもビルドを監視できます。プロトコル メッセージを含むファイルとしてアクセスできます。また、Bazel はサーバー(Build Event Service と呼ばれる)に接続してイベントをストリーミングすることもできます。
これは build.lib.buildeventservice
と build.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>
の下にディレクトリを作成します。
リポジトリの取得は次の手順で行われます。
PackageLookupFunction
はリポジトリが必要であることを認識し、RepositoryName
をSkyKey
として作成します。これにより、RepositoryLoaderFunction
を呼び出します。RepositoryLoaderFunction
は不明な理由でリクエストをRepositoryDelegatorFunction
に転送します(コードでは、Skyframe の再起動時に再ダウンロードを回避するためと記載されていますが、説得力のある理由ではありません)。RepositoryDelegatorFunction
は、リクエストされたリポジトリが見つかるまで WORKSPACE ファイルのチャンクを反復処理することで、フェッチするように求められたリポジトリ ルールを見つけます。- リポジトリの取得を実装する適切な
RepositoryFunction
が見つかりました。これは、リポジトリの Starlark 実装か、Java で実装されたリポジトリのハードコードされたマップのいずれかです。
リポジトリの取得は非常にコストがかかるため、さまざまなキャッシュ レイヤがあります。
- チェックサム(
RepositoryCache
)をキーとするダウンロード ファイル用のキャッシュがあります。この場合、WORKSPACE ファイルでチェックサムが使用可能である必要がありますが、これは密閉性にとっては優れています。これは、実行されているワークスペースや出力ベースに関係なく、同じワークステーション上のすべての Bazel サーバー インスタンスで共有されます。 $OUTPUT_BASE/external
の下に、フェッチに使用されたルールのチェックサムを含む「マーカー ファイル」がリポジトリごとに書き込まれます。Bazel サーバーが再起動してもチェックサムが変更されない場合、チェックサムは再取得されません。これはRepositoryDelegatorFunction.DigestWriter
で実装されています。--distdir
コマンドライン オプションは、ダウンロードするアーティファクトの検索に使用される別のキャッシュを指定します。これは、Bazel がインターネットからランダムなものを取得しないようにする必要があるエンタープライズ環境で役立ちます。これはDownloadManager
によって実装されます。
リポジトリがダウンロードされると、そのリポジトリ内のアーティファクトはソース アーティファクトとして扱われます。これは問題になります。通常、Bazel はソース アーティファクトに対して stat() を呼び出して最新の状態を確認しますが、これらのアーティファクトは、含まれているリポジトリの定義が変更されると無効になります。したがって、外部リポジトリ内のアーティファクトの FileStateValue
は、外部リポジトリに依存する必要があります。これは ExternalFilesHelper
によって処理されます。
リポジトリのマッピング
複数のリポジトリが、異なるバージョンで同じリポジトリに依存したい場合があります(「ダイヤモンド依存関係の問題」のインスタンス)。たとえば、ビルド内の別々のリポジトリにある 2 つのバイナリが Guava に依存する必要がある場合、両者が @guava//
で始まるラベルを持つ Guava を参照し、異なるバージョンを意味すると想定します。
したがって、Bazel では外部リポジトリラベルを再マッピングして、文字列 @guava//
が 1 つのバイナリのリポジトリ内の 1 つの Guava リポジトリ(@guava1//
など)と、もう 1 つのリポジトリ内の別の Guava リポジトリ(@guava2//
など)を参照できるようにすることができます。
また、ダイヤモンドを結合するためにも使用できます。あるリポジトリが @guava1//
に依存し、別のリポジトリが @guava2//
に依存している場合、リポジトリ マッピングを使用すると、正規の @guava//
リポジトリを使用するように両方のリポジトリを再マッピングできます。
このマッピングは、WORKSPACE ファイルで個々のリポジトリ定義の repo_mapping
属性として指定されます。その後、Skyframe で WorkspaceFileValue
のメンバーとして表示され、次に接続されます。
Package.Builder.repositoryMapping
: パッケージ内のルールのラベル値属性をRuleClass.populateRuleAttributeValues()
で変換するために使用されます。Package.repositoryMapping
: 分析フェーズで使用します(読み込みフェーズで解析されない$(location)
などの解決に使用します)。BzlLoadFunction
: load() ステートメントでラベルを解決
JNI ビット
Bazel のサーバーは、ほとんど Java で記述されています。例外は、Java を実装したときに Java だけでは実行できない部分や、Java だけでは実行できなかった部分です。これは主に、ファイル システムとのやり取り、プロセス制御、その他のさまざまな低レベルの処理に限定されます。
C++ コードは src/main/native の下にあり、ネイティブ メソッドを持つ Java クラスは次のとおりです。
NativePosixFiles
、NativePosixFileSystem
ProcessUtils
WindowsFileOperations
、WindowsFileProcesses
com.google.devtools.build.lib.platform
コンソール出力
コンソール出力を出力するのは簡単なことのように思えますが、複数のプロセスを(場合によってはリモートで)実行し、きめ細かなキャッシュ保存、見栄えの良いカラフルなターミナル出力、長時間稼働するサーバーなど、さまざまな点を考慮する必要があります。
クライアントから RPC 呼び出しが届いた直後に、2 つの RpcOutputStream
インスタンス(stdout と stderr 用)が作成され、そこに出力されたデータをクライアントに転送します。これらは OutErr
((stdout、stderr)ペア)にラップされます。コンソールに出力する必要があるものはすべて、これらのストリームを経由します。その後、これらのストリームは BlazeCommandDispatcher.execExclusively()
に渡されます。
出力はデフォルトで ANSI エスケープ シーケンスで出力されます。不要な場合(--color=no
)、AnsiStrippingOutputStream
によって削除されます。また、System.out
と System.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 種類があります。
src/test/shell
で非常に複雑な bash テスト フレームワークを使用して実装されたもの- Java で実装されたもの。これらは
BuildIntegrationTestCase
のサブクラスとして実装されます。
BuildIntegrationTestCase
は、ほとんどのテストシナリオに対応しているため、優先される統合テスト フレームワークです。Java フレームワークであるため、デバッグ性が高く、多くの一般的な開発ツールとのシームレスな統合が可能です。Bazel リポジトリには、BuildIntegrationTestCase
クラスの例が多数あります。
分析テストは BuildViewTestCase
のサブクラスとして実装されます。BUILD
ファイルを書き込むのに使用できるスクラッチ ファイル システムがあります。さまざまなヘルパー メソッドを使用して、構成されたターゲットをリクエストし、構成を変更し、分析結果についてさまざまなことをアサートできます。