ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

msbuildでビルドするときに自動でNugetパッケージを復元させる

Msbuildは自動でNugetパッケージを復元してくれない

最近のWindowsではVisualStudioなしでプロジェクトファイルがビルドできるそうですね.
Windows(正確にはWindowsでデフォルトインストールされる .NET Framework)にはMsbuildっていうツールが標準で付属していて、別途必要なビルドツールセットを追加インストールすると、VisualStudioがなくてもslnファイルやxxprojファイルからプロジェクト一式をビルドできる環境が整います.

Visual Studio 2017向け
Build Tools for Visual Studio 2017 ってのをインストールします.

Visual Studio 2015向け
C++ .NET別々に分かれてます.
- Microsoft Build Tools 2015 - .NET向け
- Visual C++ Build Tools 2015 - C++向け. 直リンク
- Visual C++ Optimizer fixes for Visual Studio 2015 Update 3 - C++ コンパイラのバグパッチ

これでVisualStudioなしでも全然困らない環境ができるんだ、スゲェ・・と思いきや、Msbuildはプロジェクトのビルドに必要なNugetパッケージを自動では復元してくれませんでした.
仕方なく nuget をダウンロードしてきて・・・

nuget.exe restore -SolutionDirectory .\hoge\fuga...

なんてやって、最初にパッケージを手動で復元しないといけなかったりします.
これは手作業としてもちょっと不便ですよね.

プロジェクト一式を配布したときに手軽にビルドしてもらえるように何とかできないか・・と、思い立ったワケです.

プロジェクトファイルに細工してNuget実行されるようにしよう

VisualStudioやmsbuildによるビルドは、projectファイルで定義されたターゲットという処理を順番に実行するということにより成り立っています.
それなら、パッケージをレストアするためのカスタムターゲットを定義して、定義したカスタムターゲットがデフォルトのビルドシーケンスの前に実行されるようにprojectファイルの定義をカスタマイズすればよさそうです.

1. ソリューション一式のディレクトリ配置

まず、ソリューションファイルのあるディレクトリに、nugetパッケージ復元処理のターゲットを定義したファイルを配置するディレクトリを作ります.
ただ、nugetを手作業でダウンロードして配置してもらうのはさすがに 面倒くさい エレガントではないので、 nugetがなければ自動で最新版をDownloadしてきて使う仕組みとします.

ソリューションのファイル配置は以下のようにします.

ソリューションのファイル配置

+ -- Hoge.sln
+ -- project
|        + -- Fuga.csproj
|        + -- packages.config
|                    :    
+ -- nuget
         + -- nuget_restore.props

2. カスタムターゲットを定義したプロパティシートを作成.

ディレクトリの中にユーザ定義プロパティシート nuget_restore.props を作成し、独自定義ターゲット DowlloadNugetNugetRestorePackage を定義します.
同時に、ビルドシーケンスで実行されるように既定ターゲットリスト一覧である BuildDependsOn をオーバーライドして、 DowlloadNugetNugetRestorePackage ターゲットを挿入します.

出来上がった nuget_restore.props

<?xml version="1.0" encoding="utf-8"?>
<!-- nuget_restore.props -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <!-- ビルドシーケンス実行前にカスタムターゲットを実行するようオーバーライド -->
        <BuildDependsOn>
            DownloadNuGet;
            NugetRestorePackages;
            $(BuildDependsOn)
        </BuildDependsOn>
        <!-- nugetの配置パス. nugetが見つからなければここにダウンロードして配置する -->
        <NugetPath>$(SolutionDir)nuget\nuget.exe</NugetPath>
        <!-- 最新版 nugetが取得できる直リンクURL -->
        <LatestNugetURL>https://dist.nuget.org/win-x86-commandline/latest/nuget.exe</LatestNugetURL>
    </PropertyGroup>

    <!-- DownloadFileByHttp Taskの定義.  MsBuild 15.8の DownloadFile タスクのWorkaround -->
    <!-- DownloadFile タスクは MSBuild 15.8(VS2017)以降でしか使えないため、自前で定義する.-->
    <!-- これでVS2015でも動作させることができる. -->
    <UsingTask TaskName="DownloadFileByHttp"
               TaskFactory="CodeTaskFactory"
               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <ParameterGroup>
            <DownloadUrl ParameterType="System.String" Required="true"/>
            <SavePath ParameterType="System.String" Required="true" />
        </ParameterGroup>
        <Task>
            <Reference Include="System" />
            <!-- 最新版の nuget.exe をダウンロードするcsコード -->
            <!-- 昨今ではSSL3ではerrorになるのでプロトコルをTLSに限定する -->
            <Code Type="Fragment" Language="cs">
                <![CDATA[
                      System.Net.ServicePointManager.SecurityProtocol
                      = System.Net.SecurityProtocolType.Tls
                      | System.Net.SecurityProtocolType.Tls11
                      | System.Net.SecurityProtocolType.Tls12;
                      (new System.Net.WebClient()).DownloadFile(DownloadUrl, SavePath);
                ]]>
            </Code>
        </Task>
    </UsingTask>

    <!-- 最新版DLを試みるターゲット DownloadNuGet の定義 -->
    <!-- nugetが NugetPath にない場合のみ実行される -->
    <Target Name="DownloadNuGet"
            Condition="!Exists($(NugetPath))">
        <Message Text="Downloading Nuget : '$(LatestNugetURL)' -> '$(NugetPath)'"/>
        <DownloadFileByHttp DownloadUrl="$(LatestNugetURL)"
                            SavePath="$(NugetPath)" />
    </Target>

    <!-- nugetによるパッケージのレストア NugetRestorePackages ターゲットの定義 -->
    <!-- &quot;はパスに空白が含まれる場合の対策. -->
    <Target Name="NugetRestorePackages">
        <!-- nuget.exe restore -SolutionDirectory (ソリューションディレクトリ)  -->
        <Exec Command="&quot;$(NugetPath)&quot; restore -SolutionDirectory &quot;$(SolutionDir).&quot;"/>
    </Target>
</Project>

3. propsをプロジェクトファイルへImportする

あとは、自動でパッケージ復元を行わせたいプロジェクトファイルの末尾にこっそりとpropsファイルをImportする以下の2行を追記しておきます.

プロジェクトファイルへの挿入

<Project>
          :
          :
  <!-- ターゲット定義をImportしてプロジェクトへ組み込む. -->
  <Import Project="$(SolutionDir)nuget\nuget_restore.props" />
</Project>

これでmsbuildを使ってプロジェクトのビルドを実行すると、nugetのダウンロード/デプロイとパッケージ復元を試みるターゲットがプロジェクトのビルドに先立って実行されるようになります.

VisualStudioでこのカスタマイズしたプロジェクトをビルドするとNugetの実行が重複しますが、Nugetは復元済みパッケージは無視するので、運用する上で目立った副作用は無さそうです.

cmakeのExternalProjectコマンドなどmsbuildを使ったビルド時はもちろん、ライセンスなどの関係でProfessional以上のVisualStudioを準備できない人向けにプロジェクトを配布したりするときなどに便利です.

(追記) プロキシ環境下でのNugetは?

こればかりは以下のかずき氏のブログにあるような手動での設定をお願いするしかないです.
NuGet v3.0で認証プロキシを突破する方法 - かずきのBlog@hatena

なんかいい方法ないかなぁ。。。

(追記) 通信プロトコルを制限する

昨今ではSSL3を使用する通信がエラーになることがあります.
プロトコルTLSに限定するようにする設定を追加しました.


参考記事