Пакетная обработка MSBuild

MSBuild разделяет списки элементов на разные категории или пакеты на основе метаданных элементов и поочередно выполняет целевой объект или задачу с использованием каждого пакета.

Пакетная обработка задач

Пакетная обработка задач упрощает работу с файлами проекта, позволяя разделить списки элементов на различные пакеты и передать задаче каждый из этих пакетов отдельно. Это означает, что задачу и ее атрибуты для файла проекта можно объявить всего один раз, а запускать ее можно многократно.

Укажите, что MSBuild нужно выполнить пакетную обработку с помощью задачи, используя нотацию %(ItemMetaDataName) в одном из атрибутов задачи. Следующий пример разделяет элемент Example на пакеты на основе значения метаданных элемента Color и передает их в задачу MyTask по отдельности.

Примечание.

Если вы не ссылаетесь на список элементов в других атрибутах задачи или имя метаданных может быть неоднозначным, можно использовать нотацию %(<коллекция_элементов.имя_метаданных_элемента>), чтобы полностью определить значение метаданных элемента для использования при пакетной обработке.

<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 нужно определить актуальность входных и выходных данных для каждого пакета элементов. В противном случае целевой объект выполняется при каждой его активации.

В следующем примере показан элемент Target, содержащий атрибут Outputs с нотацией %(ItemMetadataName). MSBuild разделит список элементов Example на пакеты с учетом метаданных элементов Color и проанализирует метки времени выходных файлов для каждого пакета. Если выходные данные пакета не актуальны, выполняется целевой объект. В противном случае он пропускается.

<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 в целевом объекте неявно является задачей, а пакетная обработка задачи выполняется, начиная с элемента %(Color) в атрибуте Condition. Существует два пакета: один для красного, а другой — для синего содержимого. Свойство %(NeededColorChange) задается только в том случае, если метаданные %(Color) обозначены синим. Параметр влияет только на отдельный элемент, соответствующий условию при выполнении синего пакета. Атрибут Text задачи Message не запускает пакетную обработку, несмотря на синтаксис %(ItemMetadataName) (из-за использования внутри преобразования элемента).

Пакеты выполняются независимо, но не параллельно. Это важно, когда вы получаете доступ к значениям метаданных, которые изменяются при пакетном выполнении. Если вы задали свойство на основе определенных метаданных в пакетном выполнении, свойство принимает последнее заданное значение:

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

После выполнения пакета свойство сохраняет конечное значение %(MetadataValue).

Хотя пакеты выполняются независимо друг от друга, важно учитывать разницу между пакетной обработкой целевых объектов и задач, а также определить, какой тип применяется к вашей ситуации. Рассмотрим следующий пример, чтобы лучше понять важность этого различия.

Задачи могут быть неявными (в отличие от явных задач), что может запутать при выполнении их пакетной обработки. Если элемент PropertyGroup или ItemGroup появляется в Target, каждое объявление свойства в группе неявно обрабатывается как отдельная задача CreateProperty или CreateItem. Это означает, что поведение отличается при наличии пакетной обработки целевого объекта и ее отсутствии (то есть, если в атрибуте Outputs отсутствует синтаксис %(ItemMetadataName)). При пакетной обработке целевого объекта элемент 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 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 экземпляр элемента происходит дважды в результирующем списке).

Чтобы явно сообщить об этом, возможно, непреднамеренное поведение, более поздние версии сообщения MSB4120о проблеме MSBuild:

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>

См. также