インサイド MSBuild

Microsoft ビルド エンジンのカスタム タスクを使用した、お好みの方法でのアプリケーションのコンパイル

Sayed Ibrahim Hashimi


この記事で取り上げる話題:

  • MSBuild の基礎
  • ターゲット、アイテム、変形、およびタスク
  • ビルド処理の拡張
  • カスタム タスクの実装

この記事で使用する技術:

  • Visual Studio 2005

サンプルコードのダウンロード: MSBuild06.exe (135KB)
翻訳元: Compile Apps Your Way With Custom Tasks For The Microsoft Build Engine (英語)


目次

  1. MSBuild 基礎
  2. ターゲットの作成
  3. アイテムおよび変形
  4. タスク
  5. ビルド処理の拡張
  6. MSBuild の拡張
  7. まとめ

補足事項

  • Web リソース

以前のバージョンの Visual Studio では、ビルド処理はたいていブラック ボックスであり、ビルド処理のカスタマイズを行うためにできることはほとんどありませんでした。Visual Studio 2005 および Microsoft .NET Framework 2.0 がリリースされたことによって、Microsoft ビルド エンジン (MSBuild) を使用してマネージ プロジェクトをビルドすることができます。MSBuild は拡張可能であり、これによりビルドの過程で行われる各手順をカスタマイズすることができます。MSBuild は XML ファイル (実際はプロジェクト ファイルですが、これに関する詳細については後ほど説明します) を使用して各手順について記述し、プロジェクトのビルド方法を簡単に変更し補うことができるようになります。

この記事では、MSBuild について紹介して、ビルドをカスタマイズするための使用方法を示します。MSBuild をコマンド ラインから使用する方法を具体的に説明して、Visual Studio 統合開発環境 (IDE) でプロジェクトをビルドするときに使用する処理そのものを複製する方法を示します。また、MSBuild は .NET Framework の一部として出荷されるので、MSBuild にプロジェクトをビルドさせるために Visual Studio を PC にインストールする必要がないことに注意することが重要です。Visual Studio 2005 に装備される MSBuild をサポートするファイルは、C#、Visual Basic、および J# プロジェクトのみをサポートしますが、プラットフォームとしての MSBuild はいかなる言語もサポートします。


1. MSBuild 基礎

MSBuild に関してまず知っておくべきことは、プロジェクト ファイルは XML ビルド ファイルであるということです。これは、いくつかの理由で重要です。第一に、Visual Studio により作成されたビルドが Visual Studio 外部で再生可能であることを保証します。第二に、Visual Studio プロジェクトを作成するときに規定のビルド ファイルを取得します。ある C# アプリケーションのプロジェクト ファイルを見てみましょう。この例のために、C# Windows ベースのアプリケーション プロジェクトを新たに作成しました。このプロジェクトが開いたら、[ソリューション エクスプローラ] でプロジェクトを右クリックして、[プロジェクトのアンロード] を選択します。そして、再度右クリックして [編集] を選択します。図 1 は、このプロジェクト ファイルを抜粋したものを示します。この抜粋から、さまざまなプロパティが定義されていることが分かります。たとえば、このプロジェクトの Platform および AssemblyName があります。

図 1 C# でのアプリケーション プロジェクト

<Project DefaultTargets="Build" 
         xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <Configuration Condition=" '$(Configuration)' == '' ">
            Debug</Configuration>
        <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
        <ProductVersion>8.0.50727</ProductVersion>
        <SchemaVersion>2.0</SchemaVersion>
        <ProjectGuid>{2763BAAA-B9EC-4D3F-8992-11A99E669682}</ProjectGuid>
        <OutputType>WinExe</OutputType>
        <AppDesignerFolder>Properties</AppDesignerFolder>
        <RootNamespace>WindowsApplication1</RootNamespace>
        <AssemblyName>WindowsApplication1</AssemblyName>
    </PropertyGroup>
    ...
</Project>

PropertyGroup 要素内のすべての宣言は、プロパティと呼ばれる名前と値の単純なペアです。大部分の Visual Studio の構成設定はプロパティ内に格納されますが、これらのプロパティは PropertyGroup 要素内に含まれていなければなりません。図 1 に示すプロジェクト ファイルでは、この宣言が PropertyGroup 要素内に含まれていることが分かります。

<AssemblyName>WindowsApplication1</AssemblyName>

この例では、AssemblyName がプロパティ名で、WindowsApplication1 がこのプロパティの値です。これらの値にアクセスするためには、$(PROPERTY_NAME) シンタックスを使用します。たとえばこの AssemblyName プロパティについては、$(AssemblyName) を使用してその値にアクセスします。プロパティを使用することで作業がどのように簡単になるか、そして実際にビルド処理全体がプロパティとどのように結び付いているかについては後ほど分かります。

MSBuild により実行される要素は、ターゲットに含まれていなければなりません。Visual Studio がプロジェクトをビルドするとき、実際には MSBuild ターゲットを実行しています。

ターゲットとは、次々に実行される関連タスクのセットのための単純なコンテナです。Compile、Build、Rebuild、および Publish などの多くのターゲットは、プロジェクトのビルドに関連する MSBuild に装備されています。WindowsApplication1 プロジェクト ファイルを見てみると、このファイルには 1 つのターゲットが定義されているわけではないことが分かります。これは、すべてのターゲットが他のファイルのセットからインポートされているためです。これが動作する方法は後ほど検討します。

