Can Msbuild dynamically generate a list of Item.Metadata? - msbuild

<SqlToMetadataMultiTask ConnectionString="$(ConnectionString)">
<Output TaskParameter="Items" ItemName="MultiStats" />
<Output TaskParameter="Columns" ItemName="MultiColumns" />
</SqlToMetadataMultiTask>
<PropertyGroup>
<OutputFormat>#(MultiColumns,',')</OutputFormat>
</PropertyGroup>
<Message Text="Columns=#(MultiColumns,',')"/>
<WriteLinesToFile File="SqlMetricsMulti.csv" Overwrite="true" Lines="#(MultiColumns,',')" />
<WriteLinesToFile File="SqlMetricsMulti.csv" Overwrite="false"
Lines="#(MultiStats->'%(db),%(num_procs),%(len_procs),%(cursors_refs),%(tt_refs),%(ifs),%(cases),%(where),%(join),%(ands),%(ors)')" />
I have a row for each database, and write out the column headers, then the metadata-stored metrics for each database.
Can I make this task more generic so that the data output columns are generated dynamically just like the Column headers are being done? In some ways this would be custom metadata whitelisting by another item group dynamically based on the input.

See this answer in the link below, which is somewhat similar, writing out item meta data to a file. You'll need to check out the docs for ITaskItem, specifically ITaskItem.CloneCustomMetadata to get the generic behavior you're looking for.
Passing Items to MSBuild Task

Related

How to reference ItemGroup Identity within the definition of the var

I have a target like below. It needs to replace content of a file with new content. I have multiple files I am matching with ItemGroup.
I couldn't figure out a way to get this working.
Here is my target definition.
<ItemGroup>
<PRSetting Include="$(settings_root)\**\settings_config_*.xml">
<NewContent>$([System.IO.File]::ReadAllText('%(Identity)')).Replace('[config.version]', '$(PR_Version)'))</NewContent>
</PRSetting>
</ItemGroup>
<Target Name="PrepSettings" Inputs="#(PRSetting)"
Outputs="#(PRSetting->'$out\$Filename.xml')" >
<Message Text="%(PRSetting.Identity) new contents:" />
<Message Text="%(PRSetting.NewContent)"/>
</Target>
I hope I explained it right what I am trying to do. When the target is built, I am getting an error that the path to File::ReadFile() can't be empty string. I am using VS 2019. This is work in progress. I am yet to figure out how to save the new content in destination file.
Update
I have the Itemgroup outside. I updated the question. The reason it is outside is because the target inputs parameter needs it.
Try the following and see if it works:
<Target Name="PrepSettings">
<ItemGroup>
<PRSetting Include="$(settings_root)\**\settings_config_*.xml" />
<PRSetting>
<NewContent Condition="%(Identity) != ''">$([System.IO.File]::ReadAllText('%(Identity)')).Replace('[config.version]', '$(PR_Version)'))</NewContent>
</PRSetting>
</ItemGroup>
<Message Text="#(PRSetting.Identity) new contents:" />
<Message Text="%(PRSetting.NewContent)"/>
</Target>
There are two changes:
There seems to be an issue with an Include that doesn't use an existing ItemGroup and metadata that is self-referencing. So, setting up PRSetting is split in two.
First, establish PRSetting with the Include.
Second, revisit to add the NewContent metadata item.
Add Condition="%(Identity) != ''" on the NewContent metadata.
I'm not able to fully test your exact scenario at present but I tested an analogue.
Here is my test analogue:
<Target Name="PrepSettings">
<ItemGroup>
<PRSetting Include="1;4;2;3"/>
<PRSetting>
<NewContent Condition="%(Identity) != ''">$([MSBuild]::Add('%(Identity)', '1'))</NewContent>
</PRSetting>
</ItemGroup>
<Message Text="PRSetting is #(PRSetting->'(%(Identity),%(NewContent))');" />
</Target>
The output is
PrepSettings:
PRSetting is (1,2);(4,5);(2,3);(3,4);
Regarding your code change to move the ItemGroup outside the target:
The technique of splitting the ItemGroup as written won't work, but if you are using VS2017 or later and working with .NET Core/.NET 5+ you can use Update.
As shown in the question, the Outputs attribute has syntax errors. I assume Outputs="#(PRSetting->'$(OutputPath)\%(Filename).xml')" (or something close) is intended.
As shown in the question, the Outputs attribute will never be satisfied because PrepSettings doesn't create the files. I assume PrepSettings as shown is not complete.

Msbuild- 'DependsOnTargets' that contain condition

