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

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.

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

How can I use perform MSBuild batching on Items that don't represent files?

The MSBuild documentation hints in several places that Items aren't necessarily the same as files.
"MSBuild items are inputs into the build system, and they typically represent files."
"Items are objects that typically represent files."
However, I can't seem to find any examples where Items do not represent files. In particular, I would like to perform batching over a set of non-file items. But every item that I create, even from a custom build task, somehow acquires file-like metadata (FullPath, RootDir, Filename, Extension, etc.). Furthermore, I'm confused about the ramifications of setting the Inputs of a target to a set of items that aren't files, and what to use as that target's Outputs.
Does anybody have an example of using non-file Items to perform batching in MSBuild?
edit
Sorry for taking so long to come up with an example. I understand things a bit more, but I'm still uncertain (and there seems to be a complete lack of documentation about this). Everything here is going off my recollection; I'm not at my work computer right now, so I can't verify any of it.
In my experience, MSBuild doesn't like to build multiple configurations of a .sln file in one go. So, this:
msbuild.exe SampleMSBuild.sln /p:Configuration=Debug%3BRelease
(The encoded semicolon being necessary so that it doesn't try to define multiple properties.)
Produces this:
"D:\src\SampleMSBuild\SampleMSBuild.sln" (default target) (1) ->
(ValidateSolutionConfiguration target) ->
D:\src\SampleMSBuild\SampleMSBuild.sln.metaproj : error MSB4126: The
specified solution configuration "Debug;Release|Any CPU" is invalid.
Please specify a valid solution configuration using the Configuration
and Platform properties (e.g. MSBuild.exe Solution.sln
/p:Configuration=Debug /p:Platform="Any CPU") or leave those properties
blank to use the default solution configuration.
[D:\src\SampleMSBuild\SampleMSBuild.sln]
So, it seems like it should be possible to use batching and items to handle this.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTarget="Rebuild"
ToolsVersion="4.0">
<ItemGroup>
<Configurations Include="Debug" />-->
<Configurations Include="Release" />-->
</ItemGroup>
<UsingTask TaskName="LogMetadata"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Items ParameterType="Microsoft.Build.Framework.ITaskItem[]"
Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[
foreach (var item in Items) {
Console.Write(item.ItemSpec);
Console.Write(" {");
foreach (string metadataKey in item.MetadataNames) {
Console.Write(metadataKey);
Console.Write("=\"");
Console.Write(item.GetMetadata(metadataKey).
ToString().Replace("\"", "\\\""));
Console.Write("\" ");
}
Console.WriteLine("}");
}]]>
</Code>
</Task>
</UsingTask>
<Target Name="Rebuild">
<LogMetadata Items="%(Configurations.Identity)" />
</Target>
</Project>
Which produces this:
Debug {
FullPath="D:\src\SampleMSBuild\Debug"
RootDir="D:\"
Filename="Debug"
Extension=""
RelativeDir=""
Directory="src\SampleMSBuild\"
RecursiveDir=""
Identity="Debug"
ModifiedTime=""
CreatedTime=""
AccessedTime=""
}
Release {
FullPath="D:\src\SampleMSBuild\Release"
RootDir="D:\"
Filename="Release"
Extension=""
RelativeDir=""
Directory="src\SampleMSBuild\"
RecursiveDir=""
Identity="Release"
ModifiedTime=""
CreatedTime=""
AccessedTime=""
}
As you can see, the items have all kinds of file metadata attached to them. I can't drop the Include attribute, since it's required, but I could synthesize the items in a custom task. HOWEVER, when I do that, they still somehow magically gain all the same file metadata.
What are the ramifications of this? Since I haven't specified these as Inputs or Outputs to a target, will the file metadata cause any problems? Will the build system skip over targets, or build more than it needs to, because the files specified in the Items' FullPath metadata do not exist? What if those files did exist? Would it cause any problems?
I use items to build several solutions after each other and doing some "manual" task with them, therefore I define my own items:
<ItemGroup>
<MergeConfigurations Include="project1\project1.SDK.sln">
<MergeOutAssemblyName>product1.dll</MergeOutAssemblyName>
<MergePrimaryAssemblyName>project1.Interfaces.dll</MergePrimaryAssemblyName>
<SolutionBinaries>project1\bin\$(FlavorToBuild)</SolutionBinaries>
</MergeConfigurations>
<MergeConfigurations Include="project1\project1.Plugin.sln">
<MergeOutAssemblyName>product1.dll</MergeOutAssemblyName>
<MergePrimaryAssemblyName>project1.Interfaces.dll</MergePrimaryAssemblyName>
<SolutionBinaries>project1\bin\plugin\$(FlavorToBuild)</SolutionBinaries>
</MergeConfigurations>
<ItemGroup>
Then I use a target to take the information and do what is necessary:
<Target Name="MergeSolution"
Inputs="%(MergeConfigurations.Identity)"
Outputs="%(MergeConfigurations.Identity)\Ignore_this">
<PropertyGroup>
<MergeSolution>%(MergeConfigurations.Identity)</MergeSolution>
<MergeOutAssemblyName>%(MergeConfigurations.MergeOutAssemblyName)</MergeOutAssemblyName>
<MergePrimaryAssemblyName>%(MergeConfigurations.MergePrimaryAssemblyName)</MergePrimaryAssemblyName>
<SolutionBinaries>%(MergeConfigurations.SolutionBinaries)</SolutionBinaries>
</PropertyGroup>
[....]
</Target>
Hope this helps to point you in the direction you need.
Tricky question. I'm an MSBuild noob, and found myself needing to understand batching recently, so figured I'd share some of what I found.
In general, yes, it seems that most everywhere you see samples and discussions about ITaskItems they tend to be about files. But the underlying implementation is very flexible and can deal with many other things as well. In my case, I've been working with strings and XML data.
This MS article gives some great examples of non-file ItemGroups and metadata.
This article was the best summary I can find that talks about the mechanics of Items and how they are different than Properties. It also covers the whole bit about # and % syntax, converting between strings and Items, and a hint as to where those file metadata properties are coming from - MSBuild is optimized for it.
Whenever you have an interface used as a parameter to a task or whatever, there is going to be a default implementation of that interface somewhere. My guess is that your code sample is newing up a number of these default objects under the hood and those define the metadata that are created by default. If you were to implement this interface yourself, I'll wager you could change this behavior. Probably beyond the scope of the question though =)

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>