January 2017

Volume 32 Number 1

Essential .NET - MSBuild の基礎: .NET Tooling 用ビルド エンジンの概要

Mark Michaelis | January 2017

Mark Michaelis読者と共にここ数年 .NET Core を追いかけてきました (あっという間でしたね)。その間、gulp の組み込みサポートの削除や Project.json の廃止など、「ビルド システム」は絶えず大きく変化してきました。こうした変化は、コラムニストにとっては厄介なものです。結局のところ、たった数か月しか利用できない機能やその詳細を学ぶのに読者の皆さんが多くの時間を費やすのは本意ではありません。そのため、たとえば .NET Core 関連のコラムは、常に、Visual Studio .NET 4.6 ベースの *.CSPROJ ファイルを基にビルドしてきました。このファイルは、実際にコンパイル済みの .NET Core プロジェクトではなく、.NET Core から NuGet パッケージを参照します。

今月は、.NET Core プロジェクトのプロジェクト ファイルが MSBuild ファイルに落ち着いたことを報告します (信じ難いかもしれませんが)。ただし、前世代 Visual Studio の MSBuild ファイルと同じではありません。改善が行われ、簡素化された MSBuild ファイルになっています。このファイルには (中かっこと山かっこが入り乱れることなく)、Visual Studio 2005 以降慣れ親しみ (おそらく愛着を抱いてきた) 従来の MSBuild ファイルに付属するツールのサポートと共に、Project.json のすべての機能が引き継がれています。つまり、オープン ソース、クロスプラットフォームの互換性、手作業で編集可能な簡易フォーマットなどの機能が含まれています。さらにワイルドカード ファイル参照を含め、完全に最新型の .NET ツール サポートも用意されています。

ツールのサポート

誤解のないように言うと、ワイルドカードなどの機能はこれまでも MSBuild でサポートされていました。しかし今回は、Visual Studio Tooling でも同様にこうした機能が利用できるようになります。MSBuild に関して最も重要な点は、すべての新しい .NET Tooling 用のビルド システムの基盤として、MSBuild が密接に統合され、.NET Core 1.0 と .NET Core 1.1 のランタイムを両方サポートすることです。新しい .NET Tooling には、DotNet.exe、Visual Studio 2017、Visual Studio Code、Visual Studio for Mac があります。

.NET Tooling と MSBuild が密接に結びついている大きなメリットは、作成する MSBuild ファイルがすべての .NET Tooling と互換性があり、任意のプラットフォームからビルドできる点にあります。

MSBuild の .NET Tooling の統合は、コマンドラインのプロセスではなく、 MSBuild API 経由で結び付けられています。たとえば、.NET CLI コマンド「Dotnet.exe Build」を実行しても、内部で msbuild.exe プロセスが呼び出されることはありません。ただし、コマンドは MSBuild API を呼び出してこの作業を実行します (MSBuild.dll アセンブリと Microsoft.Build.* アセンブリの両方)。プラットフォームが変わっても、どのツールの出力も似ています。それは、すべての .NET Tools が登録する共有ログ記録フレームワークがあるためです。

*.CSPROJ/MSBuild ファイルの構造

前述のように、ファイルのフォーマット自体は、ほぼ最小限といえるまで簡素化されています。ファイルでは、ワイルドカード、プロジェクト参照と NuGet パッケージ参照、複数フレームワークがサポートされます。さらに、以前 Visual Studio が作成していたプロジェクト ファイルに含まれていたプロジェクト型 GUID がなくなりました。

図 1 に示すのは *.CSPROJ/MSBuild ファイルのサンプルです。

図 1 .CSPROJ/MSBuild ファイルの基本サンプル

<Project>
  <PropertyGroup>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="**\*.cs" />
    <EmbeddedResource Include="**\*.resx" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NETCore.App">
      <Version>1.0.1</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Sdk">
      <Version>1.0.0-*</Version>
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

それでは、このファイルの構造と機能を詳しく見ていきます。

簡素化されたヘッダー: まず、ルート要素が Project 要素だけになっているのがわかります。名前空間とバージョンの属性はありません。

ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"

ただし、この名前空間とバージョンの属性は、RC 版の Tooling では依然作成されます。  同様に、共通プロパティのインポートも単純に省略可能です。

<Import Project=
  "$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />

プロジェクト参照: プロジェクト ファイルから、アイテム グループの要素にエントリを追加できます。

  • NuGet パッケージ:
<PackageReference Include="Microsoft.Extensions.Configuration">
  <Version>1.1.0</Version>
</PackageReference>
  • プロジェクト参照:
<ProjectReference Include="..\ClassLibrary\ClassLibrary.csproj" />
  • アセンブリ参照:
<Reference Include="MSBuild">
  <HintPath>...</HintPath>
</Reference>

一般に NuGet 参照が好まれるため、直接アセンブリ参照は例外にすべきです。

ワイルドカードのインクルード: コンパイル済みのコード ファイルとリソース ファイルは、すべてワイルドカードを使って追加できます。

<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<Compile Remove="CodeTemplates\**" />
<EmbeddedResource Remove="CodeTemplates\**" />

ただし、Remove 属性を使用して、特定のファイルを無視することもできます (ワイルドカードのサポートは多くの場合、グロビングと呼ばれます)。

マルチターゲット: ターゲットにするプラットフォームを特定するには、出力型 (省略可能) と共に、TargetFramework 要素でプロパティ グループを使用します。

<PropertyGroup>
  <TargetFramework>netcoreapp1.0</TargetFramework>
  <TargetFramework>netstandard1.3</TargetFramework>
</PropertyGroup>

これらのエントリを指定すると、各ターゲットの出力は (指定する構成に応じて) bin\Debug または bin\Release ディレクトリに組み込まれます。ターゲットが複数ある場合は、ビルドの実行によってターゲット フレームワーク名に対応するフォルダーに出力されます。

プロジェクト型 GUID の廃止: プロジェクトの型を指定するプロジェクト型 GUID を含める必要はなくなりました。

Visual Studio 2017 統合

Visual Studio 2017 では、Microsoft は引き続き、CSPROJ/MSBuild プロジェクト ファイルの編集用にリッチな UI を提供します。たとえば、図 2 はCSPROJ ファイルのリストを読み込んだ Visual Studio 2017 を示しています。このリストは、図 1 とは若干異なり、ターゲット フレームワーク要素として netcoreapp1.0 と net45、パッケージ参照として Microsoft.Extensions.Configuration 、Microsoft.NETCore.App、Microsoft.NET.Sdk、アセンブリ参照として MSBuild、プロジェクト参照として SampleLib を含んでいます。                                                                                                           

CSProj ファイルに加えてリッチな UI 備えたソリューション エクスプローラー
図 2 CSProj ファイルに加えてリッチな UI 備えたソリューション エクスプローラー

アセンブリ参照、NuGet パッケージ、プロジェクト参照といった依存関係に対応するグループ ノードが、ソリューション エクスプローラーの [Dependencies] ツリー内にあるのがわかります。

さらに、Visual Studio 2017 はプロジェクトとソリューションの動的再読み込みをサポートします。たとえば、グロビング パターンのワイルド カードの 1 つと一致するプロジェクト ディレクトリに新しいファイルが追加されると、Visual Studio 2017 によって変更が自動的に検出され、ソリューション エクスプローラーにそのファイルが表示されます。同様に、(Visual Studio のメニュー オプションまたは Visual Studio のプロパティ ウィンドウを使用して) Visual Studio プロジェクトからファイルを除外すると、それに応じて Visual Studio によってプロジェクト ファイルが自動的に更新されます (たとえば、<Compile Remove="CommandLine.cs" /> 要素を追加すると、プロジェクトをコンパイルする際に CommandLine.cs ファイルが除外されます)。 

さらに、プロジェクト ファイルの編集も Visual Studio 2017 によって自動検出され、編集後のファイルが再度読み込まれます。実は、ソリューション エクスプローラーの Visual Studio プロジェクト ノードは、組み込みの [Edit <Project File>] (<プロジェクト ファイル> の編集) メニュー オプションをサポートするようになりました。このオプションは、最初にプロジェクトのアンロードを要求しないで、Visual Studio 編集ウィンドウ内でプロジェクト ファイルを開きます。

