MSBuild copy task and condition name from file with wildcards - msbuild

I have a FilesToExclude2.txt file which contains all excludes like this :
*.settings
*#*
*.vbproj*
*.csproj*
*\Errors\*
*\Errors
*\_vti_*\*
*\_vti_*
*\CVS\*
In my tasks file, there is a copy task
<Target Name="CustomModuleCopy">
<ItemGroup>
<ModuleFiles Include="$(SolutionModuleName)\$(ProjectModuleName)\**\*.*" />
<FileToExclude Include="$(BasePath)\$(SolutionModuleName)\FilesToExclude2.txt" />
</ItemGroup>
<ReadLinesFromFile File="#(FileToExclude)">
<Output TaskParameter="Lines" ItemName="FileContents" />
</ReadLinesFromFile>
<Copy SourceFiles="#(ModuleFiles)" DestinationFiles="#(ModuleFiles->'$(DestFolder)\$(ProjectModuleName)\%(RecursiveDir)%(Filename)%(Extension)')" Condition="'%(ModuleFiles.Identity)' != #(FileContents)" ContinueOnError="false" />
<!--CallTarget Targets="RemoveCustomModuleConfigFiles" /-->
</Target>
Without the "condition" attribute on the copy task, everything's works fine, but I do not know how to implement the condition with wildcard and if it is possible
Any kind of help would be greatly appreciated :) this is freaking me out for few hours...

Would it not be easier to just use the exclude property instead?
as in
Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ModuleFiles Include="c:\code\**\*.*"
Exclude="#(FileContents)" />
</ItemGroup>
<!--<Copy SourceFiles="#(ModuleFiles)" DestinationFolder="Some|Destination" Condition="'%(ModuleFiles.Identity)' != #(FileContents)" ContinueOnError="false" />
CallTarget Targets="RemoveCustomModuleConfigFiles" /-->

the following code
<Target Name="CustomModuleCopy">
<ItemGroup>
<FileToExclude Include="$(BasePath)\$(SolutionModuleName)\FilesToExclude.txt" />
</ItemGroup>
<ReadLinesFromFile File="#(FileToExclude)">
<Output TaskParameter="Lines" ItemName="FileContents" />
</ReadLinesFromFile>
<ItemGroup>
<ModuleFiles Include="$(SolutionModuleName)\$(ProjectModuleName)\**\*.*" Exclude="#(FileContents)" />
</ItemGroup>
<Copy SourceFiles="#(ModuleFiles)" DestinationFiles="#(ModuleFiles->'$(DestFolder)\$(ProjectModuleName)\%(RecursiveDir)%(Filename)%(Extension)')" ContinueOnError="false" />
<Message Text="Exclude = #(FileContents)" />
<!--CallTarget Targets="RemoveCustomModuleConfigFiles" /-->
</Target>
produce this output in console :
copy /y "CPBMessaging\CPBMessagingWeb_NotificationMessage\Web.Debug.config" "C:\__CPB\CPBSite\CPBMessagingWeb_NotificationMessage\Web.Debug.config"
Copying file from "CPBMessaging\CPBMessagingWeb_NotificationMessage\Web.Release.config" to "C:\__CPB\CPBSite\CPBMessagingWeb_NotificationMessage\Web.Release.config"
copy /y "CPBMessaging\CPBMessagingWeb_NotificationMessage\Web.Release.config" "C:\__CPB\CPBSite\CPBMessagingWeb_NotificationMessage\Web.Release.config"
Exclude = *.cab;*.config;*.vb;*.cs;*.resx;*.xsx;*.bak;*.myapp;*.settings;*#*;
*.vbproj*;*.csproj*;*\Errors\*;*\Errors;*\_vti_*\*;*\_vti_*;*\CVS\*;*\CVS;

Related

msbuild - export all properties defined in block to MSBuild calls

