Dynamic Metadata Assignment in an ItemGroup - msbuild

I have an ItemGroup defined as:
<ItemGroup>
<ProtoFiles Include="Protos\*.proto"/>
</ItemGroup>
It yields a list of all .proto files in a directory of my project. I want each item in the group to include a piece of metadata that specifies the name of the file that will be generated based on the .proto file. I know I can do this:
<ItemGroup>
<ProtoFiles Include="Protos\*.proto">
<OutputFile>%(ProtoFiles.Filename).cs</OutputFile>
</ProtoFiles>
</ItemGroup>
But my problem is that it is not a simple mapping from .proto filename to output filename. There is some tricky logic involved that I need to encapsulate somewhere and call that when assigning metadata. I need something like:
<ItemGroup>
<ProtoFiles Include="Protos\*.proto">
<OutputFile><GetOutputFilename ProtoFilename="%(ProtoFiles.Filename)"/></OutputFile>
</ProtoFiles>
</ItemGroup>
The idea being that my custom GetOutputFilename task would be called in order to get the metadata value.
Is this possible? Am I barking up the wrong tree?

I think it's not, try instead passing the ItemGroup to a task to generate this metadata. Property Functions can operate on metadata values, but unfortunately cannot be used to define metadata.

It's hard to know if the logic is too tricky for MSBuild without knowing exactly what it is. Do you have a custom task that operates on #(ProtoFiles) to generate the output files? If so, why not alter your task (or refactor to a new one) that just calculates the output files without creating them, something like this,
<ProtoTask
Files="#(ProtoFiles)"
... other params
DryRun="true">
<Output
TaskParameter="OutputFiles"
ItemName="ProtoFiles" />
</ProtoFiles>
The task can clone the item array, calculate the metadata value, and assign it to the output item array, which in the example here overwrites the original item array passed into the task.

Related

Include item only if not previously included with different metadata

I have an MSBuild SDK that we use for our projects. We define various default properties and add custom items depending on the project type. Some of those items are added in the SDK.targets, after the user project is parsed.
I found a situation where I'd like to add some items but only if the user does not have them added themselves (to use whatever metadata values they have set).
Best way I found to achieve that is the following:
<Target Name="IncludeDefaults">
<ItemGroup>
<CustomItem Include="Foo" Value="Default"
Condition="#(CustomItem->Equals('Foo')->Distinct()) == 'False'"/>
</ItemGroup>
</Target>
I know I can exclude items using Remove but that only ignores exact matches, including metadata. I tried various other combinations of attributes and processing to get around that but nothing seems to have worked.
In particular I have multiple items to add with default metadata so I'm looking for a single line solution.
Is this the best way of doing it, or is there something I'm missing?
If I understand correctly, you have named data values and you want to provide default values if values have not already been provided.
Consider using properties.
The following example is very idiomatic MSBuild. The Condition tests that the property does not already have a value.
<PropertyGroup>
<Foo Condition="'$(Foo)' == ''">Default</Foo>
<Bar Condition="'$(Bar)' == ''">AnotherDefault</Bar>
</PropertyGroup>
In addition to PropertyGroup elements in files, properties can be defined by environment variables and by the command line /p switch so there is a lot of flexibility with defining and overriding properties.
But your question is about ItemGroup and I may be making an incorrect assumption about your intent.
Remove is for removing items that are already in the ItemGroup collection.
There is an Exclude that works with Include and behaves as a 'deny' list. In the following example if there is already a 'Foo' item in 'CustomItem', it will be excluded from the Include.
<ItemGroup>
<CustomItem Include="Foo" Value="Default" Exclude="#(CustomItem)"/>
</ItemGroup>
The Exclude doesn't check metadata. It only compares the names. If CustomItem has items 'Foo' and 'Bar', then #(CustomItem) will be 'Foo;Bar' and 'Foo' will denied from the Include.

MsBuild: Flattening of nested ItemGroup with variable number of metadata

Assume the following ItemGroup structure:
<ItemGroup>
<BinaryFiles Include="C:\">
<Binary>a.dll</Binary>
<Binary>b.dll</Binary>
</BinaryFiles>
<BinaryFiles Include="D:\">
<Binary>my.ddl</Binary>
</BinaryFiles>
</ItemGroup>
I need to flatten this to a string like this:
C:\a.dll;C:\b.dll;D:\my.dll
How would I do that? If it's not possible, is there a better way to do it?
A metadata value can have only one value. Multiple definitions and updates will override the value, so the "C:\" item will only have b.dll as Binary metadata.
If there is only one element in the metadata then #(BinaryFiles->'%(Identity)%(Binary)') would yield the result you wanted.
However, since you are using file based logic, you are better off using a BinaryFiles item for each item:
<BinaryFiles Include="C:\*.dll" />
<BinaryFiles Include="D:\*.dll" />
This will scan for all the dll files. You can even use D:\**\*.dll to scan recursively. Then you can use #(BinaryFiles->'%(FullPath') to get the list of all absolute paths.

Condition statement to be used with "AfterCompileSolution" in tfsbuild

