Add Custom Metadata to Already-defined ItemGroup from Another ItemGroup - msbuild

I have the following:
<ItemGroup>
<Files Include="C:\Versioning\**\file.version" />
<ItemGroup>
<ReadLinesFromFile File="%(Files.Identity)">
<Output TaskParameter="Lines" ItemName="_Version"/>
</ReadLinesFromFile>
where each file.version file contains simply one line which is - you guessed it - a version of the form Major.Minor.Build.Revision.
I want to be able to associate each item in the Files ItemGroup with its _Version by adding the latter as metadata, so that I can do something like:
<Message Text="%(Files.Identity): %(Files.Version)" />
and have MSBuild print out a nice list of file-version associations.
Is this possible?

This can be achieved by using target batching to add your Version member to the metadata. This involves moving your ReadLinesFromFile operation to its own target, using the #(Files) ItemGroup as an input.
This causes the target to be executed for each item in your ItemGroup, allowing you to read the contents (i.e. version number) from each individual file and subsequently update that item to add the Version metadata:
<Project DefaultTargets="OutputFilesAndVersions" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Files Include="C:\Versioning\**\file.version" />
</ItemGroup>
<Target Name="OutputFilesAndVersions" DependsOnTargets="RetrieveVersions">
<Message Text="#(Files->'%(Identity): %(Version)')" />
</Target>
<Target Name="RetrieveVersions" Inputs="#(Files)" Outputs="%(Files.Identity)">
<ReadLinesFromFile File="%(Files.Identity)">
<Output TaskParameter="Lines" PropertyName="_Version"/>
</ReadLinesFromFile>
<PropertyGroup>
<MyFileName>%(Files.Identity)</MyFileName>
</PropertyGroup>
<ItemGroup>
<Files Condition="'%(Files.Identity)'=='$(MyFileName)'">
<Version>$(_Version)</Version>
</Files>
</ItemGroup>
</Target>
</Project>

Related

msbuild to copy to multiple locations defined in item metadata

I have an item with metadata I want to copy to perform some actions on and the result to occur in multiple locations,
for example, I want to copy the file to multiple locations:
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
</ItemGroup>
how would I setup a target to create c:\blah and c:\test if they dont exist, then copy myFile.txt to c:\blah\myFile.txt and c:\test\myFile.txt
I also want to get the list of full output paths (c:\blah\myFile.txt and c:\test\myFile.txt) if I want to clean them during a clean.
If you dont want to change the structure of you ItemGroup, you need to handle that you have a nested ItemGroup (the MetaDataElement Out). Therefor you will need to batch the ItemGroup MyItem to the target and inside you can batch Out. I made a small example project:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="CopyFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
<MyItem Include="myFile2.txt">
<Out>c:\blah2;c:\test2</Out>
</MyItem>
</ItemGroup>
<Target Name="CopyFiles"
Inputs="%(MyItem.Identity)"
Outputs="%(MyItem.Identity)\ignore_this.msg">
<PropertyGroup>
<File>%(MyItem.Identity)</File>
</PropertyGroup>
<ItemGroup>
<Folders Include="%(MyItem.Out)" />
</ItemGroup>
<Message Text="%(Folders.Identity)\$(File)" />
</Target>
</Project>
The Output will be:
Project "D:\TEMP\test.proj" on node 1 (default targets).
CopyFiles:
c:\blah\myFile.txt
c:\test\myFile.txt
CopyFiles:
c:\blah2\myFile2.txt
c:\test2\myFile2.txt
Done Building Project "D:\TEMP\test.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
What you want to do is a concept called MSBuild Batching.
It allows you to divide item lists into different batches and pass each of those batches into a task separately.
<Target Name="CopyFiles">
<ItemGroup Label="MyFolders">
<Folder Include="c:\blah" />
<Folder Include="C:\test" />
</ItemGroup>
<Copy SourceFiles="myFile.txt" DestinationFolder="%(Folder.Identity)\">
<Output TaskParameter="CopiedFiles" ItemName="FilesCopy" />
</Copy>
</Target>
How about this:
<Target Name="CopyFiles">
<!--The item(s)-->
<ItemGroup>
<MyItem Include="myFile.txt"/>
</ItemGroup>
<!--The destinations-->
<ItemGroup>
<MyDestination Include="c:\blah"/>
<MyDestination Include="c:\test"/>
</ItemGroup>
<!--The copy-->
<Copy SourceFiles="#(MyItem)" DestinationFolder="%(MyDestination.FullPath)" />
<ItemGroup>
<FileWrites Include="%(MyDestination.FullPath)\*" />
</ItemGroup>
<!--The output -->
<Message Text="FileWrites: #(FileWrites)" Importance="high"/>
</Target>

