views:

533

answers:

2

I have a script that attempts to construct an ItemGroup out of all files in a certain directory while excluding files with certain names (regardless of extension).

The list of files to be excluded initially contains file extensions, and I am using Community Tasks' RegexReplace to replace the extensions with an asterisk. I then use this list in the item's Exclude attribute. For some reason the files do not get excluded properly, even though the list appears to be correct.

To try and find the cause I created a test script (below) which has two tasks: first one initialises two properties with the list of file patterns in two different ways. The second task prints both properties and the files resulting from using both these properties in the Exclude attribute.

The properties' values appear to be identical, however the resulting groups are different. How is this possible?

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="Init;Test" ToolsVersion="3.5">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

  <Target Name="Init">
    <ItemGroup>
      <OriginalFilenames Include="TestDir\SampleProj.exe"/>
      <OriginalFilenames Include="TestDir\SampleLib1.dll"/>
    </ItemGroup>
    <RegexReplace Input="@(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
      <Output TaskParameter="Output" ItemName="PatternedFilenames"/>
    </RegexReplace>
    <PropertyGroup>
      <ExcludeFilesA>TestDir\SampleProj.*;TestDir\SampleLib1.*</ExcludeFilesA>
      <ExcludeFilesB>@(PatternedFilenames)</ExcludeFilesB>
    </PropertyGroup>
  </Target>

  <Target Name="Test">
    <Message Text='ExcludeFilesA: $(ExcludeFilesA)' />
    <Message Text='ExcludeFilesB: $(ExcludeFilesB)' />
    <ItemGroup>
      <AllFiles Include="TestDir\**"/>
      <RemainingFilesA Include="TestDir\**" Exclude="$(ExcludeFilesA)"/>
      <RemainingFilesB Include="TestDir\**" Exclude="$(ExcludeFilesB)"/>
    </ItemGroup>
    <Message Text="&#xA;**AllFiles**&#xA;@(AllFiles, '&#xA;')" />
    <Message Text="&#xA;**PatternedFilenames**&#xA;@(PatternedFilenames, '&#xA;')" />
    <Message Text="&#xA;**RemainingFilesA**&#xA;@(RemainingFilesA, '&#xA;')" />
    <Message Text="&#xA;**RemainingFilesB**&#xA;@(RemainingFilesB, '&#xA;')" />
  </Target>

</Project>

Output (reformatted somewhat for clarity):

ExcludeFilesA: TestDir\SampleProj.*;TestDir\SampleLib1.*
ExcludeFilesB: TestDir\SampleProj.*;TestDir\SampleLib1.*

AllFiles:
  TestDir\SampleLib1.dll
  TestDir\SampleLib1.pdb
  TestDir\SampleLib2.dll
  TestDir\SampleLib2.pdb
  TestDir\SampleProj.exe
  TestDir\SampleProj.pdb

PatternedFilenames:
  TestDir\SampleProj.*
  TestDir\SampleLib1.*

RemainingFilesA:
  TestDir\SampleLib2.dll
  TestDir\SampleLib2.pdb

RemainingFilesB:
  TestDir\SampleLib1.dll
  TestDir\SampleLib1.pdb
  TestDir\SampleLib2.dll
  TestDir\SampleLib2.pdb
  TestDir\SampleProj.exe
  TestDir\SampleProj.pdb

Observe that both ExcludeFilesA and ExcludeFilesB look identical, but the resulting groups RemainingFilesA and RemainingFilesB differ.

Ultimately I want to obtain the list RemainingFilesA using the pattern generated the same way ExcludeFilesB is generated. Can you suggest a way, or do I have to completely rethink my approach?

A: 

ItemGroups need to be evaluated before targets execution, and the PatternedFilenames ItemGroup is being created on the fly within its target container. You could workaround this using the CreateItem task, which will ensure the PatternedFilenames scope throughout the execution:

<RegexReplace Input="@(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
  <Output TaskParameter="Output" ItemName="PatternedFilenames_tmp"/>
</RegexReplace>
<CreateItem Include="@(PatternedFilenames_tmp)">
  <Output TaskParameter="Include" ItemName="PatternedFilenames"/>
</CreateItem>
KMoraz
Oh... thanks for the workaround, I will try that. Could you possibly explain the order in which things occur in more detail please? Clearly `ExcludeFilesA` is initialised before the ItemGroup `RemainingFilesA` is created; how come the same doesn't happen for `RemainingFilesB`? It almost looks like the order is: 1. "Init" finishes; 2. ItemGroups from "Test" evaluated; 3. RegexReplace evaluated; 4. "Test" begins and prints messages. But surely that can't be so?...
romkyns
Read "Property and Item Evaluation Order" section here:http://msdn.microsoft.com/en-us/library/dd997067(VS.100).aspxThis post is related to your issue:http://elegantcode.com/2006/12/11/msbuild-items-run-time-evaluation-behavior/
KMoraz
+1  A: 

The true cause of this was revealed accidentally when a custom task threw an exception.

The actual value of ExcludeFilesA is TestDir\SampleProj.*;TestDir\SampleLib1.* like one might expect. However the actual value of ExcludeFilesB is TestDir\SampleProj.%2a;TestDir\SampleLib1.%2a.

Presumably Message unescapes the string before using it, but Include and Exclude do not. That would explain why the strings look the same but behave differently.

Incidentally, the execution order doesn't seem to have anything to do with this, and I'm pretty sure (following extensive experimentation) that everything gets executed and evaluated exactly in the order in which it appears in this script.

romkyns
Great catch... nice one!
KMoraz