次の方法で共有



March 2009

Volume 24 Number 03

MSBuild - 信頼性の高いビルド作成のベスト プラクティス (第 2 部)

Sayed Ibrahim Ibrahim | MARCH 2009

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。

  • インクリメンタル ビルドの使用
  • カスタム タスクの作成
  • ビルド参照の管理
  • 大きなソース ツリーのビルド
この記事では、次のテクノロジを使用しています。
MSBuild、Visual Studio

目次

インクリメンタル ビルドの使用
カスタム タスクの作成
ビルド参照の管理
大きなソース ツリーのビルド

以前、Visual Studio のマネージ プロジェクト用ビルド エンジンである MSBuild について、開発者向けにベスト プラクティスを紹介した記事を執筆しました。今回はその続編です。第 1 部では、ほぼすべてのプロジェクトに共通する基本的な慣例と手法をいくつか取り上げました。第 2 部では、主に規模上の理由から徹底したカスタマイズが要求されるビルド構成について、その具体的な技法を説明します。インクリメンタル ビルドの使用、カスタム タスクの作成、ビルド参照の管理、大きなソース ツリーのビルドなどを中心に話を進めていきます。

MSBuild についてあまりよく知らないという方は、過去の MSDN 記事「信頼性の高いビルド作成のベスト プラクティス (第 1 部)」、「インサイド MSBuild: Microsoft ビルド エンジンのカスタム タスクを使用した、お好みの方法でのアプリケーションのコンパイル」、「WiX の秘訣: MSBuild と Windows Installer XML を使ったリリースの自動化」をご一読ください。

インクリメンタル ビルドの使用

ビルドが複雑になってくると、アプリケーションのビルドに時間がかかるようになります。大規模な製品では、ビルドに数時間かかることも珍しくありません。ビルドは時間のかかる処理なので、古くなったターゲットだけを実行することが大切です。この概念は "インクリメンタル ビルド" と呼ばれ、MSBuild では、Target 要素の Inputs 属性および Outputs 属性を通じてサポートされます。MSBuild は、入力と出力を含んだターゲットが検出された場合、出力ファイルのタイム スタンプと、入力ファイルのタイム スタンプを比較します。入力ファイルの方が新しい場合はターゲットが実行され、それ以外の場合はスキップされます。

このメカニズムを実証するため、ここでは第 1 部の「バッチ タスク」というセクションから抜粋した例を用いることにします。一連のファイルを別の場所にコピーする例です。図 1 に示した Copy02.proj ファイルは、そのときに使用した例に変更を加えたものです。

図 1 Copy02.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <PropertyGroup>
    <SourceFolder>src\</SourceFolder>
    <DestFolder>dest\</DestFolder>
  </PropertyGroup>

  <ItemGroup>
    <!-- Get all files under src\ except svn files -->
    <SrcFiles Include="$(SourceFolder)**\*" Exclude="**\.svn\**\*"/>
  </ItemGroup>
  <Target Name="CopyToDest"
          Inputs="@(SrcFiles)"
          Outputs=
          "@(SrcFiles->'$(DestFolder)%(RecursiveDir)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(SrcFiles)"
          DestinationFiles="@(SrcFiles->'$(DestFolder)%(RecursiveDir)%(Filename)%(Extension)')"
          />
  </Target>

  <Target Name="Clean">
    <ItemGroup>
      <_FilesToDelete Include="$(Dest)**\*"/>
    </ItemGroup>

    <Delete Files="@(_FilesToDelete)"/>
  </Target>
</Project>

前回の Copy01.proj ファイルと大きく違っている点は、Inputs と Outputs が使用されていることです。CopyToDest ターゲットには、コピー元ファイルが入力として宣言され、コピー先ファイルが出力として宣言されています。コピー元ファイルが最新である場合、ターゲットはスキップされます。図 2 は、その実際のようすを示したものです。

図 2 CopyToDest ターゲットがスキップされた

C:\Samples\Batching>msbuild Copy02.proj /fl /nologo
Build started 10/26/2008 6:15:37 PM.
Project "C:\Samples\Batching\Copy02.proj" on node 0 (default targets).
Skipping target "CopyToDest" because all output files are up-to-date with respect to the input files.
Done Building Project "C:\Samples\Batching\Copy02.proj" (default targets).

