How to get Directory name in msbuild configuration file? - msbuild

Here is the simple code which I am using. Which gets all the folders in the directory and then give me the Folder name.
<TestProjectFolderPath Include="$([System.IO.Directory]::GetDirectories(`$(SolutionDir)`,`*.Tests`))" />
<TestProjectFolderNames Include="#(TestProjectFolderPath->'$([System.IO.Path]::GetDirectoryName(`$([System.IO.Path]::GetFileName(`%(Identity)`))`)',' ')" />
But in TestProjectFolderNames [System.IO.Path] functions are not getting evaluated and returned as just string eg:
$([System.IO.Path]::GetDirectoryName($([System.IO.Path]::GetFileName(C:\Some.Unit.Tests)))
I need help to understand the correct syntax to get this working.

Using property functions on Item Metadata while transforming an Item is not supported I think (maybe it is in the latest MSBuild version but I cannot test that right now). As a workaround add new Metadata yourself and because it acts like a Property things work out ok for recent MSBuild versions:
<ItemGroup>
<TestProjectFolderPath Include="$([System.IO.Directory]::GetDirectories(`$(SolutionDir)`,`*.Tests`))" />
<TestProjectFolderPath>
<FolderName>$([System.IO.Path]::GetFileName(`%(Identity)`))</FolderName>
</TestProjectFolderPath>
</ItemGroup>
<Message Text="#(TestProjectFolderPath->'%(FolderName)', ' ')" />
edit see comments, according to Sherry for older MSBuild versions the equivalent Item code is:
<TestProjectFolderPath Include="$([System.IO.Directory]::GetDirectories($(SolutionDir),*.Tests))">
<FolderName>$([System.IO.Path]::GetFileName(%(Identity)))</FolderName>
</TestProjectFolderPath>
I left out GetDirectoryName because it makes little sense calling that on the result of GetFileName.

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 Filtering ItemGroup of files with condition

This feels like it's so simple, but I cannot get it to work.
All I'm trying to achieve is a filtered list of the embedded resources. I've tried various approaches but I can't seem to get it right.
Here's what I thought was the right solution:
<ItemGroup>
<AllEmbeddedResources Include="#(EmbeddedResource)" Condition="$(FullPath.Contains('Change')"/>
</ItemGroup>
Edit...
To clarify, the results are without the condition, the list is all embedded resources, with the condition, the group is empty.
I've tried this inside and outside of target's, and I've tried getting the full list in one group, and then filtering in a separate group. I know I'm just misunderstanding some fundamental part of msbuild syntax, I just can't seem to work it out. Looking forward to being shown my stupid mistake!
Inside a target, this can be done using the batching syntax for items and using the System.String.Copy method to be able to call instance functions on the string:
<Target Name="ListAllEmbeddedResources">
<ItemGroup>
<AllEmbeddedResources Include="#(EmbeddedResource)" Condition="$([System.String]::Copy(%(FullPath)).Contains('Change'))" />
</ItemGroup>
<Message Importance="high" Text="AllEmbeddedResources: %(AllEmbeddedResources.Identity)" />
</Target>
Note that this syntax only works inside a target and not during static evaluation (item group directly under the <Project> node).
The Condition Attribute must return a boolean, and it operates on each element of the itemgroup.
You can access each element using %(Identity).
Say you have some unfiltered itemgroup called UnfilteredItems, and you want to filter those into a group called MyFilteredItems, using some regex pattern.
<ItemGroup>
<MyFilteredItems Include="#(UnfilteredItems)" Condition="$([System.Text.RegularExpressions.Regex]::Match(%(Identity),'.*\\bin\\.*').Success)"/>
</ItemGroup>

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- '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.

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>