Copy Item's Metadata in msbuild - msbuild

I am creating Item B based on item A, and would like to copy all of A's metadata to B (and add some additional meta data).
<ItemGroup>
<B Include="#A">
<M1>%(A.M1)</M1>
<M2>%(A.M2)</M2>
<M3>%(A.M3)</M3>
...
<M100>%(A.M100)</M100>
... Additional metadata specific to B ...
</B>
</ItemGroup>
Instead of copying each metadata M1 - M100 individually from A to B, is it possible to tell msbuild to copy all metadata from A to B?
Could such a "batch metadata copy" be conditioned?
Something like:
<ItemGroup>
<B Include="#A">
... Additional metadata specific to B ...
</B>
</ItemGroup>
<CopyMetadata From="#A" To="#B" Condition="... Check something ..."/>
Thanks.

When you copy items, its metadata is copied too. See the working example for MSBuild v4.0:
<Project DefaultTargets="DoSomethingWithB" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<A Include="1">
<M1>M1 (1)</M1>
<M2>M2 (1)</M2>
<M3>M3 (1)</M3>
<N4>HERE</N4>
</A>
<A Include="2">
<M1>M1 (2)</M1>
<M2>M2 (2)</M2>
<M3>M3 (2)</M3>
</A>
</ItemGroup>
<Target Name="PrepareB" Outputs="%(A.Identity)">
<ItemGroup>
<B Include="#(A)">
<M4>M4 (%(A.Identity))</M4>
<M5 Condition="'%(A.N4)'!=''">M5 (%(A.Identity) for A.N4 != '')</M5>
</B>
</ItemGroup>
</Target>
<Target Name="DoSomethingWithB"
DependsOnTargets="PrepareB">
<Message Text="ItemGroup A" />
<Message Text="%(A.Identity): M1=%(A.M1), M2=%(A.M2), M3=%(A.M3), N4=%(A.N4)" />
<Message Text="ItemGroup B" />
<Message Text="%(B.Identity): M1=%(B.M1), M2=%(B.M2), M3=%(B.M3), N4=%(B.N4), M4=% (B.M4), M5=%(B.M5)" />
</Target>
</Project>
Output:
ItemGroup A
1: M1=M1 (1), M2=M2 (1), M3=M3 (1), N4=HERE
2: M1=M1 (2), M2=M2 (2), M3=M3 (2), N4=
ItemGroup B
1: M1=M1 (1), M2=M2 (1), M3=M3 (1), N4=HERE, M4=M4 (1), M5=M5 (1 for A.N4 != '')
2: M1=M1 (2), M2=M2 (2), M3=M3 (2), N4=, M4=M4 (2), M5=

Related

MSBuild: Compare ItemGroups or access by index