MS Build copy a list of directories stored in Item

I have a text file which contains some locations of the files which I want to copy to a temp directory
---- List.txt ----
Build\Java
Build\Classes
Now, I am fetching this list into an Item
<ReadLinesFromFile File="List.txt" >
<Output TaskParameter="Lines"
ItemName="DirectoryList" />
</ReadLinesFromFile>
Now, In order to append the full path, and add some excludes, I am again storing it into another ItemGroup:
<ItemGroup>
<PackageList Include="$(BuildPath)\%(DirectoryList.Identity)\**\*.*"
Exclude="$(BuildPath)\%(DirectoryList.Identity)\**\*.pdb" />
</ItemGroup>
<Copy SourceFiles="#(PackageList)"
DestinationFiles="#(PackageList->'$(PackageTemp)\%(SourceDirectory)\%(DirInPackage)%(RecursiveDir)%(Filename)%(Extension)')" />
ISSUE:
Actual Dir -
C:\Work\Build\Java\Debug
C:\Work\Build\Java\Release
C:\Work\Build\Classes\*.class
Content in O/p
C:\temp\Debug
C:\temp\Release
C:\temp\*.class
How to make it copy the corresponding "Java" and "Classes" folder also?
You missed just a few moments in your script. First, you need to create a directory from #(PackageList). Second, in Copy Task when you set DestinationFiles you should specify subdirectory explicitly.
Take a look. That scrip does the job as you need. And it holds internal structure of all your subdirectories, specified by wildcard. For example, Java\Debug\Component1\file.ext
<Project DefaultTargets="CopyDirectories" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<BuildPath>.</BuildPath>
<SourceDirectoryListFile>Directories.txt</SourceDirectoryListFile>
<DestinationDirectory>temp</DestinationDirectory>
</PropertyGroup>
<Target Name="ReadDirectoryList">
<ReadLinesFromFile File="$(SourceDirectoryListFile)" >
<Output TaskParameter="Lines"
ItemName="DirectoryList" />
</ReadLinesFromFile>
</Target>
<Target Name="CopyDirectories" DependsOnTargets="ReadDirectoryList"
Outputs="%(DirectoryList.Identity)">
<PropertyGroup>
<ProcessingDirectory>%(DirectoryList.Identity)</ProcessingDirectory>
</PropertyGroup>
<ItemGroup>
<PackageList Include="$(BuildPath)\$(ProcessingDirectory)\**\*.*"
Exclude="$(BuildPath)\$(ProcessingDirectory)\**\*.pdb" />
</ItemGroup>
<MakeDir Directories="$(ProcessingDirectory)" />
<Copy SourceFiles="#(PackageList)"
DestinationFiles="#(PackageList->'$(DestinationDirectory)\$(ProcessingDirectory)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
Arpit,
You can use a kind of reversed solution: keep in List.txt the dirs you want excluded from copy.
Based on this you can create your copyfileslist using 2 sets of dirs.
So my solution looks like this:
---- List.txt ---- dirs to be excluded ---
Demos\AccessDatabase
Demos\ActiveDirectoryMsi
Demos\JavaToolsMsi
Demos\JavaToolsMsi\Data
Demos\LocalUserGroupsMsi
Demos\MSSQLDatabase
Demos\StringToolsMsi
Demos\SystemToolsMsi
Demos\TemplateFilesMsi
Demos\UserPrivilegesMsi
Demos\WindowsServiceMsi
Common
CustomActions
Framework
Tools
Version
WixExtensions
My msbuild.proj:
<Project DefaultTargets="run" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<Target Name="run">
<PropertyGroup>
<BuildPath>c:\tmp\msiext\msiext-1.3\trunk\src</BuildPath>
<PackageTemp>c:\tmp\</PackageTemp>
</PropertyGroup>
<ReadLinesFromFile File="List.txt" >
<Output TaskParameter="Lines"
ItemName="DirectoryList" />
</ReadLinesFromFile>
<Message Text="DirectoryList: #(DirectoryList)" />
<ItemGroup>
<PackageList Include="$(BuildPath)\%(DirectoryList.Identity)\**\*.*"
Exclude="$(BuildPath)\%(DirectoryList.Identity)\**\*.sql" />
</ItemGroup>
<!--<Message Text="PackageList: #(PackageList)" />-->
<Message Text="----------------------------------------------------------------------------" />
<CreateItem Include="$(BuildPath)\**\*.*" Exclude="#(PackageList)">
<Output TaskParameter="Include" ItemName="NeededFiles"/>
</CreateItem>
<Message Text="NeededFiles: #(NeededFiles)" />
<Message Text="----------------------------------------------------------------------------" />
<Copy SourceFiles="#(NeededFiles)" DestinationFiles="#(NeededFiles->'$(PackageTemp)\%(RecursiveDir)\%(Filename)%(Extension)')" />
</Target>
</Project>

