ページ

2015-10-10

Visual Studioソリューションの配置方針とMSBuildの活用

Visual Studioで開発をしていると、ひとつソリューションを作ってひとつのVisual Studioインスタンスでビルドしたり発行している間は良いが、そのうちそれ以上のことをしたくなってくる。例えば、複数のソリューションを複数のVisual Studioインスタンスで開いたり、CIにリリース用のzipファイルを作らせたりといったことである。

そのような状況を踏まえながら、自分がソリューション/プロジェクトを作るときに考える点について、文書としてまとめたことはなかったと思い、以下に記述する。

Visual Studio全般に通じる部分も多いと思うが、基本的にC# プロジェクト(.csproj)を想定する。

前提: ソリューション/プロジェクトとは何か?

Visual Studioでは、アプリケーションをビルド(及び実行)するために必要なものをまとめたかたまりを「プロジェクト」と呼んでおり(*1)、「プロジェクトファイル」(*.なんとかproj)には、ファイル一覧や参照ライブラリ情報が記載されている。

*1: プロジェクトは、アプリケーションをビルドするのに必要なものすべての論理的なコンテナーです。 - ソリューションとプロジェクトの作成

その「プロジェクト」を複数まとめたものが「ソリューション」となっており、プロジェクトの紐付けの情報が記録されているのが「ソリューションファイル」(*.sln)である。Visual Studioの1インスタンスは通常ひとつの「ソリューション」を開いている。

「プロジェクトファイル」(*.なんとかproj)/「ソリューションファイル」(*.sln)は、Visual StudioのGUIによって作成/編集される。

また、この「プロジェクトファイル」(*.なんとかproj) ファイルはMSBuildというビルドツールでビルドできる形式となっている。

MSBuildから実行できるプロジェクトファイルはVisual Studioに頼らずに一から作成することもでき、MSDNのドキュメント的にはそのファイルもプロジェクトファイルと呼ぶように見えるが、本記事では便宜上そのような1から書いたファイルは区別して「MSBuildファイル」と呼ぶことにする。

単一ソリューションの場合

まずはシンプルなひとつのソリューションに複数のプロジェクトという構成。Visual Studioからソリューションを作成することのほかに、やっていることがあるので、まずはその方針について記載する。

ざっくり図で表すと、以下の通りである。


詳しく説明していくと、以下の通り。

コード記述時以外のビルドの窓口となるMSBuildファイルを用意する

コードを書いているときはVisual Studioでコードを書き、ビルドし、デバッグ実行する。それ以外の作業、例えばzipへのアーカイブであったり、サーバへのデプロイといった作業についてはVisual Studioで完結させるのではなく、Visual StudioでのプロジェクトをラップしたMSBuildファイルをMSBuild.exeなどでビルドし作業する。手続きを*.〜projに記述することもできるが、それは基本的にしない。

これをする理由の一つ目は、機械的に生成される部分と人が意図を持って記述している部分を分けることにより、人が記述している部分の書きやすさ/読みやすさを向上させるためである。ツール(Visual Studioやその拡張)が生成/編集する*.〜projファイルに記述するとなると、生成/編集するツールの都合に合わせて記述しなければならない。ただの一覧のような設定ファイルであればそれほど問題ではないが、もっと自由に記述できるMSBuildでは非常にストレスフルな作業になる。また、Visual StudioのGUIから生成したわけではない追加の記述があることがGUIから分かり辛く、気づきにくい。別に記述してあれば、これらの問題はない。

二つ目は、CIシステムからのエントリポイントとして便利なためである。.csprojは先の通りMSBuild.exeでビルド可能なのだが、複数のcsprojを順にビルドするとか、渡すパラメータがいくつもあったりすると、そのMSBuild.exeの呼び出し自体がスクリプトのようになってきて、CIシステムのジョブの見通しが悪くなったり、変更管理がしにくくなったりする。しかし、CIシステム側からはこのMSBuildファイルを呼ぶだけと決めておけば、CIシステムでのジョブの記述内容が容易に想像できるようになり、またCIシステムに設定する前のタスクの事前確認がしやすくなったりする。

