アーティファクト ベースのビルドシステム

<ph type="x-smartling-placeholder"></ph> 問題を報告する ソースを表示 夜間 · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

このページでは、アーティファクト ベースのビルドシステムと、その作成の背後にある考え方について説明します。Bazel はアーティファクト ベースのビルドシステムです。タスクベースのビルドシステムはビルド スクリプトよりも優れていますが、個々のエンジニアが独自のタスクを定義できるようにすることで、個々のエンジニアに過度の権限を与えます。

アーティファクト ベースのビルドシステムには、エンジニアが制限付きで構成できる、システムによって定義されたタスクが少数あります。エンジニアは依然としてシステムに 何をビルドするかですが、どのようにビルドするかはビルドシステムが決定します。タスクベースのビルドシステムと同様に、Bazel などのアーティファクト ベースのビルドシステムにもビルドファイルがありますが、これらのビルドファイルの内容は大きく異なります。Bazel のビルドファイルは、出力の生成方法を記述するチューリング完全なスクリプト言語の命令型のコマンドのセットではなく、ビルドする一連のアーティファクト、その依存関係、ビルド方法に影響する限定的なオプションを記述する宣言型のマニフェストです。エンジニアがコマンドラインで bazel を実行すると、ビルドする一連のターゲット(what)を指定します。Bazel は、コンパイル ステップの構成、実行、スケジューリング(how)を担当します。ビルドシステムは、リソースへのアクセスを はるかに強固な保証が可能になり 正確さを保証しながら効率が上がります

機能的な視点

アーティファクト ベースのビルドシステムと関数型ビルドシステムは、簡単にたとえられます。 説明します。従来の命令型プログラミング言語(Java、C、 Python など)を使用して、連続して実行するステートメントのリストを指定します。 タスクベースのビルドシステムを使用すると、プログラマーが一連のステップを 必要があります。一方、関数型プログラミング言語(Haskell や ML など)は、一連の数学式に似た構造になっています。イン 関数型言語では、プログラマーは実行する計算を記述しますが、 計算がいつ、どのように実行されるかの詳細は あります。

これは、アーティファクト ベースのビルドシステムでマニフェストを宣言し、システムにビルドの実行方法を判断させるという考え方にマッピングされます。多くの問題は関数型プログラミングを使用して簡単に表現できませんが、関数型プログラミングから大きなメリットを得られる問題もあります。多くの場合、このようなプログラムは簡単に並列化でき、命令型言語では不可能な正確性について強力な保証を行うことができます。ML モデルを使って表現する最も 関数型プログラミングとは、1 つの部分を 一連のルールや関数を使って 別のデータに変換しますこれが ビルドシステムとは何かです。システム全体が実質的には ソースファイル(およびコンパイラなどのツール)を入力として受け取り、 出力として使用します。したがって ビルドのベースにするのが 根本的な問題になります

アーティファクト ベースのビルドシステムについて

Google のビルドシステムである Blaze は、最初のアーティファクト ベースのビルドシステムでした。Bazel は Blaze のオープンソース バージョンです。

Bazel のビルドファイル(通常は BUILD という名前)は次のようになります。

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

Bazel では、BUILD ファイルでターゲットを定義します。ここに示す 2 種類のターゲット java_binaryjava_library。すべてのターゲットは、1 つのアーティファクトに システムによって作成可能:バイナリターゲットは、転送可能なバイナリを生成する 実行され、ライブラリ ターゲットによって生成されるライブラリは、 使用できます。すべての標的には以下があります。

  • name: コマンドラインや他の環境からターゲットを参照する方法 ターゲット
  • srcs: ターゲットのアーティファクトを作成するためにコンパイルするソースファイル
  • deps: このターゲットの前にビルドしてリンクする必要がある他のターゲット

