根据项元数据分批运行任务或目标

MSBuild 根据项元数据将项列表划分为不同的类别或批,并对每批一次运行一个目标或任务。

任务批处理

借助任务批处理,可以用某种方式将项列表划分为不同批次,同时将每个批次单独地传递到任务中来简化项目文件。 这意味着项目文件只需要声明一次任务及其特性就可多次运行该任务。

通过在任务特性之一中使用 %(ItemMetaDataName) 表示法,指定希望 MSBuild 对任务执行批处理。 以下示例基于 Color 项元数据值将 Example 项列表划分为几个批次,并将每个批次单独地传递到 MyTask 任务。

注意

如果未在任务特性的其他位置引用项列表,或者如果元数据名称可能不明确,则可以使用 %(<ItemCollection.ItemMetaDataName>) 表示法完全限定要用于进行批处理的项元数据值。

<Project
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <ItemGroup>
        <Example Include="Item1">
            <Color>Blue</Color>
        </Example>
        <Example Include="Item2">
            <Color>Red</Color>
        </Example>
    </ItemGroup>

    <Target Name="RunMyTask">
        <MyTask
            Sources = "@(Example)"
            Output = "%(Color)\MyFile.txt"/>
    </Target>

</Project>

有关更具体的批处理示例,请参阅任务批处理中的项元数据

目标批处理

MSBuild 先检查目标的输入和输出是否是最新的,再运行目标。 如果输入和输出都是最新的,则跳过目标。 如果目标内的任务使用批处理,则 MSBuild 需要确定每批项的输入和输出是否是最新的。 否则,目标将在每次命中时执行。

以下示例展示了包含使用 %(ItemMetadataName) 表示法的 Outputs 特性的 Target 元素。 MSBuild 会基于 Color 项元数据将 Example 项列表划分为几个批次,并分析每个批次的输出文件的时间戳。 如果批处理的输出不是最新的,则运行目标。 否则,跳过该目标。

<Project
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <ItemGroup>
        <Example Include="Item1">
            <Color>Blue</Color>
        </Example>
        <Example Include="Item2">
            <Color>Red</Color>
        </Example>
    </ItemGroup>

    <Target Name="RunMyTask"
        Inputs="@(Example)"
        Outputs="%(Color)\MyFile.txt">
        <MyTask
            Sources = "@(Example)"
            Output = "%(Color)\MyFile.txt"/>
    </Target>

</Project>

有关目标批处理的另一个示例,请参阅目标批处理中的项元数据

项和属性突变

此部分介绍了在使用目标批处理或任务批处理时,如何理解更改属性和/或项元数据的影响。

由于目标批处理和任务批处理是两种不同的 MSBuild 操作,因此请务必确切了解在每种情况下 MSBuild 使用哪种形式的批处理。 如果批处理语法 %(ItemMetadataName) 出现在目标内的任务中,但没有出现在目标特性中,则 MSBuild 使用任务批处理。 指定目标批处理的唯一方法是,对目标特性(通常是 Outputs 特性)使用批处理语法。

对于目标批处理和任务批处理,可以认为批处理是独立运行的。 所有批处理都从复制处于相同初始状态的属性和项元数据值开始。 批处理执行期间的任何属性值突变对其他批处理都不可见。 请看下面的示例:

  <ItemGroup>
    <Thing Include="2" Color="blue" />
    <Thing Include="1" Color="red" />
  </ItemGroup>

  <Target Name="DemoIndependentBatches">
    <ItemGroup>
      <Thing Condition=" '%(Color)' == 'blue' ">
        <Color>red</Color>
        <NeededColorChange>true</NeededColorChange>
      </Thing>
    </ItemGroup>
    <Message Importance="high"
             Text="Things: @(Thing->'%(Identity) is %(Color); needed change=%(NeededColorChange)')"/>
  </Target>

输出为:

Target DemoIndependentBatches:
  Things: 2 is red; needed change=true;1 is red; needed change=