For a C++ project, I want to autogenerate a defs.h file with project definitions, such as the date, git commit, ... to automate the versioning process of my application.
Therefore I am trying to create a MSBuild Target that will extract the latest git tag, git commit, and the current date and save it to a temporary gitinfo.txt file.
Another build target will depend on that file and generate a .h file.
In order to avoid unnecessary recompiles of my project, the .h file and for that reason the gitinfo.txt file shall only be rewritten, if any of the information has changes.
So my idea is the following:
Calculate git and date info
If available, read in the existing gitinfo.txt
Compare the calculated values to those in the txt file
If anything has changed, rewrite the gitinfo.txt
I've mastered steps 1. and 2., however I am not sure how to process the values after reading them.
<!-- The purpose of this target is to update gitinfo.txt if git information (commit...) has changed -->
<Target
Name="GetHeaderInfos"
BeforeTargets="ClCompile"
Outputs="$(IntDir)\gitinfo.txt"
>
<!-- Get information about the state of this repo-->
<GitDescribe>
<Output TaskParameter="Tag" PropertyName="NewGitTag" />
<Output TaskParameter="CommitHash" PropertyName="NewGitCommitHash" />
<Output TaskParameter="CommitCount" PropertyName="NewGitCommitCount" />
</GitDescribe>
<!-- Get the current date -->
<Time Format="dd.MM.yyyy">
<Output TaskParameter="FormattedTime" PropertyName="NewBuildDate" />
</Time>
<ReadLinesFromFile File="$(IntDir)\gitinfo.txt" Condition="Exists('$(IntDir)\gitinfo.txt')">
<Output TaskParameter="Lines" ItemName="Version" />
</ReadLinesFromFile>
<!-- Comparison here! HOW TO DO IT PROPERLY -->
<PropertyGroup>
<TagChanged> <!-- `$(NewGitTag)` == `$(Version)[0]` --> </TagChanged>
<!-- Other comparisons -->
</PropertyGroup>
</Target>
And this could be the content of gitinfo.txt
v4.1.4
04fe34ab
1
31.07.2016
I am not quite sure how to compare the values now. I need to compare $(NewGitTag) to the first value in the $(Version) version variable, and so on.
I haven't found an example, that actually accesses the variables after reading them from a file. The official documentation provides no help, nor have I found anything on stackoverflow or the likes.
I only know that the $(Version) variable holds a list, and I can batch process it. How can I compare its content to the defined variables $(NewGitTag), $(NewGitCommitHash), $(NewGitCommitCount) and $(NewBuildDate)?
Suppose we start with this data:
<ItemGroup>
<Version Include="v4.1.4;04fe34ab;1;31.07.2016"/>
</ItemGroup>
<PropertyGroup>
<GitTag>v4.1.4</GitTag>
<GitSHA>04fe34ab</GitSHA>
<Count>1</Count>
<Date>31.07.2016</Date>
</PropertyGroup>
Then here are at least 3 ways to achieve comparision (apart from the one mentioned in the comment) and there are probably other ways as well (I'll post them if I can come up with something else):
Just compare the items
I'm not sure why you want to compare everything seperately when this works just as well: compare the whole ItemGroup at once.
<Target Name="Compare1">
<PropertyGroup>
<VersionChanged>True</VersionChanged>
<VersionChanged Condition="'#(Version)' == '$(GitTag);$(GitSHA);$(Count);$(Date)'">False</VersionChanged>
</PropertyGroup>
<Message Text="VersionChanged = $(VersionChanged)" />
</Target>
Batch and check if there's one difference
Each item of Version is compared with e.g. GitTag via batching. The result will be False;False;False;False if there's a difference, else it will be True;False;False;False. Count the distinct elements and if it's 2 it means we got the latter so GitTag did not change. Note this obviousle only works if each of your source items can never have the same value as one of the other items.
<Target Name="Compare2">
<PropertyGroup>
<TagChanged>True</TagChanged>
<TagChanged Condition="'#(Version->Contains($(GitTag))->Distinct()->Count())' == '2'">False</TagChanged>
</PropertyGroup>
<Message Text="TagChanged = $(TagChanged)" />
</Target>
you can then compare the other items as well and combine the result.
Use an inline task to access items by index
This comes closest to what's in your question, but it does need a bit of inline code.
<UsingTask TaskName="IndexItemGroup" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<Items Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
<Index Required="true" ParameterType="System.Int32"/>
<Item Output="true" ParameterType="Microsoft.Build.Framework.ITaskItem"/>
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[Item = Items[ Index ];]]>
</Code>
</Task>
</UsingTask>
<Target Name="Compare3">
<IndexItemGroup Items="#(Version)" Index="1">
<Output PropertyName="OldGitSHA" TaskParameter="Item"/>
</IndexItemGroup>
<PropertyGroup>
<SHAChanged>True</SHAChanged>
<SHAChanged Condition="'$(GitSHA)' == '$(OldGitSHA)'">False</SHAChanged>
</PropertyGroup>
<Message Text="OldGitSHA = $(OldGitSHA), changed = $(SHAChanged)" />
</Target>

