Search code examples
regexmsbuildmsbuild-4.0msbuild-propertygroup

Why is $([System.Text.RegularExpressions.Regex]::IsMatch()) evaluated once in ItemGroupDefinition?


So fiddling with MSBuild tasks, and I am finding that a Regex metadata property is evaluated once rather than per item.

For example

<!-- 
  actual items, we use standard project reference items and extend via 
  ItemDefinitionGroup. add project references through IDE to extend 
  coverage
-->
<ItemGroup>
  <ProjectReference Include="..\Example.UnitTests-x86\Example.UnitTests-x86.csproj">
    <Project>{7e854803-007c-4800-80f9-be908655229d}</Project>
    <Name>Example.UnitTests-x86</Name>
  </ProjectReference>
  <ProjectReference Include="..\Example.UnitTests\Example.UnitTests.csproj">
    <Project>{eaac5f22-bfb8-4df7-a711-126907831a0f}</Project>
    <Name>Example.UnitTests</Name>
  </ProjectReference>
</ItemGroup>

<!-- additional item properties, defined with respect to item declaring it -->
<ItemDefinitionGroup>
  <ProjectReference>
    <Isx86>
      $([System.Text.RegularExpressions.Regex]::IsMatch(%(Filename), '.*x86*'))
    </Isx86>
  </ProjectReference>
</ItemDefinitionGroup>

<!-- additional task target, invoke both x64 and x86 tasks here -->
<Target Name="AdditionalTasks">
  <Message 
    Text="%(ProjectReference.Filename) Isx86 '%(Isx86)' Inline 
    '$([System.Text.RegularExpressions.Regex]::IsMatch(%(Filename), '.*x86*'))'" 
    Importance="high" />
</Target>

Produces this output

Example.UnitTests-x86 Isx86 'False' Inline 'True'
Example.UnitTests Isx86 'False' Inline 'False'

Solution

  • Problem

    The documentation ItemDefinitionGroup Element (MSBuild) refers to Item Definitions which has a note which states:

    Item metadata from an ItemGroup is not useful in an ItemDefinitionGroup metadata declaration because ItemDefinitionGroup elements are processed before ItemGroup elements.

    This means that your %(Filename) metadata reference in the <ItemDefinitionGroup/> cannot be expanded. You can see this yourself with this following snippet. In the snippet, the .ToString() call converts the result to a string object, preventing MSBuild from further expanding it. (If I had left .ToString() out, MSBuild would have been left with a System.RegularExpressions.Match object. Leaving the metadata definition as a Match object seems to delay expansion to string until the <Message/>’s Text is evaluated, causing MSBuild to do a string expansion pass over it, resulting in %(Identity) being expanded when you might not expect it to be. This delayed expansion is also demonstrated in the following snippet.)

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <ItemGroup>
        <MyItem Include="MyItem’s value" />
        <MyItem Include="MyItem’s second value" />
      </ItemGroup>
      <ItemDefinitionGroup>
        <MyItem>
          <ItemDefinitionMatchedText>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), ".*").get_Groups().get_Item(0).ToString())</ItemDefinitionMatchedText>
          <ItemDefinitionMatchedTextDelayed>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), ".*").get_Groups().get_Item(0))</ItemDefinitionMatchedTextDelayed>
        </MyItem>
      </ItemDefinitionGroup>
      <Target Name="Build" Outputs="%(MyItem.Identity)">
        <Message Text="Data being matched against for item “%(MyItem.Identity)” is “%(ItemDefinitionMatchedText)”"/>
        <Message Text="Delayed string conversion causes delayed expansion: “%(MyItem.ItemDefinitionMatchedTextDelayed)”"/>
      </Target>
    </Project>
    

    Output:

    Build:
      Data being matched against for item “MyItem’s value” is “%(Identity)”
      Delayed string conversion causes delayed expansion: “MyItem’s value”
    Build:
      Data being matched against for item “MyItem’s second value” is “%(Identity)”
      Delayed string conversion causes delayed expansion: “MyItem’s second value”
    

    The note from MSBuild’s documentation indicates that Item metadata is not available in <ItemDefinitionGroup/>. It appears, from using Regex.Match(), that the property function expansion is treating %(Identity) or, in your case, %(Filename) as an unquoted free-form string. Thus, since you invoke Regex.IsMatch() with the same syntax as I invoke Regex.Match() in the above example, it follows that your Regex.IsMatch() is trying to check if the literal string %(Filename) contains x8 (optionally followed by any number of 6s whose presence or absence will not affect the match).

    Solution

    The only way I know of to dynamically calculate an Item’s metadata based on existing metadata is to create a new Item derived from the original item. For example, to create a list of <ProjectReference/>s with the metadata you need, you could use the following item definition to produce ProjectReferenceWithArch Items. I chose to use property function syntax after using [MSBuild]::ValueOrDefault() to turn it into a string in property expansion context so that I could use String.Contains() (Regex is a bit overkill for your case, but you could easily modify this to match against a regular expression if needed). I updated your <Message/> to print out the Project metadata to demonstrate that this metadata survives into the new Item’s definition.

    <ItemGroup>
      <ProjectReferenceWithArch Include="@(ProjectReference)">
        <Isx86>$([MSBuild]::ValueOrDefault('%(Filename)', '').Contains('x86'))</Isx86>
      </ProjectReferenceWithArch>
    </ItemGroup>
    <Target Name="AdditionalTasks">
      <Message 
        Text="%(ProjectReferenceWithArch.Filename) Isx86 '%(Isx86)' Inline '$([System.Text.RegularExpressions.Regex]::IsMatch(%(Filename), '.*x86*'))' Project '%(Project)'" 
        Importance="high" />
    </Target>
    

    Output:

    AdditionalTasks:
      Example.UnitTests-x86 Isx86 'True' Inline 'True' Project '{7e854803-007c-4800-80f9-be908655229d}'
      Example.UnitTests Isx86 'False' Inline 'False' Project '{eaac5f22-bfb8-4df7-a711-126907831a0f}'
    

    Alternative Solution (EDIT)

    I just noticed that you can dynamically update an Item’s metadata if you do so in a <Target/>. The syntax looks like this:

    <Target Name="AdditionalTasks">
      <ItemGroup>
        <ProjectReference>
          <Isx86>$([MSBuild]::ValueOrDefault('%(Filename)', '').Contains('x86'))</Isx86>
        </ProjectReference>
      </ItemGroup>
    </Target>
    

    Just ensure that this target runs before the target in which you need to check the Isx86 metadata or that the <ItemGroup/> appears before the metadata is needed in your <Target/>.