MSBuildで2つのアイテム(ItemGroup)を組み合わせてマトリクスを作成する方法について。 あるいはn個のアイテムとm個のアイテムから計n×m個のアイテムを生成する方法について。

例として、TargetFrameworksConfigurationRuntimeIdentifiersの組み合わせすべてをItemGroupとして作成したい場合などを想定する。

2組のアイテムを組み合わせてn×mのマトリクスを作成する

1組目のアイテムn個を元に別のアイテムを新たに作成し、そのメタデータとして2組目のアイテムの値m個を持たせる。 これにより、2組のアイテムの値を組み合わせた計n×m個のアイテムを作成する。

3列と2行のアイテムからセル6個分のアイテムを作成する
<Project DefaultTargets="Build">
  <!--
    3列分と2行分を含む2組のアイテムから、各セルに対応する6個のアイテムを作成する例

        | A  B  C
      __|_________
      1 | A1 B1 C1
      2 | A2 B2 C2
  -->
  <Target Name="Build">
    <!-- 3列分のアイテム定義 -->
    <ItemGroup>
      <Col Include="A" />
      <Col Include="B" />
      <Col Include="C" />
    </ItemGroup>

    <!-- 2行分のアイテム定義 -->
    <ItemGroup>
      <Row Include="1" />
      <Row Include="2" />
    </ItemGroup>

    <!-- 列Rowと行Colから、それぞれを組み合わせたアイテムTableを作成する -->
    <ItemGroup>
      <!-- Colの内容をアイテムの内容としてTableアイテムを作成する -->
      <Table Include="@(Col)">
        <!-- メタデータとしてRowアイテムの各値を持たせる -->
        <Row>%(Row.Identity)</Row>
      </Table>
      <!--
        ここでは、まず値としてColアイテムの内容=A, B, Cを持つ3つのTableアイテムが作成される。
        次に、それぞれのTableアイテムに対してメタデータRowを定義し、その値としてRowアイテムの内容=1, 2を持たせる。
        結果として、アイテム3組、各アイテムにメタデータ2個となり、その組み合わせから計6個のTableアイテムが作成される。
      -->
    </ItemGroup>

    <!-- アイテム値とメタデータの組み合わせを一覧として表示する -->
    <Message Text="Table: %(Table.Identity)%(Table.Row)" Importance="high" />

    <!-- Tableアイテムの値とメタデータを展開して、1次元のセル一覧となるアイテムCellを作成する -->
    <ItemGroup>
      <Cell Include="%(Table.Identity)%(Table.Row)" />
    </ItemGroup>

    <!-- Cellアイテムの内容を表示する -->
    <Message Text="Cell: @(Cell)" Importance="high" />

    <!-- プロパティTableCellsを作成し、値としてTableCellsアイテムの内容をカンマで区切った文字列を設定する -->
    <PropertyGroup>
      <TableCells>@(Cell, ',')</TableCells>
    </PropertyGroup>

    <!-- TableCellsプロパティに設定された内容を表示する -->
    <Message Text="TableCells: $(TableCells)" Importance="high" />
  </Target>
</Project>
実行結果
>dotnet msbuild -nologo
  Table: A1
  Table: B1
  Table: C1
  Table: A2
  Table: B2
  Table: C2
  Cell: A1;B1;C1;A2;B2;C2
  TableCells: A1,B1,C1,A2,B2,C2

2組のアイテムとメタデータを組み合わせてn×mのマトリクスを作成する

2組のアイテムとそのアイテムが持つメタデータから、それぞれの値を組み合わせた計n×m個のアイテムを作成する。

メタデータを含む場合も、先と同様に組み合わせてマトリクスを作成できる。

3列と3行のアイテムとメタデータから3×3の九九の表を作成する
<Project DefaultTargets="Build">
  <!--
    3×3の九九の表を作成する例

    被乗数の値をもつ3列分と乗数の値を持つ3行分の要素を含む2組のアイテムから、
    各セルの値として被乗数×乗数を乗算した9個の要素を含むアイテムを作成する例

           | A  B  C
           |(1)(2)(3)
      _______________
      1(1) | 1  2  3
      2(2) | 2  4  6
      3(3) | 3  6  9
  -->
  <Target Name="Build">
    <!-- 列のアイテムとメタデータの定義 -->
    <ItemGroup>
      <Col Include="A" Value="1" />
      <Col Include="B" Value="2" />
      <Col Include="C" Value="3" />
    </ItemGroup>

    <!-- 行のアイテムとメタデータの定義 -->
    <ItemGroup>
      <Row Include="1" Value="1" />
      <Row Include="2" Value="2" />
      <Row Include="3" Value="3" />
    </ItemGroup>

    <!-- 列Rowと行Colから、それぞれを組み合わせたアイテムTableを作成する -->
    <ItemGroup>
      <!-- Colの内容をアイテムの内容としてTableアイテムを作成する -->
      <Table Include="@(Col)">
        <!-- メタデータとしてRowアイテムの各値とメタデータの値を持たせる -->
        <RowName>%(Row.Identity)</RowName>
        <RowValue>%(Row.Value)</RowValue>
      </Table>
      <!--
        ここでは、まず値としてA, B, C(Colアイテムの内容)を持つ3つのTableアイテムが作成される。
        このとき、ColアイテムのメタデータもそのままTableアイテムにコピーされる。

        次に、それぞれのTableアイテムに対してメタデータRowとRowValueを定義し、その値としてRowアイテムとメタデータValueの値を持たせる。
        結果として、アイテム3個、各アイテムにメタデータ3組となり、その組み合わせから計9個のTableアイテムが作成される。
      -->
    </ItemGroup>

    <!-- アイテム値とメタデータの組み合わせを一覧として表示する -->
    <Message Text="Table: %(Table.Identity)%(Table.RowName) = %(Table.Value)×%(Table.RowValue)" Importance="high" />

    <!--
      Tableアイテムの値とメタデータを展開して、1次元のセル一覧となるアイテムCellを作成する
      また、セルに対応する値を計算してメタデータValueに値を代入する
    -->
    <ItemGroup>
      <Cell Include="%(Table.Identity)%(Table.RowName)">
        <Value>$([MSBuild]::Multiply(%(Table.Value), %(Table.RowValue)))</Value>
      </Cell>
    </ItemGroup>

    <!-- アイテムCellの内容を一覧表示する -->
    <Message Text="Cell %(Cell.Identity): Value=%(Cell.Value)" Importance="high" />
  </Target>