目标中的 ItemGroup 是一项隐式任务,借助 Condition 特性中的 %(Color),执行的是任务批处理。 有两个批处理:一个用于红色,另一个用于蓝色。 只有在 %(Color) 元数据为 blue 时,才设置属性 %(NeededColorChange),并且此设置只影响在运行蓝色批处理时与条件匹配的单个项。 尽管有 %(ItemMetadataName) 语法,Message 任务的 Text 特性也不会触发批处理,因为是在项转换内使用了它。

批处理单独运行,而不是并行运行。 当你访问在批处理执行中更改的元数据值时,这就会产生差异。 如果设置基于批处理执行中的某元数据的属性,则此属性接受最后设置的值:

   <PropertyGroup>
       <SomeProperty>%(SomeItem.MetadataValue)</SomeProperty>
   </PropertyGroup>

在批处理执行后,此属性保留 %(MetadataValue) 的最终值。

尽管批处理是独立运行的,但也请务必注意目标批处理与任务批处理之间的区别,并知道哪种类型适用于你的情况。 请参阅以下示例,以更好地理解这种区别的重要性。

任务可以是隐式的,而不是显式的,当对隐式任务执行任务批处理时,这可能会令人困惑。 当 Target 中出现 PropertyGroupItemGroup 元素时,组中的每个属性声明都会得到隐式处理,有点像独立的 CreatePropertyCreateItem 任务。 也就是说,对目标执行批处理和不执行批处理(即 Outputs 特性中缺少 %(ItemMetadataName) 语法)时的行为是不同的。 如果对目标执行批处理,ItemGroup 每目标执行一次,但如果对目标不执行批处理,就会使用任务批处理对 CreateItemCreateProperty 任务的隐式等效项进行批处理,所以目标只执行一次,并且组中的每个项或属性都使用任务批处理分别进行批处理。

下面的示例演示了在元数据突变时的目标批处理与任务批处理。 假设你在文件夹 A 和 B 中有一些文件:

A\1.stub
B\2.stub
B\3.stub