How to filter an ItemGroup if the items not exist in another ItemGroup

I have a ItemGroup:
<ItemGroup>
<MainItem Include="A;B;C;D;E;F" />
</ItemGroup>
I would like to filter from another ItemGroup if the items not exist in the above ItemGroup:
<ItemGroup>
<MyItem Include="A;C;G;H" />
</ItemGroup>
<ItemGroup>
<Filtered Include="#(MyItem)" Condition="If %(MyItem.Identity) not exists in #(MainItem)" />
</ItemGroup>
I expect #(Filtered) = 'G;H'.
Possibly there is a way to do this using batching as you tried, but I didn't find it immediately and even if it's possible it won't be as simple and elegant as this one:
<ItemGroup>
<Filtered Include="#(MyItem)" Exclude="#(MainItem)" />
</ItemGroup>

Parse key/value pairs from MSBuild property

Say I have a property like:
<MyProp>Foo=Bar;Hello=World</MyProp>
This seems like a reasonably common property pattern in MSBuild. How would I go about fetching the value "World"? In an ideal world this might look something like:
$(MyProp).(Hello)
Edit: To be clear, the property is not of my own creation, it is the output from another target that is out of my control, so I cannot change the way the property is declared.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MyProp>Foo=Bar;Hello=World</MyProp>
</PropertyGroup>
<Target Name="Foo">
<CreateItem Include="MyProp" AdditionalMetadata="$(MyProp)">
<Output TaskParameter="Include" ItemName="MyProp" />
</CreateItem>
<Message Text="Foo %(MyProp.Foo)" />
<Message Text="Hello %(MyProp.Hello)" />
</Target>
</Project>
You have two routes to follow as far as I am concerned
Declare a Property Group just like the following:
<PropertyGroup>
<Foo>Bar</Foo>
<Hello>World</Hello>
</PropertyGroup>
and then use the following method to access your properties
<Target Name="DoSomething">
<Message Text="Print this : $(Foo)" />
</Target>
or you might want to take ItemGroup Element approach like the following
<ItemGroup>
<MySolutionFiles Include="..\mySolution.sln" />
</ItemGroup>
<Target Name="PrintItems">
<Message Text="My Files: #(MySolutionFiles)" />
</Target>
You can have the following as well
<ItemGroup>
<MyProp
Include="Foo;Hello" />
</ItemGroup>
<Target Name="PrintMyItems">
<Message Text="MyProp: #(MyProp)" />
</Target>
If there is no choice over the input then one possible solution is parsing the input into an array and then taking it from there like the following:
<PropertyGroup>
<MyProp>Foo=Bar;Hello=World</MyProp>
<Split>$(MyProp.Split(';'))</Split>
</PropertyGroup>
and then play with the array items like the following:
<Target Name="DoPrint">
<Message text="$(Split[0])" />
</Target>
Split[0] item contains your Foo=Bar which can be split into two more strings just like above. This should keep you going for now.
You don't need a property group, it's just nice to have default values in case the user doesn't pass them.
For each property you pass, the syntax to access that property is $(PropertyName).
So if you pass:
msbuild.exe /p:P1=V1 /p:P2=V2;P3=V3
You would use the property name $(P1), $(P2), $(P3).
More on MsBuild properties here.

Can't get MSBuild Community Task RegexReplace to work