Build succeeded.
    0 Warning(s)
    0 Error(s)

特定の状況では、出力側の一部だけが新しく、それ以外は入力側の方が新しいというケースもあります。MSBuild は、そのような状況を検出した場合、入力側の一部だけを、実行対象のターゲットに渡します。

例を挙げて説明しましょう。Copy02.proj の内容に、別途ターゲットを追加した Copy03.proj というファイルがあるとします。追加するのは、DeleteRandomOutputFiles というターゲットで、出力ディレクトリから 2 つのファイルを削除するものです。

<Target Name="DeleteRandomOutputFiles">
  <!-- Arbitrarily delete two files from dest folder  -->
  <ItemGroup>
    <_RandomFilesToDelete Include="$(DestFolder)class3.cs"/>
    <_RandomFilesToDelete Include="$(DestFolder)Admin\admin_class2.cs"/>
  </ItemGroup>

  <Delete Files="@(_RandomFilesToDelete)"/>   
</Target>

この 2 つのファイルは出力先から削除されるため、入力の方が古いと考えることができます。したがって、このターゲットはスキップされません。CopyToDest が一度呼び出された後で「msbuild Copy03.proj /t:DeleteRandomOutputFiles;CopyToDest」というコマンドを実行した場合、図 3 のような結果が表示されます。

図 3 ターゲットを部分的にビルド

C:\Samples\Batching>msbuild Copy03.proj /t:DeleteRandomOutputFiles;CopyToDest
Build started 10/26/2008 11:09:56 PM.
Project "C:\Samples\Batching\Copy03.proj" on node 0 (DeleteRandomOutputFiles;CopyToDest target(s)).
  Deleting file "dest\class3.cs".
  Deleting file "dest\Admin\admin_class2.cs".