なお、このプロジェクトをラップしたMSBuildファイルは、私は「{{Name}}.msbuild」というファイル名をつけることが多いが、「{{Name}}.proj」でも別にかまわないし、そっちのほうがプロジェクトファイルとしての本筋っぽくはある。.msbuildで慣れてしまったのもあるが、Visual Studioで開くファイルではないよーということがわかっていいかなと思う。(この.msbuild拡張子を使っているプロジェクトもgithubで見かけるので、それほど好き勝手ではないと思っている。例えば、xunit

MSBuildファイルは基本的にプロジェクトファイル(*.~proj)をビルドする

MSBuildは、MSBuildタスクで他のプロジェクトファイルをビルドすることができる。MSBuildファイルは、これを利用してVisual Studioで作ったプロジェクトをビルドする。

なお、ソリューションをビルドすることもできるが、余分なビルドを減らすためにも、できる限りプロジェクトファイルを指定するほうが良い。

出力用フォルダを用意して後処理を可能にする

成果物出力フォルダはbin\Debug\のような各プロジェクトのデフォルトのパスではなく、「output\」のようなMSBuildファイル処理用の出力用フォルダを設定してそこに出力するようにする。

MSBuildファイル上の記述としては、以下のようになる。これは、KiritoriMage.csprojを「Release」でoutput\KiritoriMageに出力する指示である。(OutputDirプロパティは事前に「output」と定義、MSBuildProjectDirectoryはMSBuildファイルが配置されているディレクトリ(自動定義))


これをすることで、出力用フォルダ以下は書き換え(削除)して良いことが明確になるため、様々な後処理が行いやすくなる。もちろん、bin\Debugの状態でもできなくはないのだが、「どこ?」となりにくいので処理しやすい。

後処理とは例えば、以下のようなことである。
  • MSBuildで指定した変数に応じた環境情報を設定ファイルに設定する(*2)。
  • NuGetで入れたパッケージが不要なファイルを成果物にしてくるので、削除する。
  • 動的に生成するコンテンツを成果物に含める。
*2: おすすめはしない。なぜかというと、ビルド成果物が環境に縛られるので、各環境ごとに成果物が必要になってしまうため。可能な限りデプロイ時に構成するか、環境側に設定したい。

引き続きKiritoriMageのBuildターゲットで説明するとこんな感じである。ffmpegは使わないので消している。
やりなおしたいときもこのフォルダを削除すればよいだけというのは、いろいろな意味で楽である(この削除をする「Clean」ターゲットは用意しておく)。 ただし、この出力先フォルダの差し替えをするのが面倒なプロジェクトタイプがあった覚えがあるので、労力との見合いで実施する。

csprojには、可能な限りビルド構成は増やさない(Debug, Releaseだけで行けるならそうする)

ビルドのプロパティに関しては、MSBuildファイル側からできるだけ「Release」構成にプロパティで渡して変更することとして、可能な限りcsproj側のビルド構成(Configuration)は増やさない。

理由は、ビルド構成はGUIでの設定となる都合上、どうしても設定内容をフラットに見せざるを得なくなるため、各構成間での差分に対する見通しが悪かったり(良く見せることが難しい、と言うべきか)、一部のみ違う構成の変更作業に手間がかかったりするためである。MSBuildファイル中にコードで指示していれば「このプロパティだけ違う」というのが意図として伝わりやすい(伝えるコードに書きやすい)。

プロパティで変更できなくても、XMLな設定ファイルがちょっと違うレベルなら、ビルド後にMSBuild Community TasksのXmlUpdateで更新してしまうという手もある。

この方針も、構成間の差分次第で考える。がっつり違うのであれば、別物として用意しておくのもひとつ。

ツール群は窓口MSBuildファイルから呼べるようにする

そのプロジェクトの便利ツールがある、といった場合、窓口となるMSBuildファイルにタスクを記述しておくと、そこに存在があること、及びどんなパラメータが必要なのか明確になって便利である。CIからも呼びやすい。

正直これが便利だといえるのは、MSBuild LauncherのUIがこうだからというのも大きいが、この話だけ切り取って説明したのが「PowerShellにMSBuildLauncherで簡易GUIをつける」で、詳しい話はこちらを参照されたい。

複数のソリューションを使う場合とは

さて、複数のソリューションの方針を説明する前に、どんなときに複数になるのかという話。

Visual Studioは、基本的に一つのソリューションに対して同時に一つのデバッグ実行を行うことが想定されている(ように私には見える)。そのため、複数の実行が同時に走るようなものを一つのソリューションに押し込むと、扱いにくい(一応実行はできなくはない)。例えば、Webサーバーをデバッグ実行しながらクライアントアプリをデバッグ実行するといったケースでは、サーバー側、クライアント側でそれぞれソリューションを作成すると、二つのVisual Studioを同時に起動し、それぞれでデバッグ実行を行えて便利である。

そのため、同時に利用する複数のエントリポイントがある場合、複数のソリューションを使う。

他にも、ビルドの粒度のコントロールのために分割するという話もあるかもしれない。

複数ソリューションの場合

上記の状況となる典型的なケース、ServerとClientが及びその共通部分であるCommonというソリューションを構成する場合を例に複数ソリューションの場合の方針について記載する。 まずは図で表すと、以下の通りとなる。


なお、後の説明が通じやすいようにCommon, Server, Clientという名前をつけただけで、名前のつけかたはここでは触れない。Sharedといった他の汎用的な名前であったり、あるいはコードネームだったりとか、そういったものでも別に構わない。

各ソリューションごと、及びソリューションのまとまりごとにMSBuildファイルを用意する

先ほどのMSBuildファイルは、各ソリューションごと(子)、及びそのまとまりごと(親、通常トップにひとつ)に定義する。

子のMSBuildファイルには、各ソリューションごとの作業を記述する。親のMSBuildファイルには、MSBuildタスクによる子のターゲットの呼び出しと、組み合わせたときに必要になることを記述する。(とくになければ親側は単なる窓口になる。)

別のソリューションのプロジェクトを参照してもよい

例えば、CommonのプロジェクトはServerとClientが参照してもOK。すべては追加せず、必要な分だけ参照する。

ただし、基本的にはCommonへのメソッドの追加などはCommonのソリューションを開いて実施する。read-onlyとは言わないが、あくまで使っている側は参照用というスタイルで使う。 というのも、Common側でビルドが完結することがはっきりしないといけないし、ユニットテストが実施できないからである(ServerやClientからCommonのユニットテスト プロジェクトへの参照があるのはおかしい)。

とはいえ名前変更のようなリファクタのときは入っているほうがよかったりするので、どちらで直していいかはケースバイケースではあるが。

(大規模な変更をするときは、merge-solutionsで全部入りのAll.slnというのを作ったりするがこれは完全にバッドノウハウ。)

共通の定義を記述するMSBuildファイルを用意する

複数のプロジェクトにまたがって利用できる共通のターゲットがあれば、~.targetsという形で共有する形で追い出す。

ディレクトリ階層は揃える

これはバッドノウハウではあるのだが、csproj内には相対パスで参照される項目があるので、csprojがあるフォルダ階層は可能な限り揃えるようにする。 例えば、こんな感じ。

  • XYZ.Server.Web\XYZ.Server.Web.csproj
  • XYZ.Client.App\XYZ.Client.App.csproj

内部であってもNuGetパッケージを作成して利用する

上述のCommonは、共通に利用するアセンブリがあるということしか意味しないので、便利コードのようなものは、内部であってもNuGetパッケージにしてプライベートリポジトリで配布するとよい。

標準のファイルシステムベースのプライベートリポジトリという方法(社内の開発環境の改善&効率化のためにNuGetを活用しよう - Build Insider)もあるが、ProGetのようにこのようなパッケージを管理できる製品もある。また、使ったことはないがMyGetのようなSaaSも利用できるだろう。

なお、チームが分割されている場合、例えば「共通基盤チーム」が存在する場合、NuGet配布にするとリリースサイクルなどがコントロールしやすくなる場合もある。配布元チームは利用側チームに適切なリリースとして見せられるし、利用側チームは適切なタイミングでバージョンアップできるからである。

その他のTips

MSBuild Community Tasksの活用

MSBuild Community Tasksというものがあり、Zipを作ったり、Version情報を生成したりするTaskなどがあるので、NuGetでインストールして活用する。

csprojから参照されるtargetsファイルの構造に深入りしない

前述の通り、csprojファイルはMSBuildのプロジェクトファイルである。そのため、ビルド方法を記述したファイルをImportしており、追おうと思えばビルド作業を追うことができる。

しかし、決して処理順や参照変数などを追いやすい構造ではなく、これをやり始めるとそればっかりになってしまって、割に合わない。そして頑張って調べても、Visual Studioなどのバージョンによって変わってしまう可能性がある。

そもそも動かないといったトラブルシューティング的な部分であれば仕方ないが、~~というオプションがあれば後処理がいらなくて済むのになぁといった話は、時間を区切ってわかるレベルに留めるのが無難と思う。

MSBuildは、便利に使える範囲で使って、そこから出たらPowerShellなどのプログラムに任せる。

MSBuild Launcherの活用

上述のMSBuildファイルはVisual Studioで開くためのファイルではないので、Visual StudioをGUIとしては使えないか、あるいは無理に使えるように作ってもVisual Studioがいろいろなターゲットを実行するために作られたUIではないので不便である。

普通に考えるとMSBuild.exeで実行することになるが、そうなると「そうは言っても毎回コマンドを実行するのは辛い」という流れになり、GUIが欲しくなる。でもいいのがないね、という話で作ったのがMSBuild Launcherなので、そういう感じになったら使ってみてほしい。

例としてKiritoriMageのMSBuildファイルを開いた様子はこんな感じである。


なお、今よりもずっとノウハウがないときのプロジェクトなので全体のサンプルとしておすすめできるプロジェクトではないが、一例としてKiritoriMageのMSBuildファイルとしてはだいたいこんな感じである。

おわりに

自分でも、最初からこういうふうに作るわけではなく、ソリューションとプロジェクトファイルしかない場合や、ソリューション側のmsbuildがなかったりと、実際のところはもう少しいろいろ違っている。

いろいろ作ってきていないもの(Portableライブラリ, ストアアプリなど)もあるので、配慮が欠けている部分もあるかもしれない。とはいえ、少しでも設計の参考になれば幸いである。

なお、最新にはついていっていない部分があるはずなので、変わっている部分があればぜひ教えて欲しい(DNXの影響はかなりありそう)。

0 件のコメント:

コメントを投稿