i need your help. I am running into a situation. I am trying to copy certain binaries into a particular folder. I am adding those task into "AfterCompileSolution" . I know it is incorrect, bcos it's gonna execute this step after every solution is compiled.
Here is my situation, i tried adding a condition like a SolutionFileName, but i get empty result. The target doesn't get executed because the SolutionFileName parameter is empty.
So do you know of any parameter that i can use between solutiontobuild i.e i want to copy certain binaries only after solution "A" is completed and i want these parameters to be part of "AfterCompileSolution" or maybe "BeforeCompileSolution"
Please suggest
Thanks
Satesh
It's been a while since I've done this but I believe you reference the file name with a syntax such as:
<Target Name="AfterCompileSolution" DependsOnTargets="RandomPreReqTarget">
<SomeTask Condition="'%(SolutionToBuildItem.Identity)' == 'ConditionValue'" />
</Target>
Another cool thing you can do is product extra properties in your SolutionToBuild item and reference them as metadata also like:
<SolutionToBuild Include="$(SolutionRoot)\$(SourceBranch)\RandomDirectory\Project.csproj">
<Targets>Build</Targets>
<Properties>OutDir=$(RandomDirectory);Configuration=$(Configuration);Platform=AnyCPU</Properties>
<GAC>True</GAC>
</SolutionToBuild>
You would then be able to access the metadata like this:
<Target Name="AfterCompileSolution" DependsOnTargets="RandomPreReqTarget">
<SomeTask Condition="'%(SolutionToBuildItem.GAC)' == 'True'" />
</Target>

Empty an MSBuild ItemGroup

Is there a way to remove the contents of an ItemGroup without resorting to Targets? I'm looking for something equivalent to:
<ItemGroup>
<MyItemGroup Remove="#(MyItemGroup)"/>
</ItemGroup>
Thanks
No, as the documentation states, Remove can only be included in a ItemGroup inside a Target. I'm not sure why using a Target is an issue in your case, but if want to use the 'Remove' step for every build config, then add it to one of the BeforeXXXX AfterXXX hooks, like BeforeBuild.
ItemGroup 'Remove' Documentation
Starting in the .NET Framework 3.5, Target elements may contain ItemGroup elements that may contain item elements. These item elements can contain the Remove attribute, which removes specific items (files) from the item type. For example, the following XML removes every .config file from the Compile item type.
<Target>
<ItemGroup>
<Compile Remove="*.config"/>
</ItemGroup>
</Target>
Now there is.
What's New in MSBuild 15
Item Element outside targets has a new Update attribute. Also, the restriction on the Remove attribute has been eliminated.

how to use MSBuild ItemGroup members as individual items like Properties?

So I'm kinda getting the hang of writing in MSBuild.
I like the idea storing things in ItemGroups because its easy to iterate, and you can have multple fields.
That makes them sort of like a class (more like a struct) but they respond to the iteration syntax of targets and tasks in a way that feels like a lambda expression.
However I continue to run into situation where I want to access a value of a particular item in an item group, that is, to access it like a property.
In that case I run into the issue of how to isolate a single item within the group.
When the item group is batching in a target or task, because of addressing using a metadata name or because of addressing with the common itemgroup name, you can utilize the value of the 'current' item i.e. #(ItemsName) or %(MetaDataName).
But I want to do things like: use an Item group as a System.Configuration class that contains the values of the entries in a section of a config file. Therefore the normal thing would be to name the ItemGroup itself to match the section name in the config file. however, the ItemGroup is not an addressable element that is accessible through the build engine interface, only the items themselves are addressable.
It might be nice to individually name the items in an ItemGroup rather than name them all the same and use the Include or a metadata field like to distinguish among them. This makes them behave like properties in that they are individually addressable as distinct items. so you could easily use their values in Conditions this way: '#(UniqueItemName->'%(Value)').
However, then the iterable features are essentially lost.
To narrow this down, presume i have a config file that gets read into an Item group by and xml task so that the element names in a section become the name of the items in the item group and attributes of each config file element are attributes that become metadata:
<configItemFlag name="displayDebugMessages" value="true" note="use with abandon" />
<configItemFlag name="displaySecurityValueMessages" value="false" note="use with caution" />
When I want to test this in a Condition, I need to narrow it down to something like this:
<Messge Text="Debug Message: you are debugging!" Condition="'#(configItemFlag->'%(Name)')' == 'displayDebugMessages' AND '#(configItemFlag->'%(Value)')' == 'true'/>
But this only evaluates the comparison and frequently does not evaluate to a single boolean.
So is there any way to syntacticly get this down to a dependable test?
Does this work for what you are trying to do?
<ItemGroup>
<ConfigItemFlag Include="displayDebugMessages">
<Value>true</Value>
<Note>use with abandon</Note>
</ConfigItemFlag>
<ConfigItemFlag Include="displaySecurityValueMessages">
<Value>false</Value>
<Note>use with caution</Note>
</ConfigItemFlag>
</ItemGroup>
<Target Name="Build">
<Message
Condition="
'%(ConfigItemFlag.Identity)' == 'displayDebugMessages' AND
'%(Value)' == 'true'"
Text="Debug Message: you are debugging, %(Note)!"
/>
</Target>
Output:
Build:
Debug Message: you are debugging, use with abandon!
(response to comment)
...the only thing I can offer to be able to use meta as properties isn't all that great, unless the target will make heavy use of them throughout. Basically it involves flattening each item to properties by batching on the item and creating local properties with each batch.
<Target Name="BuildOne"
Outputs="%(ConfigItemFlag.Identity)">
<!-- flatten this batch to properties -->
<PropertyGroup>
<_Identity>%(ConfigItemFlag.Identity)</_Identity>
<_Value>%(ConfigItemFlag.Value)</_Value>
<_Note>%(ConfigItemFlag.Note)</_Note>
</PropertyGroup>
<!-- use meta as properties -->
<Message
Condition="
'$(_Identity)' == 'displayDebugMessages' AND
'$(_Value)' == 'true'"
Text="Debug Message: you are debugging, $(_Note)!"
/>
</Target>
<Target Name="Build" DependsOnTargets="BuildOne"
/>
It seems like you are running into some of the limitations of the msbuild scripting language. Have you thought about writing a custom task to perform what you are looking for? That way you would be able to bring the full power of a full programming language to bear against the simple conditional check you want to perform.