I'm trying to copy a bunch of files whose names begin with the prefix DR__, but the copies must have that prefix removed. That is, DR__foo must be copied as foo. I'm trying this, which is based in the example provided in the documentation (the .chm):
<Target Name="CopyAuxiliaryFiles">
<MakeDir Directories="$(TargetDir)Parameters" Condition="!Exists('$(TargetDir)Parameters')" />
<ItemGroup>
<ContextVisionParameterFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Files to copy and rename: #(ContextVisionParameterFiles)"/>
<RegexReplace Input="#(ContextVisionParametersFiles)" Expression="DR__" Replacement="">
<Output ItemName ="DestinationFullPath" TaskParameter="Output" />
</RegexReplace>
<Message Text="Renamed Files: #(DestinationFullPath)"/>
<Copy SourceFiles="#(ContextVisionParameterFiles)" DestinationFiles="#(DestinationFullPath)" />
</Target>
DestinationFullPath comes out empty (or that's what I see when I display it with Message). Thus, Copy fails because no DestinationFiles are specified. What's wrong here?
Edit: ContextVisionParameterFiles is not empty, it contains this:
D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_bone.alut;D:\SVN.DRA.WorkingCopy\CVParameters\DR__big_medium.gop
They're actually 40 files, but I trimmed it for the sake of clarity
Got it! It seems to have been the combination of a stupid error and a seemingly compulsory parameter. As for the first one, there were two Targets called CopyAuxiliaryFiles. As for the second one, it seems the Count parameter is needed.
The final, working version:
<Target Name="CopyCvParameters">
<ItemGroup>
<CvParamFiles Include="$(SolutionDir)CVParameters\DR__*" />
</ItemGroup>
<Message Text="Input:
#(CvParamFiles, '
')"/>
<!-- Replaces first occurance of "foo." with empty string-->
<RegexReplace Input="#(CvParamFiles)" Expression="^.*DR__" Replacement="$(TargetDir)Parameters\" Count="1">
<Output ItemName ="RenamedCvParamFiles" TaskParameter="Output" />
</RegexReplace>
<Message Text="
Output RenamedCvParamFiles:
#(RenamedCvParamFiles, '
')" />
<Copy SourceFiles="#(CvParamFiles)" DestinationFiles="#(RenamedCvParamFiles)" SkipUnchangedFiles="True" />
</Target>
Notice that:
I renamed the Target to solve the name collision (Why doesn't Visual Studio detect this as an error?)
I pretty-printed the ItemGroups with the #(CvParamFiles, '
') syntax, which seems to replace ; with line breaks
My regex replaces the absolute path and the prefix
Count="1" is now passed to RegexReplace

can msbuild resolve wildcard expressions resulting from a transform?

I expected the code below to result in identical items for List and List2 (I have a single cpp1 project in the searched path).
<ItemGroup>
<src Include="cpp1"/>
<List Include="#(src -> '..\..\..\projects\**\%(identity).vcxproj')" />
<List2 Include="..\..\..\projects\**\cpp1.vcxproj" />
</ItemGroup>
But what I get is:
List: ..\..\..\projects\**\cpp1.vcxproj
List2: ..\..\..\projects\common\cpp1\cpp1.vcxproj
So it looks like the wildcard expression is not being expanded when its the result of a transform. What am I missing here?
You can get the extra transformation, but you need to add an additional item array, and it needs to have each potential element added one at a time, with a dependent target. You also need to pass the intermediate item specification through a property,
<ItemGroup>
<src Include="cpp1"/>
<List1a Include="#(src -> '..\..\..\projects\**\%(Identity).vcxproj')" />
<List2 Include="..\..\..\projects\**\cpp1.vcxproj" />
</ItemGroup>
<Target Name="TransformWithWildcards"
Outputs="%(List1a.Identity)">
<PropertyGroup>
<_ThisList1a>#(List1a)</_ThisList1a>
</PropertyGroup>
<ItemGroup>
<List1b Include="$(_ThisList1a)" />
</ItemGroup>
</Target>
<Target Name="Transform"
DependsOnTargets="TransformWithWildcards">
<Message Text="1a. %(List1a.Identity)" />
<Message Text="1b. %(List1b.Identity)" />
<Message Text="2. %(List2.Identity)" />
</Target>
#(List1a) is the same as your original #(List), and #(List1b) contains the results you expected, after TransformWithWildcards completes.