このドキュメントでは、コードベースと Bazel の構造について説明します。これは、エンドユーザーではなく、Bazel に貢献する意欲のあるユーザーを対象としています。
はじめに
Bazel のコードベースは大きく(約 350KLOC の本番環境コードと約 260 個の KLOC テストコード)、環境全体に精通している人はいません。特定の谷を熟知している人はいますが、すべての方向にある丘にあるものはあまり知られていません。
このドキュメントでは、わかりやすい経路が失われた森の暗闇で自分自身を見つけられないように、コードベースの概要を簡単に説明します。
Bazel のソースコードの公開バージョンは GitHub(github.com/bazelbuild/bazel)にあります。これは「信頼できる情報源」ではなく、Google 内部では役に立たない追加機能を含む Google 内部のソースツリーから派生したものです。長期的な目標は、GitHub を信頼できる情報源にすることです。
コントリビューションは、通常の GitHub pull リクエスト メカニズムを介して受け入れられ、Google 社員によって内部ソースツリーに手動でインポートされてから、GitHub に再エクスポートされます。
クライアント/サーバー アーキテクチャ
Bazel の大部分は、ビルド間の RAM に残るサーバー プロセス内にあります。これにより、Bazel はビルド間で状態を維持できます。
そのため、Bazel コマンドラインには「startup」と「command」の 2 種類のオプションがあります。コマンドラインで次の操作を行います。
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
一部のオプション(--host_jvm_args=
)は、実行するコマンドの名前の前と後(-c opt
)の間にあります。前者の種類は「起動オプション」と呼ばれ、サーバー プロセス全体に影響します。後者の種類である「コマンド オプション」は、単一のコマンドにのみ影響します。
各サーバー インスタンスには単一のソースツリー(「ワークスペース」)が関連付けられ、各ワークスペースには通常、単一のアクティブなサーバー インスタンスがあります。これを回避するには、カスタム出力ベースを指定します(詳細については「ディレクトリ レイアウト」をご覧ください)。
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 接続のキャンセル呼び出しに変換し、できるだけ早くコマンドを終了します。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
で取得された外部リポジトリ。- exec ルート。現在のビルドのすべてのソースコードへのシンボリック リンクを含むディレクトリです。場所は
$OUTPUT_BASE/execroot
です。ビルド時の作業ディレクトリは$EXECROOT/<name of main repository>
です。非常に互換性のない変更であるため、長期的な計画ですが、$EXECROOT
に変更する予定です。 - ビルド中にビルドされたファイル。
コマンドの実行プロセス
Bazel サーバーが制御を取得して実行する必要があるコマンドを伝えると、次の一連のイベントが発生します。
BlazeCommandDispatcher
に新しいリクエストが通知されます。このコマンドは、実行にワークスペースが必要かどうか(バージョンやヘルプなど、ソースコードとは関係ないものを除く)と、別のコマンドを実行しているかどうかを判断します。正しいコマンドが見つかります。各コマンドには、インターフェース
BlazeCommand
を実装する必要があり、@Command
アノテーションが必要です(これはアンチパターンです。コマンドが必要とするすべてのメタデータがBlazeCommand
のメソッドによって記述されるようにすることをおすすめします)。コマンドライン オプションを解析します。コマンドごとに異なるコマンドライン オプションがあります。これについては、
@Command
アノテーションで説明しています。イベントバスが作成されます。イベントバスは、ビルド中に発生するイベントのストリームです。このビルドの一部は Build Build Protocol のベースで Bazel の外部にエクスポートされ、ビルド状況を世界に知らせます。
コマンドで制御できるようになります。最も興味深いコマンドはビルドを実行するコマンドです。ビルド、テスト、実行、カバレッジなどは
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 を壊すのに非常に有効です。残念ながら、実際には不変になるようにすることは大きな取り組みです。(FragmentOptions
の作成後、誰よりも早くこれを変更すると、参照を保持する機会が生まれ、equals()
や hashCode()
が呼び出されるまでは問題ありません)。
Bazel は、次の方法でオプション クラスを学習します。
- 一部は Bazel(
CommonCommandOptions
)に有線接続 - 各 Bazel コマンドの
@Command
アノテーションから ConfiguredRuleClassProvider
より(個々のプログラミング言語に関連するコマンドライン オプション)- Starlark ルールでは独自のオプションを定義することもできます(こちらをご覧ください)。
各オプション(Starlark で定義されているオプションを除く)は、@Option
アノテーションを含む FragmentOptions
サブクラスのメンバー変数です。このアノテーションには、コマンドライン オプションの名前とタイプ、およびいくつかのヘルプテキストが指定されています。
コマンドライン オプションで Java 型の値に指定するのは、通常は単純な文字列(整数、ブール値、ラベルなど)です。ただし、より複雑な型のオプションもサポートされています。この場合、コマンドライン文字列からデータ型に変換するジョブは com.google.devtools.common.options.Converter
の実装になります。
Bazel に表示されるソースツリー
Bazel は、ソースコードを読み取って解釈することでソフトウェアをビルドするビジネスを行っています。Bazel が動作しているソースコード全体はワークスペースと呼ばれ、リポジトリ、パッケージ、ルールで構成されています。
リポジトリ
「リポジトリ」は、デベロッパーが動作するソースツリーで、通常は 1 つのプロジェクトを表します。Bazel の祖先である Blaze は monorepo で動作します。これは、ビルドの実行に使用されるすべてのソースコードを含む単一のソースコード ツリーです。一方、Bazel はソースコードが複数のリポジトリにまたがるプロジェクトをサポートしています。Bazel の呼び出し元のリポジトリは「メイン リポジトリ」、それ以外のリポジトリは「外部リポジトリ」と呼ばれます。
リポジトリは、ルート ディレクトリ内で WORKSPACE
(または WORKSPACE.bazel
)というファイルによりマークされます。このファイルには、ビルド全体に対して「グローバル」な情報(使用可能な外部リポジトリのセットなど)が含まれています。これは通常の Starlark ファイルのように動作し、他のload()
他の Starlark ファイルを使用できます。これは一般に、明示的に参照されているリポジトリに必要なリポジトリ(これを deps.bzl
パターンと呼ぶ)を取得するために使用されます。
外部リポジトリのコードは、$OUTPUT_BASE/external
の下にシンボリック リンクまたはダウンロードされます。
ビルドを実行するとき、ソースツリー全体を分割する必要があります。これは、SymlinkForest
によって行われます。これにより、メイン リポジトリ内のすべてのパッケージが $EXECROOT
に、すべての外部リポジトリが $EXECROOT/external
または $EXECROOT/..
のいずれかにシンボリック リンクされます(そのため、前者はメイン リポジトリに external
というパッケージを含めることはできません。そのため、このリポジトリから移行します)。
パッケージ
すべてのリポジトリは、パッケージ、関連ファイルのコレクション、依存関係の仕様で構成されています。これらは、BUILD
または BUILD.bazel
というファイルで指定されます。どちらも存在する場合、Bazel は BUILD.bazel
を優先します。BUILD
ファイルが承認される理由は、Bazel の祖先である Blaze がこのファイル名を使用していることです。しかし、ファイル名の大文字と小文字が区別されない Windows では特に、よく使用されるパスセグメントであることが判明しました。
パッケージは互いに独立しています。パッケージの BUILD
ファイルが変更されても他のパッケージは変更されません。再帰 glob はパッケージ境界で停止し、BUILD
ファイルが存在すると再帰が停止するため、BUILD
ファイルを追加または削除すると他のパッケージが変更される場合があります。
BUILD
ファイルの評価は「パッケージの読み込み」と呼ばれます。これは PackageFactory
クラスに実装され、Starlark インタープリタを呼び出して動作し、使用可能なルールクラスのセットに関する知識を必要とします。パッケージの読み込みは、Package
オブジェクトになります。ほとんどの場合、文字列(ターゲットの名前)からターゲットへのマップです。
パッケージ読み込みの複雑さは大きなものです。Bazel では、すべてのソースファイルを明示的に列挙する必要はありません。その代わりに glob(glob(["**/*.java"])
など)を実行できます。シェルとは異なり、再帰 glob はサブディレクトリ(サブパッケージは不可)に表示されます。それにはファイル システムへのアクセス権が必要であり、処理が遅くなる可能性があるため、可能な限り並列かつ効率的に実行できるように、あらゆる種類の手口を導入しています。
グローブは次のクラスで実装されます。
LegacyGlobber
は、スカイフレームを知らない気軽なスモールカメラです。SkyframeHybridGlobber
: Skyframe を使用し、以前の Glob に戻すバージョンです。これにより、「Skyframe の再起動」を回避できます(下記をご覧ください)。
Package
クラス自体に WORKSPACE ファイルの解析専用であり、実際のパッケージでは意味をなさないメンバーも一部含まれています。通常のパッケージを記述するオブジェクトには、別のものを記述するフィールドを含めることはできません。たとえば、次のようなものが挙げられます。
- リポジトリのマッピング
- 登録されているツールチェーン
- 登録済みの実行プラットフォーム
理想的には、WORKSPACE ファイルの解析と通常のパッケージの解析との間により多くの分離を行い、Package
が両方のニーズに対応する必要がないようにします。残念ながら、この 2 つは非常に密接に絡み合っているため、この処理は困難です。
ラベル、ターゲット、ルール
パッケージは、次のタイプのターゲットで構成されます。
- ファイル: ビルドの入力または出力のいずれか。Bazel の用語では、これらをアーティファクトと呼びます(これについては後述します)。ビルド中に作成されたすべてのファイルがターゲットになるわけではありません。Bazel の出力にラベルが関連付けられていないことがよくあります。
- ルール:入力から出力を出力する手順を説明します。これらは通常、プログラミング言語(
cc_library
、java_library
、py_library
など)に関連付けられますが、言語に依存しないプログラミング言語(genrule
、filegroup
など)もあります。 - パッケージ グループ: 公開の項で説明されています。
ターゲットの名前は Label と呼ばれます。ラベルの構文は @repo//pac/kage:name
です。ここで、repo
はラベルが存在するリポジトリの名前、pac/kage
は BUILD
ファイルが存在するディレクトリ、name
はパッケージのディレクトリからの相対パス(ラベルがソースファイルを参照している場合)です。コマンドラインでターゲットを参照する場合は、ラベルの一部を省略できます。
- リポジトリが省略されている場合、ラベルはメイン リポジトリにあると見なされます。
- パッケージ部分が省略されている場合(
name
や:name
など)、現在の作業ディレクトリのパッケージに含まれるラベルになります(上位参照(..)を含む相対パスは使用できません)
ルールの一種(「C++ ライブラリ」など)は「ルールクラス」と呼ばれます。ルールクラスは Starlark(rule()
関数)または Java(いわゆる「ネイティブ ルール」)で実装できます。長期的には、すべての言語固有のルールが Starlark で実装されますが、従来のルール ファミリー(Java や C++ など)の中には当面の間 Java で実装されるものもあります。RuleClass
Starlark ルールクラスは、load()
ステートメントを使用して BUILD
ファイルの先頭でインポートする必要がありますが、Java ルールクラスは ConfiguredRuleClassProvider
に登録されているため、Bazel で「必然的に」認識されています。
ルールクラスには、次のような情報が含まれます。
- 属性(
srcs
、deps
など): それらの型、デフォルト値、制約など。 - 構成の移行と各属性に関連する要素(ある場合)
- ルールの実装
- 推移情報プロバイダが「通常は」作成する
用語に関する注意: コードベースでは、ルールクラスによって作成されたターゲットを「ルール」と呼ぶことがよくあります。ただし、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 の詳細についてはこちらをご覧ください。
特定の 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 は、Bazel の構成と拡張に使用されるドメイン固有の言語です。型がはるかに少なく、制御フローに対する制限が多く、最も重要な点として、同時読み取りを可能にする強力な不変性保証を備えた Python の制限付きサブセットであると考えられます。完全なチューニングではありません。チューニングが完全でないと、ユーザーは言語内で一般的なプログラミング タスクを実行しようとしません。
Starlark は net.starlark.java
パッケージに実装されています。また、こちらで個別に Go を実装しています。現在、Bazel で使用される Java 実装はインタープリタです。
Starlark は、次のようないくつかの状況で使用されます。
BUILD
言語。ここで新しいルールを定義します。このコンテキストで実行されている Starlark コードは、BUILD
ファイル自体のコンテンツと、そこで読み込まれる.bzl
ファイルにのみアクセスできます。- ルールの定義これにより、新しいルール(新しい言語のサポートなど)が定義されます。このコンテキストで実行されている Starlark コードは、直接的な依存関係によって提供される構成とデータにアクセスできます(これについては後述します)。
- WORKSPACE ファイルを参照してください。ここで、外部リポジトリ(メイン ソースツリーにないコード)が定義されます。
- リポジトリ ルールの定義。ここで新しい外部リポジトリ タイプが定義されます。このコンテキストで実行されている Starlark コードは、Bazel が実行されているマシンで任意のコードを実行し、ワークスペースの外部にアクセスできます。
BUILD
ファイルと .bzl
ファイルで使用できる言語は、表現が異なるため若干異なります。違いの一覧については、こちらをご覧ください。
Starlark の詳細については、こちらをご覧ください。
読み込み/分析フェーズ
読み込み/分析フェーズでは、特定のルールを構築するために必要なアクションを Bazel が決定します。基本単位は「構成されたターゲット」です。つまり、正確に言うと「ターゲットと構成」のペアです。
これらを「読み込み/分析」フェーズと呼ぶことにします。以前は 2 つの部分に分かれていましたが、これらは以前はシリアル化されていましたが、時間の経過とともに重ねることができます。
- パッケージを読み込む、つまり
BUILD
ファイルをそれを表すPackage
オブジェクトに変換する - 構成されたターゲットの分析(つまり、ルールの実装を実行してアクション グラフを生成する)
コマンドラインでリクエストされた構成済みターゲットの推移的閉鎖で構成された各ターゲットは、ボトムアップ、つまり、最初にリーフノードから次にコマンドライン ノードのものまで分析する必要があります。構成された単一ターゲットの分析への入力は次のとおりです。
- 構成。(そのルールの「方法」。ターゲット プラットフォーム、ユーザーが C++ コンパイラに渡したいコマンドライン オプションなど)。
- 直接的な依存関係。推移的な情報プロバイダは、分析対象のルールで使用できます。これらは、クラスパス上のすべての .jar ファイルや C++ バイナリにリンクする必要があるすべての .o ファイルなど、構成されたターゲットの推移的なクロージャの情報の「ロールアップ」を提供するため、このように呼ばれます。
- ターゲット自体。これは、ターゲットが含まれているパッケージを読み込んだ結果です。ルールの場合、これには属性(通常は重要)が含まれます。
- 構成されたターゲットの実装。ルールの場合、これは Starlark または Java のいずれかです。ルール以外の構成のターゲットはすべて Java で実装されます。
構成されたターゲットを分析すると、次の出力が表示されます。
- ターゲットに依存するターゲットを構成した推移的な情報プロバイダは、次のものにアクセスできます。
- アーティファクトと、そこで生成されるアクション。
Java ルールで提供される API は RuleContext
です。これは、Starlark ルールの ctx
引数と同じです。その API はより強力ですが、同時に Bad ThingsTM を実行する方が簡単です。たとえば、時間または空間の複雑さが 2 次またはそれ以上であるコードを書く場合、Java 例外で Bazel サーバーがクラッシュする場合、または Options
インスタンスを誤って変更した場合や構成済みのターゲットを可変にする場合などは、不変に変えるほうが簡単です。
構成されたターゲットの直接的な依存関係を決定するアルゴリズムは、DependencyResolver.dependentNodeMap()
にあります。
構成
構成とは、どのプラットフォームや、コマンドライン オプションなどを使用して、ターゲットを作成する「方法」のことです。
同じターゲットを、同じビルド内の複数の構成に対してビルドできます。これは、ビルド時やターゲット コード内で実行されるツール、クロスコンパイル時や、複数の CPU アーキテクチャ用のネイティブ コードを含むファット Android アプリをビルド中などに、同じコードを使用する場合などに役立ちます。
概念的には、構成は BuildOptions
インスタンスです。しかし実際には、BuildOptions
は BuildConfiguration
でラップされ、追加機能が提供されます。依存関係グラフの上部から下部に伝播されます。変化があった場合は、ビルドを再分析する必要があります。
これにより、たとえば、リクエストされたテスト実行の数が変更されてもテスト ターゲットにのみ影響するのであれば、ビルド全体を再分析する必要があります(そのような構成は「トリミング」する予定であるため、まだ準備ができていません)。
ルール実装で構成の一部が必要な場合は、RuleClass.Builder.requiresConfigurationFragments()
を使用して定義内で宣言する必要があります。これは、誤り(Java フラグメントを使用する Python ルールなど)を回避し、Python オプションが変更された場合に C++ ターゲットを再分析する必要がないように構成のトリミングを容易にするためです。
ルールの構成は、必ずしも「親」ルールの構成と同じであるとは限りません。依存関係のエッジで構成を変更するプロセスを「構成遷移」と呼びます。これは次の 2 つの場所で発生します。
- 依存関係のあるエッジこれらの遷移は
Attribute.Builder.cfg()
で指定され、Rule
(遷移が発生する)とBuildOptions
(元の構成)から 1 つ以上のBuildOptions
(出力構成)への関数です。 - 構成されたターゲットへの受信エッジこれらは
RuleClass.Builder.cfg()
で指定されます。
関連するクラスは TransitionFactory
と ConfigurationTransition
です。
構成遷移は次のようになります。
- ビルド中に特定の依存関係が使用され、実行アーキテクチャでビルドされることを宣言すること。
- 特定のアーキテクチャを複数のアーキテクチャ(Fat Android APK のネイティブ コード用など)用にビルドする必要があることを宣言します。
構成遷移が複数の構成である場合、スプリット遷移と呼ばれます。
構成の移行は Starlark でも実装できます(ドキュメントはこちら)。
推移的情報プロバイダ
推移的情報プロバイダは、構成されたターゲットがそれに依存する他の構成済みターゲットの情報を伝えるための方法です(_only _way)。「推移的」という名前が使われているのは、一般的には構成済みターゲットの推移的閉鎖のロールアップのようなものだからです。
一般に、Java の推移情報プロバイダと Starlark のプロバイダは 1 対 1 の対応関係にあります(DefaultInfo
は例外で、FileProvider
、FilesToRunProvider
、RunfilesProvider
は Java の直接文字変換より Starlark より優れていると判断されたため例外です)。キーは次のいずれかです。
- Java クラス オブジェクト。これは、Starlark からアクセスできないプロバイダでのみ使用できます。これらのプロバイダは
TransitiveInfoProvider
のサブクラスです。 - 文字列。これはレガシー コードであって、名前の競合が発生する可能性があるため、強くおすすめしません。このような推移的な情報プロバイダは、
build.lib.packages.Info
の直接サブクラスです。 - プロバイダ シンボル。これは、
provider()
関数を使用して Starlark から作成できます。これは、新しいプロバイダを作成する場合はおすすめの方法です。シンボルは、Java のProvider.Key
インスタンスで表されます。
Java で実装された新しいプロバイダは BuiltinProvider
を使用して実装する必要があります。NativeProvider
のサポートが終了し(まだ削除していません)、Starlark から TransitiveInfoProvider
サブクラスにアクセスできません。
構成されたターゲット
構成されたターゲットは RuleConfiguredTargetFactory
として実装されます。Java で実装されたルールクラスごとにサブクラスがあります。Starlark の構成対象は、StarlarkRuleConfiguredTargetUtil.buildRule()
を使用して作成されます。
構成されたターゲット ファクトリは、RuleConfiguredTargetBuilder
を使用して戻り値を構築する必要があります。これは次の要素で構成されます。
filesToBuild
は、「このルールが表すファイルのセット」の曖昧なコンセプトです。これらは、構成されたターゲットがコマンドラインまたは genrule の src にある場合にビルドされるファイルです。- 実行ファイル、標準、データ。
- 出力グループ。これらは、ルールで構築できるさまざまな「その他のファイルセット」です。これにアクセスするには、BUILD で filegroup ルールの output_group 属性を使用し、Java で
OutputGroupInfo
プロバイダを使用します。
ランファイル
一部のバイナリでは、データファイルを実行する必要があります。代表的な例は、入力ファイルを必要とするテストです。これは、Bazel で「runfiles」のコンセプトで表されます。「runfiles ツリー」は特定のバイナリのデータファイルのディレクトリ ツリーです。 ファイル システム内にシンボリック ツリーとして作成されます。個々のシンボリック リンクは出力ツリーのソース内のファイルを指します。
実行ファイルのセットは、Runfiles
インスタンスとして表されます。概念的には、runfiles ツリー内のファイルのパスから、それを表す Artifact
インスタンスへのマップになります。次の 2 つの理由から、1 つの Map
よりも少し複雑です。
- ほとんどの場合、ファイルの runfiles パスは execpath と同じです。RAM を節約するためにこれを使用します。
- runfile ツリーには、さまざまな種類のレガシー エントリがあります。これらも表現する必要があります。
ランファイルは RunfilesProvider
を使用して収集されます。このクラスのインスタンスは、構成されたターゲット(ライブラリなど)とその推移的なクロージングのニーズを表し、ネストされたセットのように収集されます(実際には、カバー付きのネストされたセットを使用して実装されます)。各ターゲットは、依存関係のランファイルを結合して、それ自体を依存関係グラフに追加します。RunfilesProvider
インスタンスには 2 つの Runfiles
インスタンスが含まれています。1 つは「data」属性に依存するルールの場合、もう 1 つは他のすべての種類の受信依存関係に対する場合です。これは、ターゲットがデータ属性に依存している場合、それ以外の場合は異なるランファイルが存在することがあるためです。これは望ましくないことです。この動作については、まだ削除されていません。
バイナリのランファイルは、RunfilesSupport
のインスタンスとして表されます。Runfiles
とは異なり、RunfilesSupport
には実際にビルドする機能があります(これは単なるマッピングである Runfiles
とは異なります)。これには、次の追加コンポーネントが必要です。
- 入力 runfiles マニフェスト。これは、runfiles ツリーをシリアル化した説明です。これは YAML ファイルの内容のプロキシとして使用されます。Bazel は、マニフェストの内容が変更された場合にのみ、runfiles ツリーが変更されると想定します。
- 出力の runfiles マニフェスト。これは、シンボリック リンクがサポートされていないこともある Windows などのランファイル ツリーを処理するランタイム ライブラリで使用されます。
- ランファイル メディエーション マネージャー。runfiles ツリーに存在するには、symlink ツリーと、そのシンボリック リンクが指すアーティファクトを作成する必要があります。依存関係のエッジの数を減らすには、runfile ミドルマンを使用してこれらをすべて表現します。
- コマンドライン引数。
RunfilesSupport
オブジェクトが表す実行ファイルを含むバイナリを実行します。
アスペクト
アスペクトは、「依存関係グラフに計算を伝播する」方法です。Bazel のユーザー向けの説明については、こちらをご覧ください。わかりやすい例として、プロトコル バッファがあります。proto_library
ルールは特定の言語について知るべきではありませんが、任意のプログラミング言語のプロトコル バッファ メッセージ(「プロトコル バッファの基本単位」)の実装を proto_library
ルールと結合し、同じ言語の 2 つのターゲットが同じプロトコル バッファに依存する場合は、1 回ビルドされるようにします。
構成されたターゲットと同様に、ターゲットは Skyframe に SkyValue
として表され、構成方法は構成済みのターゲットと非常によく似ています。ConfiguredAspectFactory
というファクトリ クラスが RuleContext
にアクセスできますが、構成済みのターゲット ファクトリとは異なり、構成されているターゲットとそのプロバイダも認識します。
依存関係グラフの下方に伝播される一連の要素は、Attribute.Builder.aspects()
関数を使用して各属性に対して指定されます。このプロセスには、わかりにくい名前のクラスがいくつか含まれています。
AspectClass
は、アスペクトの実装です。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 つの部分に分かれています。
- ツールチェーンが実行する実行とターゲットの制約のセットを記述し、ツールチェーンの種類(C++ や Java など)を伝える
toolchain()
ルール(後者はtoolchain_type()
ルールで表される)。 - 実際のツールチェーンを記述する言語固有のルール(
cc_toolchain()
など)
この処理を行うには、ツールチェーンの解決のためにすべてのツールチェーンの制約を知る必要があり、言語固有の *_toolchain()
ルールにはさらに多くの情報が含まれているため、読み込みに時間がかかります。
実行プラットフォームは、次のいずれかの方法で指定します。
- WORKSPACE ファイルで
register_execution_platforms()
関数を使用する - コマンドラインで --extra_execution_platforms コマンドライン オプションを使用
使用可能な実行プラットフォームのセットは、RegisteredExecutionPlatformsFunction
で計算されます。
構成されたターゲットのターゲット プラットフォームは PlatformOptions.computeTargetPlatform()
によって決定されます。最終的には複数のターゲット プラットフォームをサポートする予定ですが、まだ実装されていないため、これはプラットフォームのリストです。
構成されたターゲットに使用されるツールチェーンのセットは、ToolchainResolutionFunction
によって決まります。次の関数です。
- 登録済みのツールチェーンのセット(WORKSPACE ファイルと構成内)
- 目的の実行プラットフォームとターゲット プラットフォーム(構成内)
- 構成されたターゲットに必要なツールチェーン タイプのセット(
UnloadedToolchainContextKey)
内)。 UnloadedToolchainContextKey
の構成済みのターゲット(exec_compatible_with
属性)と構成(--experimental_add_exec_constraints_to_targets
)の実行プラットフォームの制約のセット。
その結果、UnloadedToolchainContext
になります。基本的に、これはツールチェーン タイプ(ToolchainTypeInfo
インスタンスとして表される)から、選択したツールチェーンのラベルへのマップです。ツールチェーン自体は含まれず、ラベルのみが含まれるため、「アンロード」と呼ばれます。
ツールチェーンは実際に ResolvedToolchainContext.load()
を使用して読み込まれ、リクエストされた構成済みのターゲットの実装で使用されます。
また、1 つの「ホスト」構成 1 つとターゲット構成(--cpu
など)で表されるレガシー構成に依存しているレガシー システムもあります。上記のシステムへの移行は段階的に行われています。以前の構成の値に依存するケースに対応するため、Google はプラットフォーム マッピングを実装して、以前のフラグと新しいスタイルのプラットフォーム間の制約を変換しました。コードが PlatformMappingFunction
にあり、Starlark 以外の「小さい言語」を使用している。
制約
ターゲットが少数のプラットフォームのみと互換性を持つように指定できる場合もあります。Bazel には、この目的を達成するために、次のような複数のメカニズムがあります。
- ルール固有の制約
environment_group()
/environment()
- プラットフォームの制約
ルール固有の制約は、主に Google for 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
(Target
)のサブクラスであり、Rule
(EnvironmentGroup
)ではないサブクラスです。Starlark(StarlarkLibrary.environmentGroup()
)はデフォルトで利用できる関数で、最終的に同名のターゲットを作成します。これは、各環境が属している環境グループを宣言し、各環境グループがデフォルトの環境を宣言する必要があるために、循環依存関係が発生することを回避するためです。
ビルドは、--target_environment
コマンドライン オプションを使用して特定の環境に制限できます。
制約チェックの実装は、RuleContextConstraintSemantics
と TopLevelConstraintSemantics
にあります。
プラットフォームの制約
ターゲットが互換性を持つプラットフォームを説明する最新の「公式」方法は、ツールチェーンとプラットフォームの説明に使用されるものと同じ制約を使用することです。pull リクエスト #10945 は審査中です。
公開設定
多数の開発者がいる大規模なコードベースを Google で開発している場合、コードによって他の誰かが任意に選択しないように注意してください。そうしないと、Hyrum の法則に従って、実装の詳細であるとみなされる動作を利用するようになるでしょう。
Bazel は、これを visibility というメカニズムでサポートしています。つまり、特定のターゲットは visibility 属性を使用してのみ依存できることを宣言できます。この属性は特別なものです。ラベルのリストを保持しますが、これらのラベルは特定のターゲットへのポインタではなく、パッケージ名よりパターンをエンコードできるためです。(これは設計上の欠陥です)。
これは以下の場所で実装されます。
RuleVisibility
インターフェースは、視認性の宣言を表します。定数(完全に公開または完全に非公開)またはラベルのリストのいずれかです。- ラベルは、パッケージ グループ(事前定義されたパッケージのリスト)を参照するか、パッケージを直接参照(
//pkg:__pkg__
)するか、パッケージのサブツリー(//pkg:__subpackages__
)を参照できます。これは、//pkg:*
または//pkg/...
を使用するコマンドライン構文とは異なります。 - パッケージ グループは独自のターゲット(
PackageGroup
)として構成され、構成されたターゲット(PackageGroupConfiguredTarget
)として実装されます。必要であれば、これらを単純なルールに置き換えることもできます。このロジックは、//pkg/...
(PackageGroupContents
は単一のpackage_group
のpackages
属性に対応)とPackageSpecificationProvider
(package_group
とその推移的includes
を集約する)に対応する 1 つのパターンに対応する実装によって実装されます。PackageSpecification
- 表示ラベルリストから依存関係への変換は、
DependencyResolver.visitTargetVisibility
などいくつかの場所で行われます。 - 実際のチェックは
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
で行われます。
ネストされたセット
多くの場合、構成されたターゲットは、依存関係からファイルのセットを集約し、独自のターゲットを追加して、その集約セットを推移的情報プロバイダにラップします。これにより、このターゲットに依存している構成済みターゲットでも同じ処理が可能になります。例:
- ビルドに使用される C++ ヘッダー ファイル
cc_library
の推移的なクロージャを表すオブジェクト ファイル- Java ルールのコンパイルや実行には、クラスパスに存在する必要がある .jar ファイルのセット
- Python ルールの推移的なクロージャに含まれている Python ファイルのセット
たとえば、List
や Set
を使用して単純に行うと、二次メモリの使用量が発生します。N 個のルールのチェーンがあり、それぞれのルールで 1 つのファイルを追加すると、1+2+...+N 個のコレクション メンバーが追加されます。
この問題を回避するために、NestedSet
のコンセプトを考えました。これは、他の NestedSet
インスタンスとそれ自体の複数のメンバーで構成されるデータ構造で、セットの有向非巡回グラフを形成します。不変であり、メンバーは反復できます。複数の反復順序(NestedSet.Order
): 予約購入、事後処理、トポロジ(常にノードの祖先よりも後に来る)、および「気にしないが、毎回同じにする」などを定義します。
同じデータ構造が Starlark では depset
と呼ばれています。
アーティファクトとアクション
実際のビルドは、ユーザーが希望する出力を生成するために必要な一連のコマンドで構成されています。このコマンドは Action
クラスのインスタンスとして、ファイルは Artifact
クラスのインスタンスとして表されます。「アクション グラフ」と呼ばれる 2 部からなる有向非巡回グラフに配置されます。
アーティファクトには、ソース アーティファクト(Bazel の実行開始前に利用可能になるアーティファクト)と派生アーティファクト(ビルドが必要なアーティファクト)の 2 種類があります。派生アーティファクト自体の種類は複数あります。
- **定期的なアーティファクト。**チェックは、mtime をショートカットとして計算して、最新かどうかがチェックされます。ctime が変更されていなければ、ファイルのチェックサムは実行されません。
- 未解決のシンボリック リンク アーティファクト。最新かどうかをチェックするには、readlink() を呼び出します。通常のアーティファクトとは異なり、これらはシンボリック リンクを含んでいる場合があります。通常は、一部のファイルをアーカイブにまとめた状況で使用されます。
- ツリー アーティファクト。これらは 1 つのファイルではなく、ディレクトリ ツリーです。このファイルの内容と内容をチェックすることで、最新性がチェックされます。
TreeArtifact
で表されます。 - 定数メタデータ アーティファクト。これらのアーティファクトを変更しても、再トリガーは行われません。これはビルドスタンプ情報にのみ使用されます。現在の時刻が変わっただけで再ビルドするべきではありません。
ソース アーティファクトをツリー アーティファクトや未解決のシンボリック リンク アーティファクトにできる基本的な理由はありません。ただし、ソース アーティファクトがまだ実装されていないだけです(ただし、BUILD
ファイル内のソース ディレクトリの参照は、Bazel で以前から存在する既知の問題の 1 つであり、BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM プロパティによって実現されます)。
Artifact
は注目すべき中間者です。これらは MiddlemanAction
の出力である Artifact
インスタンスで示されます。これらは、次のような特殊なケースに使用されます。
- 集約された中間体は、アーティファクトをまとめるために使用されます。これは、多くのアクションが同じ大規模な入力セットを使用する場合、N*M 依存関係のエッジはなく、N+M のみ(ネストされたセットに置き換えられる)であるためです。
- 依存関係のメディエーション ジョブのスケジュールを設定すると、アクションが別のアクションの前に実行されるようになります。
ほとんどの場合、lint チェックで使用されますが、C++ のコンパイルにも使用されます(説明については、
CcCompilationContext.createMiddleman()
をご覧ください)。 - 出力ファイルは、出力ファイル マニフェストと runfiles ツリーで参照されるすべてのアーティファクトに別々に依存する必要がないように、runfiles ツリーの存在を保証します。
アクションは、実行する必要があるコマンド、必要な環境、および生成される出力のセットとして最もよく理解されます。アクションの説明の主なコンポーネントは次のとおりです。
- 実行する必要のあるコマンドライン
- 必要な入力アーティファクト
- 環境変数の設定
- 実行する必要がある環境(プラットフォームなど)を記述するアノテーション \
Bazel が認識しているファイルを書き込むなど、その他の特殊なケースもあります。AbstractAction
のサブクラスです。ほとんどのアクションは SpawnAction
または StarlarkAction
です(同じクラスであるはずですが、Java と C++ にはそれぞれ独自のアクション タイプ(JavaCompileAction
、CppCompileAction
、CppLinkAction
)があります)。
最終的に、すべてを SpawnAction
に移動します。JavaCompileAction
はかなり近いですが、.d ファイルの解析とインクルード スキャンのため、C++ は少し特殊なケースです。
アクション グラフは、ほとんどの場合 Skyframe グラフに埋め込まれています。概念的には、アクションの実行は 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()
を使用してヒットの有無が確認されます。
名前とは逆に、派生アーティファクトのパスから出力されたアクションへのマップになります。アクションは次のとおりです。
- 入力ファイル、出力ファイル、チェックサムのセット
- アクションキー(通常は実行されたコマンドラインですが、通常は入力ファイルのチェックサムでキャプチャされないものを表します(たとえば
FileWriteAction
の場合は書き込まれたデータのチェックサム)。
また、まだ試験段階にある「トップダウン アクション キャッシュ」も開発中です。このキャッシュでは、推移ハッシュを使用してキャッシュに何度もアクセスすることを回避できます。
入力検出と入力プルーニング
一部のアクションは、一連の入力だけを使用する場合よりも複雑です。アクションの入力セットに対する変更には、次の 2 つの形式があります。
- アクションは、実行前に新しい入力を検出する場合や、一部の入力が実際に必要でないと判断する場合があります。典型的な例は C++ です。ここでは、すべてのファイルをリモート エグゼキュータに送信しないように、C++ ファイルが推移的クロージャからどのヘッダー ファイルを使うかを教育によって推測することをおすすめします。したがって、すべてのヘッダー ファイルを「入力」として登録しないオプションがあります。C ヘッダー ファイルに含まれているのは、ヘッダーに含まれているヘッダーのうち、それらのヘッダー エビデンスに該当するもののみです。
#include
- アクションが実行中に一部のファイルが使用されていないことに気づく場合があります。C++ ではこれを「.d ファイル」と呼びます。コンパイラは、事後に使用されるヘッダー ファイルを指定します。Bazel を使用すれば、Make よりもインクリメンタリティが悪化しないように、この知識を活用できます。コンパイラを使用するため、インクルード スキャナよりも推定の精度が高くなります。
以下のメソッドは Action on Action を使用して実装します。
Action.discoverInputs()
が呼び出されます。このメソッドは、必要になると判断された、ネストされたアーティファクトのセットを返します。アクション グラフ内に、構成されたターゲット グラフと同等の依存関係を持たないよう、ソース アーティファクトを指定する必要があります。- アクションは
Action.execute()
を呼び出して実行されます。 Action.execute()
の最後で、アクションはAction.updateInputs()
を呼び出して、すべての入力が必要だったことを Bazel に伝えられます。そのため、使用中の入力が未使用と報告された場合、正しくない増分ビルドが発生する可能性があります。
アクション キャッシュが(サーバーの再起動後に作成されたなど)新しいアクション インスタンスに対するヒットを返すと、Bazel が updateInputs()
自体を呼び出して、以前に行った入力検出とプルーニングの結果を一連の入力に反映させます。
Starlark アクションでは、この施設を利用して、ctx.actions.run()
の unused_inputs_list=
引数を使用して一部の入力が未使用であると宣言できます。
アクションを実行するさまざまな方法: 戦略/アクション コンテキスト
一部のアクションはさまざまな方法で実行できます。たとえば、コマンドラインは、ローカルでも、各種のサンドボックスでも、リモートでも実行できます。これを具現化したコンセプトを、名前変更の半分しか完了していないため、ActionContext
(または Strategy
)と呼びます。
アクション コンテキストのライフサイクルは次のとおりです。
- 実行フェーズが開始されると、
BlazeModule
インスタンスはどのアクション コンテキストを使用するかを尋ねられます。これは、ExecutionTool
のコンストラクタで発生します。アクション コンテキスト タイプは、ActionContext
のサブインターフェースを参照する JavaClass
インスタンスと、アクション コンテキストが実装する必要があるインターフェースによって識別されます。 - 利用可能なアクション コンテキストから適切なアクション コンテキストが選択され、
ActionExecutionContext
とBlazeExecutor
に転送されます。 - アクションは、
ActionExecutionContext.getContext()
とBlazeExecutor.getStrategy()
を使用してコンテキストをリクエストします(そのための方法が 1 つしかないはずです)。
戦略は、他の戦略を自由に呼び出して、ジョブを遂行できます。これは、ローカルとリモートの両方でアクションを開始してから終了した処理を使用する動的戦略などで使用されます。
1 つの注目すべき戦略は、永続的なワーカー プロセスを実装する戦略です(WorkerSpawnStrategy
)。考え方としては、一部のツールは起動時間が長く、アクションごとに再利用するのではなく、アクション間で再利用する必要があるということです(Bazel は、個々のリクエスト間で監視可能な状態を持たないことをワーカー プロセスの Promise に依存しているため、潜在的な正確性の問題を表します)。
ツールが変更された場合、ワーカー プロセスを再起動する必要があります。ワーカーを再利用できるかどうかは、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 log directory」に配置されます。
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
によってコンソールに出力されます。
カバレッジ コレクション
カバレッジは、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++ など)がインストルメンテーションを行い、他の言語はオンライン インストルメンテーション(実行時)を追加します。
もう一つの基本コンセプトは、ベースライン カバレッジです。これは、ライブラリ、バイナリ、またはテストでコードが実行されていない場合のカバレッジです。これにより、バイナリのテスト カバレッジを計算する場合に、すべてのテストのカバレッジを結合するだけでは不十分となります。これは、バイナリにリンクされていないテストがコードに含まれている可能性があるためです。そのため、すべてのバイナリについて、カバレッジ対象のファイルのみを含むカバレッジ ファイルを出力します。ターゲットのベースライン カバレッジ ファイルは bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
です。--nobuild_tests_only
フラグを Bazel に渡す場合は、テストに加えてバイナリとライブラリでも生成されます。
ベースライン カバレッジは現在利用できません。
各ルールのカバレッジ コレクション用に、インストルメンテーション ファイル セットとインストルメンテーション メタデータ ファイルセットの 2 つのファイル グループを追跡します。
インストルメンテーションされたファイルのセットが、インストルメント化する一連のファイルとなります。オンライン カバレッジ ランタイムの場合、実行時にこれを使用して、インストルメント化するファイルを決定できます。また、ベースライン カバレッジの実装にも使用されます。
インストルメンテーション メタデータ ファイルのセットは、テストで Bazel が必要とする LCOV ファイルの生成に必要な追加ファイルのセットです。実際には、これはランタイム固有のファイルで構成されます。たとえば、gcc はコンパイル中に .gcno ファイルを出力します。カバレッジ モードが有効になっている場合は、一連のテスト アクションに追加されます。
カバレッジが収集されているかどうかは、BuildConfiguration
に保存されます。これは、このビットに応じてテスト アクションとアクション グラフを変更する簡単な方法です。ただし、このビットを反転すると、すべてのターゲットを再分析する必要があることを意味します(C++ など、一部の言語では、カバレッジを収集するコードを発行するために異なるコンパイラ オプションが必要です。これにより、この問題がある程度軽減されます。再解析が必要です)。
カバレッジ サポート ファイルは暗黙的な依存関係でラベルに依存しているため、呼び出しポリシーでオーバーライドできます。これにより、Bazel の各バージョンでこれらのファイルが異なる場合があります。こうした差異は削除して、そのうちの 1 つを標準化するのが理想的です。
また、Bazel 呼び出しのすべてのテストで収集されたカバレッジをマージした「カバレッジ レポート」も作成します。これは CoverageReportActionFactory
によって処理され、BuildView.createResult()
から呼び出されます。最初に実行されるテストの :coverage_report_generator
属性を確認することで、必要なツールにアクセスできます。
クエリエンジン
Bazel には、さまざまなグラフについてさまざまな質問をする小さな言語があります。次の種類のクエリが用意されています。
bazel query
を使用してターゲット グラフを調査します。bazel cquery
は、構成されたターゲット グラフの調査に使用されます。bazel aquery
を使用してアクション グラフを調査します。
それぞれが AbstractBlazeQueryEnvironment
をサブクラス化することにより実装されます。QueryFunction
をサブクラス化することで、追加のクエリ関数を実行できます。ストリーミング クエリ結果のストリーミングを許可するために、query2.engine.Callback
は QueryFunction
に渡され、返されるデータ構造で返される結果が返されます。
クエリの結果は、ラベル、ラベル、ルールクラス、XML、protobuf など、さまざまな方法で出力できます。これらは OutputFormatter
のサブクラスとして実装されます。
一部のクエリ出力形式(proto はもちろん)の微妙な要件は、Bazel が出力をパッケージ化して特定のターゲットが変更されたかどうかを判断できるように、パッケージ読み込みによって提供される情報を出力する必要があることです。そのため、属性値はシリアル化可能である必要があります。そのため、複雑な Starlark 値を持つ属性がない属性タイプは、ごくわずかです。通常は、ラベルを使用して複雑な情報をそのラベルの付いたルールに添付します。それほど満足できる回避策ではありません。この要件を緩和することは非常によいことです。
モジュール システム
Bazel は、モジュールを追加することで拡張できます。各モジュールは BlazeModule
をサブクラス化する必要があります(この名前は、以前は Blaze と呼ばれていた場合、Bazel の履歴)で、コマンドの実行時にさまざまなイベントに関する情報を取得する必要があります。
これは主に、Bazel の一部のバージョン(Google で使用するものなど)にのみ必要となる、さまざまな「コア以外」の機能を実装するために使用されます。
- リモート実行システムへのインターフェース
- 次のコマンドを新しく導入しました。
BlazeModule
が提供する一連の拡張ポイントはやや危険です。適切な設計原則の例としては使用しないでください。
イベントバス
BlazeModule は他の Bazel と通信するための主要な方法として、イベントバスを使用しますEventBus
。ビルドのたびに新しいインスタンスが作成され、Bazel のさまざまな部分がイベントを送信したり、モジュールが対象のイベントのリスナーを登録したりできます。たとえば、以下はイベントとして表されます。
- ビルドするビルド ターゲットのリストが決定されます(
TargetParsingCompleteEvent
) - 最上位構成を決定しました(
BuildConfigurationEvent
) - ターゲットが正常にビルドされたかどうか(
TargetCompleteEvent
) - テストが実行されました(
TestAttempt
、TestSummary
)
これらのイベントの一部は、Bazel の外部でビルド イベント プロトコル(BuildEvent
)で表されます。これにより、BlazeModule
だけでなく、Bazel プロセス外のビルドでもビルドをモニタリングできます。プロトコル メッセージを含むファイルとして、または Bazel がイベントをビルドするためにサーバー(Build Event Service)に接続して、アクセスできます。
これは、build.lib.buildeventservice
と build.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 が Xth の load()
ステートメントまで評価するまで、WorkspaceFileFunction
が計算されます。
リポジトリの取得
リポジトリのコードを Bazel で利用できるようにするには、それをフェッチする必要があります。これにより、Bazel が $OUTPUT_BASE/external/<repository name>
の下にディレクトリを作成します。
リポジトリの取得は、次の手順で行います。
PackageLookupFunction
はリポジトリが必要であることを認識し、RepositoryLoaderFunction
を呼び出すRepositoryName
をSkyKey
として作成します。RepositoryLoaderFunction
は、不明確な理由によりリクエストをRepositoryDelegatorFunction
に転送します(コードでは、Skyframe が再起動した場合に物を再ダウンロードしないようにしていますが、これは確固とした理由ではありません)。RepositoryDelegatorFunction
は、リクエストされたリポジトリが見つかるまで WORKSPACE チャンクで反復処理を実行することで、フェッチを要求するリポジトリ ルールを検出します。- リポジトリの取得を実装する適切な
RepositoryFunction
が見つかりました。リポジトリの Starlark 実装か、Java で実装されているリポジトリのハードコードされたマップのどちらかです。
リポジトリの取得は非常に高コストになる可能性があるため、さまざまなキャッシュ レイヤレイヤが必要になります。
- ダウンロードされたファイルのキャッシュがチェックサム(
RepositoryCache
)をキーにしています。そのため、WORKSPACE ファイルではチェックサムが使用可能である必要がありますが、これは密閉には役立ちます。これは、実行されているワークスペースや出力ベースに関係なく、同じワークステーション上のすべての Bazel サーバー インスタンスによって共有されます。 - 「マーカー ファイル」は、リポジトリの取得に使用されたルールのチェックサムを含む
$OUTPUT_BASE/external
の下のリポジトリごとに書き込まれます。Bazel サーバーが再起動してもチェックサムは変わらない場合、再取得されません。これはRepositoryDelegatorFunction.DigestWriter
に実装されています。 --distdir
コマンドライン オプションでは、ダウンロードするアーティファクトの検索に使用する別のキャッシュを指定します。これは、Bazel がインターネットからランダムなものを取得しないようにする企業の設定に役立ちます。これはDownloadManager
によって実装されます。
リポジトリがダウンロードされると、そのリポジトリ内のアーティファクトはソース アーティファクトとして扱われます。Bazel は通常、それらのアーティファクトで stat() を呼び出して最新のアーティファクトをチェックするため、問題が生じます。これらのアーティファクトは、リポジトリの定義が変更されると無効になります。したがって、外部リポジトリ内のアーティファクトの FileStateValue
は、その外部リポジトリに依存する必要があります。ExternalFilesHelper
が処理します。
マネージド ディレクトリ
場合によっては、外部リポジトリがワークスペース ルートの下にあるファイルを変更する必要があります(たとえば、ダウンロードしたパッケージをソースツリーのサブディレクトリに格納するパッケージ マネージャーなど)。これは、Bazel がソースファイル自体をユーザー自身によってのみ変更し、パッケージがワークスペース ルートの下のすべてのディレクトリを参照できるようにするという点と、Bazel と矛盾します。このような外部リポジトリを機能させるために、Bazel は 2 つのことを行います。
- Bazel へのアクセスを許可するワークスペースのサブディレクトリをユーザーが指定できます。これらは
.bazelignore
というファイルにリストされており、機能はBlacklistedPackagePrefixesFunction
に実装されています。 - ワークスペースのサブディレクトリから、処理対象の外部リポジトリへのマッピングを
ManagedDirectoriesKnowledge
にエンコードし、通常の外部リポジトリの場合と同様にそれらを参照するFileStateValue
を処理します。
リポジトリのマッピング
これは、複数のリポジトリが同じリポジトリで異なるバージョンに依存したいと考えている場合(「ダイヤモンド依存関係の問題」)です。たとえば、ビルド内の別個のリポジトリにある 2 つのバイナリが Guava に依存する場合、どちらも @guava//
で始まるラベルで Guava を参照し、異なるバージョンであると想定します。
したがって、Bazel を使用すると、文字列 @guava//
は、1 つのバイナリのリポジトリにある 1 つの Guava リポジトリ(@guava1//
など)と、もう 1 つの Guava リポジトリ(@guava2//
など)を参照できるように、外部リポジトリ ラベルを再マッピングできます。
ひし形を結合するのにも使用できます。リポジトリが @guava1//
に依存し、別のリポジトリが @guava2//
に依存している場合、リポジトリ マッピングにより、正規の @guava//
リポジトリを使用するように両方のリポジトリを再マッピングできます。
マッピングは、WORKSPACE ファイルで個々のリポジトリ定義の repo_mapping
属性として指定します。その後、Skyframe に WorkspaceFileValue
のメンバーとして表示され、次の要素に組み込まれます。
Package.Builder.repositoryMapping
。RuleClass.populateRuleAttributeValues()
によってパッケージ内のルールのラベル値属性を変換するために使用されます。Package.repositoryMapping
: 分析フェーズで使用されます(読み込みフェーズで解析されない$(location)
などを解決するため)。- load() ステートメント内のラベルを解決するための
BzlLoadFunction
JNI ビット
Bazel のサーバーは主に 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 つの入力があります。
- イベントバス
- イベント ストリームはレポーターを介してパイプで渡される
コマンド実行機構(Bazel の残りの部分)は、Reporter.getOutErr()
を介してクライアントへの RPC ストリームに直接接続するだけで、これらのストリームに直接アクセスできます。これは、コマンドが大量のバイナリデータ(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 種類のテストがあります。Google では、前者を「統合テスト」、後者を「統合テスト」と呼んでいますが、これらは統合テストが少ない統合テストに似ています。また、実際の単体テストも必要になります。
統合テストのうち 2 種類:
src/test/shell
にある非常に緻密な bash テスト フレームワークを使用して実装されたフレームワーク- Java で実装されたコンポーネントこれらは
BuildIntegrationTestCase
のサブクラスとして実装されます。
ほとんどのテストシナリオに対応した、統合テストのフレームワークとして BuildIntegrationTestCase
をおすすめします。Java フレームワークであるため、一般的な開発ツールのデバッグやシームレスな統合が可能です。Bazel リポジトリには、BuildIntegrationTestCase
クラスの例が多数あります。
分析テストは、BuildViewTestCase
のサブクラスとして実装されます。BUILD
ファイルの書き込みに使用できるスクラッチ ファイル システムがあり、さまざまなヘルパー メソッドで、構成されたターゲットのリクエスト、構成の変更、分析結果に関するさまざまな事項のアサーションを行うことができます。