现在看看这两个类似项目的输出。

    <ItemGroup>
      <StubFiles Include="$(MSBuildThisFileDirectory)**\*.stub"/>

      <StubDirs Include="@(StubFiles->'%(RecursiveDir)')"/>
    </ItemGroup>

    <Target Name="Test1" AfterTargets="Build" Outputs="%(StubDirs.Identity)">
      <PropertyGroup>
        <ComponentDir>%(StubDirs.Identity)</ComponentDir>
        <ComponentName>$(ComponentDir.TrimEnd('\'))</ComponentName>
      </PropertyGroup>

      <Message Text=">> %(StubDirs.Identity) '$(ComponentDir)' '$(ComponentName)'"/>
    </Target>

输出为:

Test1:
  >> A\ 'A\' 'A'
Test1:
  >> B\ 'B\' 'B'

现在删除指定了目标批处理的 Outputs 特性。

    <ItemGroup>
      <StubFiles Include="$(MSBuildThisFileDirectory)**\*.stub"/>

      <StubDirs Include="@(StubFiles->'%(RecursiveDir)')"/>
    </ItemGroup>

    <Target Name="Test1" AfterTargets="Build">
      <PropertyGroup>
        <ComponentDir>%(StubDirs.Identity)</ComponentDir>
        <ComponentName>$(ComponentDir.TrimEnd('\'))</ComponentName>
      </PropertyGroup>

      <Message Text=">> %(StubDirs.Identity) '$(ComponentDir)' '$(ComponentName)'"/>
    </Target>

输出为:

Test1:
  >> A\ 'B\' 'B'
  >> B\ 'B\' 'B'

我们注意到,标题 Test1 只打印了一次,但是在上一个示例中,它打印了两次。 也就是说,目标未进行批处理。 因此,输出是不同的,且令人困惑。

原因在于,在使用目标批处理时,每个目标批处理都使用自己的独立副本(其中包含所有属性和项)来执行目标中的所有内容,但如果省略 Outputs 特性,属性组中的各个行就会被视为可能已批处理的不同任务。 在此示例中,批处理的是 ComponentDir 任务(它使用 %(ItemMetadataName) 语法),这样在 ComponentName 行执行时,ComponentDir 行的两个批处理都已完成,并且运行的第二个批处理确定了第二行中所示的值。

使用元数据的属性函数

可通过包括元数据的属性函数控制批处理。 例如,应用于对象的

$([System.IO.Path]::Combine($(RootPath),%(Compile.Identity)))

使用 Combine 合并根文件夹路径与编译项路径。

属性函数可能不会出现在元数据值内。 例如,应用于对象的

%(Compile.FullPath.Substring(0,3))

是不允许的。

有关属性函数详细信息,请参阅属性函数

对自引用元数据进行的项批处理

请考虑从项定义中引用元数据的以下示例:

<ItemGroup>
  <i Include='a/b.txt' MyPath='%(Filename)%(Extension)' />
  <i Include='c/d.txt' MyPath='%(Filename)%(Extension)' />
  <i Include='g/h.txt' MyPath='%(Filename)%(Extension)' />
</ItemGroup>

需要注意的是,在目标外部和目标内部进行定义时的行为会有所不同。

目标外部的项自引用元数据

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <i Include='a/b.txt' MyPath='%(Filename)%(Extension)' />
    <i Include='c/d.txt' MyPath='%(Filename)%(Extension)' />
    <i Include='g/h.txt' MyPath='%(Filename)%(Extension)' />
  </ItemGroup>
  <Target Name='ItemOutside'>
    <Message Text="i=[@(i)]" Importance='High' />
    <Message Text="i->MyPath=[@(i->'%(MyPath)')]" Importance='High' />
  </Target>
</Project>

系统按项实例解析元数据引用(不受任何先前定义或创建的项实例影响),产生预期的输出:

  i=[a/b.txt;c/d.txt;g/h.txt]
  i->MyPath=[b.txt;d.txt;h.txt]

目标内部的项自引用元数据

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name='ItemInside'>  
    <ItemGroup>
      <i Include='a/b.txt' MyPath='%(Filename)%(Extension)' />
      <i Include='c/d.txt' MyPath='%(Filename)%(Extension)' />
      <i Include='g/h.txt' MyPath='%(Filename)%(Extension)' />
    </ItemGroup>
    <Message Text="i=[@(i)]" Importance='High' />
    <Message Text="i->MyPath=[@(i->'%(MyPath)')]" Importance='High' />
  </Target>
</Project>

在这种情况下,元数据引用会产生批处理,从而可能生成异常或意外的输出:

  i=[a/b.txt;c/d.txt;g/h.txt;g/h.txt]
  i->MyPath=[;b.txt;b.txt;d.txt]

对于每个项实例,引擎将应用所有预先存在的项实例的元数据(这就是第一个项的 MyPath 为空且第二个项包含 b.txt 的原因)。 如果存在更多预先存在的实例,这会导致当前项实例倍增(这就是 g/h.txt 项实例在结果列表中出现两次的原因)。

为了明确告知有关该行为(可能是无意识的)的信息,MSBuild 的后续版本会发出消息 MSB4120

proj.proj(4,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Filename' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
proj.proj(4,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Extension' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
proj.proj(5,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Filename' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
proj.proj(5,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Extension' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
proj.proj(6,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Filename' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
proj.proj(6,11):  message : MSB4120: Item 'i' definition within target is referencing self via metadata 'Extension' (qualified or unqualified). This can lead to unintended expansion and cross-applying of pre-existing items. More info: https://aka.ms/msbuild/metadata-self-ref
  i=[a/b.txt;c/d.txt;g/h.txt;g/h.txt]
  i->MyPath=[;b.txt;b.txt;d.txt]

如果自引用是有意为之,那么根据实际方案和确切需求,你有几个选择:

使用帮助程序项和转换操作

如果要防止元数据引用引发的批处理行为,可以定义单独的项,然后利用 转换操作创建具有所需元数据的项实例:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name='ItemOutside'>  
    <ItemGroup>
      <j Include='a/b.txt' />
      <j Include='c/*' />
      <i Include='@(j)' MyPath="%(Filename)%(Extension)" />
    </ItemGroup>
    <Message Text="i=[@(i)]" Importance='High' />
    <Message Text="i->MyPath=[@(i->'%(MyPath)')]" Importance='High' />
  </Target>
</Project>