Search code examples
msbuildmsbuild-propertygroupitemgroup

MSBuild item can't be used in MSBuild task, error MSB4012


I have the following MSBuild project file:

<?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>

And when I run it I get an error:

C:\Repositories\Project\Build\Build.proj(22,16): error MSB4012: The expression "@(Base->'%(FullPath)')\Deploy" cannot be used in this context. Item lists cannot be concatenated with other strings where an item lis t is expected. Use a semicolon to separate multiple item lists.

Why do I get this error and how can it be avoided? I use item Base within ItemGroup because I need to get rid of .. in path, and Items allow to do it via %FullPath metadata. If I use just PropertyGroup then everything works fine, but I have this .. in all paths.


Solution

  • it is tough to tell exactly what is happening underneath the hood. I am not on the MSBuild so I am only loosly familiar with the actual implementation. We would need an MSBuild dev to chime in for a 100% correct answer. But here is what I'm assuming is happening (read: the remainder of this contains speculation on my part).

    Inside you target when you use the statement

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

    MSBuild notices that you have the property expansion $(BaseDirectory) used and that the parameter type for Projects on the MSBuild is an array. Also MSBuild notices that BaseDirectory is a property which contains an item. These properties do not behave like normal properties. You can think of them as "virtual properties" (yes I just made up that term). When these properties are used instead of looking up the value, there is a replacement made inline. So your Projects attribute changes to:

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

    Since Projects is an array MSBuild will attempt to perform a transformation on the expression provided. Since that is not a valid transformation an error occurs. Which is the error that you are receiving.

    Now to work around this you can change you Build target to look like:

    <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>
    

    With this approach I have created a property group within the target itself and assigned the value of those "virtual properties" into new properties. Those new properties are not virtual properties, but real properties so you can use them as you expected with no issues.

    Now on to your question, "why does the message task work WTF?!!!" Inside the Hello target you have the following:

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

    Which works without no problems. Previously I mentioned that these virtual properties will essentially be replaced inline with the definition backing them, so this would in effect become.

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

    OK hold that thought.

    The Text property on the MSBuild task is defined as a string, which is a scalar value. If you recall the Projects property on the MSBuild task is defined as ITaskItem[], since this is an array its a vector value. When a @(...) is found within a vector values property the entire expression is used for as an item transformation. In this case the statement @(Base->'%(FullPath)')\DebugConsoleApp\DebugConsoleApp.csproj is not a valid transform expression. When a '@(..)' is found inside of a scalar values property declaration the values are flattened into a string. So each instance of '@(...)' is processed and flattened into a single string value. If there are multiple values then delimiters are used.

    So hopefully that explains the behavior you are seeing, and it may actually be a bug. You can log it at http://connect.microsoft.com/ and the MSBuild team will triage it.

    More on virtual properties Earlier I mentioned that these virtual properties do not behave like normal properties in the sense the the value is not looked up, but instead the usage of $(...) is replaced with the properties expression. Don't take my word for it, see it for yourself. Here is a sample file that I created

    <?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>
    

    Here I have an item list MyItem declared and a dependent property MyProperty. Inside the Demo target I print the value for MyProperty then I add another value to the MyItem item list and print out the value for MyProperty again. Here is the result.

    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).
    

    As you can see it behaves in the way in which I stated.