既存のターゲットのいくつかを実行する方法について示し、新しいターゲットの作成方法の例をいくつか示します。MSBuild ターゲットを実行するためには、Visual Studio 2005 コマンド プロンプトを開きます。このツールを開くには、[スタート] | [プログラム] | [Microsoft Visual Studio 2005] | [Visual Studio Tools] をクリックします。コマンド プロンプト ウィンドウを開いたら、WindowsApplication1.csproj ファイルを含むフォルダに移動します。そして、このプロジェクトをクリーンにするため、次のコマンドを使用します。

msbuild.exe WindowsApplication1.csproj /t:Clean

この例では、使用するプロジェクト ファイルと実行するターゲットの 2 つのパラメータを渡します。プロジェクトのクリアに成功すると、その結果は図 2 と同様になります。

図 2 プロジェクトのクリーン
図 2 プロジェクトのクリーン

または、Build ターゲットを実行することでプロジェクトをビルドすることもできます。コマンドは、次の通りです。

msbuild.exe WindowsApplication1.csproj /t:Build

プロジェクト名およびターゲットと同様に、Msbuild.exe へ送信可能なコマンドライン パラメータが他にもいくつかあります。たとえば、/verbosity (/v) パラメータを使用してログ出力の詳細を指定したり、/property (/p) パラメータを使用してプロパティを指定したりすることができます。利用可能なパラメータおよびその使用方法の完全な一覧については、https://msdn.microsoft.com/ja-jp/library/ms164311(VS.80).aspx を参照してください。

コアの MSBuild スキーマ ドキュメント (%FrameworkDir%/MSBuild/Microsoft.Build.Core.xsd にあります) を見ることで、ターゲット要素はそのターゲット自体に割り当てられる属性を 5 つまで持つことができることがわかります (Name、Condition、DependsOnTargets、Inputs、および Outputs)。Name 属性は以前に示したように、ターゲットに対する参照です。各 MSBuild 要素は、その要素自体に関連する Condition を持つことができ、この Condition が False と評価されると、要素全体が無視されます。以前に示した Configuration プロパティ宣言から、これにはお気付きかもしれません。関連するセクションを次に示します。

<Configuration Condition=" '$(Configuration)' == '' ">
    Debug</Configuration>

この宣言では Configuration プロパティが空の場合、Debug の値が割り当てられます。ターゲットそのものを定義しておくと便利ですが、さまざまな状況によって実装は異なります。

DependsOnTargets 属性は、宣言されたターゲットの前に実行されなければならないターゲットをセミコロンで区切った一覧です。ターゲットが DependsOnTargets リストで宣言されている順番に、各ターゲットが実行されているかどうかが調査されます。MSBuild インスタンスごとに、ターゲットは多くても 1 回だけ実行されることに留意してください。常にターゲットは関連するビルド状態を持っており、初期の状態は NotStarted です。他の可能なビルド状態は InProgress、CompletedSuccessfully、CompletedUnsuccessfully、および Skipped です。ターゲットが NotStarted 以外の状態を持つ場合、再度実行されません。ターゲットを設計するときにはこれを覚えておいてください。完了したターゲットは通常スキップしたいと思いますが、これが望ましくない状態もあります。

残りのターゲット属性は、Inputs (ターゲットが処理を行うファイル) および Outputs (ターゲットが生成するファイル) です。これらの一覧は、有効期限が切れたアプリケーションの一部のみをビルドするという差分ビルドで重要な役割を果たします。

Visual Studio でのビルドの過程で、ビルドの一部分がスキップされたという旨のメッセージが表示されることが多いですが、これが差分ビルドです。ビルドの一部分がスキップされるのは、前回のビルド以来、そのセクションでは変更が行われていないためです。たとえば、多くの異なるプロジェクトを含むソリューションがある場合にビルドするとき、変更されたプロジェクトおよび変更されたプロジェクトに依存するプロジェクトのみをビルドすることが望ましいです。各ビルドについて、影響を受けていないプロジェクトを含めるのは時間の無駄です。

この差分ビルドの概念は、この Inputs および Outputs によってサポートされています。この考え方は、ターゲットが調査するファイルの Inputs リストを提供するというものです。これと同様に、ターゲットが作成するファイルの Outputs リストを提供します。Inputs が Outputs より古い場合、そのターゲットはスキップされます。多くの作業を実行して時間のかかるターゲットを作成するときは、正しい Inputs および Outputs を提供することが重要になります。そうすれば、MSBuild はそれらのターゲットの差分ビルドも同様にサポートすることができます。

ページのトップへ


2. ターゲットの作成

ここまでで基本的なことを学んだので、簡単な MSBuild ターゲットを作成するために必要なことのほとんどすべてが分かりました。残された問題はタスクのみです。タスクの詳細については別のセクションで扱いますが、ここでは簡単に説明してターゲットのビルドを開始できるようにします。MSBuild では、タスクとは作業の単純な単位です。ファイルのコピー、ファイルの署名、C# コンパイラの呼び出しなどで使用可能な、事前定義されたタスクが数多くあります。

タスクはターゲット内で実行されます。ターゲットが実行されていると、その実行中のターゲット内部に含まれる各タスクは、それが宣言されている順番に呼び出されます。たとえば、この簡単な Hello ターゲットを見てください。

<Target Name="Hello">
    <Message Text="Hello MSBuild" Importance="high"/>    
</Target>

このターゲットは Message タスクを使用して "Hello MSBuild" をロガーに送信 (この場合画面に印刷) します。このターゲットを WindowsApplication1 プロジェクト ファイルに置くと、正しく動作することを検証できます。これを行うには、WindowsApplication1 プロジェクトを Visual Studio 内部から編集します。IntelliSense が編集を支援します。