</Project>
実行結果
>dotnet msbuild -nologo
  Table: A1 = 1×1
  Table: B1 = 2×1
  Table: C1 = 3×1
  Table: A2 = 1×2
  Table: B2 = 2×2
  Table: C2 = 3×2
  Table: A3 = 1×3
  Table: B3 = 2×3
  Table: C3 = 3×3
  Cell A1: Value=1
  Cell B1: Value=2
  Cell C1: Value=3
  Cell A2: Value=2
  Cell B2: Value=4
  Cell C2: Value=6
  Cell A3: Value=3
  Cell B3: Value=6
  Cell C3: Value=9

3組のプロパティ・アイテムから出力パスの組み合わせ一覧を作成する

応用・具体例として、build-configuration\target-framework\assembly-file形式の出力パス一覧を作成する。

この例ではtarget-frameworkのみセミコロン区切りのプロパティTargetFrameworksから取得する場合を考える。

Configuration・TargetFramework・AssemblyFileの3つのアイテムを組み合わせて出力パス一覧を生成する
<Project DefaultTargets="Build">
  <PropertyGroup>
    <TargetFrameworks>net6.0;net48;netstandard2.1</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <BuildConfiguration Include="Debug" />
    <BuildConfiguration Include="Release" />
  </ItemGroup>

  <ItemGroup>
    <OutputAssemblyFile Include="LibA.dll" />
    <OutputAssemblyFile Include="LibB.dll" />
    <OutputAssemblyFile Include="Exe.dll" />
  </ItemGroup>

  <Target Name="Build">
    <!-- TargetFrameworks, BuildConfiguration, OutputAssemblyFileの各値を組み合わせたアイテムOutputFilePathを作成する -->
    <ItemGroup>
      <!-- セミコロン区切りの値を含むプロパティTargetFrameworksから、アイテム_TargetFrameworkを作成する -->
      <_TargetFramework Include="$(TargetFrameworks)" />

      <!-- BuildConfigurationと_TargetFrameworkを組み合わせたマトリクスとなるアイテムを作成する -->
      <_Matrix1 Include="@(BuildConfiguration)">
        <TargetFramework>%(_TargetFramework.Identity)</TargetFramework>
      </_Matrix1>

      <!-- 上記マトリクスを展開して1次元のアイテムに変換する -->
      <_Flatten1 Include="%(_Matrix1.Identity)\%(_Matrix1.TargetFramework)\" />

      <!-- 1次元化したConfigurationとTargetFrameworkのマトリクスと、OutputAssemblyFileを組み合わせたマトリクスとなるアイテムを作成する -->
      <_Matrix2 Include="@(_Flatten1)">
        <OutputAssemblyFile>%(OutputAssemblyFile.Identity)</OutputAssemblyFile>
      </_Matrix2>

      <!-- 上記マトリクスを展開して1次元のアイテムOutputFilePathに変換する -->
      <OutputFilePath Include="%(_Matrix2.Identity)%(_Matrix2.OutputAssemblyFile)" />
    </ItemGroup>

    <Message Text="OutputFilePath: %(OutputFilePath.Identity)" Importance="high" />
  </Target>
</Project>
実行結果
>dotnet msbuild -nologo | sort
  OutputFilePath: Debug\net48\Exe.dll
  OutputFilePath: Debug\net48\LibA.dll
  OutputFilePath: Debug\net48\LibB.dll
  OutputFilePath: Debug\net6.0\Exe.dll
  OutputFilePath: Debug\net6.0\LibA.dll
  OutputFilePath: Debug\net6.0\LibB.dll
  OutputFilePath: Debug\netstandard2.1\Exe.dll
  OutputFilePath: Debug\netstandard2.1\LibA.dll
  OutputFilePath: Debug\netstandard2.1\LibB.dll
  OutputFilePath: Release\net48\Exe.dll
  OutputFilePath: Release\net48\LibA.dll
  OutputFilePath: Release\net48\LibB.dll
  OutputFilePath: Release\net6.0\Exe.dll
  OutputFilePath: Release\net6.0\LibA.dll
  OutputFilePath: Release\net6.0\LibB.dll
  OutputFilePath: Release\netstandard2.1\Exe.dll
  OutputFilePath: Release\netstandard2.1\LibA.dll
  OutputFilePath: Release\netstandard2.1\LibB.dll