Empty an MSBuild ItemGroup - msbuild

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.

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.

Dynamic Metadata Assignment in an ItemGroup

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.

re-evaluate an msbuild item group

I have an item group that includes a location which may or may not contain files. If there are no files present at the point the item group is declared, is it possible to re-evaluate the item group at a later time to pick up files that may have been generated in the new location, or will I have to declare an identical item group at this time and use that?
Item groups declared statically (outside of a Target, as a child element of the ) will be evaluated when the file is loaded. Item groups declared dynamically (within a <Target>) will be evaluated at the moment the execution passes through the target. For cases where the files are created during the build, you really should use a dynamic Item group.
I think you will have to create a new itemgroup. They are evaluated once and the value is saved, not the formula used to select files. Thus you can't "re-evaluate" these items.
You can re-define the ItemGroup by first removing the items, and the re-including the items:
<Target Name="Later on" >
<ItemGroup>
<ClCompile Remove="#(ClCompile)" />
<ClCompile Include="something here of your choice" />
</ItemGroup>
</Target>
Or if you don't want nor need to remove items, you can always simply append or add to the previously existing item group:
<Target Name="Later on" >
<ItemGroup>
<ClCompile Include="Add Even more stuff" />
</ItemGroup>
</Target>