このターゲットをプロジェクト ファイルに追加するには、これを Project 要素の閉じタグのすぐ上に置きます。これが Project 要素の直下の子である限り、この場合場所は関係ありません。WindowsApplication1 プロジェクト ファイルの Hello ターゲットを呼び出すことで実際に動作することを検証してください。検証するには、次のコマンドを実行します。

msbuild WindowsApplication1.csproj /t:Hello

結果は次のようになります。

Target Hello:
  Hello MSBuild

ページのトップへ


3. アイテムおよび変形

ビルドはファイルに非常に依存するため、ファイルの操作が楽になるように構造体を持つのは当然です。これがアイテムの目的です。アイテムはファイルとともに使用することに限られているわけではありませんが、それが意図する目標であることは確かです。アイテムとは、ファイル (既存または既存ではない) またはファイルのグループへの参照です。アイテムは、関連するファイルのセットを 1 つに集めてそれに対して処理を行います。アイテム参照は、WindowsApplication1 プロジェクト ファイルで次のように宣言されます。

<ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Deployment" />
    <Reference Include="System.Drawing" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
</ItemGroup>

お分かりのように、アイテムは ItemGroup という XML 要素に含まれます。アイテムは、Include リスト、Exclude リスト、および Metadata の 3 つの部分に分解されます (図 3 を参照)。

図 3 アイテム属性

属性 説明
Include アイテムに含まれる、セミコロンで区切られたファイルのリスト。ワイルドカードを使用してファイルを特定することもできます。
Exclude アイテムへの挿入時に除外される、セミコロンで区切られたファイルのリスト。この属性でも同様にワイルドカードを使用することができます。
Metadata アイテムに関連するデータ。2 種類のメタデータがあります。既知のメタデータはすぐに利用可能で、FileName や FullPath などが含まれます。カスタム メタデータは、ビルド ファイル内のアイテムに添付されます。

Reference アイテムには、これに関連する System、System.Data、System.Deployment、System.Drawing、System.Windows.Forms、および System.Xml という値があります。次のようにより簡潔に宣言を行うことで、これとまったく同じ結果を得ることができます。

<Reference Include="System;System.Data;System.Deployment;
    System.Drawing;System.Windows.Forms;System.Xml"/>

図 4 のコードは、アイテムを初期化するためにワイルドカードを使用することができることを示します。ワイルドカード要素には、?、*、および ** の 3 つがあります。? ワイルドカードは、1 文字とあらゆる可能な文字とを置き換えます。

図 4 Compile 宣言

<ItemGroup>
    <Compile Include="Form1.cs">
        <SubType>Form</SubType>
    </Compile>
    <Compile Include="Form1.Designer.cs">
        <DependentUpon>Form1.cs</DependentUpon>
    </Compile>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
        <EmbeddedResource Include="Properties\Resources.resx">
            <Generator>ResXFileCodeGenerator</Generator>
            <LastGenOutput>Resources.Designer.cs</LastGenOutput>
            <SubType>Designer</SubType>
        </EmbeddedResource>
    <Compile Include="Properties\Resources.Designer.cs">
        <AutoGen>True</AutoGen>
        <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
        <AutoGen>True</AutoGen>
        <DependentUpon>Settings.settings</DependentUpon>
        <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
</ItemGroup>

<ItemGroup>
    <MyItem Include="Fi?e.txt"/>
</ItemGroup>

この宣言では、MyItem に Fire.txt、File.txt、Fine.txt などのファイルを含むことができますが、Fie.txt または FilingCabinet.txt などのファイルを含みません。MSBuild がこのアイテム宣言に出会うと、式と一致するファイルがあるかどうか、現在のディレクトリを調査します (パス情報が宣言に提供されていなかったため)。この式と一致するファイルは、MyItem リストに含まれます。

宣言内の * ワイルドカードは、ゼロまたはそれ以上の文字の組み合わせと置き換えられます。

<ItemGroup>
    <MyItem Include="Fi*e.txt"/>
</ItemGroup>

現在 MyItem には Fie.txt、File.txt、Fiddle.txt、Fi5e.txt などを含む名前を持つファイルが含まれます。

** ワイルドカードは、アイテムについて、現在のディレクトリと同様にすべてのサブディレクトリを循環的に検索するよう MSBuild に指示します。

<ItemGroup>
    <MyItem Include="InputFiles\**\Fi*e.txt"/>
</ItemGroup>

MSBuild がこの宣言に出会うと、Fi*e.txt というパターンと一致するファイルがあるかどうか InputFiles フォルダを検索し、その配下の各フォルダを循環的に検索します。このようなファイルすべてが MyItem リストに含まれます。

変形は、時々分かりづらく思われることがありますが、MSBuild の非常に重要な一部分です。アイテムの変形とは、2 つのアイテム リスト間の 1 対 1 のマッピングです。たとえば、ある場所から別の場所へファイルをコピーする場合に変形を使用する必要があり、変形の一部としてメタデータを使用します。