I tried to have a condition on a Target tag, but resulted with the error:
target has a reference to item metadata. References
to item metadata are not allowed in target conditions unless they are part of an item transform.
So i found this work around:
How to add item transform to VS2012 .proj msbuild file
and tried to implement it, but i can't figure up what i am doing wrong because it's not working as expected.
<CallTarget Targets="CopyOldWebConfigJs" />
<Target Name="CopyOldWebConfigJs"
Inputs="#(ContentFiltered)"
Outputs="%(Identity).Dummy"
DependsOnTargets="webConfigJsCase">
<Message Text="web.config.js Case" />
</Target>
<!-- New target to pre-filter list -->
<Target Name="webConfigJsCase"
Inputs="#(FileToPublish)"
Outputs="%(Identity).Dummy">
<ItemGroup>
<ContentFiltered Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(FileToPublish.Filename)%(FileToPublish.Extension)', 'web.config.js'))" />
</ItemGroup>
</Target>
I thought that Inputs="#(ContentFiltered)" will contain the lines that DependsOnTargets="webConfigJsCase" find.
But when i run it , i am getting this message: Skipping target "CopyOldWebConfigJs" because it has no inputs.
I know for a fact that the regex work, and it do find a filename_ext that equals web.config.js so it return True
What do i do or understand wrong?
In <ItemGroup><Item/></ItemGroup>, no change will be made to the Item item because no action was specified. If you want to add entries to the item, you must specify Include="".
The <Item/> documentation describes the various attributes for item elements inside of an <ItemGroup/>. Note that at the top-level of an MSBuild file, directly under the <Project/> element, you would use the attributes Include and Exclude while in a <Target/> you would use the attributes Include and Remove. Not including any attributes at all is nonsensical and—as far as I know—no different than simply deleting the entire line. I am surprised MSBuild doesn’t throw an error or warning this is almost certainly a mistake and not intentional.
The Inputs and Outputs attributes on your <Target Name="webConfigJsCase"/> are unnecessary. In fact, they slow MSBuild down by making it loop over the target unnecessarily. You can filter just in the <Item/> like this:
<Target Name="webConfigJsCase">
<ItemGroup>
<ContentFiltered Condition="'%(FileToPublish.Filename)%(FileToPublish.Extension)' == 'web.config.js'" Include="#(FileToPublish)" />
</ItemGroup>
</Target>
Additionally, I assume that you intended your regular expression to match web.config.js but not match webaconfigbjs. You don’t need to use an advanced feature like Regular Expressions here because MSBuild’s built-in condition operators already support simple string comparison. If fixed the condition above to be more readable.

Different ItemDefinitionGroup for different ItemGroup

I'm currently working using MSBuild, and in one of the vcxproj files, I'd like to have different ItemDefinitionGroup for different ItemGroup, without overriding each other, so that each ItemGroup has its own specific definition, e.g.
<ItemDefinitionGroup Label="ItemDefGroupA">
<CLCompile>
<AdditionalOptions> /option_for_item_group_A</AdditionalOptions>
</CLCompile>
</ItemDefinitionGroup>
<ItemGroup Label="ItemGroupA">
<CLCompile Include="src\main.cpp" />
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<CLCompile>
<AdditionalOptions> /option_for_item_group_B</AdditionalOptions>
</CLCompile>
</ItemDefinitionGroup>
<ItemGroup Label="ItemGroupB">
<CLCompile Include="src\main2.cpp" />
</ItemDefinitionGroup>
Is this even possible?
Thank you very much in advance, and have a nice day.
The Label attribute is ignored by MSBuild execution engine. The only place where it is used is by IDE code that needs to know a location inside project file to insert new entities, which happens if you modify your project in Visual Studio. The MSDN blog gives some information on how Labels are used by IDE.
So, what you are doing would not work. You only have one CLCompile item group, and your multiple item definition groups override each other. Whatever definition group is evaluated last, it wins. Note that the Item Definitions are evaluated first, after that Items are evaluated on next pass (link).
One possible workaround for your scenario is to create couple of auxiliary groups, like this:
<ItemGroup>
<GroupA Include="src\file1.cpp" />
<GroupA Include="src\file2.cpp" />
</ItemDefinitionGroup>
<ItemGroup>
<GroupB Include="src\file3.cpp" />
<GroupB Include="src\file4.cpp" />
</ItemDefinitionGroup>
Then you initialize your CLCompile group from a combination of the two with different metadata values:
<ItemGroup>
<CLCompile Include="#(GroupA)">
<AdditionalOptions> /option_for_item_group_A</AdditionalOptions>
</CLCompile>
<CLCompile Include="#(GroupB)">
<AdditionalOptions> /option_for_item_group_B</AdditionalOptions>
</CLCompile>
</ItemDefinitionGroup>

