tmori3y2のブログ

主にWindowsのプログラムなど

OnBuildを参考に.csprojにインストールしたMSBuild TargetをImportするMSBuild TargetのNuGet Packageを作った

2016/03/24追記

2016/03/26 Gist参照

ちょっと、タイトルが良くわかんないかもしれませんが、暫しお付き合いを・・・

この1週間、仕事で疲れた体に鞭を打って、布団の中でウトウトしつつ、MSBuild TargetとNuGetのMSBuild Target展開と格闘していた。

元ネタはこちらのOnBuild。

NuGet で MsBuild ターゲットが展開できるのが便利すぎて全俺が(ry | kazuk は null に触れてしまった

ブログ主は、ちょっとぶっ飛んだちょいエロ腕利きプログラマといった印象で、マニアックな記事は楽しく拝見させていただきました。

最近は、Twitter Onlyなのか、ブログが更新されていないのがちょっと残念。

前から元ネタは知ってたけど、MSBuildに手を出す前だったので

アセンブリのNuGet packだけなら、Edit ProjectでExec Task貼り付けたら良い」

というのが以前の記事。

tmori3y2.hatenablog.com

さて、元ネタで色々説明されているMSBuild Targetの展開だが、利点についてはそちらを見れば良く分かるので割愛するが、

NuGet 2.5

にある通りで、

  • パッケージにbuildフォルダを追加できる
  • buildフォルダ、あるいはFramework依存のサブフォルダに、{packageid}.targets/{packageid}.propsを配置できる
  • NuGetが{packageid}.targets/{packageid}.propsをインストールしたときに、プロジェクトファイルに<Import>要素を自動で追加する
  • {packageid}.propsは、プロジェクトファイル上位にある既存の.propsファイルの<Import>要素の上に追加
  • {packageid}.targetsは、プロジェクトファイル下位にある既存の.targetsファイルの<Import>要素の下に追加

実際、OnBuildで試してみると、更に以下のことが分かった。

  • {packageid}.targets/{packageid}.propsは、パッケージリポジトリのファイルが直接インポートされる

「何かちょっとイマイチで惜しい・・・」

NuGetのMSBuild Target展開が、イマイチだと思った理由は、

複数ターゲットのインポート順が、インストール順に依存している・・・」

「プロパティを、インストールしたプロジェクトで設定したくても、パッケージリポジトリに実体があるので手が出ない・・・」

「プロパティを設定するMSBuild Targetをプロジェクトに追加しても、インポートしなければ有効にならない・・・」

元ネタでも説明されているように、

「csproj を人がいじらないでも、その部品の付け外しが NuGet パッケージのインストール/アンインストールで簡単にできる」

のが、MSBuild Targetの展開をNuGet Packageで行う動機なわけで、

「一つでも、<Import>要素を手動でプロジェクトファイルに書かせたら負けだよね・・・」

何とかならないものかと思っているとき、ふと神が下りてきた・・・

「展開するMSBuild Targetとは別に、プロジェクトにコンテンツとしてインストールしたMSBuild Targetを用意して、展開したMSBuild Targetからインポートしたらいいんじゃないの?」

自分でも何言ってるか分からないので、プロジェクト構成を示すと・・・

  • プロダクトルート (C:\Projects\NuGetPackages)

    • Nuget.config

    • nuget.exe

    • NuGetDeploy

      • MSBuildTasks.ImportTasks.0.1.0.0.nupkg
    • packages

      • repositories.config

      • MSBuildTasks.ImportTasks.0.1.0.0

        • MSBuildTasks.ImportTasks.0.1.0.0.nupkg

        • build

          • MSBuildTasks.ImportTasks.props

          • MSBuildTasks.ImportTasks.targets

        • content

          • build

            • MSBuildTasks.ImportedTask.props

            • MSBuildTasks.ImportedTask.targets

    • MSBuildTasksソリューション

      • MSBuildTasks.sln

      • MSBuildTasks.ImportTasksプロジェクト

        • MSBuildTasks.ImportTasks.csproj

        • MSBuildTasks.ImportTasks.nuspec

        • Properties

        • build

          • MSBuildTasks.ImportTasksPack.props

          • MSBuildTasks.ImportTasksPack.targets

        • nupkg

          • build

            • MSBuildTasks.ImportTasks.props

            • MSBuildTasks.ImportTasks.targets

          • content

            • build

              • MSBuildTasks.ImportedTask.props

              • MSBuildTasks.ImportedTask.targets

      • MSBuildTasks.ImportTasks.Testプロジェクト

        • MSBuildTasks.ImportTasks.Test.csproj

        • packages.config

        • Properties

          • AssemblyInfo.cs
        • build

          • MSBuildTasks.ImportedTask.props

          • MSBuildTasks.ImportedTask.targets

となっていて・・・

MSBuildTasks.ImportTasksがMSBuild Targetを展開するNuGet Packageで、MSBuildTasks.ImportTasks.Testがインストール先となる場合、

  • MSBuildTasks.ImportTasksがインストールされると、packagesフォルダの下に展開される
  • 展開されたcontentフォルダ以下の項目がMSBuildTasks.ImportTasks.Testのプロジェクトルートにフォルダ構成ごとコピーされる
  • MSBuildTasks.ImportTasks.Testのpackages.configにMSBuildTasks.ImportTasksのエントリが追加される
  • 展開されたcontentフォルダ以下の項目に対する<ItemGroup>要素が、MSBuildTasks.ImportTasks.Test.csprojの既存の<ItemGroup>要素の下に追加される
  • 展開されたMSBuildTasks.ImportTasks.propsに対する<Import>要素が、MSBuildTasks.ImportTasks.Test.csprojの既存の<Import>要素の上に追加される
  • 展開されたMSBuildTasks.ImportTasks.targetsに対する<Import>要素が、リポジトリへの展開が成功しているかをチェックする<Target>要素と共に、MSBuildTasks.ImportTasks.Test.csprojの既存の<Import>要素の下に追加される

Import elements sample of automatic imported MSBuild target in .csproj file

あとは、MSBuildTasks.ImportTasksターゲットで、MSBuildTasks.ImportedTaskターゲットがインポートされていれば、インストール先のプロジェクトに好きにターゲットを追加できるのだが、一つ注意点がある。

.propsでは、以下のプロパティしか使用できないこと・・・

MSBuild Reserved and Well-Known Properties

もちろん、$(MSBuildProjectDirectory)を使えば、MSBuildTasks.ImportTasksターゲットが、呼び出し元のプロジェクトの位置を特定できるので、MSBuildTasks.ImportedTaskターゲットをインポートできる。

Import Element (MSBuild)

以下、サンプル。

Automatic imported MSBuild project property sheet file which imports another one installed at the same time by NuGet

Automatic imported MSBuild target file which imports another one installed at the same time by NuGet

今回は、インストール先のプロジェクトでビルドをする前に、呼び出し元のプロジェクトやリポジトリのMSBuildTasks.ImportTasksの位置や、インストールされたMSBuildTasks.ImportedTasksのターゲットの位置を表示するデバッグ用ターゲットを実装してみた。

プロジェクトにインストールされて、自由に編集できるImportされる側のターゲットはこちら・・・

MSBuild project property sheet file which is imported by MSBuildTasks.ImportTasks.props.

MSBuild target file which is imported by MSBuildTasks.ImportTasks.targets.

  • プロパティをfalseにすれば、デバッグ用の出力を抑制することができる
  • 別のターゲットファイルを更にインポートする場合は、複数ターゲットをNuGetで展開したときと異なり、ターゲットのインポート順は自由に変更できる

「編集するので、ソースコード管理してくださいね。」

あと、

「もうひとつ大事なことがあります・・・」

Import Element (MSBuild)

のメモにある通り、

  • IDEでは、インポート条件の再評価は行わない

このことは、

  • IDEでインポートされるターゲットの内容を変更しても、すぐにデバッグ出来ない・・・
  • 変更した内容にバグがある場合には、最悪バグを直さない限り、プロジェクトの読み込みに失敗する

ということを意味している・・・

もっとも、これは今回の話に限ったことではなく、元々そのような動きをするのだが、初心者は躓くところなので記憶にとどめておくとよい。

なお、MSBuildターゲットはアセンブリではないので、NuGet Packageの作成は通常のアセンブリとは異なる。

そのためのターゲットと.nuspecがこちら・・・

MSBuild project property sheet file which is imported in the project manually.

MSBuild target file which is imported in the project manually.

NuSpec for automatic imported MSBuild target file which imports another one installed at the same time by NuGet

ポイントは、

  • packコマンドは、.nuspecファイルを明示的に指定すること
  • Toolオプションを指定すること
  • .nuspecファイルは、プレースホルダは使用できないこと
  • MSBuildTasks.ImportTasksターゲットはbuild以下のtargetにコピーすること
  • MSBuildTasks.ImportedTaskターゲットはcontent以下のtargetにコピーすること
  • プロジェクト出力は全て除外すること

最後に、

「MSBuildTasks.ImportTasksPackターゲットを.csprojにマニュアルでインポートすることも忘れずに・・・」

おっと、

「もうひとつ大事なことがあった・・・」

パッケージソースをローカルフォルダに設定して、NuGet Packageをソースコントロール管理していないと、古いバージョンがロストした状態になることがあるが、ロストしたバージョンをインストールしたプロジェクトがあった場合、通常のアセンブリのNuGet Packageと同様(VS2013の場合)、アンインストールしても.csprojにゴミが残る。

当然、インポートの部分を消すことになるが、アセンブリ参照を消すより、.csprojをエディタで編集する分、難易度が上がるので、MSBuildの習熟度が低い開発部隊では要注意・・・

「一つでも、<Import>要素を手動でプロジェクトファイルから削除させたら負けだよね・・・」

ローカルサーバーを立てる前のお試し期間は、ソースコントロール管理するのが無難。

2016/03/24追記:

別件で、コンテンツをインストールするNuGet Packageを作って分かったのですが、インストールされるコンテンツはソースコントロール管理に入れておかないいけないみたいです。

インストール/アンインストールは出来るけど、パッケージの復元ではプロジェクトファイルのItemGroupに入っているコンテンツは、復元できない。

編集してアンインストールしたときに残ったファイルがあるときは、再インストールするときには上書きするか聞いてくるのに、無いと復元できないのは何だか腑に落ちないが・・・

(VS2013/NuGet v2.8.6)