<Target Name="micropython_prebuild">
<PropertyGroup>
<uP_PrebuildPyExe>"$(ProjectDir)\Source\micropython\py\make_prebuild_wrapper.exe"</uP_PrebuildPyExe>
<GnuCat>$(uP_PrebuildPyExe) gnu_cat</GnuCat>
<GnuSed>$(uP_PrebuildPyExe) gnu_sed</GnuSed>
<uP_GenHdrFolder>$(ObjectFolder)\genhdr</uP_GenHdrFolder>
<uP_QSTR_GEN_EXTRA_CFLAGS>-DNO_QSTR -DN_X64 -DN_X86 -DN_THUMB -DN_ARM -DN_XTENSA</uP_QSTR_GEN_EXTRA_CFLAGS>
<uP_SRC>$(ProjectDir)\Source\micropython</uP_SRC>
<uP_MPY_CROSS>"$(ProjectDir)\Source\micropython\mpy-cross\mpy-cross.exe"</uP_MPY_CROSS>
</PropertyGroup>
<MakeDir Directories="$(uP_GenHdrFolder)\"/>
<MSBuild
Projects="$(MSBuildProjectFile)"
Condition="'' == ''"
Targets="prebuild_mpversion"
Properties="uP_PrebuildPyExe=$(uP_PrebuildPyExe);
uP_GenHdrFolder=$(uP_GenHdrFolder)"
/>
</Target>
How do I "export" all the properties I've defined at the top to the calls to MSBuild within this target?
Otherwise, I have to set the Properties of each MSBuild.
You can also create a single property that contains all the definitions:
<Target Name="micropython_prebuild">
<PropertyGroup>
<PrebuildProperties>
uP_PrebuildPyExe="$(ProjectDir)\Source\micropython\py\make_prebuild_wrapper.exe";
GnuCat=$(uP_PrebuildPyExe) gnu_cat;
GnuSed=$(uP_PrebuildPyExe) gnu_sed;
uP_GenHdrFolder=$(ObjectFolder)\genhdr;
uP_QSTR_GEN_EXTRA_CFLAGS=-DNO_QSTR -DN_X64 -DN_X86 -DN_THUMB -DN_ARM -DN_XTENSA;
uP_SRC=$(ProjectDir)\Source\micropython;
uP_MPY_CROSS="$(ProjectDir)\Source\micropython\mpy-cross\mpy-cross.exe";
</PrebuildProperties>
</PropertyGroup>
<MakeDir Directories="$(uP_GenHdrFolder)\"/>
<MSBuild
Projects="$(MSBuildProjectFile)"
Condition="'' == ''"
Targets="prebuild_mpversion"
Properties="$(PrebuildProperties)"
/>
</Target>

Retrieving the nupkg version number in msbuild

I've modified a csproj file in VS2017 to create a Nuget package when my .NET 4.5 project is release-built. The next automation step is to add this package to my private feed on a network share. Here are the commands I'm using:
<Exec Command="nuget.exe pack -Properties "Configuration=Release" -Symbols $(ProjectName).csproj" />
<Exec Command="nuget.exe add $(ProjectName).$(ProductVersion).nupkg -source \\MYSERVER\Nuget packages" />
Line 1 works and produces a nupkg file of the form productname.nn.nn.nn.nn.
However line 2 is not returning a value for the ProductVersion token (which was a guess on my part).
I've struggled to find a reference for MSBUILD tokens (that in itself would be useful to know), but what I really need to know is the correct MSBUILD token/variable/property for the the version - and that is the same value as the generated Nuget package.
I explored $(PackageVersion) suggested by Martin Ullrich, but it's not going to work with older projects even with Nuget 4.10 installed. Also I couldn't get Troopers sample to work how I wanted and I ended up with a variation on one of the posts here. I adapted it to give me the assembly version as opposed to the file version.
<UsingTask TaskName="GetVersionParts" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<AssemblyPath ParameterType="System.String" Required="true" />
<MajorVersion ParameterType="System.Int32" Output="true" />
<MinorVersion ParameterType="System.Int32" Output="true" />
<BuildVersion ParameterType="System.Int32" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.Reflection" />
<Code Type="Fragment" Language="cs">
<![CDATA[
Version v = AssemblyName.GetAssemblyName(this.AssemblyPath).Version;
this.MajorVersion = v.Major;
this.MinorVersion = v.Minor;
this.BuildVersion = v.Build;
]]>
</Code>
</Task>
</UsingTask>
<Target Name="AfterBuild" Condition=" '$(Configuration)' == 'Release'">
<Message Text="**** After-build process starting ****" />
<Exec Command="nuget.exe pack -Properties "Configuration=Release" -Symbols $(ProjectName).csproj" />
<GetVersionParts AssemblyPath="$(OutputPath)$(AssemblyName).dll">
<Output TaskParameter="MajorVersion" PropertyName="MajorVersionNumber" />
<Output TaskParameter="MinorVersion" PropertyName="MinorVersionNumber" />
<Output TaskParameter="BuildVersion" PropertyName="BuildVersionNumber" />
</GetVersionParts>
<Exec Command="nuget.exe add $(MSBuildProjectName).$(MajorVersionNumber).$(MinorVersionNumber).$(BuildVersionNumber).nupkg -source "\\My feed server\Shared location"" />
<Exec Command="move *.symbols.nupkg "\\My feed server\Shared location\Symbols"" />
<Message Text="**** After-build process completed ****" />
</Target>
Although this works, I can't help feeling that it should be easier.
Further reading
https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-inline-tasks
https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-properties
https://learn.microsoft.com/en-us/nuget/tools/nuget-exe-cli-reference#add
You could get the assembly version with this macro. I call it before the postbuild event for example
<!--Macro to get the version -->
<Target Name="PostBuildMacros">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<ItemGroup>
<VersionNumber Include="%(CurrentAssembly.Version)" />
</ItemGroup>
</Target>
<!-- override PostBuildEvent to call PostBuildMacros -->
<PropertyGroup>
<PostBuildEventDependsOn>
$(PostBuildEventDependsOn);
PostBuildMacros;
</PostBuildEventDependsOn>
<PostBuildEvent>
...
</PostBuildEvent>
</PropertyGroup>