In MSBuild, can I use the String.Replace function on a MetaData item?

In MSBuild v4 one can use functions (like string.replace) on Properties. But how can I use functions on Metadata?
I'd like to use the string.replace function as below:
<Target Name="Build">
<Message Text="#(Files->'%(Filename).Replace(".config","")')" />
</Target>
Unfortunately this outputs as (not quite what I was going for):
log4net.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration.Replace(".config","");dataProductsGraphConfiguration.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration
Any thoughts?
You can do this with a little bit of trickery:
$([System.String]::Copy('%(Filename)').Replace('config',''))
Basically, we call the static method 'Copy' to create a new string (for some reason it doesn't like it if you just try $('%(Filename)'.Replace('.config',''))), then call the replace function on the string.
The full text should look like this:
<Target Name="Build">
<Message Text="#(Files->'$([System.String]::Copy("%(Filename)").Replace(".config",""))')" />
</Target>
Edit: MSBuild 12.0 seems to have broken the above method. As an alternative, we can add a new metadata entry to all existing Files items. We perform the replace while defining the metadata item, then we can access the modified value like any other metadata item.
e.g.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Files Include="Alice.jpg"/>
<Files Include="Bob.not-config.gif"/>
<Files Include="Charlie.config.txt"/>
</ItemGroup>
<Target Name="Build">
<ItemGroup>
<!--
Modify all existing 'Files' items so that they contain an entry where we have done our replace.
Note: This needs to be done WITHIN the '<Target>' (it's a requirment for modifying existing items like this
-->
<Files>
<FilenameWithoutConfig>$([System.String]::Copy('%(Filename)').Replace('.config', ''))</FilenameWithoutConfig>
</Files>
</ItemGroup>
<Message Text="#(Files->'%(FilenameWithoutConfig)')" Importance="high" />
</Target>
</Project>
Result:
D:\temp>"c:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" /nologo test.xml
Build started 2015/02/11 11:19:10 AM.
Project "D:\temp\test.xml" on node 1 (default targets).
Build:
Alice;Bob.not-config;Charlie
Done Building Project "D:\temp\test.xml" (default targets).
I needed to do something similar, the following worked for me.
<Target Name="Build">
<Message Text="#(Files->'%(Filename)'->Replace('.config', ''))" />
</Target>
Those functions works in properties only (as I know). So create target which will perform operation throw batching:
<Target Name="Build"
DependsOnTargets="ProcessFile" />
<Target Name="ProcessFile"
Outputs="%(Files.Identity)">
<PropertyGroup>
<OriginalFileName>%(Files.Filename)</OriginalFileName>
<ModifiedFileName>$(OriginalFileName.Replace(".config",""))</ModifiedFileName>
</PropertyGroup>
<Message Text="$(ModifiedFileName)" Importance="High"/>
</Target>
Do you really need in your example such kind of task? I mean there exists MSBuild Well-known Item Metadata
EDIT: I should specify that this task processes all items in #(Files).
i dont think you can use functions directly with itemgroups and metadata (that would be easy)
However you can use batching:
Taking the ideas from this post:
array-iteration
I was trying to trim an itemgroup to send to a commandline tool (i needed to lose .server off the filename)
<Target Name="ProcessFile" DependsOnTargets="FullPaths">
<ItemGroup>
<Environments Include="$(TemplateFolder)\$(Branch)\*.server.xml"/>
</ItemGroup>
<MSBuild Projects=".\Configure.msbuild"
Properties="CurrentXmlFile=%(Environments.Filename)"
Targets="Configure"/>
</Target>
<Target Name="Configure" DependsOnTargets="FullPaths">
<PropertyGroup>
<Trimmed>$(CurrentXmlFile.Replace('.server',''))</Trimmed>
</PropertyGroup>
<Message Text="Trimmed: $(Trimmed)"/>
<Exec Command="ConfigCmd $(Trimmed)"/>
</Target>
For MSBuild 12.0, here's an alternative.
<Target Name="Build">
<Message Text="$([System.String]::Copy("%(Files.Filename)").Replace(".config",""))" />
</Target>
Got the same problem (except with MakeRelative), so I passed with another solution : Using good old CreateItem that take a string and transform to Item :)
<ItemGroup>
<_ToUploadFTP Include="$(PublishDir)**\*.*"></_ToUploadFTP>
</ItemGroup>
<CreateItem Include="$([MSBuild]::MakeRelative('c:\$(PublishDir)','c:\%(relativedir)%(filename)%(_ToUploadFTP.extension)'))">
<Output ItemName="_ToUploadFTPRelative" TaskParameter="Include"/>
</CreateItem>
<FtpUpload Username="$(UserName)"
Password="$(UserPassword)"
RemoteUri="$(FtpHost)"
LocalFiles="#(_ToUploadFTP)"
RemoteFiles="#(_ToUploadFTPRelative->'$(FtpSitePath)/%(relativedir)%(filename)%(extension)')"
UsePassive="$(FtpPassiveMode)" ></FtpUpload>

MSBuild Copy task not copying files the first time round

I created a build.proj file which consists of a task to copy files that will be generated after the build is complete. The problem is that these files are not copied the first time round and I have to run msbuild again on the build.proj so that the files can be copied. Please can anyone tell me whats wrong with the following build.proj file:
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<SourcePath Condition="'$(SourcePath)' == ''">$(MSBuildProjectDirectory)</SourcePath>
<BuildDir>$(SourcePath)\build</BuildDir>
</PropertyGroup>
<ItemGroup>
<Projects
Include="$(SourcePath)\src\myApp\application.csproj">
</Projects>
</ItemGroup>
<Target Name="Build">
<Message text = "Building project" />
<MSBuild
Projects="#(Projects)"
Properties="Configuration=$(Configuration)" />
</Target>
<ItemGroup>
<OutputFiles Include ="$(MSBuildProjectDirectory)\**\**\bin\Debug\*.*"/>
</ItemGroup>
<Target Name="CopyToBuildFolder">
<Message text = "Copying build items" />
<Copy SourceFiles="#(OutputFiles)" DestinationFolder="$(BuildDir)"/>
</Target>
<Target Name="All"
DependsOnTargets="Build; CopyToBuildFolder"/>
</Project>
The itemgroups are evaluated when the script is parsed. At that time your files aren't there yet. To be able to find the files you'll have to fill the itemgroup from within a target.
<!-- SQL Scripts which are needed for deployment -->
<Target Name="BeforeCopySqlScripts">
<CreateItem Include="$(SolutionRoot)\04\**\Databases\**\*.sql">
<Output ItemName="CopySqlScript" TaskParameter="Include"/>
</CreateItem>
</Target>
This example creates the ItemGroup named "CopySqlScript" using the expression in the Include attribute.
Edit:
Now I can read your script: add the CreateItem tag within your CopyToBuildFolder target

Updating Assembly information with MSBuild failing

All
i am trying to automatically update the assembly information of a project using AssemblyInfo task, before build however the target appears to do nothing (no failure/error) just no update/creation
Below is the build.proj file I am using (obviously some contents altered)
Can anyone help?
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Microsoft.VersionNumber.targets"/>
<PropertyGroup>
<Major>1</Major>
<Minor>0</Minor>
<Build>0</Build>
<Revision>0</Revision>
</PropertyGroup>
<PropertyGroup>
<BuildDir>C:\svn\Infrastructure</BuildDir>
</PropertyGroup>
<ItemGroup>
<SolutionsToBuild Include="Infrastructure.sln"/>
</ItemGroup>
<Target Name="Build" DependsOnTargets="ChangeDataAccessAssemblyInfo">
<RemoveDir Directories="$(BuildDir)\Builds" Condition="Exists('$(BuildDir)\Builds')" />
<MSBuild Projects="#(SolutionsToBuild)" Properties="Configuration=Debug" Targets="Rebuild" />
</Target>
<ItemGroup>
<TestAssemblies Include="Build\Logging\Logging.UnitTests.dll" />
</ItemGroup>
<!--<UsingTask TaskName="NUnit" AssemblyFile="$(teamcity_dotnet_nunitlauncher_msbuild_task)" />
<Target Name="Test" DependsOnTargets="Build">
<NUnit NUnitVersion="NUnit-2.4.6" Assemblies="#(TestAssemblies)" />
</Target>-->
<Target Name="ChangeDataAccessAssemblyInfo" >
<Message Text="Writing ChangeDataAccessAssemblyInfo file for 1"/>
<Message Text="Will update $(BuildDir)\DataAccess\My Project\AssemblyInfo.vb" />
<AssemblyInfo CodeLanguage="VB"
OutputFile="$(BuildDir)\DataAccess\My Project\AssemblyInfo_new.vb"
AssemblyTitle="Data Access Layer"
AssemblyDescription="Message1"
AssemblyCompany="http://somewebiste"
AssemblyProduct="the project"
AssemblyCopyright="Copyright notice"
ComVisible="true"
CLSCompliant="true"
Guid="hjhjhkoi-9898989"
AssemblyVersion="$(Major).$(Minor).1.1"
AssemblyFileVersion="$(Major).$(Minor).5.7"
Condition="$(Revision) != '0' "
ContinueOnError="false" />
<Message Text="Updated Assembly File Info"
ContinueOnError="false"/>
</Target>
</Project>
I think you are missing the specification of the AssemblyInfoFiles attribute on your AssemblyInfo task. Here's how it looks on a project I'm working on...
<Target Name="AfterGet">
<Message Text="In After Get"/>
<CreateItem Include="$(SolutionRoot)\Source\SomeProject\My Project\AssemblyInfo.vb">
<Output ItemName="AssemblyInfoFiles" TaskParameter="Include"/>
</CreateItem>
<Attrib Files="#(AssemblyInfoFiles)"
ReadOnly="false"/>
<AssemblyInfo AssemblyInfoFiles="#(AssemblyInfoFiles)"
AssemblyDescription="$(LabelName)">
</AssemblyInfo>
</Target>
What we're doing is first using to create a property that contains the name of the file we'll be updating. We have to do this via createItem because when we start the build the file doesn't exist (and that is when MSBuild evaluates the and definitions in your build file.
We then take the readonly bit off the file.
Finally we invoke the AssemblyInfo task passing it the file(s) to update and a custom assembly name that we want to give it (in this case we put the TFS build label into the Assembly Description field so that we can easily tell which team build the assembly came from.