依存関係は、同じパッケージ内(MyBinary:mylib に対する依存関係など)または同じソース階層内の別のパッケージ(mylib//java/com/example/common に対する依存関係など)に存在できます。

タスクベースのビルドシステムと同様に、Bazel のコマンドラインを使用してビルドを実行します。 ツールです。MyBinary ターゲットをビルドするには、bazel build :MyBinary を実行します。クリーンなリポジトリでこのコマンドを初めて入力すると、Bazel は次の処理を行います。

  1. ワークスペース内のすべての BUILD ファイルを解析し、依存関係のグラフを作成する 確認しました。
  2. グラフを使用して MyBinary の推移的依存関係を特定します。 MyBinary が依存するすべてのターゲットと、そのターゲットが依存するすべてのターゲットです。 再帰的に確認します。
  3. それぞれの依存関係を順番にビルドします。Bazel は、まず他の依存関係がない各ターゲットをビルドし、各ターゲットでまだビルドが必要な依存関係を追跡します。ターゲットのすべての依存関係がビルドされるとすぐに、Bazel はそのターゲットのビルドを開始します。このプロセス MyBinary の推移的依存関係のすべてが満たされるまで継続します。 構築しました。
  4. MyBinary をビルドし、すべてのインスタンスとリンクする最終的な実行可能バイナリを生成します。 依存関係をインストールします。

基本的に、ここで行われている処理は、タスクベースのビルドシステムを使用した場合とそれほど変わらないように見えるかもしれません。実際、 最終的な結果は同じバイナリであり、それを生成するプロセスは それらの依存関係を見つけるために多くのステップを分析し 説明しますただし、重要な違いがあります。1 つ目は、 ステップ 3: Bazel は、各ターゲットが Java ライブラリのみを生成することを認識しているため、 Java コンパイラを実行するだけでよく、 これらのステップを並行して実行しても安全であることを認識します。 これにより、事前トレーニング済みモデルよりも マルチコア マシン上で一度に 1 つのターゲットを 1 つずつターゲットとし、 アーティファクト ベースのアプローチでは、実行はビルドシステムが独自に行う 並列処理に関する保証を強化できます。

ただし、メリットは並列処理だけにとどまりません。このアプローチの次の効果は、デベロッパーが変更を加えずに bazel build :MyBinary を 2 回目に入力したときに明らかになります。Bazel は 1 秒未満で終了し、ターゲットが最新であるというメッセージが表示されます。これは、 前に説明した関数型プログラミングのパラダイムによって (Bazel は、各ターゲットが Java の実行のみの結果であることを認識しています) 生成され、Java コンパイラからの出力が 入力内容が変更されない限り、出力を再利用できます。 この分析はあらゆるレベルで機能します。MyBinary.java が変更された場合、Bazel は MyBinary を再構築しますが、mylib は再利用します。宣言型のソースファイルが //java/com/example/common の変更により、Bazel はそのライブラリを再ビルドします。 mylibMyBinary ですが、//java/com/example/myproduct/otherlib を再利用します。 Bazel は各ステップで実行されるツールの特性を認識しているため、 毎回最小限のアーティファクトしか再構築できず 古いビルドが生成されないことを保証します。

タスクではなくアーティファクトの観点からビルドプロセスを再構成するのは微妙 パワフルですプログラマーにさらされる柔軟性を下げて、ビルドの ビルドの各ステップで行われていることを詳しく知ることができます。機能 この知識を活用してビルドを並列化し、ビルドを効率化します。 その出力の再利用に役立ちますしかし、これはほんの第一歩にすぎません。並列処理と再利用のこれらの構成要素は、分散型で高度にスケーラブルなビルドシステムの基礎を形成します。

その他の便利な Bazel のトリック

アーティファクト ベースのビルドシステムは、タスクベースのビルドシステムに固有の並列処理と再利用の問題を根本的に解決します。ただし、以前に発生した問題の一部は、まだ解決されていません。Bazel の優れた機能 それらについて話し合ってから次に進みます。

依存関係としてのツール

以前直面した問題の一つは、ビルドがインストールされたツールに依存することで さまざまなシステム間でビルドを再現することは、 ツールのバージョンや場所に応じて異なります。プロジェクトで、ビルドまたはコンパイル対象のプラットフォーム(Windows と Linux など)に応じて異なるツールを必要とする言語を使用している場合、問題はさらに複雑になります。これらのプラットフォームでは、同じジョブを実行するために、それぞれに少し異なるツールセットが必要です。

Bazel は、この問題の第 1 段階で、ツールを依存関係として扱い、 作成されます。ワークスペース内のすべての java_library は、Java コンパイラに暗黙的に依存します。これはデフォルトでよく知られたコンパイラになります。Bazel は java_library をビルドするたびに、指定されたコンパイラが既知の場所にあることを確認します。他の依存関係と同様に、Java コンパイラが 依存するすべてのアーティファクトが再構築されます。

Bazel は、ビルド構成を設定して、問題の 2 番目の部分であるプラットフォームの独立性を解決します。Google Pixel 8 と 構成の種類によって異なります。

  • ホストの構成: ビルド中に実行されるビルドツール
  • ターゲット構成: 最終的にリクエストしたバイナリのビルド

ビルドシステムの拡張

Bazel では、一般的なプログラミング言語向けのターゲットが エンジニアは常により多くのことを希望します。これはタスクベースの あらゆる種類のビルドプロセスをサポートできる点です。 アーティファクト ベースのビルドシステムであきらめない方がよいでしょう。 幸い、Bazel では、カスタムルールを追加することで、サポートされているターゲット タイプを拡張できます。

Bazel でルールを定義するには、ルールの作成者が、ルールに必要な入力(BUILD ファイルで渡される属性の形式)と、ルールが生成する固定の出力セットを宣言します。作成者は、作成するアクションも定義します。 そのルールによって生成されます。各アクションは入力と出力を宣言し、 特定の実行可能ファイルの実行、特定の文字列のファイルへの書き込みを行います。 入力と出力を介して他のアクションに接続できます。つまり、アクションはビルドシステムの最下位レベルの構成可能ユニットです。宣言された入力と出力のみを使用する限り、アクションは任意の処理を行うことができます。Bazel は、アクションのスケジュール設定と結果のキャッシュ保存を適切に処理します。

アクション デベロッパーがアクションの一部として非決定的プロセスを導入するようなことを止める方法がないため、このシステムは万全ではありません。ただし、実際にはこのようなことはあまり起こらず、不正使用の可能性をアクション レベルまで下げることで、エラーの発生を大幅に減らすことができます。多くの一般的な言語とツールをサポートするルールはオンラインで広く入手できます。ほとんどのプロジェクトでは、独自のルールを定義する必要はありません。それでも、ルール定義はリポジトリの 1 か所の中央で定義するだけで済みます。つまり、ほとんどのエンジニアは、実装を心配することなく、これらのルールを使用できます。

環境の分離

アクションは、他のシステムのタスクと同じ問題に直面する可能性があるように思えます。同じファイルに書き込み、最終的に競合するアクションを作成することはできないでしょうか?実際、Bazel はサンドボックスを使用して、このような競合を回避します。ON: サポート対象 すべてのアクションがファイルシステムを介して他のすべてのアクションから分離されている できます。実質的に、各アクションが参照できるのは、 宣言した入力と出力を含むファイル システム 生成します。これは、Linux 上の LXC などのシステムで強制適用されます。 Docker の背後で行われます。つまり、宣言していないファイルを読み取ることができず、宣言していないファイルを書き込んでも、アクションの終了時に破棄されるため、アクションが競合することはありません。Bazel ではサンドボックスを使用して、アクションによる通信を制限しています。 通信できます。

外部依存関係を確定的に設定する

まだ 1 つの問題が残っています。ビルドシステムでは、依存関係(ツールやライブラリ)を直接ビルドするのではなく、外部ソースからダウンロードすることがよくあります。これは、Maven から JAR ファイルをダウンロードする @com_google_common_guava_guava//jar 依存関係の例で確認できます。

現在のワークスペースの外部にあるファイルに依存するとリスクが高くなります。これらのファイルはいつでも変更される可能性があるため、ビルドシステムで最新かどうかを常に確認する必要があります。ワークスペースのソースコードが変更されずにリモート ファイルが変更されると、再現できないビルドになることもあります。気付かれない依存関係の変更が原因で、ビルドが 1 日目は動作し、翌日は何の理由もなく失敗することがあります。最後に、外部依存関係により、新たなセキュリティ リスクが リスクとは、攻撃者がネットワークへの侵入に 依存関係ファイルを何かに置き換えることができます。 ユーザーがビルドを完全に制御できる可能性がある その出力を確認します。

根本的な問題は、このようなイベントをビルドシステムに認識させたいことです。 ソース管理にチェックインせずに使用できます。依存関係の更新 意識的に選択すべきだが、その選択は中央で一度行うべき 個々のエンジニアが管理したり、管理者が自動的に ありませんこれは、「Live at Head」モデルでも、ビルドを確定的にしたいためです。つまり、先週の commit をチェックアウトした場合、依存関係は現在の状態ではなく、その時の状態が表示される必要があります。

Bazel などのビルドシステムでは、ワークスペース内のすべての外部依存関係の暗号ハッシュを一覧表示するワークスペース全体のマニフェスト ファイルを必要とすることで、この問題に対処しています。ハッシュは、ファイル全体をソース管理にチェックインせずに、ファイルを一意に表す簡潔な方法です。ワークスペースから新しい外部依存関係が参照されるたびに、その依存関係のハッシュが手動または自動でマニフェストに追加されます。Bazel がコンテナ イメージを実行した場合、 キャッシュに保存された依存関係の実際のハッシュを、想定される ハッシュが異なる場合のみファイルを再ダウンロードします。

ダウンロードしたアーティファクトのハッシュが、 そのマニフェスト内のハッシュが更新されない限り、ビルドは失敗します。この 変更は自動的に行われますが、その変更が承認され、チェックインされる必要があります。 ビルドが新しい依存関係を受け入れる前にソース管理を行う必要があります。つまり、依存関係が更新された日時が常に記録され、ワークスペース ソースで対応する変更が行われない限り、外部依存関係を変更することはできません。また、古いバージョンのソースコードをチェックアウトするときに、そのバージョンがチェックインされた時点で使用していたものと同じ依存関係がビルドで使用されることが保証されます(それらの依存関係が使用できなくなった場合は、失敗します)。

もちろん、リモート サーバーが利用できなくなったり、 破損したデータの提供が開始されるため、すべてのビルドが失敗する可能性があります。 依存関係の別のコピーがない場合は、これを回避するには 重要なプロジェクトの場合は、そのすべてのプロジェクトのすべての 信頼して制御しているサーバーやサービスにデプロイできます。それ以外の場合 ビルドシステムの設定については、常にサードパーティが管理し、 安全であることを保証します。