Visual Studio 2017 には、プロジェクトを新しい MSBuild 形式に変換するための組み込みの移行サポートもあります。表示されるメッセージを受け入れると、プロジェクトによって Project.json/*.XPROJ 型から MSBUILD/*.CSPROJ 型に自動的にアップグレードされます。このようにアップグレードを行うと、Visual Studio 2015 .NET Core プロジェクトとの下位互換性がなくなるため、チーム内で Visual Studio 2017 と Visual Studio 2015 の両方を使用しながら同じ NET Core プロジェクトを進めることができなくなります。

MSBuild

2016 年 3 月号で Microsoft が MSBuild をオープン ソースとして GitHub (github.com/Microsoft/msbuild) に公開し、NET Foundation (dotnetfoundation.org) に貢献したことを報告しなかったのは不注意でした。MSBuild をオープン ソースとして確立することによって、Mac や Linux へのプラットフォーム移植性が高まり、最終的にはすべての .NET Tooling の基盤となるビルド エンジンになります。

MSBuild バージョン 15 では、前述の CSPROJ\MSBuild ファイルの PackageReference 要素以外には、オープンソースやクロス プラットフォームのほかにあまり多くの機能を追加していません。実際、コマンドラインを比較すると、オプションがまったく同じであることがわかります。まだ MSBuild バージョン 15 を使い慣れていない方のために、基本構文「MSBuild.exe [options] [project file]」で知っておくべき最も一般的なオプションを以下に示します。

/target:<target>: プロジェクト ファイル内のビルド ターゲットを指定し、そのビルド ターゲットの依存関係と共に実行します (このオプションの省略形は /t です)。

/property:<n>=<v>: (プロジェクト ファイルの ProjectGroup 要素で指定された) プロジェクト プロパティを設定またはオーバーライドします。たとえば、/property:Configuration=Release;OutDir=bin\ のように、構成や出力ディレクトリを変更できます (このオプションの省略形は /p です)。

/maxcpucount[:n]: 使用する CPU の数を指定します。既定では msbuild は1 つの CPU (シングルスレッド) で実行されます。同期が問題にならない場合は、同時実行のレベルを指定することで CPU の数を増やすことができます。値を添えずに /maxcpucount オプションを指定すると、msbuild はコンピューターにあるプロセッサをすべて使用します。

/preprocess[:file]: 含めるターゲットをすべてインライン化して、集約プロジェクト ファイルを生成します。これは問題が発生した場合のデバッグに役立ちます。

@file: オプションを含む応答ファイルを 1 つ (または複数) 指定します。応答ファイルには、各コマンドライン オプションを個別の行で含めます (コメントの先頭には “#” を付けます)。既定では、MSBuild はプロジェクトまたはソリューションの最初のビルドから msbuild.rsp というファイルをインポートします。応答ファイルにより、たとえば、どの環境 (開発、テスト、運用) でビルドするかに応じて、異なるビルド プロパティやターゲットを指定できます。

Dotnet.exe

.NET 用の dotnet.exe コマンドラインは .NET Core ベースのプロジェクトの生成、ビルドおよび実行のクロスプラットフォーム メカニズムとして約 1 年前に導入されました。既に述べたとおり、dotnet.exe コマンドラインは更新され、妥当な箇所ではほとんどのdotnet.exe コマンドラインの内部エンジンとして、MSBuild に大きく依存するようになっています。

さまざまなコマンドの概要を以下に示します。

dotnet new: 初期プロジェクトを作成します。このプロジェクト ジェネレーターが既定でサポートするプロジェクトの種類は、Console、Web、Lib、MSTest および XUnitTest です。ただし、今後はカスタム テンプレートを使用して独自のプロジェクトの種類を生成できるようになると期待しています (偶然ですが、new コマンドはプロジェクトの生成に MSBuild を使用しません)。

dotnet restore: プロジェクト ファイルで指定される依存関係をすべて読み取り、そこで指定されている NuGet パッケージとツールの中で不足しているものをダウンロードします。プロジェクト ファイル自体は引数として指定するか、現在のディレクトリのものを使用します (現在のディレクトリに複数のプロジェクト ファイルが存在する場合は、使用するファイルを指定する必要があります)。restore は MSBuild エンジンを利用するため、dotnet コマンドでは追加の MSBuild コマンドライン オプションが許可されます。

dotnet build: MSBuild エンジンを呼び出し、既定ではプロジェクト ファイル内にあるビルド ターゲットを実行します。restore コマンドと同様、dotnet build コマンドに MSBuild 引数を渡すことができます。たとえば、「dotnet build /property:configuration=Release」コマンドは Debug ビルド (既定値) ではなく、Release ビルドをトリガーして出力します。同様に、/target (または /t) を使用して MSBuild ターゲットを指定できます。たとえば、「dotnet build /t:compile」コマンドは、compile ターゲットを実行します。

dotnet clean: ビルド出力をすべて削除し、インクリメンタル ビルドではなく、フル ビルドを実行します。

dotnet migrate: Project.json/*.XPROJ ベースのプロジェクトを *.CSPROJ/MSBuild 形式にアップグレードします。

dotnet publish: すべてのビルド出力と依存関係を 1 つのフォルダーに組み合わせ、別のコンピューターへの配置を準備します。コンパイル出力と依存パッケージだけでなく .NET Core ランタイム自体も含む自己完結型の配置に特に便利です。自己完結型のアプリケーションには、ターゲット コンピューターに既に特定のバージョンの .NET プラットフォームがインストールされているという前提条件はありません。

dotnet run: .NET ランタイムを起動し、プロジェクトやコンパイル済みのアセンブリをホストしてプログラムを実行します。ASP.NET の場合、プロジェクト自体をホストできるため、コンパイルは必要ありません。

msbuild.exe と dotnet.exe の実行には重複する点が多いため、どちらを実行するかを選択する必要があります。既定の msbuild ターゲットをビルドしている場合は、プロジェクト ディレクトリ内からコマンド ”msbuild.exe” を実行し、コンパイルして、ターゲットを出力するだけです。これと同じ dotnet.exe コマンドは「dotnet.exe msbuild」です。 一方、“clean” ターゲットを実行している場合のコマンドラインは、MSBuild では「msbuild.exe /t:clean」、dotnet では「dotnet.exe clean」になります。さらに、どちらのツールも拡張機能をサポートします。MSBuild は、プロジェクト ファイル自体の中と、.NET アセンブリ経由の両方に、包括的な拡張性フレームワークを有しています (bit.ly/2flUBza 参照)。同様に、dotnet も拡張可能ですが、この場合も、基本的には MSBuild の拡張が推奨され、加えて若干の操作が必要になります。

dotnet.exe の考え方もお勧めですが、結局、MSBuild よりも優れている点はそれほどありません。ただし、MSBuild がサポートしないこともサポートします (中でも最も重要なのは dotnet new と dotnet run です)。一方 MSBuild は、必要に応じて複雑なことが行えますが、簡単なことは簡潔に行えるようにしています。また、妥当な既定値を用意すれば、MSBuild で複雑なことも簡素化できます。最終的に、dotnet と MSBuild のどちらがお勧めかは好みによります。時間が経てば開発コミュニティが CLI のフロント エンドとしてどちらを選択するかが明らかになるでしょう。

global.json

Project.json 機能は CSPROJ に移行されていますが、global.json は依然として完全にサポートされています。global.json ファイルはプロジェクト ディレクトリとパッケージ ディレクトリの指定を可能にし、使用する SDK のバージョンを特定できるようにします。以下に、global.json ファイルの例を示します。

{
  "projects": [ "src", "test" ],
  "packages": "packages",
  "sdk": {
    "version": "1.0.0-preview3",
    "runtime": "clr",
    "architecture": "x64"
  }
}

以下の 3 つのセクションが global.json ファイルの主な目的を表しています。

projects: .NET プロジェクトが配置されるルート ディレクトリを特定します。projects ノードは .NET Core ソース コードのデバッグで重要になります。ソース コードをコピー後、そのディレクトリを projects ノードに追加します。その結果、Visual Studio によってソリューション内のプロジェクトとして自動的に読み込まれます。

packages: NuGet パッケージ フォルダーの場所を特定します。

sdk: 使用するランタイムのバージョンを指定します。

まとめ

今回は、.NET Tooling スイート内で MSBuild が利用される箇所すべてについての概観を示しました。最後に、MSBuild について数千行ものコードを備えたプロジェクトに携わった経験から、1 つだけアドバイスします。XML MSBuild スキーマのように、宣言型で、型指定が厳密ではない言語でスクリプトを大量に記述することにならないようにしてください。それは本来の目的から外れます。逆に、プロジェクト ファイルは、ビルド ターゲットの順序と依存関係を指定する比較的小さいラッパーにしてください。MSBuild プロジェクト ファイルが大きくなりすぎると、メンテナンスが大変になります。プロジェクト ファイルをデバッグ可能で単体テストを行うのが容易な C# MSBuild タスクにリファクタリングするのに長時間掛かるようにはしないでください。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。彼は約 20 年間 Microsoft MVP に認定され、2007 年から Microsoft Regional Director を務めています。Michaelis は、C#、Microsoft Azure、SharePoint、Visual Studio ALM など、マイクロソフト ソフトウェアの設計レビュー チームにも所属しています。開発者を対象としたカンファレンスで講演を行い、多数の書籍を執筆しています。最近では、『Essential C# 6.0 (5th Edition)』(Addison-Wesley Professional、2015 年) を執筆しました (itl.tc/EssentialCSharp、英語)。連絡先は、Facebook (facebook.com/Mark.Michaelis、英語)、ブログ (IntelliTect.com/Mark、英語)、Twitter (@markmichaelis、英語)、または電子メール mark@IntelliTect.com (英語のみ) です。

この記事のレビューに協力してくれた IntelliTect 技術スタッフの Kevin Bost、Grant Erickson、Chris Finlayson、Phil Spokas、および Michael Stokesbary に心より感謝いたします。