how is MS Build properties hierarchy maintained? - msbuild

Can anyone tell me how does the MsBuild picks up the value of the property..??
eg.
<TempProperty>Property Value</TempProperty>
now I can use $(TempProperty) anywhere to get the value of it.
now the scenario is I have made custom task that has configuration like this..
<PropertyGroup>
<ItemList>
<ConfigChange>
<PlaceHolder>#MACHINE_NAME#</PlaceHolder>
<Value>$(TempProperty)</Value>
<IsList>False</IsList>
</ConfigChange>
</ItemList>
</PropertyGroup>
now instead of getting "Property Value" in the tag I am getting $(TempProperty)... can anyone tell me how to get actual value in tag ???
thanks in advance.
Hey, Guys I have got the actual Problem and solution too... I think I was not able to describe my problem here.. .the problem was.. In my custom task I was passing the file path of the file containing the above ItemList tag..now when I tried to parse the XMLNode "ItemList" it was getting "$(TempProperty)" as value in the Item.. and I think thats correct because thats what is present in the passed XML.
So to overcome the issue , I did two things..
I created ItemGroup instead of propertyGroup and passed that ItemGroup to my custom task instead of file path.. thus now at my code I am getting the desired values.
thanks for your replies.

There's not enough there to really diagnose your problem. It looks correct at first glance, but where is the PropertyGroup for TempProperty declared?
For an illustrative example, here's a snippet from a C# project file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
...
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
...
</PropertyGroup>
The Configuration Property is being set to Debug (if it is blank at at that point). In the following group, it keys off of the Confuration and Platform properties.
The only gotcha tidbit is that for a property value to show up properly, it must be declared before it is used.
Please try to provide a little more context, that may help with understanding the issue.

I just tried this:
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="4.0"
DefaultTargets="Demo" >
<PropertyGroup>
<TempProperty>property value</TempProperty>
<ItemList>
<ConfigChange>
<PlaceHolder>#MACHINE_NAME#</PlaceHolder>
<Value>$(TempProperty)</Value>
<IsList>False</IsList>
</ConfigChange>
</ItemList>
</PropertyGroup>
<Target Name="Demo">
<Message Text="TempProperty: $(TempProperty)"/>
<Message Text="ItemList: $(ItemList)"/>
</Target>
</Project>
And my results where:
Task "Message"
TempProperty: property value
Done executing task "Message".
Task "Message"
ItemList:
<ConfigChange xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PlaceHolder>#MACHINE_NAME#</PlaceHolder>
<Value>property value</Value>
<IsList>False</IsList>
</ConfigChange>
Done executing task "Message".
Are you seeing something else?

on Ritch's suggestion I am adding my solution.
Initially I had
<TempProperty>Property Value</TempProperty>
<PropertyGroup>
<ItemList>
<ConfigChange>
<PlaceHolder>#MACHINE_NAME#</PlaceHolder>
<Value>$(TempProperty)</Value>
<IsList>False</IsList>
</ConfigChange>
</ItemList>
</PropertyGroup>
now my problem was I was providing my properties file part to my custom task
like
<UpdatePegasusConfigXML
Environment="$(Environment)"
Instance="$(Instance)"
BuildSourceRoot="$(BuildSourceRoot)"
></UpdatePegasusConfigXML>
now since I was providing file path itself so it took the value which was put in "Value" tag and was not picking up the property value, thus at code level I was getting "$(TempProperty)" instead of "Property Value"
now what I did was instead of creating property Group I created ItemGroup like this
<ItemGroup>
<PlaceHolders Include="#MACHINE_NAME#">
<Value>$(TempProperty)</Value>
<IsList>True</IsList>
</PlaceHolders>
</ItemGroup>
now I updated my custom task to take IteamGroup as one of the inputs, thus calling changed to
<UpdatePegasusConfigXML
Environment="$(Environment)"
Instance="$(Instance)"
BuildSourceRoot="$(BuildSourceRoot)"
PlaceHolders="#(PlaceHolders)"
></UpdatePegasusConfigXML>
now at code level I am able to get the value in "Value" tag of the ItemGroup.
I hope I had explained my solution that it was understandable.

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 - Remove a character from a variable without knowing its position

