Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one modify an ItemDefinitionGroup from an MSBuild target?

Tags:

c++

msbuild

I have an msbuild script I wrote to compile Google Protocol Buffers files:

<ItemGroup>
  <ProtocolBuffer Include="Whitelist.proto" />
  <ProtocolBuffer Include="Whitelist2.proto" />
</ItemGroup>
<ItemDefinitionGroup>
  <ProtocolBuffer>
    <ProtoPath>$(ProjectDir)</ProtoPath>
  </ProtocolBuffer>
</ItemDefinitionGroup>
<PropertyGroup>
  <ProtoC>$([System.IO.Path]::GetFullPath($(ProjectDir)..\ThirdParty\protobuf-2.4.1\protoc.exe))</ProtoC>
  <ProtoOutPath>$(IntDir)CompiledProtocolBuffers</ProtoOutPath>
</PropertyGroup>
<Target Name="CompileProtocolBuffers"
        BeforeTargets="ClCompile"
        Inputs="@(ProtocolBuffer)"
        Outputs="@(ProtocolBuffer->'$(ProtoOutPath)\%(FileName).pb.cc');@(ProtocolBuffer->'$(ProtoOutPath)\%(FileName).pb.h')">
  <MakeDir Directories="$(ProtoOutPath)" />
  <Exec
    Command="&quot;$(ProtoC)&quot; --proto_path=&quot;$([System.IO.Path]::GetDirectoryName(%(ProtocolBuffer.ProtoPath)))&quot; --cpp_out=&quot;$(ProtoOutPath)&quot; &quot;%(ProtocolBuffer.FullPath)&quot; --error_format=msvs"
        />
  <ItemGroup>
    <ClInclude Include="$(ProtoOutPath)\%(ProtocolBuffer.FileName).pb.h" />
    <ClCompile Include="$(ProtoOutPath)\%(ProtocolBuffer.FileName).pb.cc">
      <AdditionalIncludeDirectories>$(MSBuildThisDirectory)..\ThirdParty\protobuf-2.4.1\src</AdditionalIncludeDirectories>
      <PrecompiledHeader></PrecompiledHeader>
      <DisableSpecificWarnings>4244;4276;4018;4355;4800;4251;4996;4146;4305</DisableSpecificWarnings>
      <PreprocessorDefinitions>GOOGLE_PROTOBUF_NO_RTTI</PreprocessorDefinitions>
      <WarningLevel>Level3</WarningLevel>
    </ClCompile>
  </ItemGroup>
</Target>

This compiles the protocol buffers files perfectly, and adds them to the compiler's inputs (yay!). However, my other source files that want to include the .pb.h files need to know where these files got generated -- that generation location needs to be put on the include path.

Therefore, if and only if the user has included a <ProtocolBuffer item somewhere in their script, I want to add the generation location (in this case $(ProtoOutPath) to ClCompile's <AdditionalIncludeDirectories>.

Is that possible or do I need to make .cpp files that want to use these generated bits jump through hoops?

like image 817
Billy ONeal Avatar asked Aug 19 '13 00:08

Billy ONeal


1 Answers

Read your question and thought "can't be that hard". Man, was I wrong. First I thought just putting a condition on it, but of course one can't use ItemGroups in toplevel conditions because of evaluation order. Then I figured it's also not possible to put an ItemDefinitionGroup in a target (cause there one can use conditions) and modify it there. Then I bonked my head on the keyboard a couple of times after I realized that's probably why you asked the question :] (btw you know including a nonexisting directory is not really a problem since the compiler will happily ignore it?)

Maybe there's a simpler solution, but lastly I figured: if nothing works, my favourite msbuild toy aka CodeTaskFactory must be able to fix it. It does (I hope, didn't fully test the result), but it's not straightforward at all. Here you go, make sure to invoke the Test target somewhere before the C++ build starts.

<!--Uncomment the below to define some ProtocolBuffers-->  
<!--<ItemGroup>
  <ProtocolBuffer Include="Whitelist.proto" />
  <ProtocolBuffer Include="Whitelist2.proto" />
</ItemGroup>-->

<!--Suppose these are your default include files defined in your C++ project-->
<ItemDefinitionGroup Label="DefaultIncludes">
  <ClCompile>
    <AdditionalIncludeDirectories>/path/to/x;/path/to/y</AdditionalIncludeDirectories>
  </ClCompile>
</ItemDefinitionGroup>

<!--Include at least one item so we can play with it-->
<ItemGroup>
  <ClCompile Include="iamaninclude"/>
</ItemGroup>

<!--Use code to append to AdditionalIncludeDirectories-->
<UsingTask TaskName="AppendMetadata" TaskFactory="CodeTaskFactory" 
           AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
  <ParameterGroup>
    <Append ParameterType="System.String" Required="true"/>
    <ItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true"/>
    <OutputItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
  </ParameterGroup>
    <Task>
        <Code>
        <![CDATA[
            const string dirz = "AdditionalIncludeDirectories";
            foreach( var item in ItemList )
            {
              var cur = item.GetMetadata( dirz );
              item.SetMetadata( dirz, cur + ";" + Append );
            }
            OutputItemList = ItemList;
        ]]>
    </Code>
  </Task>
</UsingTask>

<!--Main target-->  
<Target Name="Test">
  <!--stage 1: copy the itemgroup, then clear it:
  if an Output TaskParameter is an Itemgroup, apparently the content
  gets appended to the group instead of replacing it.
  Found no documentation about this whatsoever though???-->
  <ItemGroup Condition="@(ProtocolBuffer) != ''">
    <ClCompileCopy Include="@(ClCompile)"/>
    <ClCompile Remove="@(ClCompile)"/>
  </ItemGroup>

  <!--stage 2: append 'ProtoBufIncludeDir' to AdditionalIncludeDirectories,
  and append the result to the origiginal again-->
  <AppendMetadata ItemList="@(ClCompileCopy)" Append="ProtoBufIncludeDir" Condition="@(ProtocolBuffer) != ''">
    <Output ItemName="ClCompile" TaskParameter="OutputItemList"/>
  </AppendMetadata>

  <!--stage 3: use modified itemgroup-->
  <Message Text="@(ClCompile->'%(Identity): %(AdditionalIncludeDirectories)')"/>
</Target>

This prints

iamaninclude: /path/to/x;/path/to/y

unless the ProtocolBuffer is not empty in which case it prints

iamaninclude: /path/to/x;/path/to/y;ProtoBufIncludeDir
like image 138
stijn Avatar answered Oct 11 '22 12:10

stijn