MSBuild 根据项元数据将项列表划分为不同的类别或批,并为每个批运行一次目标或任务。
任务分批
任务批处理允许你简化项目文件,通过将项目列表划分成不同的批次,并将每个批次分别传递到各个任务中。 批处理意味着项目文件只需要将任务及其属性声明一次,即使它可以多次运行。
您可以通过在任务属性中使用 %(ItemMetaDataName) 表示法,来指定希望 MSBuild 对任务进行批处理。 以下示例根据Example项元数据值将项列表拆分Color为批,并将每个批分别传递给MyTask任务。
注释
如果不在任务属性的其他部分引用项目列表,或者元数据名称可能不明确,则可以使用 %(<ItemCollection.ItemMetaDataName>)语法来确保完全限定要用于批处理的项元数据值。
<Project>
<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 需要确定每个批项目的输入和输出是否为最新。 否则,每次命中目标时都会执行。
以下示例显示了一个 Target 元素,其中包含一个带有 %(ItemMetadataName) 表示法的 Outputs 属性。 MSBuild 根据Color项元数据将Example项列表划分为多个批处理,并分析每个批处理的输出文件的时间戳。 如果批次输出不是最新的,将运行目标。 否则,将跳过目标。
<Project>
<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)来执行任务批处理。 有两批:一个用于红色,另一个用于蓝色。 仅当%(NeededColorChange)元数据为蓝色时设置该属性%(Color),并且该设置仅影响运行蓝色批处理时与条件匹配的单个项。 尽管MessageText语法如此,但任务%(ItemMetadataName)的属性不会触发批处理,因为它在项转换中使用。
批处理独立运行,但不并行运行。 当你访问在批处理执行中更改的元数据值时,这会产生差异。 如果在批处理执行中基于某些元数据设置属性,该属性将采用最后设置的值。
<PropertyGroup>
<SomeProperty>%(SomeItem.MetadataValue)</SomeProperty>
</PropertyGroup>
批处理执行后,该属性将保留最终值 %(MetadataValue)。
尽管批处理独立运行,但请务必考虑目标批处理和任务批处理之间的差异,并知道哪种类型适用于你的情况。 请考虑以下示例,以更好地了解此区别的重要性。
任务可以是隐式任务,而不是显式任务,当任务批处理与隐式任务一起发生时,这可能会造成混淆。 当某个PropertyGroup或ItemGroup元素出现在Target中时,组中的每个属性声明被视作一个单独的CreateProperty或CreateItem任务。 此行为意味着在对目标进行批处理时生成执行不同,而不是当目标未批处理(即,当目标缺少 %(ItemMetadataName) 属性中的 Outputs 语法时)。 当目标被批处理时,ItemGroup 对每个目标执行一次;当目标未被批处理时,CreateItem 或 CreateProperty 任务的隐式等效项将通过任务批处理进行处理,因此目标只执行一次,并且组中的每个项或属性都通过任务批处理分别批处理。
以下示例演示了在元数据发生可变的情况下的目标批处理与任务批处理。 请考虑一种包含 一些文件的文件夹 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>
<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>
<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>
<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>