I read a build number from my TFS Team build which looks like "AB-1.2.3.4-CDE-REV.1". I want to edit this number and remove the last decimal point and make it look like "AB-1.2.3.4-CDE-REV1".
Usually when you want to manipulate strings in msbuild you're looking to use Property Functions. In the documentation of those you'll read you can use String functions so next up is figuring out which methods of System.String you need. In this case: LastIndexOf and Remove should do the trick:
<!-- BuildNumber property is fetched elsewhere -->
<PropertyGroup>
<BuildNumber>AB-1.2.3.4-CDE-REV.1</BuildNumber>
</BuildNumber>
<Target Name="ManipulateBuildNumber">
<PropertyGroup>
<BuildNumber>$(BuildNumber.Remove($(BuildNumber.LastIndexOf('.')),1))</BuildNumber>
</PropertyGroup>
<Message Text="New build number is $(BuildNumber)" />
</Target>
Thanks for the solution stijn. It works. I had figured out another lame and crude way of doing it.
<BuildNumber>AB-1.2.3.4-CDE-REV.1</BuildNumber>
<Part1>$(BuildNumber.Split('.')[0])</Part1>
<Part2>$(BuildNumber.Split('.')[1])</Part2>
<Part3>$(BuildNumber.Split('.')[2])</Part3>
<Part4>$(BuildNumber.Split('.')[3])</Part4>
<Part5>$(BuildNumber.Split('.')[4])</Part5>
<BuildNumber>$(Part1).$(Part2).$(Part3).$(Part4)$(Part5)</BuildNumber>

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.

MSBuild 4.0 property functions cannot access properties inside of them

Is it a limitation of MSBuild 4.0 property functions that I cannot access a property from inside of one?
Here is an example that works just fine:
<PropertyGroup>
<PartialConnection>$(TargetConnectionString.Substring( 0 + 12))</PartialConnection>
</PropertyGroup>
Here is another example that doe snot work. (I replace the 0 with another property)
<PropertyGroup>
<LocationOfDataSource>$(TargetConnectionString.IndexOf("Data Source="))</LocationOfDataSource>
</PropertyGroup>
<Message Importance="high" Text="Location is = $(LocationOfDataSource)"/>
<PropertyGroup>
<PartialConnection>$(TargetConnectionString.Substring( $(LocationOfDataSource) + 12))</PartialConnection>
</PropertyGroup>
this outputs
Location is = 0
Error MSB4184: The expression ""Data Source=MySQLServer;Integrated Security=True;Pooling=False".Substring(0 + 12)" cannot be evaluated. Input string was not in a correct format.
I took the output and plugged into a console app and it works just fine. I have tried several variations and I they always fail when I put a property inside a property function. (I even tried access the same property twice in a my property function and that failed too.)
Do property functions not support accessing properties?
I think my issue was assuming that math came for free.
I needed to do this kind of thing:
<PropertyGroup>
<LocationOfDataSource>$(TargetConnectionString.IndexOf("Data Source="))</LocationOfDataSource>
<LenthOfDataSourceString>12</LenthOfDataSourceString>
<LocationOfEndOfDataSourceString>$([MSBuild]::Add($(LocationOfDataSource), $(LenthOfDataSourceString)))</LocationOfEndOfDataSourceString>
<PartialConnectionString>$(TargetConnectionString.Substring($(LocationOfEndOfDataSourceString)))</PartialConnectionString>
</PropertyGroup>
Note that I am adding using Add($(Property), $(Property)) in this version. Add is one of the built-in MSBuild Property Functions (since MSBuild 4).
It seems to be working now.

How can task parameters be defaulted in MSBuild

In mytask.targets, I have something like:
<UsingTask TaskName="DoStuff" AssemblyFile="....etc....."/>
<PropertyGroup>
<RequiredParamDefault>hello</RequiredParamDefault>
</PropertyGroup>
This task currently has a required parameter (which could be changed from required if necessary).
When the task is used:
<DoStuff RequiredParam="$(RequiredParamDefault)" OtherParam="wobble"/>
Currently, RequiredParam has to be specified everytime. Is there anyway that when UsingTask is defined, the default can be set up so it doesn't have to be specified on every use of DoStuff?
I know the default could be hardcoded in the assembly, but I'd like to be able to define different defaults with different UsingTask statements.
Thanks.
You can't do this at the UsingTask or Task but instead you can using properties that you pass into the task. For example.
<Target>
<PropertyGroup>
<ReqParam Condition=" '$(ReqParam)'=='' ">Param-Default-Value</ReqParam>
</PropertyGroup>
<DoStuff RequiredParam="$(ReqParam)" OtherParam="wobble"/>
</Target>
In this case I define the property ReqParam to be Param-Default-Value only if the property doesn't already have a value. This is not exactly what you are looking for, but it may be your best option unless you can change the task itself.