Is it reasonable to define repeated ItemMetadata?

In MSBuild, we may define Item metadata as:
<ItemGroup>
<DProjs Include="$(GroupProjPath)app.dproj">
<DCP>test1</DCP>
</DProjs>
</ItemGroup>
I may also define duplicate Item metadata:
<ItemGroup>
<DProjs Include="$(GroupProjPath)app.dproj">
<DCP>test1</DCP>
<DCP>test2</DCP>
<DCP>test3</DCP>
</DProjs>
</ItemGroup>
But how would I access 3 distinct item metadata value?
<Message Text="%(DProjs.DCP)" />
always return test3.
You could make the metadata value <DCP>test1;test2;test3</DCP> which is what you would expect if repeated values were allowed. You can use the CreateItem task to turn it into a list of items that can then be batched (looped over), or use it however you meant.

MSBuild: How to update default metadata in an item?

I have the following problem in scripting with MSBuild:
I create a default item "itemA" with two metadata "metadata1" and "metadata2", whereby metadata2 refers to metadata1.
When I define itemA later and overwrite metadata1, the metadata2 contains still the default value of metadata1. How can I make the metadata2 to refer to the "new" metadata1?
Illustration in code as below:
<ItemDefinitionGroup>
<itemA>
<Metadata1>default</Metadata1>
<Metadata2>%(itemA.Metadata1)</Metadata2>
</itemA>
</ItemDefinitionGroup>
<ItemGroup>
<itemA Include="first" >
<Metadata1>m_data1</Metadata1>
</itemA>
</ItemGroup>
But see the print
<Message Text="itemA.Metadata1 = %(itemA.Metadata1)" />
<Message Text="itemA.Metadata2 = %(itemA.Metadata2)" />
delivers:
itemA.Metadata1 = m_data1 ***<-- correctly updated***
itemA.Metadata2 = default ***<-- why showing the default value, not* m_data1??**
how can I make itemA.Metadata2 to have the same value as itemA.Metadata1 after it has been updated?
I think this is not possible because order of evaluation Item Definitions - Value Sources - Note:
Item metadata from an ItemGroup is not useful in an ItemDefinitionGroup metadata declaration because ItemDefinitionGroup elements are processed before ItemGroup elements.
You have to override itemA's Metadata2 value in ItemGroup
<ItemDefinitionGroup>
<itemA>
<Metadata1>default</Metadata1>
<Metadata2>%(Metadata1)</Metadata2>
</itemA>
</ItemDefinitionGroup>
<ItemGroup>
<itemA Include="first" >
<Metadata1>m_data1</Metadata1>
<Metadata2>%(Metadata1)</Metadata2>
</itemA>
</ItemGroup>
As palo states, since Metadata2 has already been evaluated, you'll have to explicitly overwrite the value. Your change to Metadata1 won't automatically propagate to other places where it was referenced during initialization.
However, you can "re-evaluate" your items' metadata by starting a new instance of MSBuild and passing the updated metadata in as a property. Running msbuild /t:Wrapper on this project from the command line will result in Metadata1 and Metadata2 printing the same value:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<DefaultMetadata1 Condition="DefaultMetadata1==''">default</DefaultMetadata1>
</PropertyGroup>
<ItemDefinitionGroup>
<itemA>
<Metadata1>$(DefaultMetadata1)</Metadata1>
<Metadata2>%(itemA.Metadata1)</Metadata2>
</itemA>
</ItemDefinitionGroup>
<ItemGroup>
<itemA Include="first" >
<Metadata1>m_data1</Metadata1>
</itemA>
</ItemGroup>
<Target Name="Wrapper">
<MSBuild
Projects="$(MSBuildProjectFile)"
Targets="Worker"
Properties="DefaultMetadata1=%(itemA.Metadata1)"
/>
</Target>
<Target Name="Worker">
<Message Text="itemA.Metadata1 = %(itemA.Metadata1)" />
<Message Text="itemA.Metadata2 = %(itemA.Metadata2)" />
</Target>
</Project>
The usefulness of this approach will depend on what you're trying to accomplish. You can undoubtedly find an alternate solution using properties instead of item metadata.
While the solution above works for the case you describe, it can quickly get out of hand. There's probably a more simple solution that may involve some redundant code.
My recommendation would be to use the simple solution and eliminate as much redundancy as you reasonably can without inventing new ways to get around MSBuild's small feature set. Clever tricks here probably won't save you that many LOC at the end of the day and may result in less readable code, making it more difficult for newcomers to understand what's going on.