CopyToDest:
Building target "CopyToDest" partially, because some output files are out of date with respect to their input files.
Copying file from "src\Admin\admin_class2.cs" to "dest\Admin\admin_class2.cs".
Copying file from "src\class3.cs" to "dest\class3.cs".
Done Building Project "C:\Samples\Batching\Copy03.proj" (DeleteRandomOutputFiles;CopyToDest target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

この出力からは、CopyToDest ターゲットが部分的にビルドされた (入力の一部だけがターゲットに渡された) ことがわかります。コピーする必要があるのは 2 つのファイルだけで、それ以外はスキップすることができます。複雑なビルドでは、インクリメンタル ビルドが欠かせません。ターゲットを作成する際には、必ず、このメカニズムをサポートするようにしてください。

カスタム タスクの作成

カスタム タスクを作成する際は、できるだけ再利用しやすいように設計します。たとえば、SendEmailToJoe (Joe に電子メールを送信する) タスクよりも SendEmail (電子メールを送信する) タスクとした方が、再利用性は高くなります。次に、カスタム タスクを作成する場合のガイドラインをいくつか紹介します。続けて、1 つ 1 つのガイドラインについて解説していきます。

  • Task クラスまたは ToolTask クラスを継承する。
  • メッセージを適切に記録する。
  • タスク間の通信を透過的にする。
  • 必須の入力パラメータにはすべて Required 属性を使用する。
  • プロジェクト ファイルで使用される可能性のあるすべてのプロパティには Output 属性を使用する。
  • ファイル参照には、文字列ではなく ITaskItem を使用する。
  • 入力に関連した新しい出力値のメタデータを常に転送する。

タスクを作成する際に、直接 ITask (Microsoft.Build.Framework) インターフェイスを実装する必要はありません。Task クラスまたは ToolTask クラスを継承した方が簡単です。どちらも、Microsoft.Build.Utilities 名前空間に存在します。作成しているタスクが、実行可能ファイルをラップする場合は、ToolTask クラスを使用する必要があります。それ以外のケースでは、Task クラスを使用します。

この 2 つのクラスのいずれかを継承する際は、Log プロパティを使用して、アタッチされているロガーにメッセージを送るようにします。Console.WriteLine を使用してコンソールにメッセージを送るような手法は避けてください。メッセージをログに記録する場合は、ロガーの処理効率を考慮し、適切なログ レベルで記録することが大切です。たとえば、単なる警告レベルのイベントをエラーとして記録することは避けます。また、ITask インターフェイスのコントラクトは、タスクが失敗した場合、Execute メソッドから False を返すようになっています。この場合も、少なくとも 1 つのエラーが記録されていれば、ユーザーは問題の解決方法を知ることが可能です。Execute メソッドで True が返されるようなら、ログにエラーが記録されないようにします。これは、return ステートメントで !Log.HasLoggedErrors を返すだけの簡単な方法で実現できます。

タスクの作成で、もう 1 つ重要なポイントは、タスクの独立性を確保することです。ビルドに関してタスク側が把握しておく必要のある情報は、自分に渡されたプロパティのみです。また、逆のことも言えます。つまり、プロジェクト ファイルは、タスクからの情報をその出力によってのみ収集する必要があります。タスクを設計する際、そのプロジェクト ファイル (またはロガー) とのやり取りに他の方法を使用すると、相互の依存度が増し、複雑になって保守性が低下します。タスクを構築する際は、プロジェクト ファイルのコードからは、タスク側で宣言された入力と出力だけが見えるようにするという意味で、常に "What You See Is What You Get" のアプローチを採用するようにしてください。

タスク側で必要となる入力プロパティは、Required 属性 (Microsoft.Build.Framework 名前空間) で修飾する必要があります。この要件は、実行時に MSBuild によって適用され、必須のプロパティが指定されていない場合はログにエラーが記録されます。タスク内のプロパティが、プロジェクト ファイルで使用される可能性が少しでもある場合は、それらのプロパティに Output 属性 (Microsoft.Build.Framework) を指定するようにしてください。出力パラメータはコード内で Output 属性を使って明示的に宣言する必要があります。出力するプロパティを増やすたびにタスクを再配置しなくても済むように、他にもいくつかのプロパティを公開しておくことをお勧めします。

特定のファイルまたはひとまとまりのファイルに対して何かの処理を実行するように設計されたタスクの場合、タスクとのファイルの受け渡しには、文字列パスではなく、ITaskItem インターフェイスを使用する必要があります。たとえば、図 4 に示したような 2 つの実装があるとします。

図 4 ファイル ベースのプロパティの例

//This example shows how NOT to declare file based properties
public class Move1 : Task
{
    [Required]
    public string SourceFile
    { get; set; }

    [Required]
    [Output]
    public string DestinationFile
    { get; set; }

    public override bool Execute()
    {
        //TODO: Implementation here
        throw new NotImplementedException();
    }
}
//This example shows how to declare file based properties
public class Move2 : Task
{
    [Required]
    public ITaskItem SourceFile
    { get; set; }

    [Required]
    [Output]
    public ITaskItem DestinationFile
    { get; set; }

    public override bool Execute()
    {
        //TODO: Implementation here
        throw new NotImplementedException();
    }
}

Move1 クラスでは、タスクとのファイルの受け渡しに文字列が使用されています。これに対し、Move2 では、ITaskItem のインスタンスが公開されています。文字列を渡した場合は、単純な文字列しか取得できません。一方、ITaskItem オブジェクトには、メタデータが関連付けられています。

コンシューマの観点から見た実装方法は、どちらも同じです。必要な変換処理はすべて MSBuild によって行われます。以下に示したターゲットは、この 2 つのタスクの利用方法を示したものです。

<Target Name="Example">

  <ItemGroup>
    <FileToMove Include="class1.cs"/>
  </ItemGroup>

  <Move1 SourceFile="@(FileToMove)" DestinationFile="dest\class1.cs" />
  <Move2 SourceFile="@(FileToMove)" DestinationFile="dest\class1.cs" />
</Target>

それならば、タスクでメタデータが使用できなかったとして、何が問題なのでしょうか。ファイル ベースの参照に ITaskItem が使用されたタスクは、非常に柔軟性が高くなります。この単純な例では、どちらのタスクも一度に受け取ることができるのは、1 つのファイルだけです。しかし、本当なら、複数のファイルを移動できるように作成した方が何かと便利です。この場合、プロパティは、ITaskItem の代わりに ITaskItem[] として定義する必要があります。これにより、タスクのインスタンスを複数作成するオーバーヘッドが減り、より使いやすいタスクにすることが可能となります。

最後に、入力に関連した新しい出力値のメタデータを常に転送する、という点について説明します。特定の入力項目を受け取り、その入力に直接関連した新しい出力を作成する場合、元のソースから受け取ったメタデータはすべて (自分が把握していないものも含めて)、新しく作成された出力項目に転送する必要があります。たとえば、C++ のファイルからオブジェクト ファイルを作成するようなタスクを設計する場合は、ソース ファイルの入力プロパティだけでなく、オブジェクト ファイルの出力も、ITaskItem[] として宣言するようにします。実際に出力項目を作成すると、各オブジェクト ファイルに、対応する入力項目のメタデータが割り当てられることになります。

なぜこれが大切かというと、パイプライン内の連続する各ステップで、項目にメタデータを追加できるためです。たとえば、DoNotLink という名前のメタデータを、いずれかの C++ ファイルに追加したとします。コンパイラは、このメタデータを認識しません。しかし、コンパイラは、受け取ったメタデータをそのまま適切なオブジェクト ファイル項目に渡すので、Link ターゲットで、そのオブジェクト ファイルを除外することができます。

ビルド参照の管理

ビルド参照の扱いで最も重要なのは、グローバル アセンブリ キャッシュ (GAC) に存在するアセンブリを決して参照しないということです。GAC はランタイムに特化して設計されており、参照の検索パスに含めるべきではありません。より適切なアプローチは、プロジェクト参照またはアセンブリ参照を厳密な名前で使用することです。

アセンブリ参照を作成する場合、HintPath を使用して、アセンブリの検索場所を MSBuild に対して指定できます。たとえば、次のように参照を定義することができます。

<Reference Include="IronMath, Version=1.1.0.0, Culture=neutral, 
                 PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <HintPath>..\contrib\IronPython-1.1\IronMath.dll</HintPath>
</Reference>

多数の参照に対して HintPath を設定する場合は、ちょっとしたコツがあります。独自のディレクトリ名を追加することによって参照の検索パスを拡張する方法です。C# および Visual Basic .NET プロジェクトにおいて、参照解決に使用されるパスのリストは、AssemblySearchPaths 項目に含まれています。Microsoft.CSharp.targets または Microsoft.VisualBasic.targets の Import ステートメントの後、この項目に独自のパスを追加すると、そのパスが最初に検索されるようになります。宣言済みのプロパティで、これを実装するには、次のようにします。

<PropertyGroup>
  <AssemblySearchPaths>
    ..\..\MyReferences\;
    $(AssemblySearchPaths)
  </AssemblySearchPaths>
</PropertyGroup>

AssemblySearchPaths と HintPath の値には、どちらも相対パスを使用する必要があります。フル パスで値を指定した場合、他のコンピュータでプロジェクトをビルドすることが難しくなります。

大規模なプロジェクトでは、参照の CopyLocal フラグを True に設定しないでください。あるプロジェクトでローカル コピーの対象として指定されたファイルは、そのプロジェクトを参照するすべてのプロジェクトにコピーされます。次の例について考えてみましょう。

  • ClassLibray1: CopyLocal 参照が 10 個
  • ClassLibrary2: CopyLocal 参照が 5 個、ClassLibrary1 を参照
  • WindowsFormApp1: ClassLibrary2 を参照

この場合、ClassLibrary1 で参照されているすべてのファイルが、ClassLibrary1、ClassLibrary2、および WindowsFormApp1 の出力フォルダにコピーされます。また、ClassLibrary2 で参照されているすべてのファイルは、ClassLibrary2 および WindowsFormApp1 の出力フォルダにコピーされます。したがって、たった 3 つのプロジェクトでさえ、フル ビルドの場合、40 ファイル (10*3 + 5*2 = 40) のコピーが作成されます。これが 100 個以上のプロジェクトから成るビルドの場合、多大な影響が生じることは明らかです。

CopyLocal の動作を回避するには、参照の Private メタデータの値を False に設定します。たとえば、参照ファイルがローカル コピーされないようにするには、次のように指定します。

<Reference Include="IronMath, Version=1.1.0.0, Culture=neutral, 
               PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <HintPath>..\contrib\IronPython-1.1\IronMath.dll</HintPath>
  <Private>False</Private>
</Reference>

参照に対するメタデータが別途設定されているため、ファイルはローカル コピーされません。また、ItemDefinitionGroup 要素を使用して、項目に対する既定のメタデータ値を定義する方法もあります。参照のケースでは、次のスニペットをプロジェクト ファイルのどこかに追加しておけば、Private メタデータの既定値を False に設定できます。

<ItemDefinitionGroup>
  <Reference>
    <Private>False</Private>
  </Reference>
</ItemDefinitionGroup>

既定では、このメタデータの値は設定されません。したがって、参照ファイルがローカル コピーの対象として明示的に指定されていなければ、この設定が適用されます。

ビルドの規模が大きく、すべてのファイルをローカル コピーの対象から除外する必要がある場合は、コピー ローカルの動作を制御する _CopyFilesMarkedCopyLocal ターゲットをオーバーライドします。Microsoft.CSharp.targets または Microsoft.VisualBasic.targets の Import ステートメントの後でこのターゲットをオーバーライドすれば、いずれのファイルもローカル コピーされません。一般に、アンダースコアで始まる要素をオーバーライドすることはお勧めできませんが、これは、特殊なケースと考えてください。ビルドの規模が大きい場合は、参照ファイルのローカル コピーは、所要時間の面でもドライブの空き容量の面でも、あまりにも無駄が多いので、要素のオーバーライドは十分な正当性があります。

大きなソース ツリーのビルド

大量の (100 個以上など) プロジェクトを扱う場合は、プロジェクトを体系化して、ビルド プロセスに各プロジェクトのニーズを満たせるだけの効率と柔軟性を確保することが大切です。このセクションでは、ソース コードを体系化し、その構造にビルド プロセスを当てはめる方法について取り上げます。ここで取り上げる構造が必ずしもすべてのチームやすべての製品に適切であるとは限りませんが、重要なのは、ビルド プロセスをいかにモジュール化し、共通のビルド要素を、いかにビルド対象のすべての製品に当てはめるかという点です。

ソースは、最も一般的なプロジェクトを最上位とする、関連するプロジェクトのツリーとして体系化できます。この体系化されたツリーは、特定のプロジェクトがビルドするのは、子レベルのプロジェクトと兄弟レベルのプロジェクトのみとし、自分より上位のプロジェクトを直接ビルドしてはならないという前提で成立します。たとえば、図 5 は、複数の架空のプロジェクトの依存関係を示しています。

fig05.gif

図 5 プロジェクトの依存関係 (クリックすると拡大画像が表示されます)

この図には、SCalculator と SNotepad という 2 つの製品と、それらが依存している 4 つのライブラリが存在します。これらのプロジェクトは、図 6 のようなツリーに体系化することが可能です。

図 6 SCalculator と SNotepad の体系ツリー

ROOT
+---Common
    +---Common.IO
    ¦   ¦
    +---Common.UI
    ¦   ¦
    ¦   +---Common.UI.Editors
    ¦   
    +---Contrib
    ¦
    +---Products
    ¦   ¦
    ¦   +---SCalculator
    ¦   ¦   
    ¦   +---SNotepad
    ¦       
    +---Properties

Common プロジェクトは、すべてのプロジェクトが依存しているため、ROOT ノードの直下に置かれています。SCalculator プロジェクトと SNotepad プロジェクトは、Products フォルダ内に配置されます。

そこで、特定のサブツリーに従事している開発者が、構造全体をビルドすることなく、必要なファイルだけをビルドする方法があれば便利です。これは、それぞれのフォルダに次の 3 つの MSBuild ファイルを格納するという決まりを設けることによって実現することができます。

  • NodeName.setting
  • NodeName.traversal.targets
  • dirs.proj

NodeName は、現在のノードの名前です (root、common、common.ui.editors など)。NodeName.setting ファイルには、ビルド プロセス中に使用される設定 (プロパティまたは項目として収集される) が格納されます。たとえば、BuildInParallel、Configuration、Platform などの設定がここに格納されます。NodeName.traversal.targets ファイルには、プロジェクトのビルドに使用されるターゲットが格納されます。最後に、dirs.proj ファイルには、サブツリーのビルドに必要な一連のプロジェクトが (ProjectFiles 項目内に) 格納されます。

NodeName.setting ファイルと NodeName.traversal.targets ファイルは、常に最上位の対応するファイル (root.setting および root.traversal.targets) をインポートします。これらの最上位のファイルには、グローバルな設定とターゲットが格納され、それに対するカスタマイズをノード レベルのファイルに挿入することができます。通常、これらのノード レベルのファイルがインポートするのは、ルート ファイルのみです。

図 7 は、root.traversal.targets ファイルの内容を示しています。このファイルに存在するターゲットは、基本的に Build、Rebuild、Clean の 3 つです。その他のターゲットおよびプロパティは、この 3 つのターゲットをサポートするためだけに存在します。このファイルに使用される ProjectFiles 項目は、その特定のディレクトリの dirs.proj ファイルで宣言されます。dirs.proj ファイルの要件は、次のとおりです。

  1. ビルド対象のすべてのプロジェクトを ProjectFiles で定義する。
  2. NodeName.setting ファイルを先にインポートする。
  3. NodeName.targets ファイルを後でインポートする。

図 7 root.traversal.targets ファイル

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Targets used to build all the projects -->

  <PropertyGroup>
    <BuildDependsOn>
      $(BuildDependsOn);
      CoreBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target Name="Build" DependsOnTargets="$(BuildDependsOn)" />
  <Target Name="CoreBuild">
    <!--
    Properties BuildInParallel and SkipNonexistentProjects
    should be defined in the .setting file.
    -->
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Build"
             />
  </Target>

  <PropertyGroup>
    <RebuildDependsOn>
      $(RebuildDependsOn);
      CoreRebuild
    </RebuildDependsOn>
  </PropertyGroup>
  <Target Name="Rebuild" DependsOnTargets="$(RebuildDependsOn)" />
  <Target Name="CoreRebuild">
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Rebuild"
             />
  </Target>

  <PropertyGroup>
    <CleanDependsOn>
      $(CleanDependsOn);
      CoreClean
    </CleanDependsOn>
  </PropertyGroup>
  <Target Name="Clean" DependsOnTargets="$(CleanDependsOn)" />
  <Target Name="CoreClean">
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Clean"
             />
  </Target>
</Project>

dirs.proj ファイルには、同じディレクトリおよびそのサブディレクトリ以下のすべてのプロジェクトが含まれている必要があります。C# プロジェクト、Visual Basic .NET プロジェクト、他の (サブディレクトリ内の) dirs.proj プロジェクトなど、通常の MSBuild プロジェクトを追加できます。このファイルに、上位のディレクトリに存在するプロジェクトは含めません。dirs.proj ファイルを使用するには、そのディレクトリ構造の上位に存在する、必要なプロジェクトがビルド済みであることが必要です。

ビルドしようとしているプロジェクトが、ディレクトリ構造において上位のプロジェクトを参照しており、そのプロジェクトが古い場合は、その上位のプロジェクトは自動的にビルドされます。そのため、dirs.proj ファイルで、上位のプロジェクトをビルドするように指定する必要はありません。また、ビルドの規模があまりにも大きい場合は、プロジェクト参照ではなく、ファイル参照の使用をお勧めします。このアプローチに従ってプロジェクト参照に切り替えた場合、参照のみ変更すればよく、ビルド プロセスを変更する必要はありません。

次に、root.setting ファイルの内容を示します。

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">
  <!--
  Global properties defined in this file
  -->
  <PropertyGroup>
    <BuildInParallel 
      Condition="'$(BuildInParallel)'==''">true</BuildInParallel>
    <SkipNonexistentProjects 
      Condition="'$(SkipNonexistentProjects)'==''">false</SkipNonexistentProjects>
  </PropertyGroup>

</Project>

このファイルには、BuildInParallel と SkipNonexistentProjects の 2 つのプロパティがあります。よく見ると、プロパティに条件が指定されています。既存の値がある場合は、決して上書きしないようになっています。こうすることで、プロパティをカスタマイズしやすくしているのです。図 8 は、ROOT ディレクトリの dirs.proj ファイルの内容です。

図 8 ROOT ディレクトリの dirs.proj ファイルの内容

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Insert any customizations for settings here -->

  <Import Project="root.setting"/>

  <!-- Define all ProjectFiles here -->
  <ItemGroup>
    <ProjectFiles Include="Common\dirs.proj"/>
  </ItemGroup>

  <Import Project="root.traversal.targets"/>

  <!-- Insert any customizations for targets here -->

</Project>

この dirs.proj ファイルは、先ほど挙げた 3 つの条件をすべて満たしています。root.settng ファイルの値をカスタマイズするための指定が必要な場合は、そのファイルの Import よりも前に配置し、ターゲットに対するカスタマイズはそのファイルの Import よりも後に配置する必要があります。この dirs.proj ファイルの ProjectFiles 項目の定義を見ると、その内容をビルドするための Common\dirs.proj ファイルがインクルードされています。ROOT フォルダには、それ以外、ビルドする必要のあるプロジェクトは存在しません。Common\dirs.proj ファイルを図 9 に示します。

図 9 Common\dirs.proj ファイル

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Insert any customizations for settings here -->
  <PropertyGroup>
    <SkipNonexistentProjects>true</SkipNonexistentProjects>
  </PropertyGroup>


  <Import Project="common.setting"/>

  <!-- Define all ProjectFiles here -->
  <ItemGroup>
    <ProjectFiles Include="Common.csproj"/>
    <ProjectFiles Include="Common.IO\dirs.proj"/>
    <ProjectFiles Include="Common.UI\dirs.proj"/>
    <ProjectFiles Include="Products\dirs.proj"/>
  </ItemGroup>

  <Import Project="common.traversal.targets"/>

  <!-- Insert any customizations for targets here -->

  <PropertyGroup>
    < BuildDependsOn >
      CommonPrepareForBuild;
      $(BuildDepdnsOn);
      CommonBuildComplete;
    </BuildDependsOn>
  </PropertyGroup>

  <Target Name="CommonPrepareForBuild">
    <Message Text="CommonPrepareForBuild executed"
             Importance="high"/>
  </Target>
  <Target Name="CommonBuildComplete">
    <Message Text="CommonBuildComplete executed"
             Importance="high"/>
  </Target>
</Project>

このファイルでは、SkipNonexistentProjects プロパティをオーバーライドし、True に設定しています。ProjetFiles 項目には、4 つの値が設定されています。そのうちの 3 つは dirs.proj ファイルです。また、ビルドの依存関係リストに 2 つのターゲットが追加されています。「msbuild.exe dirs.proj /t:Build」というコマンドで Common\dirs.proj ファイルをビルドした場合、すべてのプロジェクトがビルドされ、カスタム ターゲットが実行されます。紙面の都合上、その結果をここに記載することはできませんが、この記事の他のサンプルと同様、これらのファイルのソースをダウンロードできます。

この記事では、製品に対する、よりよいビルド プロセスを形成するためのポイントをいくつか紹介しました。どのようなベスト プラクティスにも言えることですが、ここで紹介した規則がすべての状況に適しているとは限りません。ある程度の調整が必要になることもあります。ここで紹介した事柄を 1 つ 1 つ自分で試しながら、最適な方法を見つけるようにしてください。この記事が皆さんのお役に立てればさいわいです。ご意見やご感想をぜひお寄せください。

この記事の執筆にご協力いただいた Dan Moseley 氏 (MSBuild チーム) および Brian Kretzler 氏に感謝します。

Sayed Ibrahim Hashimi は、フロリダ大学で学士号を得たコンピュータ エンジニアです。Visual Studio 2005 のプレビュー版がリリースされた当初から、MSBuild を使用してきました。著作としては、『Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build』(Microsoft Press、2009 年) があります。同氏は、『Deploying .NET Applications: Learning MSBuild and Click Once』(Apress、2006 年) の共著者でもあり、『MSDN Magazine』を含め、さまざまな雑誌の記事を担当してきました。金融、教育、債権回収など、各方面の豊富な知識を活かし、フロリダ州ジャクソンビルを拠点に、コンサルタントやトレーナーとして活動しています。連絡先については、同氏のブログ (sedodream.com) を参照してください。