メタデータには、既知またはカスタムという 2 つの種類があります。既知のメタデータはすべてのファイル アイテムについて利用可能です。既知のメタデータの例には、Filename、FullPath、および Extension などがあります。これらはファイルのファイル名、ファイルへのフォルダ パス、およびファイルのファイル名の拡張子をそれぞれ返します (既知のメタデータを完備する一覧については、https://msdn.microsoft.com/ja-jp/library/ms164313(VS.80).aspx を参照してください)。カスタム メタデータは、プロジェクト ファイル内のアイテムに直接関連します。

メタデータと変形の使用について説明するために、コンパイルされるファイルを別の場所にコピーしてみましょう。まず初めに、それらのファイルを参照する方法を知る必要があります。WindowsApplication1 プロジェクト ファイルを再度見てみると、Compile という名前のアイテム宣言があるのが分かります。これが、C# コンパイラに送信されるファイルのセットです。このアイテムの完全な宣言は、図 4 に示されます。

この宣言は、以前に示した参照の宣言とはまったく異なっていることに注意してください。これは、Compile アイテムがカスタム メタデータをそれらのアイテムと関連付けるためです。カスタム メタデータには、このアイテム内に含まれる XML 要素 (このファイルが自動生成されたものであることを Visual Studio に知らせる AutoGen など) が含まれます。

これらのファイルを、プロジェクト ディレクトリの SourceCopy という名前のフォルダに移動しましょう。予約されている MSBuildProjectDirectory プロパティを使用して、プロジェクト ディレクトリを決定します。他の予約されたプロパティの一覧については、https://msdn.microsoft.com/ja-jp/library/ms164309(VS.80).aspx を参照してください。

変形のシンタックスは次の通りです。

@(ItemName->'TransformationDetails')

ItemName は変形するアイテムの名前で、TransformationDetails には raw テキスト、評価されたプロパティ、およびメタデータ参照が含まれます。CopyFiles ターゲットにより、ファイルが指定された場所にコピーされます。

<Target Name="CopyFiles">
    <!-- If the CopyLocation doesn't exist then create it -->
    <MakeDir Directories="$(CopyLocation)" 
        Condition="!Exists('$(CopyLocation)')"/>
    <!-- Now actually copy the files -->
    <Copy SourceFiles="@(Compile->'%(FullPath)')"
        DestinationFiles="@(Compile->
            '$(MSBuildProjectDirectory)
            \$(CopyLocation)\%(RelativeDir)\%(FileName)%(Extension)')"
    />
</Target>

CopyFiles ターゲットはまず、ディレクトリが存在しない場合は MakeDir タスクを呼び出して、ディレクトリを作成します。次に、Copy タスクがファイルを指定された場所にコピーします (Copy タスクは、MSBuild により提供されます。組み込みの MSBuild タスクに関する情報は、https://msdn.microsoft.com/ja-jp/library/7z253716(VS.80).aspx で得ることができます)。

この例では、Copy タスクに SourceFiles および DestinationFiles という 2 つのパラメータを渡します。SourceFiles について、タスクは Compile アイテムの FullPath を抽出しています。DestinationFiles の変形指定はもう少し複雑であるため、構成要素に分解します。評価されるプロパティには、$(MSBuildProjectDirectory) および $(CopyLocation) の 2 つがあります。この後、つなぎ合わせてパス全体を作成するために役立つ 3 つのメタデータの部分があります。使用されるメタデータは、%(RelativeDir) (パス情報について)、%(FileName)、および %(Extension) です。これらをつなぎ合わせるとパスが出来上がります。この処理は、Copy タスクを 1 回だけ呼び出すことに注意することが重要です。

ここで、WindowsApplication1 プロジェクト ファイルで CopyFiles ターゲットを実行することで、タスクの作業が意図するように行われていることを検証する必要があります。Visual Studio 2005 コマンド プロンプトから、そのプロジェクト ファイルを含むディレクトリに移動して、次のコマンドを実行します。

msbuild WindowsApplication1 /t:CopyFiles

このターゲットの実行結果は、図 5 に示されます。

図 5 CopyFiles ターゲットの出力
図 5 CopyFiles ターゲットの出力

Visual Studio プロジェクト ファイルが MSBuild 機能をすぐに提供する方法の例についても、見てみましょう。次のシナリオについて考えます。ハード ドライブにいろいろなプロジェクトが多く保存されていて、それらすべてをクリーンしたいとします。一見して、\bin および \obj のすべてのディレクトリを単に削除することを考えるかもしれませんが、重要なファイルを誤って削除してしまう可能性があります。理想的な解決法は、各プロジェクトにどれが必要でどれが削除可能かを決定させるというものです。MSBuild は、まさにこれを行うことができるようにします。非常に簡単な MSBuild プロジェクト ファイルである CleanAllProjects.proj を見てみましょう。

 

<Project xmlns=https://schemas.microsoft.com/developer/msbuild/2003 
    DefaultTargets="CleanAllProjects">
    <!-- Find all projects in or below the current directory -->
    <ItemGroup>
        <Projects Include="**\*.*proj" />
    </ItemGroup>
    <Target Name="CleanAllProjects">
        <MSBuild Projects="@(Projects)" Targets="Clean" 
            StopOnFirstFailure="false" ContinueOnError="true">
        </MSBuild>
    </Target>
</Project>

このプロジェクトには、単独のアイテムおよび現在のディレクトリ内または配下にあるすべてのプロジェクト ファイルを含む Projects があります。このアイテムに加えて、CleanAllProjects ターゲットがあります。このタスクは、呼び出されると Projects アイテムに含まれているプロジェクト ファイルのすべてについて Clean ターゲットを呼び出します。これは、既定の MSBuild タスクを使用することで実現することができ、プロジェクト ファイルを処理する MSBuild の新しいインスタンスを作成できるようになります。この解決法によって、プロジェクトに決定を行わせているため、必要のないファイルが削除されると自信を持って考えることができます。Visual Studio 2005 と MSBuild 以前には、この種類の機能はカスタム ソリューション以外では利用不可能でした。

ページのトップへ


4. タスク

MSBuild では、タスクとは行われる作業の単純な単位です。各タスクは、すべてのほかのタスクに依存しておらず、元々いかなる他のタスクにも気付きません。タスクの例はファイルをコピーし、C# ファイルをコンパイルし、ディレクトリを作成するなどを行います。タスクは、そのタスク自体に関連するパラメータのセットおよびタスクが返すパラメータを持つことができます。これらのパラメータは、そのタスク自体が呼び出している MSBuild プロジェクト ファイルとタスクがやり取りを行う手段です。

MSBuild には、複数の便利なタスクが装備されています。完全な一覧については、%WinDir%\Microsoft.NET\Framework\v2.0.50727 ディレクトリにある Microsoft.Common.tasks ファイルを参照してください。頻繁に使用されるタスクのいくつかを、図 6 に要約します。MSBuild に装備されるこれらの、または他のタスクの使用方法に関する具体的な詳細情報については、「MSDN MSBuild タスク リファレンス」を参照してください。

図 6 MSBuild タスク

タスク 説明
CreateItem アイテムを作成します。アイテムはビルドの開始時に評価されるため、ビルドにより生成されたファイルをアイテムに含めることが必要な場合は、このタスクを使用しなければなりません。作成されているアイテムが既に存在する場合、それが付加されます。
CreateProperty プロパティを作成します。プロパティはビルドの開始時に評価されます (CreateProperty により作成されたプロパティを除く)。作成されているプロパティが存在する場合、上書きされます。
Copy ある場所から別の場所へファイルをコピーします。
Delete ファイルを削除します。
Error エラーが発生したことを MSBuild に通知します。通常このタスクは、何かが失敗したことを意味する Condition 属性とともに使用されます。
Exec 実行ファイルを呼び出します。
Message メッセージを MSBuild ロガーに渡します。
MSBuild 他のプロジェクト ファイルで MSBuild を呼び出します。Exec タスクを使用して Msbuild.exe を開始することもできますが、MSBuild タスクには、実行されたターゲットの出力を取得するなどの利点があります。

以前 Message タスクを使用してメッセージをコンソール ロガーに出力する方法の例を確認しました。

<Target Name="Hello">
    <Message Text="Hello MSBuild" Importance="high"/>    
</Target>

このタスクの呼び出しにより、Text と Importance という 2 つの情報が Message タスクに渡されます。このタスクを使用するときに、各タスクにそれぞれの入力および出力があることにすぐ気付くでしょう。たとえば、Message タスクには Text という名前の入力がありますが、ファイルの Copy にはありません。正しい入力および出力は、タスクのドキュメントまたはソース コードから決定することができます。たとえば Message タスクはいかなる出力も作成しません。

タスク出力を具体的に説明するために、CreateItem タスクを使用します。CreateItem タスクについて、作成されたアイテムに含めるファイルの一覧を指定する必要があります。この入力の名前が Include です。これは、新たに生成されたアイテムの取得に使用する出力でもあります。WindowsApplication1 サンプルでは、プロジェクト ファイルの最後に次のコードを追加しています。

<Target Name="PrintOutputFiles" DependsOnTargets="Build">
    <!-- First create an Item that contains these files. 
     $(OutputPath) is the property that contains the 
     locations where built files are placed. -->
    <CreateItem Include="$(OutputPath)\**\*">
        <Output TaskParameter="Include" ItemName="OutputFiles" />
    </CreateItem>

    <!-- Now send these values to the logger -->
    <Message Text="@(OutputFiles->'%(Filename)%(Extension)')" 
     Importance="high"/>
</Target>

お分かりのように、CreateItems タスクが呼び出されると OutputFiles アイテムが作成され、そして、コンソールに値を出力するために Message 要素で使用されます。OutputFiles アイテムは CreateItem タスクが完了するとすぐに利用可能となり、現在実行しているターゲットだけでなく、すべての他のターゲットからも利用可能となります。

ページのトップへ


5. ビルド処理の拡張

MSBuild により (したがって Visual Studio により) 使用されるビルド処理全体は、プロジェクト ファイルにより定義されます。Visual Studio を使用してプロジェクトを作成するときは、ファイルのセットはプロジェクト ファイルにインポートされます。ファイルは Import 要素を使用してインポートされます。C# プロジェクトでは、Microsoft.CSharp.targets ファイルがインポートされます。このファイルには、C# コンパイラ (CSC) を呼び出す手順の定義など、C# プロジェクトのビルド方法に関する具体的な情報が含まれています。そして、Microsoft.Common.targets ファイルをインポートします。このファイルが、すべてのマネージ ビルドの過程で使用される汎用的な手順の定義を担当します。ビルド処理の多くはこのファイルで定義されます。

Microsoft.Common.targets ファイルから、ビルド ターゲットを定義する方法について見てみましょう。

<PropertyGroup>
    <BuildDependsOn>
        BeforeBuild;
        CoreBuild;
        AfterBuild
    </BuildDependsOn>
</PropertyGroup>
<Target
    Name="Build"
    Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
    DependsOnTargets="$(BuildDependsOn)"
Outputs="$(TargetPath)"
/>

これが、Visual Studio を使用してプロジェクトをビルドする場合に既定で呼び出されるターゲットです。このターゲット自体は何も行わないことがわかります。ターゲットは、そのターゲット自体が依存するターゲットにすべてを任せていて、このターゲットがすべての作業を行います。これは、DependsOnTargets リストを使用して指定されます。以前に述べたように、これが、このターゲットが呼び出される前に実行される必要があるターゲットのリストです。この場合、このリストはプロパティ BuildDependsOn で定義されます。これにより、ターゲットの実行に必要な手順のカスタマイズがより簡単になります。

これが動作する方法のみを示すために、ビルド処理の途中に手順を 1 つ追加します。CoreBuildDependsOn リストを編集することで、これを行います (図 7 を参照)。これが、CoreBuild ターゲットが使用するターゲットのリストです。CoreBuildDependsOn は、WindowsApplication1 プロジェクト ファイルで再定義されます。この再定義に加えて、プロジェクトに特有で必要となるターゲットを追加します。

図 7 CoreBuildDependsOn

<PropertyGroup>
    <CoreBuildDependsOn>
        BuildOnlySettings;
        PrepareForBuild;
        PreBuildEvent;
        UnmanagedUnregistration;
        ResolveReferences;
        PrepareResources;
        ResolveKeySource;
        Compile;
        MyCustomStep;
        GenerateSerializationAssemblies;
        CreateSatelliteAssemblies;
        GenerateManifests;
        GetTargetPath;
        PrepareForRun;
        UnmanagedRegistration;
        IncrementalClean;
        PostBuildEvent
    </CoreBuildDependsOn>
</PropertyGroup>
<Target Name="MyCustomStep">
    <Message Text="Inside of MyCustomStep target" Importance="high"/>
</Target>

MyCustomStep ターゲットは、WindowsApplication1 プロジェクトの次のステートメントの後で、依存リストに追加されます。

<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />

BuildDependsOn プロパティは、Import ステートメントの後に挿入しなければなりません。なぜなら、Microsoft.Common.targets もこのプロパティを定義するためです。プロパティが複数回定義されると、最後に定義されたものが使用されます。この定義を Import ステートメントより前に置くと、単にオーバーライドされます。

MyCustomStep は、メッセージを MSBuild ロガーに渡します。プロジェクトをビルドすると、手順の追加に成功していることを検証することができます。Visual Studio 2005 コマンド プロンプトは、次のコマンドを実行します。

msbuild WindowsApplication1.csproj /t:Build

この結果は、図 8 に示されます。

図 8 MyCustomStep のビルド出力
図 8 MyCustomStep のビルド出力

カスタム手順をビルドに追加する方法は他にもあり、多くのターゲットがこの目的のためだけに作成されています。たとえば、BeforeBuild および AfterBuild ターゲットは既定で空であり、オーバーライドされるように設計されています。ステップをすばやく、簡単に実行する必要があるときは、これらのターゲットのうち 1 つがオーバーライドされ、適切な時に呼び出されます。ですが、この技法を使用するには難点もあります。同じターゲットがオーバーライドされる場所が複数ある場合、オーバーライドのうちの 1 つだけが保たれます。DependsOn プロパティを持つターゲットの前または後に手順を追加する必要がある場合は、そのリストに追加するのが最良の選択肢です。たとえば、BeforeBuild ターゲットをオーバーライドする代わりに、ターゲットを定義して BuildDependsOn プロパティの前にそれを追加することができます。

<PropertyGroup>
    <BuildDependsOn>
        CustomBeforeBuild;
        $(BuildDependsOn);
    </BuildDependsOn>
</PropertyGroup>
<Target Name="CustomBeforeBuild">
    <!-- Your steps here -->
</Target>

この再定義は、$(BuildDependsOn) によってアクセスされる BuildDependsOn プロパティの既存の値を使用して、追加します。これが複数回実行されると、すべての編集が存在し、ターゲット実行が失われません。

ページのトップへ


6. MSBuild の拡張

MSBuild には、主にカスタム タスクとカスタム ロガーという 2 つの拡張方法があります。カスタム タスクの記述方法については、この記事で後ほど示します。それに対して MSBuild ロガーは、ビルド イベント通知を受け取り、それらの処理を担当するオブジェクトです。MSBuild には、既定でコンソール ロガーとファイル ロガーの 2 つのロガーが装備されています。Msbuild.exe のコマンドライン パラメータを使用して、どちらのロガーを使用するかを宣言できます。ここでカスタム ロガーについて掘り下げるだけの十分な余裕はありませんが、カスタム ロガーに関する情報は、https://msdn.microsoft.com/ja-jp/library/ms171470.aspx にあります。

Microsoft.Common.tasks ファイルを見れば、多くのタスクが宣言されていることが分かります。Copy タスクや Delete タスクがありますが、Move タスクがありません。通常ビルド過程では、コピー操作やファイル削除でさえも行うことができますが、多数のファイルを移動する必要はありません。この機能の必要性を感じて、これを行うタスクがあればよいと考えることがあるかもしれませんが。

タスクを作成するときには、優れたサブルーチンのように、そのタスクを再利用可能にしたいことに留意してください。このため、タスクは小さい手順となっていなければなりません。非常に多くの処理を実行するタスクを作成したくありません。また、このタスクが他の構成要素とのやりとりをどのように行うかを考慮する必要があります。このやりとりは、タスクの入力および出力によって容易になります。サポートされる入力および出力の型には、文字列および他の組み込みの型のすべて、加えて Microsoft.Build.Framework.ITaskItems およびそれらのオブジェクトの配列があります。ITaskItem は、MSBuild によって使用されるアイテムの基底インターフェイスです。ですので、プロジェクト ファイルでは、アイテムを作成するときに、実際には 1 つまたは複数の ITaskItems を作成しています。タスクに ITaskItems が含まれている場合は、プロジェクト ファイルで行っているのと同様の方法で ITaskItems のメタデータにアクセスすることができます。

すべての MSBuild タスクは Microsoft.Build.Framework.ITask インターフェイスを実装しなければなりません。ITask には 2 つのプロパティ (BuildEngine および HostObject) そして 1 つのメソッド (Execute) があります。Execute メソッドは、適当な時にタスクのアクティビティを実行するために呼び出されます。これは非常に簡単なインターフェイスであり、実装が容易です。

自分のタスクを記述したら、最も良いのはアブストラクト Microsoft.Build.Utilities.Task クラスを実際に拡張することです。このクラスは 2 つのプロパティの実装を行い、いくつかの他の処理を支援します。こうすることで、タスクの背後のロジックの記述に専念することができ、MSBuild の特徴を考慮することがなくなります。

Move タスクは単に Task クラスを拡張します。Move タスクには、4 つのプロパティおよび Execute メソッドがあります。Move クラスのクラス ダイアグラムは、図 9 に示されます。4 つのプロパティから、可能な入力として SourceFiles、DestinationFiles、および DestinationFolder がデザインされます。残りの MovedFiles プロパティは、MSBuild 出力です。これには、移動されたファイルの一覧が含まれています。出力 MovedFiles の宣言方法について見てみましょう。

private ITaskItem[] movedFiles;

[Output]
public ITaskItem [] MovedFiles
{
    get { return this.movedFiles; }
}

図 9 Move クラス ダイアグラム
図 9 Move クラス ダイアグラム

このプロパティは、ITaskItem オブジェクトの配列を返します。この返された値に Output 属性 (Microsoft.Build.Framework.OutputAttribute) が添付されているために出力として利用可能なことが分かります。すべての出力にはこの属性がなければならず、またパブリックの get アクセサもなければなりません。入力プロパティは 図 10 に示されます。

図 10 Move 入力プロパティ

private ITaskItem[] sourceFiles;
private ITaskItem[] destFiles;
private ITaskItem destFolder;

[Required]
public ITaskItem [] SourceFiles
{
    get { return this.sourceFiles; }
    set { this.sourceFiles = value; }
}

public ITaskItem [] DestinationFiles
{
    get { return this.destFiles; }
    set { this.destFiles = value; }
}

public ITaskItem DestinationFolder
{
    get { return this.destFolder; }
    set { this.destFolder = value; }
}

3 つの入力プロパティは標準プロパティのように見えますが、1 つ例外があります。SourceFiles プロパティには Required 属性 (Microsoft.Build.Framework.RequiredAttribute) があります。この属性を使用することで、入力パラメータに値が設定されていないとタスクは実行されません。他の入力プロパティは任意です。ITaskItems があるため、それらのメタデータに GetMetadata および SetMetadata methods を使用してアクセスすることができます。Execute メソッドは図 11 に示すように、どのプロパティが設定されているかを単に確認し、エラー チェックを行い、ファイルを 1 つずつ移動します (必要であれば移動先ディレクトリを作成します)。

図 11 Move タスク内の Execute メソッド

public override bool Execute()
{
    Log.LogMessageFromText("Starting move", MessageImportance.Normal);
    bool allSucceeded = true;

    //ここで、実際に移動を実装する必要がある
    if (this.SourceFiles == null || this.SourceFiles.Length <= 0)
    {
        //移動するものがなければ何もせず終了する
        this.DestinationFiles = new ITaskItem[0];
        Log.LogMessageFromText("Nothing to move", 
            MessageImportance.Normal);
        return true;
    }

    if (this.DestinationFiles == null && this.DestinationFolder == null)
    {
        Log.LogError("Unable to determine destination for files");
        return false;
    }

    if (this.DestinationFiles != null && this.DestinationFolder != null)
    {
        Log.LogError("Both DestinationFiles & DestinationFolder " +
            "were specified, only one can be defined");
        return false;
    }

    if (this.DestinationFiles != null && 
       (this.DestinationFiles.Length != this.SourceFiles.Length) )
    {
        //移動元と移動先のアイテム数が一致しない
        Log.LogError("SourceFiles and DestinationFiles differ in length");
        return false;
    }

    if (this.DestinationFiles == null)
    {
        //DestinationFolder から値を設定
        this.DestinationFiles = new ITaskItem[this.SourceFiles.Length];
        for (int i = 0; i < this.SourceFiles.Length; i++)
        {
            string destFile; 
            try
           {
               destFile = Path.Combine(
                   this.DestinationFolder.ItemSpec,Path.GetFileName(
                       this.SourceFiles[i].ItemSpec));
           }
           catch(Exception ex)
           {
               Log.LogError("Unable to move files; " + ex.Message,null);
               this.DestinationFiles = new ITaskItem[0];
               return false;
           }
           this.DestinationFiles[i] = new TaskItem(destFile);
           this.SourceFiles[i].CopyMetadataTo(this.DestinationFiles[i]);
       }
   }

   this.movedFiles = new ITaskItem[this.SourceFiles.Length];
   //これで、次に進みすべてのファイルを移動できる
   for (int i = 0; i < SourceFiles.Length; i++)
   {
       string sourcePath = this.SourceFiles[i].ItemSpec;
       string destPath = this.DestinationFiles[i].ItemSpec;
       try
       {
           string message = string.Format(
               "Moving file {0} to {1}", sourcePath, destPath);
           Log.LogMessageFromText(message, MessageImportance.Normal);
           FileInfo destFile = new FileInfo(destPath);
           DirectoryInfo parentDir = destFile.Directory;
           if (!parentDir.Exists) parentDir.Create();
           File.Move(sourcePath, destPath);
           this.movedFiles[i] = new TaskItem(destPath);
       }
       catch (Exception ex)
       {
           Log.LogError("Unable to move file: " + sourcePath + " to " + 
               destPath + "\n" + ex.Message);
           allSucceeded = false;
       }
   }

   return allSucceeded;
}

この時点で、完了したタスクがあり、それを MSBuild プロジェクトで使用することができます。そのため、MoveFiles.proj という空のプロジェクト ファイルを作成しました。このタスクを使用するには、タスクを含むアセンブリをビルドしなければなりません。アセンブリの名前は Tasks.dll です。このアセンブリをビルドしたら、それを示されたディレクトリに置きます。この場合、SharedTasks というディレクトリにアセンブリを置いています。プロジェクトでカスタム タスクを使用する場合は、MSBuild にどのタスクを使用しようとしているか、そしてその場所を指示しなければなりません。UsingTask 要素によってこれを行います。この例のプロジェクトの場合、宣言は次のようになります。

<PropertyGroup>
    <!-- Location of the shared tasks directory -->
    <SharedTasksDir>..\..\SharedTasks</SharedTasksDir>
    <SourceFolder>SourceFiles</SourceFolder>
    <DestFolder>DestFolder</DestFolder>
</PropertyGroup>

<UsingTask AssemblyFile="$(SharedTasksDir)\Tasks.dll" TaskName="Move"/>

UsingTask 宣言は、必要なアセンブリ (AssemblyFile) がどこにあるか、そしてタスク名 (TaskName) を述べています。AssemblyFile 属性の代わりに、AssemblyName 属性を使用することができます。対象のアセンブリをロードするために、AssemblyFile は MSBuild が Assembly.LoadFrom を使用するようにします。一方 AssemblyName を使用すると、MSBuild は Assembly.Load を使用します。その違いの詳細については、UsingTask のドキュメントを参照してください。

このタスクを実際に動作させて具体的に説明するために、MoveFiles というターゲットを作成しました。

<Target Name="MoveFiles">
    <Move SourceFiles=
            "@(SourceFiles->'%(FullPath)')" 
        DestinationFiles=
            "@(SourceFiles->'$(DestFolder)\
             %(Filename)%(Extension)')">
        <Output TaskParameter="MovedFiles" 
            ItemName="FilesMoved"/>
    </Move>
    <!-- Print out the files so we can see 
         where they moved to -->
    <Message Text=
        "Moved files:%0d%0a  
         @(FilesMoved->'%(FullPath)','%0d%0a  ')"/>
</Target>

このターゲットでは、他のタスクで行ったように Move タスクを呼び出します。入力プロパティをパラメータとして指定し、出力を Output 要素に収集します。これで、このターゲットを呼び出して、このタスクが実際に動作することを検証できます (図 12 を参照)。

図 12 MoveFiles ターゲット出力
図 12 MoveFiles ターゲット出力

GetDate、GetRegKey、および TempFile という他の 3 つのカスタム タスクは、この記事のソース コードに含まれており、MSDN マガジン Web サイトから利用可能です。GetDate タスクは、指定された形式に基づく現在の日付および時刻をプロパティに設定します。GetRegKey タスクは、レジストリ値を取得します。TempFile タスクは、テンポラリ ファイルを作成します。カスタム タスクの作成方法に関するより深い見識を得るために、これらのタスクと対応するプロジェクト ファイルの例について見てみます。

ページのトップへ


7. まとめ

MSBuild は、表示されずに動作するよう設計されており、その結果として多くの開発者はその存在に気付くことすらありません。この記事を読んでから、特定の必要性に適合するようにビルド処理をカスタマイズすることに自信を持たれたのではないでしょうか。ここでは、MSBuild の概要を広く示しましたが、多くのテクノロジのように、1 つの記事ですべてを扱うことはできません。MSBuild に関して学ぶ最良の方法は、使ってみることです。

カスタム タスクを記述することについて知っておくべきことのすべてがこの記事にあるわけではありませんが、ここで提供された情報から、作業を簡単にする多くのタスクを記述することができるはずです。この点から、実行する必要があるさまざまなビルド手順を扱うために、自分のカスタム タスクを記述することができます。検討しませんでしたが、調査する価値のある他の問題としては、そのタスク自体のアプリケーション ドメイン内で実行したり、あるタスク内部のアイテムにメタデータを割り当てるタスクなどがあります。

うまくいかないときに参照することができる多くのさまざまなリソースが Web 上で利用可能です。これらのリソースの一覧は、"Web リソース" サイドバーで利用可能です。MSBuild フォーラムは、具体的な疑問に回答が必要なときに訪れるのに絶好の場所です。

ページのトップへ


Web リソース

ページのトップへ


Sayed Ibrahim Hashimi は、フロリダ大学のコンピュータ エンジニアリングの学位を取得しており、Florida、Jacksonville で開発および設計を担当しています。同氏は会計、教育、および回収産業の専門家であり、主に .NET に集中して取り組んでいます。


この記事は、MSDN マガジン - 2006 年 6 月からの翻訳です。

QJ: 060601

ページのトップへ