Files Include/Exclude based on subfolder name

I have the following file structure:
\RootFolder\
\RootFolder\SubFolder1\
\RootFolder\SubFolder1\SubfolderA\
\RootFolder\SubFolder1\SubfolderA\bin\
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile1.txt
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile2.txt
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile3.txt
\RootFolder\SubFolder1\SubfolderA\bin\DontWantFile.txt
\RootFolder\SubFolder1\SubfolderA\obj\
\RootFolder\SubFolder1\SubfolderA\obj\MySameNameFile1.txt
\RootFolder\SubFolder1\SubfolderA\obj\MySameNameFile2.txt
\RootFolder\SubFolder1\SubfolderA\obj\MySameNameFile3.txt
\RootFolder\SubFolder2\
\RootFolder\SubFolder2\SubfolderB\
\RootFolder\SubFolder2\SubfolderB\bin\
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile1.txt
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile2.txt
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile3.txt
\RootFolder\SubFolder2\SubfolderB\bin\DontWantFile.txt
\RootFolder\SubFolder2\SubFolder2\obj\
\RootFolder\SubFolder2\SubfolderB\obj\MySameNameFile1.txt
\RootFolder\SubFolder2\SubfolderB\obj\MySameNameFile2.txt
\RootFolder\SubFolder2\SubfolderB\obj\MySameNameFile3.txt
\RootFolder\SubFolder3\
\RootFolder\SubFolder3\SubfolderC\
\RootFolder\SubFolder3\SubfolderC\bin\
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile1.txt
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile2.txt
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile3.txt
\RootFolder\SubFolder3\SubfolderC\bin\DontWantFile.txt
\RootFolder\SubFolder3\SubfolderC\obj\
\RootFolder\SubFolder3\SubfolderC\obj\MySameNameFile1.txt
\RootFolder\SubFolder3\SubfolderC\obj\MySameNameFile2.txt
\RootFolder\SubFolder3\SubfolderC\obj\MySameNameFile3.txt
I'm trying to get all "MySameNameFile*.*" files, but NOT the ones in the \obj\ directories.
What I've tried:
<PropertyGroup>
<MyRootFolderVariable>.\RootFolder\</MyRootFolderVariable>
</PropertyGroup>
<Target Name="MyTarget">
<ItemGroup>
<MyExcludeFiles Include="$(MyRootFolderVariable)\**\obj\*.*" />
</ItemGroup>
<ItemGroup>
<MyIncludeFiles Include="$(MyRootFolderVariable)\**\*MySameNameFile*.txt" Exclude="#(MyExcludeFiles)" />
</ItemGroup>
<PropertyGroup>
<BinFilesButNoObjFiles>#(MyIncludeFiles->'"%(fullpath)"' , ' ')</BinFilesButNoObjFiles>
</PropertyGroup>
<Message Text="BinFilesButNoObjFiles=$(BinFilesButNoObjFiles)"/>
<Message Text=" "/>
<Message Text=" "/>
</Target>
Files I'm after:
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile1.txt
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile2.txt
\RootFolder\SubFolder1\SubfolderA\bin\MySameNameFile3.txt
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile1.txt
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile2.txt
\RootFolder\SubFolder2\SubfolderB\bin\MySameNameFile3.txt
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile1.txt
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile2.txt
\RootFolder\SubFolder3\SubfolderC\bin\MySameNameFile3.txt
I was close. Note the second "**" in the answer below.
<MyExcludeFiles Include="$(MyRootFolderVariable)\**\obj\**\*.*" />

Why does the MsbuildExtension Detokenise class reload the project?

