kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

BUILD ネタという事で – C# Advent Calender 12/9


//BUILD/ と思わせておいて msbuild と Visual Studio のネタ。

という訳で、「Hay U! 思い通りにビルドをコントロールできてるかい?」

うちの Blog はたびたび msbuild のネタを取り上げるという事で、生粋のビルドヲタクである事は読者の皆様にはバレバレだと思いますが、C# Advent Calender に参加して、ビルドヲタたるものこれは知っとくべきとか、こんぐらい当然なのよっていう物を世間に知らしめておきたいかなと。

msbuild の基礎の基礎(1) 処理対象の項目一覧 ItemGroup とアイテム

msbuild は処理対象の項目の一覧を ItemGroup として定義します。シンプルに csproj の中でファイルやアセンブリ参照が ItemGroup として定義されています。

VIsual Studio でクラスライブラリのプロジェクトを作り、デフォルトである Class1 に加えてClass2,Class3を作った状態の csproj には以下の2つのItemGroupが定義されています。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>

ソースファイルは Compile Itemが作成されていて、参照設定はReference になっていますね。

この Compile/Reference は最終的に Import されている Microsoft.CSharp.Targets を介して csc タスク(C# Compiler Task)に引き渡されてビルドされるというわけ。

単純にコンパイルするソースであれば Compile で単純に足せばよいという事ですが、コンパイル出来ればビルドとか素人の考え、色々な処理の仕方に応じた分類はいくらでも増やしたいものです。

Web やってれば画像をなんらか処理したり、cssやjsをminify したり色々やる事があるでしょう、その他やっている事に応じて色々やりたいことなんて幾らでもあるでしょう。

やる事を増やすには Target / Task を新たに定義してって事になるのですが、処理対象としてはこの ItemGroup を種類で抽出して受け取る事になるので結局は種類を増やす以外は有りません。

単純に Visual Studio でテキストファイルを追加すると種別は Content として入ります。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>
<
ItemGroup>
<
Content Include=TextFile1.txt/>
</
ItemGroup>


この状態のソリューションエクスプローラーとプロパティは以下の通り。

image

このプロパティでのビルドアクションを「なし」に変えると ItemGroup 定義は以下の様になります。

<ItemGroup>
<
None Include=TextFile1.txt/>
</
ItemGroup>

これで ItemGroup の種別が ビルドアクションで選択される事はみなさんの知識になりました。

要するに Visual Studio のファイルプロパティでビルドアクションのリストに必要な ItemGroup 種別を足すことができればテキストなりXMLなりを足してそれに必要な ItemGroup 種別を割り当てる事ができ、それに応じた処理をさせる事ができます。

ここで特殊な ItemGroup を csproj に追加します。

<ItemGroup>
<
AvailableItemName Include=TextToBitmap>
<
Visible>false</Visible>
</
AvailableItemName>
</
ItemGroup>
<
ItemGroup>
<
TextToBitmap Include=TextFile1.txt/>
</
ItemGroup>

AvailableItemName という ItemGroup にTextToBitmap をVisibleメタデータをfalse で追加しました、ここで追加したTextToBitmap を TextFile1.txt に割り当てています。このファイルのプロパティと、ビルドアクションのドロップダウン内容が以下になります。

imageimage

AvailableItemName で指定した項目がビルドアクションのドロップダウンに追加されています、他のクラスとかにも TextToBitmap が選択できるようになったのが確認できるでしょう。

これでビルドの細かい事とか理解していない非ビルドヲタの人にはビルドアクションを指定してねだけで済ませる事ができるわけです。

ItemGroup の操作についてはターゲット内での操作とか含めてこの先もチョロチョロ出てきますが基本中の基本としてはビルド作業の細かい処理に送りつけるファイルを分類する事ができるという事です。

C# Advent Calender なのに C# がここまでで一行も出てこないとか、気にしないで下さい、先の方でちょっとだけ出てきますので。

msbuild の基礎の基礎(2) ターゲット実行順序の制御

ターゲットの構造は Target の属性による入出力ファイルの指定や依存関係によるビルド処理の順序指定と、Target内に記述されたタスクでの処理の定義に分かれます。

標準で Import されている Microsoft.CSharp.Target やその中から Import される Microsoft.Common.Targets の内容を熟知すると立派なビルドヲタなわけですが必要知識が極端に上がるだけで説明の邪魔なので一端 Microsoft.CSharp.Targets の Import をコメントアウトして Build ターゲットを自作してみます。

<ItemGroup>
<
Reference Include=System/>
<
Reference Include=System.Core/>
<
Reference Include=System.Xml.Linq/>
<
Reference Include=System.Data.DataSetExtensions/>
<
Reference Include=Microsoft.CSharp/>
<
Reference Include=System.Data/>
<
Reference Include=System.Xml/>
</
ItemGroup>
<
ItemGroup>
<
Compile Include=Class1.cs/>
<
Compile Include=Class2.cs/>
<
Compile Include=Class3.cs/>
<
Compile Include=Properties\AssemblyInfo.cs/>
</
ItemGroup>
<
ItemGroup>
<
AvailableItemName Include=TextToBitmap>
<
Visible>false</Visible>
</
AvailableItemName>
</
ItemGroup>
<
ItemGroup>
<
TextToBitmap Include=TextFile1.txt/>
</
ItemGroup>
<!–
<Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” /> —>
<
Target Name=Build>
<
Error Text=まだ作ってないよ/>
</
Target>

Error タスクを使ってメッセージを出して単純に失敗する Build ターゲットを作りました、結果はこの通り。

image

意図通り動いている事が確認できました。まず依存関係での処理順序の制御をしてみましょう。

<!– <Import Project=”$(MSBuildToolsPath)\Microsoft.CSharp.targets” /> —>
<
Target Name=BeforeBuild>
<
Warning Text=BeforeBuild is running/>
</
Target>
<
Target Name=BuildCore>
<
Warning Text=BuildCore is running/>
</
Target>
<
Target Name=AfterBuild>
<
Warning Text=AfterBuild is running/>
</
Target>
<
Target Name=BuildDependsOnTargets=BeforeBuild; BuildCore; AfterBuild>
<
Error Text=まだ作ってないよ/>
</
Target>

DependsOnTargets で各タスクを作成しました、実行結果は以下の通り、今回は出力ウインドウも付けています。出力ウインドウの表示は本当に実行順序設定どおりなのですが、エラー一覧は色々な並べ替えがあるので注意が必要です。エラー一覧の2番目のカラムの数値で並べ替えすると出力順序に一致します。

結果としては DependsOnTargets で指定したターゲットが指定の順番で実行され、各ターゲットの実行が完了した後にDependsOnTargets を指定したターゲットが実行される事が解ります。これでターゲットの実行順序は設定できる事が解ります。

image

msbuild 4より前ではこれが唯一のビルドターゲットの実行順序の制御方法でした、msbuild 4では AfterTargets, BeforeTargets 属性によって既存のターゲットの実行順序の指定の位置でターゲットを実行する事を指定する事ができます。

After – Before という順序で書いたのはアルファベット順などという事でなく、実行順序の関係がそうだという事です。 AfterTargets で指定したターゲットの後、BeforeTargets で指定したターゲットの前で指定を行ったターゲットが実行されます。

以下のターゲット定義を設定する事で BeforeBuild, BuildCore の間で Transform が実行されます。

<Target Name=Transform
AfterTargets=BeforeBuild
BeforeTargets=BuildCore>
<
Warning Text=Transformが割り込んでみた/>
</
Target>

image

この関係を利用すると Microsoft.CSharp.Targets や Microsoft.Common.Targets での定義済みターゲットの実行が行われる間に割り込みが可能である事が解ります。

msbuild の基礎の基礎 (3) ItemGroup の処理、一括処理と順次処理

一度 Build のみの簡単な構成に戻して Task の呼び出しの例を紹介しましょう。

<Target Name=Build>
<
Warning Text=Example1: @(Compile)/>
<
Warning Text=Example2: @(Compile->’%(filename).generated%(extension)’)/>
<
Warning Text=Example3: %(Compile.filename)/>
</
Target>

若干ややこしそうな記述になりましたがこれをビルドすると以下の出力が出ます。

warning : Example1: Class1.cs;Class2.cs;Class3.cs;Properties\AssemblyInfo.cs
warning : Example2: Class1.generated.cs;Class2.generated.cs;Class3.generated.cs;AssemblyInfo.generated.cs
warning : Example3: Class1
warning : Example3: Class2
warning : Example3: Class3
warning : Example3: AssemblyInfo

Example3 のWarning が1行に関わらず4回、各 cs について実行されている事が解ります。

このようにパラメータの与え方によってmsbuild は自動的にタスクの呼び出し方を区別し繰り返し制御を実行します。

@(項目種別) で指定した場合、項目の集合がそのまま渡されます。

@(項目種別->’射影定義’) で指定した場合、項目の集合が射影されて項目集合として渡されます。(集合の変換)

%(項目種別 .参照するメタデータ) と記述すると参照するメタデータを参照する項目定義を要素に分割して繰り返しタスクないしはターゲットに渡します。

これによりにファイルを個別に処理する方法と複数のファイルを一括してタスク処理する方法を理解した事になります。

しかし標準タスクでできる事なんてたかが知れてるぜっていうビルドヲタの人は次のステップへ進みましょう。

msbuild ビルドヲタの第一歩 msbuild inline タスク

やっと C# Advent Calender らしい内容になりますよー、C#コードで msbuild のビルド処理で使われるタスクを記述する事にします。

まずは普通の inline タスクです。

<UsingTask TaskName=SampleTask
TaskFactory=CodeTaskFactory
AssemblyFile=$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll>
<
ParameterGroup />
<
Task>
<Code Type=FragmentLanguage=cs>
<![CDATA[
Log.LogWarning(“this is inline task”);
]]>
</
Code>
</
Task>
</
UsingTask>
<
Target Name=Build>
<
SampleTask />
</
Target>

Type属性で Fragment を指定した場合 Taskクラスの派生クラスの Execute メソッドの内部を記述する事になります。上記例では TaskクラスのLogプロパティを介して Warning メッセージを出力しています。

image

簡単に言えば Execute メソッド内のコードとして有効なコードであれば基本的には何でも書けます。 メソッドの分割が必要なレベルの複雑なコードでも Func<>, Action<> デリゲートとラムダで上手くやれば何とでもなるのが C# の素晴らしさ、記述規模さえ気にしなければどうにでもなります。

タスクにパラメータが必要であれば ParameterGroup で指定すれば良く、外部アセンブリを参照する事も using による名前空間参照も指定できますので必要であれば LINQ を使う事もできますし、XML処理をXLINQでも簡単です。

ゆとる msbuild ビルドヲタ msbuild inline タスクのソースファイルオプション

ドキュメントには Source 指定ができるとしか書いてなく、例も何にもなくという酷い扱いのSource 指定でのカスタムタスク定義を紹介しましょう。

Source オプションを使う場合にはソースを収容するクラスライブラリプロジェクトを用意すると便利です、自分は半分お約束的に Build.Utilities という名前のクラスライブラリをソリューションのトップに用意しています。

最も簡単な例として単にWarningを吐くタスクから。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Utilities;

namespace Build.Utilities
{
    public class SourceBasedCustomTask : Task {
        public override bool Execute()
        {
            Log.LogWarning("this is Source based custom task");
            return true;
        }
    }
}

これを読み込む csproj 側の記述は以下の通り。

<UsingTask TaskName=SourceBasedCustomTask
TaskFactory=CodeTaskFactory
AssemblyFile=$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll>
<
ParameterGroup />
<
Task>
<
Code
Type=Class
Language=cs
Source=..\Build.Utilities\SourceBasedCustomTask.cs/>
</
Task>
</
UsingTask>

なにがゆとれているか、そうです、インテリセンスです。作成したBuild.Utilitiesプロジェクトで、Microsoft.Build.FrameworkとMicrosoft.Build.Utilities.v4.0を参照する事で普通のコードを書いているのと同様の完全な Visual Studio 支援下でカスタムタスクを書く事が出来ています。

image

Source の位置指定は該当プロジェクトファイルからの相対となりますのでソース管理にそのまま突っ込んであげれば、ビルドサーバだったり同僚のマシンでも普通に動きます。

msbuild のインラインタスクが出る前はビルドを細かく制御したいというビルドヲタクをするには一つ厄介な問題がありました、カスタムタスクアセンブリの配布です、カスタムタスクアセンブリは csproj と共に共同して開発を行う同僚やCI ビルドサーバへ配布する必要がありました。ソリューションの中にバイナリを入れてしまえば良い、それは確かにそうなのですが、そのバイナリを更新する方法や手段は手動?ビルドヲタクに手動でなんかやれって言って素直にやると思って?

これらの問題は単純にソースでタスクを渡せる事で一発解消です、ソース管理にソースを入れるという単純な事で何とでもなってしまいます。

さて、ここまでの内容をもってすれば、ビルド時に少なくともC#で書ける事であれば何でもでき、それをVisual Studioの支援下で記述できるというのは納得してもらえるでしょうか。

出来るのは解ったけど性能は?:インクリメンタルビルドのサポート

はい、ビルドでは性能重要です。ちょっと変な事するとあっというまに毎回コンパイルが走り、依存しているプロジェクトすべてが毎回ビルドされるようになってしまいます。

そうなると開発作業の大半がビルド待ちにもっていかれるわけで、ビルド時にやってくれる事の価値によってはぶっちゃけ邪魔に感じる事でしょう。

出来るのは良い、本当に必要な時だけ実行されるならもっと良いという訳で、ファイルの更新状況に応じてそれをやるかやらないか制御されなければなりません。

msbuildはもちろんインクリメンタルビルドをサポートしていて、ターゲットの Inputs / Outputs 属性でこれが制御されます。

制御ルールとしては単純で、Inputs に記述されたファイルより Outputs に記述されたファイルが新しければターゲットの実行は省略されます。

Inputs / Outputs それぞれは@(項目グループ)で ItemGroup を受ける事が出来ますので、複数のファイルの更新関係を元にターゲットのそれぞれを実行するかを制御できるという事になります。

まとめ

csproj を開いたことないとか言う人は悔い改めてちょうだい。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。