I feel like I've fixed this before, but I can't remember how.
I have a tasks file that looks like this (CustomTasks.tasks):
<UsingTask AssemblyFile="CustomTasks.dll" TaskName="MyCustomTask"/>
it references an assembly (namely Ionic.Zip.dll). Ionic.Zip.dll is not in the GAC (and I don't want it to be). It sits right next to my CustomTasks.dll.
I have a directory called MSBuild one level up from my sln file which has CustomTasks.tasks, CustomTasks.dll and Ionic.Zip.dll in it.
I have a csproj that references the tasks file and calls the custom task:
<Import Project="$(ProjectDir)\..\MSBuild\CustomTasks.tasks" />
<MyCustomTask ..... />
at build time, this yields:
The "MyCustomTask" task could not be loaded from the assembly ....MyCustomTasks.dll. Could not load file or assembly 'Ionic.Zip,......' or one of its dependencies.
Got tired and frustrated and took a direct approach...I don't think this is the same way I solved the problem previously...but maybe this will help someone else. Other, more elegant solutions are more than welcome.
<Target Name="BeforeBeforeBuild" BeforeTargets="BeforeBuild">
<HandleAssemblyResolve SearchPath="$(ProjectDir)\..\MSBuild\" />
</Target>
<UsingTask TaskName="HandleAssemblyResolve" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<SearchPath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Reflection" />
<Code Type="Fragment" Language="cs">
<![CDATA[
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
var assemblySearchPath = Path.Combine(SearchPath, e.Name.Split(',')[0]);
if (File.Exists(assemblySearchPath)) return Assembly.LoadFrom(assemblySearchPath);
return null;
};
]]>
</Code>
</Task>
</UsingTask>
This is actually easy to fix. Put your custom build tasks and dependencies in a different folder. Then dependencies are loaded correctly.
For example like so:
<UsingTask AssemblyFile="..\BuildTools\CustomTasks.dll" TaskName="MyCustomTask"/>
Related
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>
We have an (old) build definition is using the UpgradeTemplate.xaml on TFS 2015 (and an underlying TFSBuild.proj which has a heap of custom actions). As such, the task of properly modernising the build is going to take time.
I'd like to hack the UpgradeTemplate to add in C#6/VB14 support without requiring a full re-write of the build definition, in order to keep the devs happy.
I attempted to edit the UpgradeTemplate.xaml to add a ToolPath property on the TfsBuild. However, now that I have done this, I get the following error on nearly all my projects:
ArgumentOutOfRangeException: Index and length must refer to a location within the string. Parameter name: length
On investigation, the lines of code in these projects all look like this:
<MSBuild.ExtensionPack.VisualStudio.TfsVersion TaskAction="GetVersion"
BuildName="$(BuildDefinition)" TfsBuildNumber="$(BuildNumber)"
VersionFormat="DateTime" DateFormat="MMdd" Major="$(MajorVersion)"
Minor="$(MinorVersion)">
The values of these variables as set printed out by Message tasks on the vbproj:
BuildDefinition: MyBuild-Testing
BuildNumber: 57902
MajorVersion: 43
MinorVersion: 2
The Build server has version 3.5.10 on the MSBuild ExtensionPack installed.
How do I resolve this issue? I'm testing this with a new build definition to allow devs to continue working while I get this set up, so I don't want to replace the ExtensionPack with the latest release (if possible) if it is likely to break the existing build.
Upgrade Template
<Activity mc:Ignorable="sad" x:Class="TfsBuild.Process" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mtbc="clr-namespace:Microsoft.TeamFoundation.Build.Client;assembly=Microsoft.TeamFoundation.Build.Client" xmlns:mtbw="clr-namespace:Microsoft.TeamFoundation.Build.Workflow;assembly=Microsoft.TeamFoundation.Build.Workflow" xmlns:mtbwa="clr-namespace:Microsoft.TeamFoundation.Build.Workflow.Activities;assembly=Microsoft.TeamFoundation.Build.Workflow" xmlns:mtbwt="clr-namespace:Microsoft.TeamFoundation.Build.Workflow.Tracking;assembly=Microsoft.TeamFoundation.Build.Workflow" xmlns:mtvc="clr-namespace:Microsoft.TeamFoundation.VersionControl.Client;assembly=Microsoft.TeamFoundation.VersionControl.Client" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:sad="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:sad1="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:this="clr-namespace:TfsBuild;" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Members>
<x:Property Name="ConfigurationFolderPath" Type="InArgument(x:String)" />
<x:Property Name="AgentSettings" Type="InArgument(mtbwa:AgentSettings)" />
<x:Property Name="MSBuildArguments" Type="InArgument(x:String)" />
<x:Property Name="MSBuildPlatform" Type="InArgument(mtbwa:ToolPlatform)" />
<x:Property Name="DoNotDownloadBuildType" Type="InArgument(x:Boolean)" />
<x:Property Name="LogFilePerProject" Type="InArgument(x:Boolean)" />
<x:Property Name="SourcesSubdirectory" Type="InArgument(x:String)" />
<x:Property Name="BinariesSubdirectory" Type="InArgument(x:String)" />
<x:Property Name="TestResultsSubdirectory" Type="InArgument(x:String)" />
<x:Property Name="RecursionType" Type="InArgument(mtvc:RecursionType)" />
<x:Property Name="Verbosity" Type="InArgument(mtbw:BuildVerbosity)" />
<x:Property Name="Metadata" Type="mtbw:ProcessParameterMetadataCollection" />
<x:Property Name="SupportedReasons" Type="mtbc:BuildReason" />
</x:Members>
<this:Process.ConfigurationFolderPath>
<InArgument x:TypeArguments="x:String" />
</this:Process.ConfigurationFolderPath>
<this:Process.AgentSettings>[New Microsoft.TeamFoundation.Build.Workflow.Activities.AgentSettings() With {.MaxWaitTime = New System.TimeSpan(4, 0, 0), .MaxExecutionTime = New System.TimeSpan(0, 0, 0), .TagComparison = Microsoft.TeamFoundation.Build.Workflow.Activities.TagComparison.MatchExactly }]</this:Process.AgentSettings>
<this:Process.MSBuildArguments>
<InArgument x:TypeArguments="x:String" />
</this:Process.MSBuildArguments>
<this:Process.MSBuildPlatform>[Microsoft.TeamFoundation.Build.Workflow.Activities.ToolPlatform.Auto]</this:Process.MSBuildPlatform>
<this:Process.DoNotDownloadBuildType>[False]</this:Process.DoNotDownloadBuildType>
<this:Process.LogFilePerProject>[False]</this:Process.LogFilePerProject>
<this:Process.SourcesSubdirectory>
<InArgument x:TypeArguments="x:String" />
</this:Process.SourcesSubdirectory>
<this:Process.BinariesSubdirectory>
<InArgument x:TypeArguments="x:String" />
</this:Process.BinariesSubdirectory>
<this:Process.TestResultsSubdirectory>
<InArgument x:TypeArguments="x:String" />
</this:Process.TestResultsSubdirectory>
<this:Process.RecursionType>[Microsoft.TeamFoundation.VersionControl.Client.RecursionType.OneLevel]</this:Process.RecursionType>
<this:Process.Verbosity>[Microsoft.TeamFoundation.Build.Workflow.BuildVerbosity.Normal]</this:Process.Verbosity>
<this:Process.Metadata>
<mtbw:ProcessParameterMetadataCollection />
</this:Process.Metadata>
<this:Process.SupportedReasons>All</this:Process.SupportedReasons>
<mva:VisualBasic.Settings>Assembly references and imported namespaces serialized as XML namespaces</mva:VisualBasic.Settings>
<Sequence mtbwt:BuildTrackingParticipant.Importance="None">
<Sequence.Variables>
<Variable x:TypeArguments="mtbc:IBuildDetail" Name="BuildDetail" />
</Sequence.Variables>
<mtbwa:GetBuildDetail DisplayName="Get the Build" Result="[BuildDetail]" />
<mtbwa:InvokeForReason DisplayName="Update Build Number for Triggered Builds" Reason="Triggered">
<mtbwa:UpdateBuildNumber BuildNumberFormat="["$(BuildDefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)"]" DisplayName="Update Build Number" />
</mtbwa:InvokeForReason>
<mtbwa:AgentScope DisplayName="Run On Agent" MaxExecutionTime="[AgentSettings.MaxExecutionTime]" MaxWaitTime="[AgentSettings.MaxWaitTime]" ReservationSpec="[AgentSettings.GetAgentReservationSpec()]">
<mtbwa:AgentScope.Variables>
<Variable x:TypeArguments="x:String" Name="buildDirectory" />
</mtbwa:AgentScope.Variables>
<mtbwa:GetBuildDirectory DisplayName="Get the Build Directory" Result="[buildDirectory]" />
<If Condition="[Not String.IsNullOrEmpty(ConfigurationFolderPath)]" DisplayName="If Not String.IsNullOrEmpty(ConfigurationFolderPath)">
<If.Then>
<mtbwa:TfsBuild BinariesSubdirectory="[BinariesSubdirectory]" BuildDirectory="[buildDirectory]" CommandLineArguments="[MSBuildArguments]" ConfigurationFolderPath="[ConfigurationFolderPath]" DisplayName="Run TfsBuild for Configuration Folder" DoNotDownloadBuildType="[DoNotDownloadBuildType]" LogFilePerProject="[LogFilePerProject]" RecursionType="[RecursionType]" SourcesSubdirectory="[SourcesSubdirectory]" TestResultsSubdirectory="[TestResultsSubdirectory]" ToolPath="C:\Program Files (x86)\MSBuild\12.0\Bin\" ToolPlatform="[MSBuildPlatform]" Verbosity="[Verbosity]" />
</If.Then>
</If>
<If Condition="[BuildDetail.CompilationStatus = Microsoft.TeamFoundation.Build.Client.BuildPhaseStatus.Unknown]" DisplayName="If CompilationStatus = Unknown">
<If.Then>
<mtbwa:SetBuildProperties CompilationStatus="[Microsoft.TeamFoundation.Build.Client.BuildPhaseStatus.Succeeded]" DisplayName="Set CompilationStatus to Succeeded" PropertiesToSet="CompilationStatus" />
</If.Then>
</If>
<If Condition="[BuildDetail.TestStatus = Microsoft.TeamFoundation.Build.Client.BuildPhaseStatus.Unknown]" DisplayName="If TestStatus = Unknown">
<If.Then>
<mtbwa:SetBuildProperties DisplayName="Set TestStatus to Succeeded" PropertiesToSet="TestStatus" TestStatus="[Microsoft.TeamFoundation.Build.Client.BuildPhaseStatus.Succeeded]" />
</If.Then>
</If>
</mtbwa:AgentScope>
<mtbwa:InvokeForReason Reason="CheckInShelveset">
<mtbwa:CheckInGatedChanges DisplayName="Check In Gated Changes" />
</mtbwa:InvokeForReason>
</Sequence>
</Activity>
In Particular, I added ToolPath="C:\Program Files (x86)\MSBuild\12.0\Bin\" to line 58.
TFSBuild.proj
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\SDC\Microsoft.Sdc.Common.tasks" />
<Import Project="$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks"/>
<ProjectExtensions>
<!-- Team Foundation Build Version - DO NOT CHANGE -->
<ProjectFileVersion>2</ProjectFileVersion>
</ProjectExtensions>
<PropertyGroup>
<RunCodeAnalysis>Never</RunCodeAnalysis>
<UpdateAssociatedWorkItems>false</UpdateAssociatedWorkItems>
<AdditionalVCOverrides></AdditionalVCOverrides>
<CustomPropertiesForClean></CustomPropertiesForClean>
<CustomPropertiesForBuild></CustomPropertiesForBuild>
<SkipGetChangesetsAndUpdateWorkItems>False</SkipGetChangesetsAndUpdateWorkItems>
<SkipWorkItemCreation>true</SkipWorkItemCreation>
<BuildConfigurationsInParallel>true</BuildConfigurationsInParallel>
<SkipDropBuild>false</SkipDropBuild>
</PropertyGroup>
<ItemGroup>
<SolutionToBuild Include="$(BuildProjectFolderPath)/SolutionsToBuild/Common.sln">
<Targets></Targets>
<Properties></Properties>
</SolutionToBuild>
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<PropertyGroup>
<SkipClean>false</SkipClean>
<SkipInitializeWorkspace>true</SkipInitializeWorkspace>
<ForceGet>true</ForceGet>
<IncrementalBuild>false</IncrementalBuild>
</PropertyGroup>
</Project>
Update
I think it might be something to do with running a private build (Latest + Shelveset). When I run a normal build, the BuildNumber variable is MyBuild-Testing_20170328.1. This appears to be working fine.
This issue is due to a difference between Private and Public builds. With Public Builds, the build is immediately numbered as BuildName_DateFormat.BuildNumber. Private builds however, are just numeric (e.g. 57902 above).
The Code in the extension does the following:
string buildstring = this.TfsBuildNumber.Replace(string.Concat(this.BuildName, "_"),
string.Empty);
char[] chrArray = new char[] { '.' };
string[] buildParts = buildstring.Split(chrArray, StringSplitOptions.RemoveEmptyEntries);
DateTime t = new DateTime(Convert.ToInt32(buildParts[0].Substring(0, 4),
CultureInfo.CurrentCulture),
Convert.ToInt32(buildParts[0].Substring(4, 2),
CultureInfo.CurrentCulture),
Convert.ToInt32(buildParts[0].Substring(6, 2),
CultureInfo.InvariantCulture));
Are you can see, its substringing on the build name, assuming it's been stripped down to the date component (20170328). Private builds aren't this long, and so fail.
This build works fine when running as a public build - basically it means that private builds on this definition are not available until an upgrade takes place.
How can I get the list of project files from a solution when using MSBUILD?
For example getting all the .csproj from a .sln.
I was previously using MSBuild Community Tasks's GetSolutionProjects for this but unfortunately it has a dependency on .NET 3.5.
To accomplish this using a CodeTask (available since .NET 4) do the following:
<UsingTask TaskName="GetProjectsFromSolutionCodeTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup>
<Solution ParameterType="System.String" Required="true"/>
<Output ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true"/>
</ParameterGroup>
<Task>
<Reference Include="System.Xml"/>
<Reference Include="Microsoft.Build"/>
<Using Namespace="Microsoft.Build.Construction"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
var _solutionFile = SolutionFile.Parse(Solution);
Output = _solutionFile.ProjectsInOrder
.Where(proj => proj.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat)
.Select(proj => new TaskItem(proj.AbsolutePath))
.ToArray();
]]>
</Code>
</Task>
</UsingTask>
and invoke it like so:
<!-- Gets the projects composing the specified solution -->
<Target Name="GetProjectsFromSolution">
<GetProjectsFromSolutionCodeTask Solution="%(Solution.Fullpath)">
<Output ItemName="ProjectFiles" TaskParameter="Output"/>
</GetProjectsFromSolutionCodeTask >
</Target>
This will populate a ProjectFiles item collection with the absolute path of all the projects within the solution.
Please note: path to CodeTaskFactory varies by MSBuild version. Example here is for MSBuild 14.0.
Can I find out the sizes of a set of files from an MSBuild script without writing my own code?
MSBuild itself some metadata on individual items (for example, the last modified time at %(ModifiedTime)
), but no sizes. I can't see anything at http://msbuildextensionpack.com/.
Edit: based on Seva's answer, here's an inline task that returns the total size of an array of items:
<UsingTask TaskName="GetFileSize" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<TotalSize ParameterType="System.Int64" Output="true"/>
</ParameterGroup>
<Task>
<Using Namespace="System.IO"/>
<Code Type="Fragment" Language="cs"><![CDATA[
long l = 0;
foreach (var item in Files) {
var fi = new FileInfo(item.ItemSpec);
l += fi.Length;
}
TotalSize = l;
]]></Code>
</Task>
</UsingTask>
You can actually use a sub-set of .Net API within msbuild projects using inline tasks.
E.g. the following prints out a file size of a single file:
<UsingTask TaskName="GetFileSize" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<FileName Required="true" />
<FileSize ParameterType="System.Int64" Output="true"/>
</ParameterGroup>
<Task>
<Using Namespace="System.IO"/>
<Code Type="Fragment" Language="cs"><![CDATA[
FileInfo fi = new FileInfo(FileName);
FileSize = fi.Length;
]]></Code>
</Task>
</UsingTask>
<Target Name="PrintFileSize" >
<GetFileSize FileName="$(MyFileName)">
<Output TaskParameter="FileSize" PropertyName="MyFileSize" />
</GetFileSize>
<Message Text="file size of $(MyFileName) is $(MyFileSize)" />
</Target>
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)" />