This is problematic because any Properties being passed in are lost.
Further Explanation: I pass in a property to the project file. This property is a path to a .props file. It contains tokens and replacement values for the detokenise class. The task apparently reloads the project and the path is not maintained. This doesn't seem to be the case for other task, for example the guid tasks.
In the example I am using a example proj entitled guids.proj
Invoked Using :
<MSBuild.ExtensionPack.FileSystem.Detokenise TaskAction="Detokenise" TargetFiles="#(FileCollectionToBeDetokenized )"/>
Some command line out put follows :
Task "MSBuild.ExtensionPack.FileSystem.Detokenise" (TaskId:11)
Detokenise Task Execution Started [13:04:35] (TaskId:11)
Loading Project: C:\Users\bstrausser\Desktop\guids.proj (TaskId:11)
Detokenising Collection: 1 files (TaskId:11)
C:\Users\*****\Desktop\guids.proj(37,9): error : Property not found: Asset
Directory
Full project file :
Project ToolsVersion="4.0" DefaultTargets="Default" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks" Condition="Exists('$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks')"/>
<Import Project="C:\Program Files (x86)\MSBuild\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks" Condition="!Exists('$(ParentMSBuildProjectDirectory)\Bin\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks') AND Exists('C:\Program Files (x86)\MSBuild\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks')"/>
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
<PROPS>$(DACP)</PROPS>
</PropertyGroup>
<Import Project="$(PROPS)" Condition="'$(DACP)' != ''" />
<Target Name="Default">
<Message text = "$(DACP)" />
<!-- Create a new Guid and get the formatted and unformatted values -->
<MSBuild.ExtensionPack.Framework.Guid TaskAction="Create">
<Output TaskParameter="FormattedGuidString" PropertyName="FormattedGuidString1" />
<Output TaskParameter="GuidString" PropertyName="GuidStringItem" />
</MSBuild.ExtensionPack.Framework.Guid>
<Message Text="GuidStringItem: $(GuidStringItem)"/>
<Message Text="FormattedGuidString: $(FormattedGuidString1)"/>
<!-- Create a new cryptographically strong Guid and get the formatted and unformatted values -->
<MSBuild.ExtensionPack.Framework.Guid TaskAction="CreateCrypto">
<Output TaskParameter="FormattedGuidString" PropertyName="FormattedGuidString1" />
<Output TaskParameter="GuidString" PropertyName="GuidStringItem" />
</MSBuild.ExtensionPack.Framework.Guid>
<Message Text="GuidStringItem Crypto: $(GuidStringItem)"/>
<Message Text="FormattedGuidString Crypto: $(FormattedGuidString1)"/>
<ItemGroup>
<FileCollectionToBeDetokenized Include="C:\Code\MSBuildGit\Configuration\TaskExecutorConfigTransforms\App.GREEN.SCRATCH.config"/>
</ItemGroup>
<Message text = "BaseUrl : $(BaseUrl)" />
<Message text = "DetokenizedTransformFile : #(FileCollectionToBeDetokenized)" />
<MSBuild.ExtensionPack.FileSystem.Detokenise TaskAction="Detokenise" TargetFiles="#(FileCollectionToBeDetokenized )"/>
</Target>

Copy Item's Metadata for batching

