Элемент MSBuild нельзя использовать в задаче MSBuild, ошибка MSB4012

У меня есть следующий файл проекта MSBuild:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Deploy" ToolsVersion="4.0">
  <ItemGroup>
    <Base Include="$(MSBuildProjectDirectory)\.." />
  </ItemGroup>

  <PropertyGroup>
    <BaseDirectory>@(Base->'%(FullPath)')</BaseDirectory>
    <DeployDirectory>$(BaseDirectory)\Deploy</DeployDirectory>
    <Configuration>Release</Configuration>
  </PropertyGroup>

  <Target Name="Deploy" DependsOnTargets="Hello;Clean;Build" />

  <Target Name="Hello">
    <Message Text="Hello world. BaseDirectory=$(BaseDirectory), DeployDirectory=$(DeployDirectory)" />
  </Target>

  <Target Name="Clean">
    <RemoveDir Directories="$(DeployDirectory)" />
  </Target>

  <Target Name="Build">
    <MSBuild Projects="$(BaseDirectory)\DebugConsoleApp\DebugConsoleApp.csproj" Properties="Configuration=$(Configuration);OutputPath=$(DeployDirectory)" ContinueOnError="false" />
  </Target>

</Project>

И когда я запускаю его, я получаю сообщение об ошибке:

C:\Repositories\Project\Build\Build.proj(22,16): ошибка MSB4012: выражение "@(Base->'%(FullPath)')\Deploy" нельзя использовать в этом контексте. Списки элементов нельзя объединять с другими строками, где ожидается список элементов. Используйте точку с запятой для разделения нескольких списков элементов.

Почему я получаю эту ошибку и как ее избежать? Я использую элемент Base в ItemGroup, потому что мне нужно избавиться от .. в пути, а Items позволить сделать это через метаданные %FullPath. Если я использую только PropertyGroup то все работает нормально, но у меня эта .. во всех путях.


person Dmitrii Lobanov    schedule 16.04.2011    source источник


Ответы (2)


трудно сказать точно, что происходит под капотом. Я не состою в MSBuild, поэтому я лишь приблизительно знаком с фактической реализацией. Нам понадобится разработчик MSBuild, чтобы дать 100% правильный ответ. Но вот что, как я предполагаю, происходит (читай: остальная часть этого содержит спекуляции с моей стороны).

Внутри вы нацеливаетесь, когда используете оператор

Projects="$(BaseDirectory)\DebugConsoleApp\DebugConsoleApp.csproj"

MSBuild замечает, что используется расширение свойства $(BaseDirectory) и что тип параметра для проектов в MSBuild — это массив. Также MSBuild замечает, что BaseDirectory — это свойство, содержащее элемент. Эти свойства не ведут себя как обычные свойства. Вы можете думать о них как о «виртуальных свойствах» (да, я только что придумал этот термин). Когда эти свойства используются вместо поиска значения, происходит встроенная замена. Таким образом, ваш атрибут Projects изменится на:

Projects="@(Base->'%(FullPath)')\DebugConsoleApp\DebugConsoleApp.csproj" 

Поскольку Projects является массивом, MSBuild попытается выполнить преобразование предоставленного выражения. Поскольку это недопустимое преобразование, возникает ошибка. Какую ошибку вы получаете.

Теперь, чтобы обойти это, вы можете изменить цель сборки, чтобы она выглядела следующим образом:

<Target Name="Build">
  <PropertyGroup>
    <_BaseDir>$(BaseDirectory)</_BaseDir>
    <_DeployDir>@(Base->'%(FullPath)')</_DeployDir>
  </PropertyGroup>

  <Message Text="_BaseDir: $(_BaseDir)"/>
  <Message Text="DeployDirectory: $(DeployDirectory)"/>

  <MSBuild Projects="$(_BaseDir)\DebugConsoleApp\DebugConsoleApp.csproj"
            Properties="Configuration=$(Configuration);OutputPath=$(_Tmp2)"
            ContinueOnError="false" />
  <!--<MSBuild Projects="$(BaseDirectory)\DebugConsoleApp\DebugConsoleApp.csproj" 
            Properties="Configuration=$(Configuration);OutputPath=$(DeployDirectory)" 
            ContinueOnError="false" />-->
</Target>

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

Теперь перейдем к вашему вопросу: "Почему задача с сообщением работает, чёрт возьми?!!!" Внутри цели Hello у вас есть следующее:

<Message Text="Hello world. BaseDirectory=$(BaseDirectory), DeployDirectory=$(DeployDirectory)" />

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

<Message Text="Hello world. BaseDirectory=@(Base->'%(FullPath)'), DeployDirectory=@(Base->'%(FullPath)')\Deploy" />

Хорошо, держи эту мысль.

Свойство Text в задаче MSBuild определяется как строка, представляющая собой скалярное значение. Если вы помните, свойство Projects в задаче MSBuild определяется как ITaskItem[], так как это массив и его векторное значение. Когда @(...) находится в свойстве векторных значений, все выражение используется как преобразование элемента. В этом случае оператор @(Base->'%(FullPath)')\DebugConsoleApp\DebugConsoleApp.csproj не является допустимым выражением преобразования. Когда '@(..)' находится внутри объявления свойства скалярных значений, значения выравниваются в строку. Таким образом, каждый экземпляр '@(...)' обрабатывается и сводится к одному строковому значению. Если значений несколько, используются разделители.

Так что, надеюсь, это объясняет поведение, которое вы видите, и на самом деле это может быть ошибка. Вы можете зарегистрировать его на странице http://connect.microsoft.com/, и команда MSBuild проверит его.

