MSBuild csproj 小ネタ集

[MSBuild の各要素・アイテム・プロパティーの俯瞰と逆引き大辞典](http://dd-kaihatsu-room.blogspot.com/2018/09/about-msbuild-elements-items-properties.html) を昔書きましたが…
Qiita では非常にコードを掲載しやすい有り難さがあるので、具体的に実装しようという話です。

# ItemGroup をタスク (別々に実行) へ結びつける

つぎのように 1 入力 1 出力のコマンドは容易だと思いますが、

```bat
EncFile input.xml bin\Debug\output.bin
```

`IV` や `Key` などを個別に持たせるには?

```bat
EncFile2 encode IV Key input.xml bin\Debug\output.bin
```

メタデータを活用します。参考: [プロジェクト ファイルで項目メタデータを参照する](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-items?view=vs-2017#BKMK_ReferencingItemMetadata)

```1.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="15.0">
  <PropertyGroup>
    <OutputPath>bin\Debug\</OutputPath>
  </PropertyGroup>
  <ItemGroup>
    <EncFile2 Include="first.xml">
      <OutputTo>first.dat</OutputTo>
      <IV>IV111</IV>
      <Key>Key111</Key>
    </EncFile2>
    <EncFile2 Include="second.xml">
      <OutputTo>second.dat</OutputTo>
      <IV>IV222</IV>
      <Key>Key222</Key>
    </EncFile2>
  </ItemGroup>
  <Target Name="BuildEncFile"
          AfterTargets="Build">
    <Exec Command="EncFile2.exe ^
      encode ^
      &quot;%(EncFile2.IV)&quot; ^
      &quot;%(EncFile2.Key)&quot; ^
      &quot;%(EncFile2.Identity)&quot; ^
      &quot;$(OutputPath)%(EncFile2.OutputTo)&quot;"/>
  </Target>
</Project>
```

`%(EncFile2.Identity)` の意は `アイテム EncFile2 のメタデータ Identity を参照` です。

実行してみます:

```text
H:\Proj\MSBuildCSProj\1>msbuild
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
Copyright (C) Microsoft Corporation.All rights reserved.

2019/10/18 21:50:08 にビルドを開始しました。
ノード 1 上のプロジェクト "H:\Proj\MSBuildCSProj\1\1.csproj" (既定のターゲット)。
BuildEncFile:
  EncFile2.exe ^
        encode ^
        "IV111" ^
        "Key111" ^
        "first.xml" ^
        "bin\Debug\first.dat"
  EncFile2.exe        encode       "IV111"       "Key111"       "first.xml"       "bin\Debug\first.dat"
  EncFile2.exe ^
        encode ^
        "IV222" ^
        "Key222" ^
        "second.xml" ^
        "bin\Debug\second.dat"
  EncFile2.exe        encode       "IV222"       "Key222"       "second.xml"       "bin\Debug\second.dat"
プロジェクト "H:\Proj\MSBuildCSProj\1\1.csproj" (既定のターゲット) のビルドが完了しました。


ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:00.20
```

# プロパティ関数で既定値を適用

[プロパティ関数](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/property-functions?view=vs-2017) を使い、既定値を適用できるようにしましょう。

```xml
<EncFile2 Include="first.xml" />
<EncFile2 Include="$([MSBuild]::ValueOrDefault(`$(firstYml)`, `first.yml`))" />
```

firstYml の指定は、`set firstYml=1.yml` または `msbuild /p:firstYml=1.yml` などです。

# PDB 中に含まれるソースコードのパスを変更する

参考: [PDB 中に含まれるソースコードのパスを相対パスに変えるための csproj 設定(Rosnly リポジトリ内を漁ってたらこういう書き方だった)](https://gist.github.com/ufcpp/f3cc89e8c266997063eb2c633114668a)

実施例:

```xml
  <PropertyGroup>
    <DeterministicSourceRoot>C:\yourApp\</DeterministicSourceRoot>
    <RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\'))</RepoRoot>
    <PathMap>$(RepoRoot)=$(DeterministicSourceRoot)</PathMap>
  </PropertyGroup>
```

# ItemGroup をタスク (一度に実行) へ結びつける

参考: [MSBuild バッチ](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-batching?view=vs-2019)

つぎのように、一度のコマンド実行で、複数ファイルを指定したい事案です:

```bat
signtool sign /a A.exe B.dll C.dll
```

`アイテムの種類` を活用します。参考: [プロジェクト ファイルの項目を参照する](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-items?view=vs-2017#reference-items-in-a-project-file)

```2.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="15.0">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
          Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <OutputPath>bin\Debug\</OutputPath>
  </PropertyGroup>
  <ItemGroup>
    <PreSignTarget Include="$(TargetPath)" />
    <PreSignTarget Include="B.dll" />
    <PreSignTarget Include="C.dll" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Target Name="PreSignTarget"
          BeforeTargets="CreateSetup">
    <Exec Condition=" $(SignOptions) != '' "
          Command="signtool.exe ^
                   sign ^
                   $(SignOptions) ^
                   @(PreSignTarget-&gt;'&quot;%(FullPath)&quot;', ' ')"
          />
  </Target>
  <Target Name="CreateSetup"
          AfterTargets="Build">
  </Target>
</Project>

```

`@(PreSignTarget-&gt;'&quot;%(FullPath)&quot;', ' ')` は XML の都合で実体参照を用いますが、
`@(PreSignTarget->'"%(FullPath)"', ' ')` のように解釈されるはずです。

`アイテムの種類` を `@(<ItemType>)` または `@(<ItemType>, '<separator>') ` の書式で展開できます。
参考: [プロジェクト ファイルで項目メタデータを参照する](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-items?view=vs-2017#BKMK_ReferencingItemMetadata)

`@(CppFiles -> '%(Filename).obj')` のような指定は `変換` と呼ばれます。
参考: [メタデータを使用してアイテムの種類を変換する](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-items?view=vs-2017#BKMK_Transforming)

実行してみます:

```text
H:\Proj\MSBuildCSProj\2>set SIGNOPTIONS=/a

H:\Proj\MSBuildCSProj\2>msbuild /t:CreateSetup
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
Copyright (C) Microsoft Corporation.All rights reserved.

2019/10/18 22:02:52 にビルドを開始しました。
ノード 1 上のプロジェクト "H:\Proj\MSBuildCSProj\2\2.csproj" (CreateSetup ターゲット)。
PreSignTarget:
  signtool.exe ^
    sign ^
    /a ^
    "H:\Proj\MSBuildCSProj\2\bin\Debug\2.exe" "H:\Proj\MSBuildCSProj\2\B.dll" "H:\Proj\MSBuildCSProj\2\C.dll"
  signtool.exe sign /a "H:\Proj\MSBuildCSProj\2\bin\Debug\2.exe" "H:\Proj\MSBuildCSProj\2\B.dll" "H:\Proj\MSBuildCSProj\2\C.dll"
プロジェクト "H:\Proj\MSBuildCSProj\2\2.csproj" (CreateSetup ターゲット) のビルドが完了しました。

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:00.19
```

# ItemGroup をタスク (個別実行と一度に実行) へ結びつける

NSIS で 2 種類 (通常版、スーパー版) のセットアップを作成したい。しかして署名は一度にしたい場合など。

実行イメージ:

```text
makensis.exe /DOUTFILE=Setup_Normal.exe /DSUPER=0 Setup.nsi
makensis.exe /DOUTFILE=Setup_Super.exe  /DSUPER=1 Setup.nsi
signtool.exe sign /a "Setup_Normal.exe" "Setup_Super.exe"
```

```3.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="15.0">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
  <PropertyGroup>
    <OutputPath>bin\Debug\</OutputPath>
  </PropertyGroup>
  <ItemGroup>
    <MakeNsis Include="Setup.nsi">
      <OutputTo>Setup_Normal.exe</OutputTo>
      <Options>/DSUPER=0</Options>
    </MakeNsis>
    <MakeNsis Include="Setup.nsi">
      <OutputTo>Setup_Super.exe</OutputTo>
      <Options>/DSUPER=1</Options>
    </MakeNsis>
  </ItemGroup>
  <Target Name="CreateSetup" AfterTargets="Build">
    <Exec Command="makensis.exe /DOUTFILE=%(MakeNsis.OutputTo) %(MakeNsis.Options) %(MakeNsis.Identity)"/>
    <Exec Command="signtool.exe sign $(SignOptions) @(MakeNsis-&gt;'&quot;%(OutputTo)&quot;', ' ')" Condition=" $(SignOptions) != '' "/>
  </Target>
</Project>
```

実行してみました:

```text
H:\Proj\MSBuildCSProj\3>msbuild
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
Copyright (C) Microsoft Corporation.All rights reserved.

2019/10/18 22:48:31 にビルドを開始しました。
ノード 1 上のプロジェクト "H:\Proj\MSBuildCSProj\3\3.csproj" (CreateSetup ターゲット)。
CreateSetup:
  makensis.exe /DOUTFILE=Setup_Normal.exe /DSUPER=0 Setup.nsi
  makensis.exe /DOUTFILE=Setup_Super.exe /DSUPER=1 Setup.nsi
  signtool.exe sign /a "Setup_Normal.exe" "Setup_Super.exe"
プロジェクト "H:\Proj\MSBuildCSProj\3\3.csproj" (CreateSetup ターゲット) のビルドが完了しました。


ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:00.27
```

# ItemGroup アイテム メタデータのテンプレート的な利用

`<OutputTo>Setup_$(AssemblyVersion)_Normal.exe</OutputTo>` のように、
メタデータの指定をテンプレート化したいが…

Target の中でしか解決できないなど、`$(AssemblyVersion)` を直ちに利用できない場合の回避策についてです。

Target 内部で `%(MakeNsis.OutputToResolved)` を定義することで回避します。

```xml
  <Target>
    <ItemGroup>
      <MakeNsis>
        <OutputToResolved>...</OutputToResolved>
      </MakeNsis>
    </ItemGroup>
  </Target>
```

有名な策として、`String.Copy` という静的メソッドを用い、プロパティ関数の体で文字列を加工し出力する方法があります。

```xml
<OutputToResolved>$([System.String]::Copy('%(OutputTo)')</OutputToResolved>

<OutputToResolved>$([System.String]::Copy('%(OutputTo)').Replace('{VER}', '$(Ver)'))</OutputToResolved>

<OutputToResolved>$([System.String]::Copy('%(OutputTo)').Replace('{VER}', '$(Ver.Replace('.', '_'))'))</OutputToResolved>
```

出来上がりです:

```4.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="15.0">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
  <PropertyGroup>
    <OutputPath>bin\Debug\</OutputPath>
    <Ver>1.2.3</Ver>
  </PropertyGroup>
  <ItemGroup>
    <MakeNsis Include="Setup.nsi">
      <OutputTo>Setup_{VER}_Normal.exe</OutputTo>
      <Options>/DSUPER=0</Options>
    </MakeNsis>
    <MakeNsis Include="Setup.nsi">
      <OutputTo>Setup_{VER}_Super.exe</OutputTo>
      <Options>/DSUPER=1</Options>
    </MakeNsis>
  </ItemGroup>
  <Target Name="CreateSetup" AfterTargets="Build">
    <ItemGroup>
      <MakeNsis>
        <OutputToResolved>$([System.String]::Copy('%(OutputTo)').Replace('{VER}', '$(Ver.Replace('.', '_'))'))</OutputToResolved>
      </MakeNsis>
    </ItemGroup>
    <Exec Command="makensis.exe /DOUTFILE=%(MakeNsis.OutputToResolved) %(MakeNsis.Options) %(MakeNsis.Identity)"/>
    <Exec Command="signtool.exe sign $(SignOptions) @(MakeNsis-&gt;'&quot;%(OutputToResolved)&quot;', ' ')" Condition=" $(SignOptions) != '' "/>
  </Target>
</Project>
```

実行してみました:

```txt
H:\Proj\MSBuildCSProj\4>msbuild
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
Copyright (C) Microsoft Corporation.All rights reserved.

2019/10/27 3:25:40 にビルドを開始しました。
ノード 1 上のプロジェクト "H:\Proj\MSBuildCSProj\4\4.csproj" (既定のターゲット)。
CreateSetup:
  makensis.exe /DOUTFILE=Setup_1_2_3_Normal.exe /DSUPER=0 Setup.nsi
  makensis.exe /DOUTFILE=Setup_1_2_3_Super.exe /DSUPER=1 Setup.nsi
  signtool.exe sign /a "Setup_1_2_3_Normal.exe" "Setup_1_2_3_Super.exe"
プロジェクト "H:\Proj\MSBuildCSProj\4\4.csproj" (既定のターゲット) のビルドが完了しました。


ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:00.28
```

# nupkg で、ファイルをコピー

.NET Core スタイルの csproj で nupkg を構築し (`dotnet pack` でビルド)、多数のファイルをビルド後にコピーする事案です。

ThePackage.csproj

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <None Pack="true" IncludeInPackage="false" Update="ThePackage.targets" PackagePath="build/ThePackage.targets">
    </None>

    <None Pack="true" IncludeInPackage="false" Update="GPL/**" PackagePath="binaryFiles">
    </None>
  </ItemGroup>
</Project>
```

ThePackage.targets

```xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <ThePackageFiles Include="$(MSBuildThisFileDirectory)/../binaryFiles/**"/>
  </ItemGroup>

  <Target Name="CopyThePackage" AfterTargets="AfterBuild">
    <Copy SourceFiles="@(ThePackageFiles)" DestinationFolder="$(OutDir)/%(RecursiveDir)" SkipUnchangedFiles="true" />
  </Target>
</Project>
```

Notes: 
- nupkg の `/binaryFiles` フォルダを経由してコピーします。
- `ThePackage` をプロジェクト名で置き換えます。