This is an extension to this question.
Suppose I'm creating items from other item's metadata:
<ItemGroup>
<A>
<files>f1;f2;f3;...</files>
<x>...</x>
<y>...</y>
<z>...</z>
...
</A>
<B Include="%(A.files)">
<x>%(A.x)</x>
<y>%(A.y)</y>
<z>%(A.z)</z>
...
</B>
</ItemGroup>
%(A.files) is a list of files separated by ;, such that for each A item I'm creating many B items (one for each file).
But frequently when I process B item I need the original's A item metadata. In this example I copied each metadata manually from A to B.
Is there a way to copy all of A's metadata to B without explicitly specifying each one of them?
Little late, but I like this solution better:
<B Include="#(A->Metadata('files'))" />
Full Example:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<A Include="A1">
<files>a1_file1.htm;a1_file2.htm</files>
<x>a1</x>
<y>b1</y>
<z>c1</z>
</A>
<A Include="A2">
<files>a2_file.proj</files>
<x>a2</x>
<y>b2</y>
<z>c2</z>
</A>
<B Include="#(A->Metadata('files'))" />
</ItemGroup>
<Target Name="Build">
<Message Text="A: #(A->'%(Identity) x:%(x) y:%(y) z:%(z) files:%(files)', '
')" />
<Message Text="B: #(B->'%(Identity) x:%(x) y:%(y) z:%(z) files:%(files)', '
')" />
</Target>
</Project>
Output:
Build:
A: A1 x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
A2 x:a2 y:b2 z:c2 files:a2_file.proj
B: a1_file1.htm x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
a1_file2.htm x:a1 y:b1 z:c1 files:a1_file1.htm;a1_file2.htm
a2_file.proj x:a2 y:b2 z:c2 files:a2_file.proj
As far as I can tell, MSBuild only copies metadata when there is a one-to-one mapping between the input and output item lists. In your case, you're starting with one item and expanding to many items. To get around this, I suggest using item batching:
<?xml version="1.0" encoding="iso-8859-1"?>
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="4.0"
DefaultTargets="Print">
<ItemGroup>
<A Include="A1">
<files>test.htm;test_sol1.htm</files>
<x>a1</x>
<y>b1</y>
<z>c1</z>
</A>
<A Include="A2">
<files>test.proj</files>
<x>a2</x>
<y>b2</y>
<z>c2</z>
</A>
</ItemGroup>
<Target Name="ExpandA">
<ItemGroup>
<ExpandedA Include="%(A.files)">
<Original>%(Identity)</Original>
</ExpandedA>
</ItemGroup>
</Target>
<Target
Name="CopyMetadata"
Outputs="%(ExpandedA.Identity)"
DependsOnTargets="ExpandA">
<PropertyGroup>
<ExpandedAIdentity>%(ExpandedA.Identity)</ExpandedAIdentity>
<ExpandedAOriginal>%(ExpandedA.Original)</ExpandedAOriginal>
</PropertyGroup>
<ItemGroup>
<ExpandedAMetadata Include="#(A)" Condition=" '%(Identity)' == '$(ExpandedAOriginal)' ">
<Expanded>$(ExpandedAIdentity)</Expanded>
</ExpandedAMetadata>
</ItemGroup>
</Target>
<Target Name="Print" DependsOnTargets="CopyMetadata">
<ItemGroup>
<B Include="#(ExpandedAMetadata->'%(Expanded)')" />
</ItemGroup>
<!--Use commas to illustrate that "files" has been expanded-->
<Message Text="A: %(A.files)" />
<Message Text="ExpandedA: #(ExpandedA, ',')" />
<Message Text="ExpandedAMetadata: #(ExpandedAMetadata, ',')" />
<Message Text="B: #(B->'%(Identity) x:%(x) y:%(y) z:%(z)', ',')" />
</Target>
</Project>
and the output of the "Print" target:
Print:
A: test.htm;test_sol1.htm
A: test.proj
ExpandedA: test.htm,test_sol1.htm,test.proj
ExpandedAMetadata: A1,A1,A2
B: test.htm x:a1 y:b1 z:c1,test_sol1.htm x:a1 y:b1 z:c1,test.proj x:a2 y:b2 z:c2
ExpandedA is similar to B in your original question; it is the expanded version of A but without any metadata. Then I run the CopyMetadata target once for each item in ExpandedA (thanks to item batching). Each run, the original A item is copied to the ExpandedAMetadata item group along with all of its metadata. The Original metadata is used to ensure that the correct A item is associated with each file. Finally, in the Print target, B is constructed using an item transformation, so that all the metadata from ExpandedAMetadata is copied over as well.
I'm not sure if I get what you need, but you can copy all A metedata to B as in the answer for related question:
<B Include="#(A)">
It should copy all metedata from A to B.
Here is a direct answer to the original question:
<!-- Copy all metadata from an input item to an output item -->
<UsingTask
TaskName="CopyMetadataTask"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<InputItem ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
<OutputItem ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
InputItem.CopyMetadataTo(OutputItem);
]]>
</Code>
</Task>
</UsingTask>
You can use this task to copy metadata between items the way it was originally intended, and it can easily be adapted to multiple items on input/output, though doing so does induce issues whereby it is necessary to match the inputs and outputs 1/1, or make other compromises, though it can easily be adapted to the exact situation since you can modify the C# code however is necessary. E.g.:
<UsingTask
TaskName="CopyMetadataTask"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<InputItem ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<OutputItem ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
for (int i = 0; i < InputItem.Length; ++i)
{
InputItem[i].CopyMetadataTo(OutputItem[i]);
}
]]>
</Code>
</Task>
</UsingTask>
The task can then be called to copy metadata directly, for example (using the second version):
<ItemGroup>
<ItemListA Include="a;b;c" Metadata="MetadataValue" />
<ItemListB Include="d;e;f" OtherMetadata="OtherValue" />
</ItemGroup>
<CopyMetadataTask InputItem="#(ItemListA)" OutputItem="#(ItemListB)" />