このページでは、タスクベースのビルドシステムの仕組みと、タスクベース システムとそれがどのように発生するかについて説明します。シェル スクリプトに続いて、タスクベースのビルドシステムは、ビルドの次の論理的な進化です。
タスクベースのビルドシステムについて
タスクベースのビルドシステムでは、基本的な作業単位はタスクです。各タスクは、あらゆる種類のロジックを実行できるスクリプトであり、タスクは、その前に実行する必要がある依存関係として他のタスクを指定します。現在使用されている主なビルドシステム(Ant、Maven、Gradle、Grunt、Rake など)のほとんどはタスクベースです。最近のほとんどのビルドシステムでは、シェル スクリプトではなく、ビルドの実行方法を記述したビルドファイルを作成する必要があります。
次に示すのは、Ant マニュアルの例です。
<project name="MyProject" default="dist" basedir=".">
<description>
simple example build file
</description>
<!-- set global properties for this build -->
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init"
description="compile the source">
<!-- Compile the Java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>
<target name="dist" depends="compile"
description="generate the distribution">
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>
<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
</target>
<target name="clean"
description="clean up">
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
ビルドファイルは XML で記述され、ビルドに関するいくつかの単純なメタデータとタスクのリスト(XML の <target>
タグ)を定義します。(Ant は「ターゲット」という用語をタスクに使用し、「タスク」をコマンドに言及しています)。各タスクは、Ant によって定義された可能性のあるコマンドのリストを実行します。リストには、ディレクトリの作成と削除、javac
の実行、JAR ファイルの作成などがあります。この一連のコマンドは、ユーザーが指定したプラグインで拡張でき、あらゆるロジックに対応できます。各タスクは、依存する属性を介して依存するタスクを定義することもできます。図 1 に示すように、これらの依存関係は非巡回グラフを形成します。
図 1. 依存関係を示す非巡回グラフ
ユーザーは、Ant のコマンドライン ツールにタスクを提供することで、ビルドを実行します。たとえば、ユーザーが ant dist
と入力すると、Ant は次の手順を行います。
- 現在のディレクトリに
build.xml
という名前のファイルを読み込み、解析して、図 1 に示すグラフ構造を作成します。 - コマンドラインで指定した
dist
という名前のタスクを探し、compile
という名前のタスクと依存関係があることを確認します。 compile
という名前のタスクを探し、init
という名前のタスクと依存関係があることを確認します。init
という名前のタスクを探し、依存関係がないことを確認します。init
タスクで定義されたコマンドを実行します。- タスクの依存関係がすべて実行されている場合、
compile
タスクで定義されたコマンドを実行します。 - タスクの依存関係がすべて実行されている場合、
dist
タスクで定義されたコマンドを実行します。
最終的に、dist
タスクの実行時に Ant が実行するコードは、次のシェル スクリプトと同等になります。
./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*
構文が取り除かれても、ビルドファイルとビルド スクリプトはそれほど変わっていません。しかし、それによってすでに多くのことを獲得しています。他のディレクトリに新しいビルドファイルを作成し、それらをリンクできます。既存のタスクに依存する新しいタスクを、任意かつ複雑な方法で簡単に追加できます。1 つのタスクの名前を ant
コマンドライン ツールに渡すだけで、実行する必要があるものがすべて決定されます。
Ant はもともと 2000 年にリリースされたソフトウェアのことで、Maven や Gradle などの他のツールは、年月をかけて Ant を改善してきました。実質的に、外部依存関係の自動管理や XML のないクリーンな構文などの機能を追加することで、置き換えられました。しかし、これらの新しいシステムの本質は変わりません。エンジニアは、原則としてモジュール化された方法でタスクとしてビルド スクリプトを記述し、それらのタスクを実行してシステム間の依存関係を管理するためのツールを提供できます。
タスクベースのビルドシステムの暗い側面
これらのツールは本質的に、あらゆるスクリプトをタスクとして定義できるため、エンジニアが想像できることをほぼすべて実行できます。ただし、その性能には欠点があり、ビルド スクリプトが複雑になるにつれてタスクベースのビルドシステムを扱うのが難しくなる可能性があります。そのようなシステムの問題点は、結局、エンジニアに過剰な電力を供給しすぎて、システムに十分な電力を供給できないことです。スクリプトが何をするのかはシステムに認識されていないため、ビルドステップのスケジュール設定と実行の方法に非常に保守的でなければならず、パフォーマンスが低下します。そのため、各スクリプトが期待どおりに動作していることを確認する方法はありません。このため、スクリプトが複雑になり、最終的にデバッグが必要になってきます。
ビルドステップの並列化の難しさ
最新の開発ワークステーションは、複数のビルドステップを並行して実行できる複数のコアを備えているため、非常に強力です。しかし、タスクベース システムでは、可能であるように思えても、多くの場合、タスク実行を並列化できません。タスク A がタスク B とタスク C に依存しているとします。タスク B とタスク C は互いに依存関係を持たないため、システムがタスク A にすばやくアクセスできるように、これらを同時に実行しても安全でしょうか。同じリソースに触れないならば、ただし、そうではないとしても、同じファイルを使用してステータスをトラッキングし、同時に実行した場合、競合が発生する可能性があります。一般に、システムがこれを把握する方法がないため、このような競合のリスク(まれに発生するデバッグの難易度が高い問題)が発生したり、ビルド全体が単一プロセス内の単一のスレッドで実行されるように制限したりする必要があります。これは強力なデベロッパー マシンの大きな無駄になる可能性があります。ビルドを複数のマシンに分散させる可能性を完全に排除します。
増分ビルドの実行の難易度
エンジニアが優れたビルドシステムを使用することで、小規模な増分ビルドでコードベース全体を最初から作り直さなくても、信頼性の高い増分ビルドを行えるようになります。これは、ビルドシステムが低速で、前述の理由でビルドステップを並列化できない場合に特に重要です。しかし残念なことに、タスクベースのビルドシステムもここで苦労しています。タスクは何でも実行できるため、一般的には、すでに処理済みかどうかを確認することはできません。多くのタスクでは、一連のソースファイルを使用してコンパイラで実行し、一連のバイナリを作成します。そのため、基盤となるソースファイルが変更されていなければ、再実行する必要はありません。しかし、追加情報がなければ、システムはこれをはっきりとは言えません。タスクが変化したファイルをダウンロードする可能性や、実行ごとに異なるタイムスタンプが書き込まれる可能性があることです。正確性を保証するために、システムは通常、各ビルド中にすべてのタスクを再実行する必要があります。一部のビルドシステムでは、タスクの再実行が必要な条件をエンジニアが指定できるようにすることで、増分ビルドを有効にしようとしています。この方法は可能な場合もありますが、多くの場合、実際に問題ではありません。たとえば、他のファイルからファイルを直接取り込むことができる C++ のような言語では、入力ソースを解析しなくても、変更を監視する必要があるファイルのセット全体を特定することはできません。エンジニアはしばしばショートカットを取得することになり、タスク結果が実際と異なる場合でも、それを何度も再利用するのは問題になります。この状況が頻繁に発生すると、エンジニアはすべてのビルドの前にクリーンに実行して新しい状態を取得する習慣を身に付け、そもそも増分ビルドを行う目的を完全に台なしにします。タスクの再実行が必要なタイミングを判断するのは驚くほど繊細で、ジョブは人間よりもマシンで適切に処理されます。
スクリプトのメンテナンスとデバッグが困難
最後に、タスクベースのビルドシステムが課すビルド スクリプトは、多くの場合、扱いにくいものです。ビルド スクリプトはそれほど精査されていないことが多いですが、システムをビルドするようなコードであり、バグを隠しやすい場所です。タスクベースのビルドシステムを扱う際に非常によく見られるバグの例を次に示します。
- タスク A は、特定のファイルを出力として生成するためにタスク B に依存します。タスク B のオーナーは、他のタスクがそれに依存していることを認識していないため、別の場所で出力を生成するように変更しています。これは、誰かがタスク A を実行しようとし、失敗を検出するまで検出できません。
- タスク A はタスク B に依存します。タスク B は、タスク A で必要とされる出力として特定のファイルを生成しているタスク C に依存します。タスク B のオーナーが、タスク C にこれ以上依存する必要はないと判断し、タスク B がタスク C をまったく考慮しなくても、タスク A が失敗するようにします。
- 新しいタスクの開発者は、ツールの場所や特定の環境変数の値など、タスクを実行しているマシンを想定しています。このタスクは自身のマシンで動作しますが、別のデベロッパーが試行するたびに失敗します。
- タスクには、インターネットからファイルをダウンロードするか、ビルドにタイムスタンプを追加するなど、非決定的なコンポーネントが含まれます。現在、人々はビルドを実行するたびに潜在的な異なる結果を得る可能性があります。つまり、エンジニアは、互いの障害や自動ビルドシステムで発生する障害を常に再現して修正できるとは限りません。
- 複数の依存関係を持つタスクは、競合状態を生み出す可能性があります。タスク A がタスク B とタスク C の両方に依存し、タスク B と C の両方が同じファイルを変更した場合、タスク A は、タスク B と C のどちらが最初に終了したかに応じて異なる結果になります。
ここで説明するタスクベースのフレームワーク内で、これらのパフォーマンス、正確性、保守性の問題を解決する汎用的な方法はありません。そのため、エンジニアがビルド時に実行する任意のコードを記述できる場合、ビルドを迅速かつ正確に実行するのに十分な情報がシステムになくなります。この問題を解決するために、エンジニアの手からいくつかのパワーを取り出し、システムの手に委ね、実行中のタスクではなく、アーティファクトを生成するシステムの役割を再定義する必要があります。
このアプローチにより、Blaze や Bazel などのアーティファクト ベースのビルドシステムが作成されました。