目次
Visual Studio (MSBuild)
MSBuild
MSBuild は VisualStudio が利用している Build Tool です。 Build rule やファイル名等の項目は xml ファイルに記述します。 C++ の場合プロジェクト作成時に作られる .vcxproj ファイルがその xml ファイルに相当します。
通常は VisualStudio の GUI 上から設定できるため直接触る必要はほとんどありません。 ですが、Makefile のように直接定義やルールを記述することでより柔軟な使い方ができるようになります。 例えば Custom Build Step の代わりに直接 MSBuild の内部コマンドを活用すると、外部のコマンドやバッチファイルを経由すること無くファイル操作や追加のタスク実行を行うことができます。 外部シェルを起動しない分だけ高速で、また複雑な条件指定も可能です。条件指定によって不要な場合は処理を省くことが出来るため Build 手順の最適化に繋がります。
以下 Makefile (主に GNU make) と比較しながら MSBuild の xml 記述方法を簡単に説明します。
参考ページ
MSBuild の基本
MSBuild Project の書式と実行
<?xml version="1.0" encoding="utf-8"?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Target Name="Start"> <Message Text="Hello, world!"/> </Target> </Project>
任意のファイルに保存し、コマンドラインから “ msbuild /target:Start 保存したファイル名 ” で実行することができます。
シンボル定義
Makefile の変数のように任意のシンボルを定義することができます。 MSBuild では定義可能なシンボルとして Property と Item の 2 種類あります。 Property は単一の文字列で任意の場所に挿入し展開することができます。 Item は複数の項目からなるリストに相当します。
Makefile の場合変数とリストの区別がなく、スペースで区切った項目をそのままリストとみなすことができました。 MSBuild ではこの両者を明確に区別しています。
Property
<PropertyGroup> <PROPERTY_NAME> 定義内容 </PROPERTY_NAME> </PropertyGroup>
参照する場合は $(PROPERTY_NAME) となります。$( ~ ) による書式は Makefile と同じです。ただし常にその場で展開されるため Makefile とは性質が異なってきます。
実際の定義例
<PropertyGroup> <CopySrc>C:/temp/data</CopySrc> <CopyFile>$(CopySrc)/imagedata.dds</CopyFile> </ProepertyGroup>
上の例では CopyFile には $(CopySrc) が展開されて “C:/temp/data/imagedata.dds” が入ります。よって Makefile でいえば := に相当します。 Makefile で同様の定義をしてみましょう。
# Makefile COPY_FILE=$(COPY_SRC)/imagedata.dds COPY_SRC=C:/temp/data
上の Makefile の例では COPY_FILE には展開せずに “$(COPY_SRC)/imagedata.dds” が入ります。 マクロ展開は最終的に必要になるまで遅延されるので、上のように COPY_SRC をあとから定義することが可能です。 実際に参照されるまでの間に限りますが、事実上後方参照に相当します。
もしその場で展開したければ := を使います。 下記の例では COPY_FILE に “C:/temp/data/imagedata.dds” が入るため、あとから COPY_SRC を定義しても反映されません。
# Makefile COPY_SRC=C:/temp/data COPY_FILE:=$(COPY_SRC)/imagedata.dds
MSBuild は Makefile のようなマクロの遅延展開がありません。 後方参照はなく、あとからシンボルを定義することができないので xml ファイルに記述する順番が非常に重要になってきます。 未定義シンボルは空文字列になるので、下記の例では CopyFile には “/imagedata.dds” が入ります。
<PropertyGroup> <CopyFile>$(CopySrc)/imagedata.dds</CopyFile> <CopySrc>C:/temp/data</CopySrc> </ProepertyGroup>
Item
<ItemGroup> <ITEM_NAME Include="項目1" /> <ITEM_NAME Include="項目2" /> ... </ItemGroup>
定義した Item は、@(ITEM_NAME) の形で参照することができます。定義内容すべてが展開され、デフォルトでは各項目がセミコロン ';' で区切られます。 区切り文字は変更可能で、例えば @(ITEM_NAME, ' ') の形で参照すればスペースで区切られた文字列になります。
実際の定義例
<ItemGroup> <FileList Include="A" /> <FileList Include="B;C" /> </ItemGroup>
上の例では FileList に 3 つの項目 'A', 'B', 'C' を定義してます。Include には単一の項目だけでなく、セミコロン ';' 区切りで複数の項目を与えることが可能です。 また実在するファイル名ならワイルドカードで定義することもできます。 定義した Item は @(FileList) で参照できるので、下記の FileListProp には “A;B;C” が入ります。 FileListProp2 では区切り文字を空文字にしているので連結された “ABC” が入ります。
<PropertyGroup> <FileListProp>@(FileList)</FileListProp> <FileListProp2>@(FileList,'')</FileListProp2> </PropertyGroup>
下記は存在しているソースファイルを SrcList に定義しています。 例えば main.cpp, main.h, window.cpp, window.h が存在しているなら、SrcList は “main.cpp;window.cpp;main.h;window.h” になります。 Makefile の $(wildcard) に相当します。
<ItemGroup> <SrcList Include="*.cpp" /> <SrcList Include="*.h" /> </ItemGroup>
タスクの実行
Target
Makefile の場合 Build 時に指定する Target とは最終的に生成するファイルのことです。 例えば “ make test.exe ” のように Target を指定すると、test.exe を作るためのルールが順次適用されます。
また “ make all ” のように、ファイル名ではなく何らかの Action 名を使うことがあります。 all というファイルは実際には生成されないので、毎回 Build 対象と判定されることを利用しているわけです。 この場合本当に 'all' という名のファイルが存在していると意図した動作にならないので、Action を意味する仮想的なファイル名であることを明確に宣言することもできます。 下記の .PHONY 宣言がそれに相当します。
# Makefile .PHONY: all all: echo 'Action all'
MSBuild の場合 Target はファイル名ではなく、最初から Action を意味しています。 下記は MSBuild での定義例です。make の場合と同じように、“ msbuild /target:All ” と直接実行することが可能です。
<Target Name="All"> <Message Text="Action all"/> </Target>
Task
Makefile では、Target を生成するために必要な実際の Build 手順を 2 行目以降にそのまま記述します。 Shell 経由でそのまま実行されるコマンドなので非常にシンプルです。 Build 手順の各行は Tab で始まっている必要があります。
# Makefile .PHONY: copy copy: echo 'Action copy' copy package.zip backup.zip
MSBuild の場合は Task と呼ばれる実行可能な手順を記述します。 下記の例では Message と Copy が Task に相当します。 多くの Task コマンドが内蔵されており拡張も可能。
<Target Name="Copy"> <Message Text="Action copy" /> <Copy SourceFiles="package.zip" DestinationFiles="backup.zip" /> </Target>
Makefile のように外部コマンドを呼び出す Task もあります。 下記は make の例と同じようにシェルコマンドを実行しています。
<Target Name="Copy"> <Message Text="Action copy" /> <Exec Command="copy package.zip backup.zip" /> </Target>
Target の依存関係
Makefile においては Target は生成されるファイルそのものを指します。 実際に Build 手順を実行するトリガは下記のように判定されます。
- Target ファイルの存在
- 存在していなければ Build 手順の実行
- Target ファイルが存在している場合、依存元のファイルとタイムスタンプを比較
- Target の方が古ければ Build 手順の実行
.PHONY 宣言された Target は、常にファイルが存在していないものとみなします。
# Makefile TARGET : 依存ファイルのリスト 実行するビルド手順(コマンド) ...
実際の例 : “ make all ” を実行すると all → backup2.zip → backup1.zip → package.zip と依存解析が行われ、(1) → (2) の順にコマンドが実行されます。 一度実行すると backup1.zip や backup2.zip が作られるので、特にファイルの更新がなければ 2回目以降は copy の実行をスキップします。
# Makefile .PHONY: all all: backup2.zip ; # (1) backup1.zip: package.zip copy package.zip backup1.zip # (2) backup2.zip: backup1.zip copy backup1.zip backup2.zip
MSBuild の場合、Target 同士の依存関係と実際に生成するファイルのタイムスタンプ判定は別の概念となります。
- Target 同士の依存関係は Target を実行する順番を決定する
- Target の Build 手順を実行するかどうかは、パラメータで与えた入出力ファイルのタイムスタンプによって判定
Target 毎にそれぞれ入出力のファイル指定可能でタイムスタンプ判定が行われます。 この判定は Target 単独で完結しています。 出力ファイルが存在し、かつ新しければ Build 手順を実行しません。 入出力指定がなければ、Makefile の .PHONY 宣言同様に常に実行が行われます。
Makefile のような入出力ファイル名による Target 同士の依存解析は行われません。 Target の依存関係は、入出力ファイル名とは別指定となっています。
<Target Name="All" DependsOnTargets="Copy2" /> <Target Name="Copy1" Inputs="package.zip" Outputs="backup1.zip"> <Copy SourceFiles="package.zip" DestinationFiles="backup1.zip" /> <Touch Files="backup1.zip" /> </Target> <Target Name="Copy2" Inputs="backup1.zip" Outputs="backup2.zip" DependsOnTargets="Copy1"> <Copy SourceFiles="backup1.zip" DestinationFiles="backup2.zip" /> <Touch Files="backup2.zip" /> </Target>
“ msbuild /target:All ” を実行すると、DependsOnTargets の順番に従い Copy1, Copy2 と実行が行われます。 Task を実行するかどうかは Inputs Outpus に記述されたファイルによって決定します。 出力ファイルの Timestamp を更新しているため、元ファイルに更新がなければ不要な Copy Task は実行されません。このあたりの動作は Makefile の例と同じです。 上の例は敢えて Makefile に近い動作にしていますが Copy Task の SkipUnchangedFiles=“true” を使うともっと簡単にできます。
Target の挿入
MSBuild では Target の実行順番を柔軟に定義することができます。 下記の定義で “ msbuild /target:Start ” を実行すると B → C → Start → D の順番で実行が行われます。
<Target Name="B" /> <Target Name="Start" DependsOnTargets="B" /> <Target Name="C" BeforeTargets="Start" /> <Target Name="D" AfterTargets="Start" />
Start と B は通常の依存関係です。Start は B に依存しているため、Start よりも先に B を実行する必要があります。 Makefile で定義する依存関係と同じです。 C と D はあとから挿入した Target です。Start の定義を変えること無く、Build 手順に任意の動作を挿入することができます。 Makefile には相当する機能はありません。
Item の列挙
Makefile はパターンマッチングで複数の Target の列挙ができます。
# Makefile (gmake 系) FILE_LIST=A.bak B.bak C.bak backup: $(FILE_LIST) ; %.bak: %.dds copy $< $@
nmake は若干書式が違います。
# Makefile (nmake) FILE_LIST=A.bak B.bak C.bak backup: $(FILE_LIST) ; .dds.bak: copy $< $@ .SUFFIXES: .bak .dds
$@ は列挙したターゲット自身、$< はマッチングで得られたソースファイルになります。 それぞれ下記の内容に置き換わりながら、copy が 3 回実行されます。 もちろん実際に copy が呼ばれるのは *.bak が存在しない場合か *.dds よりもタイムスタンプが古い場合だけです。
$@ | $< |
---|---|
A.bak | A.dds |
B.bak | B.dds |
C.bak | C.dds |
MSBuild では Task で Item metadata を参照することで Item の要素ごとに分解することができます。
<ItemGroup> <FileList Include="A.bak;B.bak;C.bak" /> </ItemGroup> <Target Name="Backup" Inputs="@(FileList->'%(FileName).dds')" Outputs="@(FileList)"> <Copy SourceFiles="%(FileList.FileName).dds" DestinationFiles="%(FileList.Identity)" /> <Touch Files="%(FileList.Identity)" /> </Target>
Item Metadata は %(ITEM_NAME.METADATA_NAME) の書式で参照できます。 上の例では Copy Task が FileList の個々の要素に対する %(FileName) と %(Identity) を参照しています。 それぞれ下記の表の内容に置き換わりながら、Copy Task が 3回呼び出されることになります。
%(FileName) | %(Identity) |
---|---|
A | A.bak |
B | B.bak |
C | C.bak |
FileName は Item の各要素の拡張子とパスを除いたファイル名、Identity は Item の要素そのものに展開されます。 つまり Makefile の推論ルールで言えば '$*' と '$@' に相当します。
Makefile と違いパターンマッチではなく明示的にリストを展開すること、またタイムスタンプ比較は Target 単位でまとめて行われる点が異なっています。 B.dds だけ更新した場合、make の場合 B.bak のコピーだけが行われます。 MSBuild では 3 file すべてのコピーが行われます。
Item metadata は FileName や Identity などのプリセットだけでなく、ユーザー定義のパラメータを追加することもできます。 下記の例では、%(FileList.ID) で参照を行うと 100, 200 がそれぞれ列挙されることになります。
<ItemGroup> <FileList Include="A.bak"> <ID>100</ID> </FileList> <FileList Include="B.bak"> <ID>200</ID> </FileList> </ItemGroup>
@(ITEM_NAME) では ';' で区切って要素すべての展開、%(ITEM_NAME.Identity) では要素の数だけ Task 呼び出しが展開されます。 Item metadata の個別参照で展開できるのは Task だけです。
下記の書式で Item のリストをまるごと置換することができます。置換パターンでは Item metadata を参照することができます。
@(ITEM_NAME -> '置換パターン~')
例えば下記の例は拡張子を置き換えています。つまり “A.bak;B.bak;C.bak” → “A.dds;B.dds;C.dds” となります。
@(FileList -> '%(FileName).dds')
Makefile では foreach に相当します。
$(foreach file,$(FILE_LIST),$(basename $(file)).dds)
条件付き実行
MSBuild ではすべてのタグに条件式を指定することができます。 条件式は Condition アトリビュートで指定します。 下記の例は、Build 時の Configuration が Debug か Release かによって Property $(LinkFilePath) の定義内容を変えています。
<PropertyGroup> <LinkFilePath Condition="'$(Configuration)'=='Debug'">lib/Debug/</LinkFilePath> <LinkFilePath Condition="'$(Configuration)'=='Release'">lib/Release/</LinkFilePath> </PropertyGroup>
Condition アトリビュートはあらゆる場所に指定することができます。 複数の Property をまとめて条件分岐させたいなら、PropertyGroup に Condition を設けることもできるわけです。
Makefile であれば ifeq ~ else ~ endif が相当します。
# Makefile ifeq ($(CONFIGURATION),Debug) LINK_FILE_PATH=lib/Debug/ else ifeq ($(CONFIGURATION,Release) LINK_FILE_PATH=lib/Release/ endif
下記は $(LinkFilepath) Property が定義されていない場合にデフォルト値の設定を行います。 他の場所ですでに定義されている場合は何もしません。
<PropertyGroup> <LinkFilePath Condition="'$(LinkFilePath)'==''">lib/</LinkFilePath> </PropertyGroup>
Makefile にそのまま翻訳するなら下記の通り。(または ifndef を使う)
ifeq ($(LINK_FILE_PATH),) LINK_FILE_PATH=lib/ endif
ただし Makefile にはもっと便利な書き方があるので、実際には下記の 1 行で済みます。
LINK_FILE_PATH?=lib/
VisualStudio での利用
VisualStudio で生成された Project File を編集して、自分で Property や Item の定義、Target の追加などを行うことができます。 例えば外部参照しているライブラリのパスなど、任意の Property を追加しておけば VisualStudio の設定画面で参照できます。
Custom Build に相当する Target を追加することもできます。 Target の AfterTargets, BeforeTargets を使えば、VisualStudio に組み込まれた Build 手順の任意の場所に、好きな処理を挿入することが可能となります。
定義を共有する
複数の Project で定義内容を共有するには Import を使います。 例えば下記のように build_def.props と build_def.targets を作成しておきます。
<?xml version="1.0" encoding="utf-8"?> <!-- build_def.props --> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> ... </PropertyGroup> <ItemGroup> ... </ItemGroup> </Project>
<?xml version="1.0" encoding="utf-8"?> <!-- build_def.targets --> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Target Name=""> ... </Target> </Project>
VisualStudio の *.vcxproj ファイルの先頭と最後にそれぞれ Import で build_def.props , build_def.targets を挿入します。 2箇所に分けるのは、シンボル定義はできるだけ先頭で行い、それらの定義を参照して実行する Target の定義はできるだけ後方で行いたいからです。
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="build_def.props"/> ~ ~ <Import Project="build_def.targets"/> </Project>
実際の使用例
build_def.props の例。 自分独自のデフォルトの include path, lib path を設定しておくことができます。 VisualStudio の設定で @(MyIncludePath) や @(MyLibPath) を追加しておけば、build_def.props の編集だけで複数のプロジェクトにまたがる設定を変更することが可能。 また Compiler の CommandLine に $(MyCFlags) を追加しておけば、コンパイラオプションも一箇所で変更可能になります。 下記のように Release, Debug それぞれ異なる設定にすることも可能。
- Configuration Properties → C/C++ → General → Additional Include Directories の最後に @(MyIncludePath) を追加
- Configuration Properties → C/C++ → Command Line → Additional Options に $(MyCFlags) を追加
- Configuration Properties → Linker → General → Additional Library Directories に @(MyLibPath) を追加
<?xml version="1.0" encoding="utf-8"?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <MyIncludePath Include="" /> <MyLibPath Include="" /> </ItemGroup> <PropertyGroup> <MyCFlags Condition="'$(Configuration)'=='Debug'"></MyCFlags> <MyCFlags Condition="'$(Configuration)'=='Release'">-fp:fast -arch:AVX2</MyCFlags> </PropertyGroup> </Project>
build_def.targets の例。 vcxproj ファイルと同じ場所に CustomPreBuild.bat または CustomPostBuild.bat が存在していれば Build の前後に実行します。 VisualStudio 上から設定する Custom Build との違いは、bat ファイルが存在していなければ何もしないことです。 無駄なシェルの起動などを回避できます。
<?xml version="1.0" encoding="utf-8"?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Target Name="CustomPreBuild" Condition="Exists('$(MSBuildProjectDirectory)/CustomPreBuild.bat')" BeforeTargets="Build"> <Exec Command="$(MSBuildProjectDirectory)/CustomPreBuild.bat" /> </Target> <Target Name="CustomPostBuild" Condition="Exists('$(MSBuildProjectDirectory)/CustomPostBuild.bat')" AfterTargets="Build"> <Exec Command="$(MSBuildProjectDirectory)/CustomPostBuild.bat" /> </Target> </Project>
動作の確認方法
VisualStuido ではデフォルトでは Message などの出力内容が表示されません。 動作確認する場合はは次のように設定を変更しておくと便利です。
- Menu の Tools → Options → Projects and Solutions → Build and Run を開く
- MSBuild project build output verbosity と MSBuild project build log file verbosity を変更
- default が Minimal なので好きな Level まで上げます。
- Message Task を利用したデバッグ出力だけなら Normal で十分です。
- 条件判定の結果などより詳しい情報が必要なら Detailed にします。
Command Line からの呼び出し
例
msbuild /property:OutputDir=..\..\..\Binaries\DotNET\ /target:build AutomationTool.csproj