kazuk は null に触れてしまった
C# / .NET 系技術ネタ縛りでお送りしております
月別アーカイブ: 12月 2011
fsyacc/lex でDSL – F# Advent Clender 2011 12/24
2011/12/24
投稿者: : どーもです。イブのひと時如何お過ごしでしょうか。私事ながら、結婚とかしましてカップルお断り!な感じないいお店とか行けなくて残念って感じにイブを過ごすんじゃねーかな程度な師走時でございます。
という訳で最近始めましたなF#、といってもfsyacc/fslex を使ってパーサーを書いたりしかしてなかったり、しかもプロダクトコードというよりはビルド中に一定の解釈変換をしたい所の処理を書く為にやってるだけという酷い F# の使い方しかして無くて申し訳ない人ですが、その辺に関わるノウハウ的な物という事で書きたいと思います。
C# で定義された AST をfsyacc で出力する方法
やる事が DSL で出力回りに T4 とか使ってると C#コードからASTのトラバースとかしたいわけですが、C#側からF#で定義された判別共用体をグチャグチャいじるのは若干やりづらかったり、出力物はすでに .NET Framework 定義された現物を利用したくて AST を派手に定義したくなかったりする事ってありますよね。たとえば .NET Framework の Expression をパース結果として取りたい時とか。
という訳で F# Parsed Language Starter の AST を C# で置き換えてみましょう。
普通にプロジェクトをまずは作ります。そして C# クラスライブラリを作る事の Ast という事で、Ast 名前空間にぶつけます。
参照設定でF#での言語側プロジェクトから Ast クラスライブラリを参照します。
Class1.cs の内容を以下の様にして、fsy のアクションを以下の様に書き換えます。 Program.fs の eval 操作を削ると以下の通り。
namespace Ast { public class Factor {} public class Term {} public class Expr {} public class Equation {} internal class Integer : Factor { internal int Value { get; set; } } internal class Float : Factor { internal double Value { get; set; } } internal class ParenEx : Factor { internal Expr Expr { get;set; } } internal class Times : Term { internal Term Term { get; set; } internal Factor Factor { get; set; } } internal class Divide : Term { internal Term Term { get; set; } internal Factor Factor { get; set; } } internal class TermFactor : Term { internal Factor Factor { get; set; } } internal class Plus : Expr { internal Expr Expr { get; set; } internal Term Term { get; set; } } class Minus : Expr { internal Expr Expr { get; set; } internal Term Term { get; set; } } class ExprTerm : Expr { internal Term Term { get; set; } } class ExprEquation : Equation { internal Expr Expr { get; set; } } public class AstBuilder { public static Factor Integer(int value) { return new Integer { Value=value }; } public static Factor Float(double value) { return new Float { Value=value }; } public static Factor ParenEx(Expr expr) { return new ParenEx { Expr=expr }; } public static Term Times(Term term, Factor factor) { return new Times { Term=term, Factor=factor }; } public static Term Divide(Term term, Factor factor) { return new Divide { Term=term, Factor=factor }; } public static Term Factor(Factor factor) { return new TermFactor { Factor=factor }; } public static Expr Plus(Expr expr, Term term) { return new Plus { Expr=expr, Term=term }; } public static Expr Minus(Expr expr, Term term) { return new Minus { Expr=expr, Term=term }; } public static Expr Term(Term term) { return new ExprTerm { Term=term }; } public static Equation Equation(Expr expr) { return new ExprEquation { Expr=expr}; } } }
start: Prog { AstBuilder.Equation($1) } Prog: | Expr EOF { $1 } Expr: | Expr PLUS Term { AstBuilder.Plus($1, $3) } | Expr MINUS Term { AstBuilder.Minus($1, $3) } | Term { AstBuilder.Term($1) } Term: | Term ASTER Factor { AstBuilder.Times($1, $3) } | Term SLASH Factor { AstBuilder.Divide($1, $3) } | Factor { AstBuilder.Factor($1) } Factor: | FLOAT { AstBuilder.Float($1) } | INT32 { AstBuilder.Integer($1) } | LPAREN Expr RPAREN { AstBuilder.ParenEx($2) }
// This project type requires the F# PowerPack at http://fsharppowerpack.codeplex.com/releases
// Learn more about F# at http://fsharp.net
// Original project template by Jomo Fisher based on work of Brian McNamara, Don Syme and Matt Valerio
// This posting is provided “AS IS” with no warranties, and confers no rights.
open System
open Microsoft.FSharp.Text.Lexing
open Ast
open Lexer
open Parser
printfn “Calculator”
let rec readAndProcess() =
printf “:”
match Console.ReadLine() with
| “quit” -> ()
| expr ->
try
printfn “Lexing [%s]” expr
let lexbuff = LexBuffer<char>.FromString(expr)
printfn “Parsing…”
let equation = Parser.start Lexer.tokenize lexbuff
printfn “Evaluating Equation…”
let result = equation
printfn “Result: %s” (result.ToString())
with ex ->
printfn “Unhandled Exception: %s” ex.Message
readAndProcess()
readAndProcess()
めでたく
—— ビルド開始: プロジェクト: Ast, 構成: Debug Any CPU ——
Ast -> c:\users\kazuk\documents\visual studio 2010\Projects\FParsedLanguageStarter1\Ast\bin\Debug\Ast.dll
—— ビルド開始: プロジェクト: FParsedLanguageStarter1, 構成: Debug x86 ——
FParsedLanguageStarter1 -> c:\users\kazuk\documents\visual studio 2010\Projects\FParsedLanguageStarter1\FParsedLanguageStarter1\bin\Debug\Language.exe
========== ビルド: 正常終了または最新の状態 2、失敗 0、スキップ 0 ==========
ビルド成功と。
internal 宣言をマメにやってますが、Ast で internal になっている要素はパーサー実装にとっては要らない子という事です(実際に見えませんがビルドは通ってるのは上記のとおり)
ましてや判別共用体である事やムニャムニャはちっともパーサーにとっては必要のない事なわけです。これで式木をASTとして出力するパーサーを書けと言われても簡単なお話でExpression のノードを保持する型を EBNF の出力部に合せた型で保持して返すだけでよいという事も解るわけですね。
C# からのパーサーの呼び出し
ASTをF#以外の言語でもできたわけで、これでF#固有の言語セマンティックスである判別共用体をC#からモニャモニャする方法を悩まなくて良い様になりました。これでC#コードであるT4からASTを扱う処理は普通に書けばいいでしょって事になります。
ではパーサーを呼ぶ方法です。これが出来ないとT4から呼ぶとか、カスタムツールから呼ぶとか、ビルド時タスクから呼ぶとかパーサーが本当に欲しい人にとっては意味がないよって事で、ここをどうするかのお話になります。
まず単純にやってみて打ちひしがれましょう。
F#のパーサーアセンブリの出力をコンソールアプリケーションからクラスライブラリに変更します。
いきなりビルドできなくなります。(一撃目)
とりあえず Program.fs を全部コメントアウトしてビルドを通そうとしても無駄で、同じエラーになり、ちゃんとF#を勉強してこいと蹴っ飛ばされます。(二撃目)
仕方ないので Program.fs を削除!するのはもったいないのでビルドアクションにNoneを指定してコンパイル範囲から外してビルドを通します。
また C# のクラスライブラリを作ってF#のパーサープロジェクトを参照させます。
どんな型が定義されてるのかなと、オブジェクトブラウザを開こうとして打ちひしがれます。
以上、ありがとうございました。と終わると暗にF#をdisるエントリーの出来上がりです。
F# Advent Calender でそれをやる勇気はないので、ちゃんと呼べるようにしましょう。
namespace ParserExpose open Microsoft.FSharp.Text.Lexing open Ast open Lexer open Parser type PaserExpose = static member Parse (text:string) = let lexbuff = LexBuffer<char>.FromString(text) Parser.start Lexer.tokenize lexbuff
単純に LexBuffer<char> を構築して Parser.start を呼ぶだけです。
これを呼び出すメソッドは以下の通り。
using ParserExpose; namespace ParserConsumer { public class Class1 { public void DoParse(string text) { var equation = PaserExpose.Parse(text); } } }
簡単に静的メソッドを呼ぶだけですね。
ここまでくれば T4 でなんかするのも簡単です。
見えなくても良いよを示す為に internal にしたのが裏目に出たのでサックリと InternalsVisibleTo で Ast アセンブリのinternal をParserConsumer に公開。
前処理されたテキストテンプレートを追加
こんな T4 Template を書くと
<#@ template language="C#" #> <#@ import namespace="Ast" #> <#+ public void WriteEquation( Equation equation ) { var equ = equation as ExprEquation; #><span class="equation"><#+ WriteExpr( equ.Expr ); #></span><#+ } public void WriteExpr( Expr expr ) { var p = expr as Plus; if( p!=null ) { #><span class="expr plus"><#+ WriteExpr( p.Expr );#>+<#+ WriteTerm( p.Term ); #></span><#+ } var m= expr as Minus; if( m!=null ) { #><span class="expr minus"><#+ WriteExpr( m.Expr );#>-<#+ WriteTerm( m.Term ); #></span><#+ } var t = expr as ExprTerm; if( t!=null ) { #><span class="expr"><#+ WriteTerm( t.Term );#></span><#+ } } public void WriteTerm( Term term ) { var t = term as Times; if( t!=null ) { #><span class="term times"><#+ WriteTerm( t.Term );#>*<#+ WriteFactor( t.Factor ); #></span><#+ } var d = term as Divide; if( d!=null ) { #><span class="term divide"><#+ WriteTerm( d.Term );#>/<#+ WriteFactor( d.Factor ); #></span><#+ } var f = term as TermFactor; if( f!=null ) { #><span class="term"><#+ WriteFactor( f.Factor ); #></span><#+ } } public void WriteFactor( Factor factor ) { var i = factor as Integer; if( i!=null ) { #><span class="factor integer"><#= i.Value #></span><#+ } var f = factor as Float; if( f!=null ) { #><span class="factor float"><#= f.Value #></span><#+ } var p = factor as ParenEx; if( p!=null ) { #><span class="factor">(<#+ WriteExpr( p.Expr ); #>)</span><#+ } } #>
これでspanでマークアップされたHTMLフラグメントになるわけですね。
マトメ
fsyacc/fslexは作るASTは外部定義の物を簡単に使えるし、作ったパーサーもC#から呼ぶための口を開いてあげれば普通に呼べるしという事で、使えば色々使える代物でございます。
バイナリーのフォーマット定義とか C言語での構造体記述が共通記述言語だったりしますよね、それを手で変換するとかしてたなら軽くパーサー書いてあげればT4でコードに落としたりできるし、バイナリーフォーマットのDSLたるC言語の構造体表現に対応した事になるわけです。その表現に従って BinaryWriter / BinaryReader を連打するコードを吐かせれば古き良きバイナリープロトコルでのOLTPもどんと来いです。
LALR(1)のパーサーが出来るのですから頑張れば C# のコードもパースできるはずです。あなたのマシンにも入ってるC#言語仕様のドキュメントのEBNFを引っ張りだして C#コードに埋められたDSLを解釈しても良いでしょう。
パーサーが必要になるのはDSLに限りません、Lexer / LexBuff は char のシーケンス処理です。ゲームパッドの上下左右やボタンを文字にマップして流し込めば UUDDLRLRBA でコナミコマンドを解釈するなんてのもきっと出来るでしょう。連続するポーズを文字にマップして解釈すればKinectでポーズを読んで仮面ライダーの変身ポーズを Accept するパーサーだって書けるはずですね。
簡単に流れ制御構文をいくつか受け取ってWindows Workflow の流れ制御系アクティビティのインスタンスを生成する事ができるパーサーとか書くと、「俺たちはテキストなコードを書くドメインスペシャリストなんだが、MSの中の人達なんか勘違いしてないか?」とか言えますね。見た目かっこよく素人受けの良いフォーマットでとかは生成されたアクティビティを XamlWriter に渡してXAML書かせてVSでデザイナ表示すればできる事
マトメのマトメ
メソッドをちゃんと定義してあげても結局の所、オブジェクトブラウザで見ようとすると、こうなるのは。
—————————
Microsoft Visual Studio
—————————
利用可能でないか、またはビルドされていないため、このプロジェクトをオブジェクト ブラウザーで表示することができません。プロジェクトが利用可能でビルド済みであることを確認してください。
—————————
OK
—————————
F#たんのこの画像の雰囲気どおりの仕様で良いと思います。図書館の文学少女は誰がなんと言おうと正義です。出来上がったアセンブリがVSで表示できないからとILDASMに読み込むときの背徳感最高です。文学少女を体育倉庫に連れ込んでイケナイ事をしてる妄想がテンションを上げてくれます。F#たんサイコーです。
ASP.NET MVC プロジェクトのサブシステム分割とマージ
2011/12/23
投稿者: : なんとなく出来ちゃったという物のおすそわけ blog.
何を解決する物ですか?
ある意味 ASP.NET MVC による大規模アプリケーション開発をサポートします。
大規模というのは、サブシステム分割が必要となるレベルであり、例えば Controller が数十からそれ以上に達する可能性の高いシステムです。
このような大規模アプリケーションを作る場合、ASP.NET MVC の Area 等を利用してアプリケーション内で分割するのは一つのアプリケーションの規模の増大を招くだけであり、規模の暴力に対してのアプローチとはなりえません。
根本的にはアプリケーションをサブシステムに分割し、各個サブシステムを統合するという手順が望ましいわけです。
単純に msbuild プロジェクトファイル一個でこの分割によって分かれたアプリケーションの統合が出来ましたよって事です。
ソースと動作原理
Build.Utilities\HugeWebApplication.targets
<?xml version="1.0" encoding="utf-8" ?> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <_copyContentSourceContent Include="@(Content)" Exclude="**/*.config" /> <_copyContentsOutputFiles Include="@(_copyContentSourceContent->'$(CopyContentOutputPath)\%(identity)')" /> </ItemGroup> <Target Name="CopyProjectContents"> <Message Text="$(MSBuildProjectName) からコンテンツをコピーします 出力先 $(CopyContentOutputPath)" /> <Copy SourceFiles="@(_copyContentSourceContent)" DestinationFiles="@(_copyContentsOutputFiles)" SkipUnchangedFiles="true" /> </Target> <Target Name="MargeProjectContents" AfterTargets="BeforeBuild" > <MSBuild Targets="CopyProjectContents" Projects="%(ProjectReference.Identity)" Properties="CopyContentOutputPath=$(MSBuildProjectDirectory)" /> </Target> </Project>
動作原理としては MargeProjectContents ターゲットが BeforeBuildの前に実行され ProjectReference で参照しているプロジェクトに対して CopyProjectContents ターゲットを実行します。
CopyProjectContents ターゲットは呼び出されたプロジェクト上で定義された Content を呼び出し元プロジェクトのディレクトリに Copy します。
結果として空のASP.NETプロジェクトで MVC プロジェクトをプロジェクト参照すれば MVC プロジェクトの Contents は空のASP.NETプロジェクトに転送されます。bin に MVC プロジェクトの出力が配置されるのはプロジェクト参照のデフォルトの挙動で、そこに対しては何も変更していませんので bin にMVCアプリケーションのアセンブリは配置されます。
使い方
ソリューション上に ASP.NET MVC アプリケーションを複数作成し、統合先となる空の ASP.NET アプリケーションを作成します。統合先プロジェクトから各 ASP.NET MVC アプリケーションをプロジェクト参照します。
各 ASP.NET MVC アプリケーション、および 統合先 ASP.NET アプリケーションの csproj の末尾に以下の一行を追加します。
<Import Project="..\Build.Utilities\HugeWebApplication.targets" />
後はビルドするだけです。
スクリーンショットではWebApplication1 が MvcApplication1 / MvcApplication4 を参照しています。
ビルド結果では WebApplication1 の配下に Views その他の Content がコピーされている事が解ります。
コピー対象としてかち合う事が見え見えの web.config 等 config 類は含ませていません。
Global.asax/.cs どうするかなーとか解決すべき課題が無いわけではありませんが、ここまでの労力が msbuild のtargetsファイル 20行ってのは上々の戦果といっても悪くないかなーと。
統合先として今回は空のASP.NET アプリケーションで紹介しましたが、ASP.NET MVC アプリケーションでも構わない=依存する基盤部の取り込みを各部でする為に応用も出来るという事も備考まで。
やってみたらコレ困るわな事があればコメントにどーぞです。
という訳で、ハッピーなクリスマスを!
BUILD ネタという事で – C# Advent Calender 12/9
2011/12/09
投稿者: : //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>
この状態のソリューションエクスプローラーとプロパティは以下の通り。
このプロパティでのビルドアクションを「なし」に変えると 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 に割り当てています。このファイルのプロパティと、ビルドアクションのドロップダウン内容が以下になります。
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 ターゲットを作りました、結果はこの通り。
意図通り動いている事が確認できました。まず依存関係での処理順序の制御をしてみましょう。
<!– <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=“Build” DependsOnTargets=“BeforeBuild; BuildCore; AfterBuild“>
<Error Text=“まだ作ってないよ” />
</Target>
DependsOnTargets で各タスクを作成しました、実行結果は以下の通り、今回は出力ウインドウも付けています。出力ウインドウの表示は本当に実行順序設定どおりなのですが、エラー一覧は色々な並べ替えがあるので注意が必要です。エラー一覧の2番目のカラムの数値で並べ替えすると出力順序に一致します。
結果としては DependsOnTargets で指定したターゲットが指定の順番で実行され、各ターゲットの実行が完了した後にDependsOnTargets を指定したターゲットが実行される事が解ります。これでターゲットの実行順序は設定できる事が解ります。
msbuild 4より前ではこれが唯一のビルドターゲットの実行順序の制御方法でした、msbuild 4では AfterTargets, BeforeTargets 属性によって既存のターゲットの実行順序の指定の位置でターゲットを実行する事を指定する事ができます。
After – Before という順序で書いたのはアルファベット順などという事でなく、実行順序の関係がそうだという事です。 AfterTargets で指定したターゲットの後、BeforeTargets で指定したターゲットの前で指定を行ったターゲットが実行されます。
以下のターゲット定義を設定する事で BeforeBuild, BuildCore の間で Transform が実行されます。
<Target Name=“Transform”
AfterTargets=“BeforeBuild”
BeforeTargets=“BuildCore” >
<Warning Text=“Transformが割り込んでみた” />
</Target>
この関係を利用すると 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=“Fragment” Language=“cs“>
<![CDATA[
Log.LogWarning(“this is inline task”);
]]>
</Code>
</Task>
</UsingTask>
<Target Name=“Build” >
<SampleTask />
</Target>
Type属性で Fragment を指定した場合 Taskクラスの派生クラスの Execute メソッドの内部を記述する事になります。上記例では TaskクラスのLogプロパティを介して Warning メッセージを出力しています。
簡単に言えば 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 支援下でカスタムタスクを書く事が出来ています。
Source の位置指定は該当プロジェクトファイルからの相対となりますのでソース管理にそのまま突っ込んであげれば、ビルドサーバだったり同僚のマシンでも普通に動きます。
msbuild のインラインタスクが出る前はビルドを細かく制御したいというビルドヲタクをするには一つ厄介な問題がありました、カスタムタスクアセンブリの配布です、カスタムタスクアセンブリは csproj と共に共同して開発を行う同僚やCI ビルドサーバへ配布する必要がありました。ソリューションの中にバイナリを入れてしまえば良い、それは確かにそうなのですが、そのバイナリを更新する方法や手段は手動?ビルドヲタクに手動でなんかやれって言って素直にやると思って?
これらの問題は単純にソースでタスクを渡せる事で一発解消です、ソース管理にソースを入れるという単純な事で何とでもなってしまいます。
さて、ここまでの内容をもってすれば、ビルド時に少なくともC#で書ける事であれば何でもでき、それをVisual Studioの支援下で記述できるというのは納得してもらえるでしょうか。
出来るのは解ったけど性能は?:インクリメンタルビルドのサポート
はい、ビルドでは性能重要です。ちょっと変な事するとあっというまに毎回コンパイルが走り、依存しているプロジェクトすべてが毎回ビルドされるようになってしまいます。
そうなると開発作業の大半がビルド待ちにもっていかれるわけで、ビルド時にやってくれる事の価値によってはぶっちゃけ邪魔に感じる事でしょう。
出来るのは良い、本当に必要な時だけ実行されるならもっと良いという訳で、ファイルの更新状況に応じてそれをやるかやらないか制御されなければなりません。
msbuildはもちろんインクリメンタルビルドをサポートしていて、ターゲットの Inputs / Outputs 属性でこれが制御されます。
制御ルールとしては単純で、Inputs に記述されたファイルより Outputs に記述されたファイルが新しければターゲットの実行は省略されます。
Inputs / Outputs それぞれは@(項目グループ)で ItemGroup を受ける事が出来ますので、複数のファイルの更新関係を元にターゲットのそれぞれを実行するかを制御できるという事になります。
まとめ
csproj を開いたことないとか言う人は悔い改めてちょうだい。