Подробнее о виртуальных свойствах Ранее я упоминал, что эти виртуальные свойства не ведут себя как обычные свойства в том смысле, что значение не ищется, а вместо этого использование $(...) заменяется на выражение свойств. Не верьте мне на слово, посмотрите сами. Вот пример файла, который я создал

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <ItemGroup>
    <MyItem Include="C:\temp\01.txt"></MyItem>
  </ItemGroup>

  <PropertyGroup>
    <MyProperty>@(MyItem->'%(FullPath)')</MyProperty>
  </PropertyGroup>

  <Target Name="Demo">
    <Message Text="MyProperty: $(MyProperty)" />
    <!-- Add to the item -->
    <ItemGroup>
      <MyItem Include="C:\temp\01.txt"></MyItem>
    </ItemGroup>
    <Message Text="MyProperty: $(MyProperty)" />
  </Target>

</Project>

Здесь у меня объявлен список элементов MyItem и зависимое свойство MyProperty. Внутри демонстрационной цели я печатаю значение для MyProperty, затем добавляю другое значение в список элементов MyItem и снова распечатываю значение для MyProperty. Вот результат.

PS C:\temp\MSBuild\SO> msbuild .\Build.proj /nologo
Build started 4/26/2011 10:17:08 PM.
Project "C:\temp\MSBuild\SO\Build.proj" on node 1 (default targets).
First:
  MyProperty: C:\temp\01.txt
  MyProperty: C:\temp\01.txt;C:\temp\01.txt
Done Building Project "C:\temp\MSBuild\SO\Build.proj" (default targets).

Как видите, он ведет себя именно так, как я сказал.

person Sayed Ibrahim Hashimi    schedule 27.04.2011

Вы боретесь с оценочным заказом. Переместите объявление группы свойств в цель «Hello», и оно будет работать так, как вы ожидаете. Еще лучше переместите его в его собственную цель и установите эту цель в любых DependsOnTargets для других целей, которые требуют выполнения оценки перед их выполнением, или, наоборот, установите эти цели как «BeforeTargets» для вашей новой цели.

(редактировать)

Это будет работать для всех целей:

<ItemGroup>
  <Base Include="$(MSBuildProjectDirectory)\.." />
</ItemGroup>

<Target Name="Deploy" DependsOnTargets="Hello;Clean;Build" />

<Target Name="CalcProps">
  <PropertyGroup>
    <BaseDirectory>@(Base->'%(FullPath)')</BaseDirectory>
    <DeployDirectory>$(BaseDirectory)\Deploy</DeployDirectory>
    <Configuration>Release</Configuration>
  </PropertyGroup>
</Target>

<Target Name="Hello" DependsOnTargets="CalcProps">
  <Message
    Text="Hello world. BaseDirectory=$(BaseDirectory), DeployDirectory=$(DeployDirectory)"
    />
</Target>

<Target Name="Clean" DependsOnTargets="CalcProps">
  <RemoveDir Directories="$(DeployDirectory)" />
</Target>

<Target Name="Build" DependsOnTargets="CalcProps">
  <MSBuild 
    Projects="$(BaseDirectory)\DebugConsoleApp\DebugConsoleApp.csproj" 
    Properties="Configuration=$(Configuration);OutputPath=$(DeployDirectory)" 
    ContinueOnError="false"
    />  
</Target>

Я бы предположил, что оценка аргумента Projects для задачи MSBuild, поскольку он имеет тип ITaskItem[], может использовать невычисленную строку в $(BaseDirectory), и поскольку это преобразование элемента, возникает ошибка, поскольку в случай, когда преобразуемый элемент имеет более одного члена (хотя в данном случае это не так). Использование вами одного и того же свойства в задаче «Сообщение» передается аргументу типа System.String, который может иметь другую последовательность вычислений.

person Brian Kretzler    schedule 16.04.2011
comment
Но цель Hello выводит все правильно, я вижу на экране именно то, что ожидаю увидеть. Почему это работает в цели Hello и вызывает ошибку в цели сборки? - person Dmitrii Lobanov; 17.04.2011
comment
Спасибо, ваше предложение было верным, оно работает. Хотя я его немного улучшил: нет необходимости вычислять свойства перед каждой задачей, достаточно сделать это только один раз для каждой корневой задачи, т.е. если я буду вызывать только Deploy target, то CalcProps можно поставить в DependsOnTargets атрибут из Deploy задачи. Другие задачи вызываются из задачи Deploy, а не напрямую из командной строки. - person Dmitrii Lobanov; 17.04.2011
comment
На самом деле, как я это написал, он будет вычислять свойства только один раз. MSBuild не будет запускать цель дважды, она только гарантирует, что она будет запущена один раз, прежде чем какая-либо зависимая цель будет запущена первой. Вы можете проверить это, поместив задачу Message в цель CalcProps, а затем вызвав /t:Deploy, сообщение появится только один раз. - person Brian Kretzler; 18.04.2011
comment
Или, моя забота заключалась не в многократной оценке, меня скорее беспокоила необходимость указывать цель несколько раз, я бы хотел вообще не указывать ее или, по крайней мере, сделать ее как можно более редкой. - person Dmitrii Lobanov; 18.04.2011
comment
В конце концов, я использовал атрибут InitialTargets элемента Project для пересчета свойств. Атрибут InitialTargets гарантирует, что указанные в нем цели будут выполнены до того, как будет выполнена какая-либо задача. - person Dmitrii Lobanov; 30.04.2011