rider.お品書きサクッと書いて公開して、後でちまちま修正する事がママあります。なんじゃこりゃ?と思ったら、数日してから見て貰えると嬉しいです。「スクラムの実践方法を分かりやすくマンガも交えて解説。実例で学ぶゲーム3D数学NuGetでビルドプロセスに介入するパッケージを作るNuGetを使うと、サードパーティ(MSではない)アセンブリの導入が簡単に行えます。他にも、MSBuildのビルドプロセスをカスタマイズ出来るパッケージを作る事も出来ます。Studio上でも、MSBuildによるコマンドラインのビルドでも、シームレスにビルドプロセスをカスタマイズ出来ます。C#やF#のソースコードをコンパイルする直前に、カスタムコードを自動生成させたりする事ができます。このようなスクリプトをNuGetパッケージ化すると、配布と導入が非常に簡単になり、アンインストールも簡単で、多くの人に気軽に使って貰えるようになります。MSBuildを介さない操作のカスタマイズは出来ません。Studio自体のカスタマイズは、VSIX拡張(VSSDK)を使いますがここでは触れません。この記事では、どうやって最小限の労力でNuGetパッケージを作るのかのポイントを示します。NuGetのパッケージを作ったことがある方が、より理解しやすいでしょう。MSBuildとtargetsスクリプトMSBuildは、*.csprojのようなプロジェクトファイルを入力として、一種の宣言ベーススクリプト処理を行います。中身を見たことがある人が多いと思いますが、XMLで記述します。大まかには、以下のような構造になっています(一部のみ抜粋)。--></Project>場合によってはもっと複雑なスクリプトになっている事もあります。MSBuildはこのXMLを読み取って、ビルド対象の情報を取得しています。Frameworkのターゲットバージョンや、アセンブリ名など)が含まれています。この定義は一種の変数のように振る舞い、スクリプト内からこれらの名前で参照する事ができます。眺めると、Referenceタグでアセンブリ参照を定義し、Compileタグでコンパイル対象のファイルを定義しているように見えますね。この記事で一番のポイントは、「Import」タグです。このタグのProject属性には、「*.targets」ファイルへのパスが示されています。上記の例ではImportタグは二か所ありますが、例えば最後のタグでは「$(MSBuildToolsPath)\Microsoft.CSharp.targets」を指定していますね。「$(MSBuildToolsPath)」がPropertyGroupなどで定義された値を参照する構文で、このフォルダ配下の「Microsoft.CSharp.targets」というファイルが指定されています。このImportタグは、この場所にここで示されたファイルの定義をインポートするという定義です。「Microsoft.CSharp.targets」ファイルを読み取り、ここに挿入すると考えてください。C言語のincludeのように機能します(XMLなので実際は違いますが)。Microsoft.Common.propsやMicrosoft.CSharp.targetsファイルには、非常に多くの定義が含まれています。全てMSBuildのスクリプトです。Microsoft.Common.propsには、先ほどのMSBuildToolsPathのような共通のPropertyGroup定義が含まれているため、ほぼすべてのプロジェクトファイルはこのファイルをImportします。Microsoft.CSharp.targetsはC#のビルドに必要な定義が含まれています。これには、*.csのコンパイルだけではなく、*.resxの変換やXAMLのコンパイル定義なども含まれます。F#のような他の言語では、「Microsoft.FSharp.Targets」のような感じで、その言語に合わせて変えています。カスタムなスクリプトを直接プロジェクトファイルに書くことも出来るのですが、別のファイルにカスタムスクリプトを書いて、それをImportした方が、プロジェクトファイルを損傷させる可能性が減る(XMLなので容易に構造を壊してしまう可能性がある)ため、ベターです。基本的な方針は、この「カスタムtargetsスクリプト」を用意することと、これをどうやって既存のプロジェクトファイルに「自動的に挿入(Importタグを追加)」させるのかと言う事になります。2010未満の環境では、VSIX拡張を使って自動的に挿入させることができました(この手法は今でも使えます)。VSIX拡張は作ったり保守したりするのが非常に困難なため、お勧めしません。Studio本体)が必要になってしまい、MSBuildによる継続インテグレーションが出来なかったり自由度が失われてしまいます。わかりやすい例として、AppVeyorのようなクラウドベースのビルドエージェントで継続インテグレーションが困難です(不可能ではありませんが、事前インストールと自動構成が必要)。2010からNuGetが使えるようになり、このtargetsスクリプトをNuGetで配布する事が出来るようになりました。いう事かと言うと:NuGetパッケージに、targetsスクリプトとその他必要なファイル群を加えるNuGetパッケージを配布するNuGetパッケージをプロジェクトに導入すると、プロジェクトファイルに自動的にImportタグが追加され、targetsの定義が利用可能になるという流れです。targetsスクリプトを含めれば良い訳ではありません。以下の条件が存在します。パッケージ内に「build」フォルダを追加し、その直下にtargetsスクリプトを配置する。パッケージを作ったことがある方なら分かると思いますが、通常ライブラリアセンブリを配布する場合は「lib」フォルダ配下に「net40」「net20」のようなプラットフォームターゲット名のフォルダを作り、その中にアセンブリファイルを配置します。targetsスクリプトファイルは必ずbuildフォルダ配下に配置しなければなりません。targetsスクリプトファイル名は、必ず「[NuGetパッケージID].targets」でなければならない。パッケージIDが「CenterCLR.RelaxVersioner」の場合は、「build/CenterCLR.RelaxVersioner.targets」に配置します。名前が違っていると、パッケージの生成には成功して配布も出来るのに、導入時に「ターゲットプラットフォームが存在しない」という不可解なエラーで失敗します。targetsスクリプトで使用する追加のファイル群がある場合は、それらもbuildフォルダ内に全て配置する。targetsスクリプトからファイルを相対参照する場合、起点はbuildフォルダになります。しても別フォルダにしたい場合でも一旦buildフォルダに配置して、参照できることを確認してから移動した方が良いでしょう。スクリプト実行レベルでの挿入や乗っ取りの方法さて、これで物理的なパッケージングの枠組みは出来たのですが、実際カスタムスクリプトはどうやって書けば良いのでしょうか?MSBuildのカスタムスクリプトは、これだけで膨大なテーマであるので、全部網羅的に調べるのは大変です。そこで以下のポイントを知っておけば、取り掛かりが楽になると思います。PropertyGroupタグ内には、参照可能な定義を含める(これは前述で説明しました)。定義した値は、「$(Name)」という記法で参照出来ます。ItemGroupタグ内には、操作対象のファイルなどを指定させる。「Compile」タグはコンパイル対象を指定させますが、これは「@(Compile)」という記法で参照できます。これらは複数定義されることがあるため、@(Compile)の評価結果はコレクション(複数)となります。Condition属性に式を書くことで、そのタグがMSBuildの評価の対象になるかどうかが決定されます。csprojの例を見るとわかりますが、デバッグビルドやプラットフォームの識別を式で判定して、それぞれに定義されるPropertyGroupを変えたり、と言う事が出来ます。これはPropertyGroupだけではなく、ほぼすべてのタグで使用できます。PropertyGroupやItemGroup内の定義は、串刺し的に評価される。PropertyGroupが何個定義されていても構わず、それらすべての定義が一つのPropertyGroupに定義されているのと同じように評価されます。この機能を利用して、カスタムスクリプト内で自由に定義を追加したり、Condition属性に応じて定義するかしないかを切り替えたりと言ったハックが実現出来ます。余談ですが、「Debug|AnyCPU」という値の判定には、意味ありげに演算子っぽい「|」を使っていますが、これ、文字列としてただ連結しているだけです(ConfigurationとPlatformの値が結合して別の値と誤認しないようにするためと思われる)。:)このような前提で、まずは「Microsoft.Common.props」や「Microsoft.CSharp.targets」ファイルを眺めてみて下さい。(x86)\MSBuild\12.0」や、「C:\Windows\Microsoft.NET\Framework\v4.0.30319」配下にあります。標準のMSBuildスクリプト群は非常に巧妙で複雑なのですが、ビルドを実現する戦略として以下のような手法を使っているようです。Targetタグを論理的にネストさせて、ビルドシーケンスに階層構造を構築する以下はMicrosoft.Common.targetsの一部です。/>この定義は、「Targetタグ」によって「Build」という名前の「ターゲット」を定義しています。この名前は、MSBuildのコマンドラインオプションで指定することができます。ビルドする場合は、ここからスクリプトの評価が開始されると思って下さい。このTargetタグは空タグなので、このターゲット自体は何も実行しません。その代わり、「DependsOnTargets属性」で参照されるサブターゲットを評価します。DependsOnTargets属性は、「BuildDependsOn」の値が参照されています。前半のPropertyGroup内に定義されている、「BeforeBuild;CoreBuild;AfterBuild」が対象です。改行と空白は無視し、セミコロンで区切られます。このTargetタグの後、更に別のTargetタグによって「BeforeBuild」「CoreBuild」「AfterBuild」それぞれのターゲットが定義されています。このリストは、まずBeforeBuildの定義が評価され、次にCoreBuildの定義が評価され、最後にAfterBuildの定義が評価される、というように、リストに書かれた順に従って、サブターゲットが評価されることになります。CoreBuildの定義は後で示しますが、BeforeBuildとAfterBuildはそれぞれ以下のように定義されています。実は、csprojのデフォルトのテンプレートには、BeforeBuildとAfterBuildのひな型が含まれています。--></Project>つまり、csprojでこのタグのコメントアウトを外してここに何か書くと、BeforeBuildとAfterBuildの内容をオーバーライドできると言う事です。いうカラクリで、プロジェクトファイルにカスタムスクリプトを記述出来るようになっています。</Target>Buildとおなじような構造の定義になっています(細かい相違は省略)。CoreBuildもまた空タグなので、このターゲット自体は何も実行しません。DependsOnTargets値は「CoreBuildDependsOn」を参照し、今度は更に細かくサブターゲット群が定義されています。このリストそれぞれに対応するターゲット定義が、この後のスクリプトに全て含まれていて、この順にTargetタグが評価されます。このように、ネストしたサブターゲットを再帰的に探索してビルドを実現しているのです。名前からして分かりやすそうな「Compile」というターゲットは、各処理系(C#やF#)のスクリプトに定義されているTarget定義に処理が移譲され、更に細かくターゲットがネストし、最終的にはC#やF#のコンパイラを起動してコンパイルを行っています。必要となるタイミングに対応するTargetタグを調べて(実はここが大変)、そのターゲットと同じ名前でオーバーライドしてしまうか、あるいは「*DependsOn」をオーバーライドして独自のターゲット名のリストに入れ替え、そこに自分のターゲット名を挿入しておいたり、標準のターゲットの代わりに自分のターゲットを実行させるように入れ替えたりすれば良い訳です。私が取り組んでいるプロジェクトに、RelaxVersionerというパッケージがあります。これは、Gitを前提として、バージョン番号の埋め込みなどを自動的に行うNuGetベースのパッケージです。:)デザインの指針は、とにかく簡単(ほぼパッケージをインストールするだけ)ビルドの副作用による影響なし独自のUIを作らない。必要であればカスタマイズ可能というところを目指しています。現状で大体実現出来ています(mono対応が出来てない。使い方はGitHubの方を見てもらうとして、とにかく導入が簡単で影響を最小化したかったので、この記事で説明したような、NuGetによる導入で即使用可能というところが重要でした。やっていることは、各ソースファイルのコンパイル直前に、Gitのローカルリポジトリから吸い上げた情報を元に、AssemblyVersion属性を含んだソースファイルをテンポラリフォルダに自動生成して、コンパイルの対象に含める、という内容です。</Target></Project>RelaxVersionerOutputPathは、中間ファイルフォルダの配下にソースファイルを配置するためのパスを示します。Conditionはパスのカスタマイズの余地を残すためです。このパスは、ほとんどの場合、プロジェクトフォルダ配下の「obj\Debug\RelaxVersioner.cs」のようなパスとなります。CoreBuildDependsOnをオーバーライドし、この直前にRelaxVersionerBuildターゲットを実行するように再定義します。リストを丸ごと置き換えると、標準targetsスクリプトが更新された時におかしくなってしまう可能性があるので、元の定義を生かしたまま挿入するハックです。CoreBuildではなく、もっと深いコンパイルに近い位置でのターゲットでも良いかもしれません。どこの位置が適切なのかが難しいところですが、RelaxVersionerはマルチ言語対応の予定があり、言語毎に細分化されたターゲットに挿入すると意味がないので、CoreBuildを選択しています。実際にソースコードを生成するのは、「CenterCLR.RelaxVersioner.exe」という実行ファイルで、これはNuGetパッケージのbuildフォルダに一緒に含めておきます。ほかに必要なアセンブリも全て含める必要があります。RelaxVersionerはlibgit2sharpを使っているので、これらも配置してあります。Taskはデバッグとかバージョンとかつらいんです。後でTask化にトライする予定。Frameworkのバージョン情報などを渡して、生成するソースコードの内容を変えています。最後に、生成したソースコードがコンパイルのターゲットとなるように、Compileタグで追加しておきます。Core環境は、現在のところMSBuildを使用しないことになっているために、この記事の手法が使えません。Coreでは「project.json」がプロジェクト定義となっているのですが、これはdotnetコマンドが解釈を行います。Coreのプロジェクトを作った場合、project.jsonの他に*.xprojが生成されます。このxprojはMSBuildのプロジェクトスクリプトなのですが、NuGetの管理を行わない(NuGetの管理はproject.jsonで行われるため、結局Importタグが埋め込まれない)ので、やはり機能しません。Coreはプロジェクト定義をproject.jsonベースに移行する事を前提として作業していたのですが、RTMのリリース直前になってこれがキャンセルされ、MSBuildをマルチプラットフォームに移植するという方針に変わりました。2016年5月23日しかし、結局RTMには移植が間に合わず、project.jsonベースでNuGetを管理する状態のままリリースされてしまいました。Coreでのスムーズなビルドのカスタマイズは出来ないと言う事になりますが、MSBuildが移植される方針なので、将来的に同じ手法でビルドプロセスをコントロール出来そうです。mono環境ではMSBuildのmono実装である「XBuild」が使えます。サブセットであるので一部互換性がありませんが、概ね同じような手法が使えると思います。見るのと、私自身も思っているのですが、C#/F#/VB.netなど、複数の言語を一つのプロジェクト内でまとめて扱えるようにして欲しいという要望があります。現在のMSBuildのtargetsスクリプト群を見てわかる通り、これを実現するには「標準のtargets全部書き直し」しか無いですね。Coreチームはxprojもcsprojに戻すことを選択したようで、がっかりです。なんか、何でそこだけjsonなんだよ、分離したければどっちかに統一すれば良いのに、と思った。あと「project.json」っていう命名も誤解の元。互換性無くなるなら無くなるで、一部だけどうにかするんじゃなくて全体から(つまりMSBuild自体)見直して欲しかった。MSBuild、複雑すぎます。なった感が大きいです。初期導入が古いとは言え、最初から不満でした。階層構造をタグの論理的なネストで実現したのも、正直言ってXMLで処理系作ってその枠組みで無理やり実現しました感があるし、既存のスクリプトを壊さないように処理を追加したり交換したりといった事が簡単に出来ないし、targetsスクリプトを追って行くのがとにかく大変で、この記事のようにシンプルにまとめ上げれるようになるまでにかなり時間が掛かりました。こんな構造だから、拡張のためのエコシステムが発展しないんだと思う...仕事でVSIX拡張を触っているのですが、VSIXの難解なインターフェイスと、MSBuildの複雑構造と、NuGetのどうしようもなく整理されていない感が組み合わさって、出自が違うので仕方ないとは言え本当につらい。この記事は備忘録も兼ねて書いたのですが、少しでもお役に立てれたら嬉しいです。WordPress%d人のブロガーが「いいね」をつけました。