MSBuild Pivoting ItemGroup on Delimited String - msbuild

I have an ItemGroup of installers and each one contains a Property which is the name of out output folder (delimited) like this:
<ItemGroup>
<Installers Include="Installer1.msi">
<Folders>Folder1,Folder4</Folders>
</Installers>
<Installers Include="Installer2.msi">
<Folders>Folder1,Folder2,Folder3,Folder4</Folders>
</Installers>
</ItemGroup>
I would like to be able to copy each installer into each folder that's specified. I've been back and forth trying to figure out a way to add additional metadata to Installers, but just can't seem to get a final list of something like this:
Installer1.msi:Folder1
Installer1.msi:Folder4
Installer2.msi:Folder1
Installer2.msi:Folder2
Installer2.msi:Folder3
Installer2.msi:Folder4
So I could execute something like this:
<Copy SourceFiles="#(Installers)" DestinationFolder="%(Installers.Folder)" />
I know I can modify my ItemGroup to this:
<ItemGroup>
<Installers Include="Installer1.msi">
<Folder>Folder1</Folder>
</Installers>
<Installers Include="Installer1.msi">
<Folder>Folder4</Folder>
</Installers>
<Installers Include="Installer2.msi">
<Folder>Folder1</Folder>
</Installers>
<Installers Include="Installer2.msi">
<Folder>Folder2</Folder>
</Installers>
<Installers Include="Installer2.msi">
<Folder>Folder3</Folder>
</Installers>
<Installers Include="Installer2.msi">
<Folder>Folder4</Folder>
</Installers>
</ItemGroup>
but I'd rather not, as that's what we have now and people always miss things (because it's so verbose).

Separate folder names with semicolons, then invert the installer and folder values:
<Project ToolsVersion="4.0" DefaultTargets="Inversionator" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Folders Include="Folder1;Folder4">
<Installer>Installer1.msi</Installer>
</Folders>
<Folders Include="Folder1;Folder2;Folder3;Folder4">
<Installer>Installer2.msi</Installer>
</Folders>
</ItemGroup>
<Target Name="Inversionator">
<Message Text="%(Folders.Installer) %(Folders.Identity) " Importance="high" />
</Target>
</Project>
Output for the task is now:
Installer1.msi Folder1
Installer1.msi Folder4
Installer2.msi Folder1
Installer2.msi Folder2
Installer2.msi Folder3
Installer2.msi Folder4

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>

How to give a different OutputPath per project per build configuration with MSBuild?

Multiple projects have to be build with one ore more configurations (debug/release/...).
The output of the build needs to be copied to a folder (BuildOutputPath).
There is a default BuildOutputFolder, but for some project you can indicate that the output needs to be put in a extra child folder.
For example:
Configuration are:
- debug
- release
The projects are:
Project1 (BuildOutputFolder)
Project2 (BuildOutputFolder)
Project3 (BuildOutputFolder\Child)
The end result should look like this:
\\BuildOutput\
debug\
project1.dll
project2.dll
Child\
Project3.dll
release\
project1.dll
project2.dll
Child\
Project3.dll
I got this far atm, but can't figure out how to override the OutputPath per project.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="Build" >
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<PropertyGroup>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</PropertyGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" />
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)" />
</Target>
</Project>
How would you accomplish this in a MSBuild project file ?
Your'e attempting to call a task recursively in two different contexts. 2 configurations and 3 projects requires 6 calls to the build task. You need to layout the project in such a way that for each item in ConfigList a call is made multiplied by each item in Projects.
Also use ItemDefinitionGroup to set default shared properties:
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<ItemDefinitionGroup>
<Projects>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</Projects>
</ItemDefinitionGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" >
<Subfolder>Child</Subfolder>
</Projects>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_BuildSingleConfiguration"
Properties="Configuration=%(ConfigList.Identity)" />
</Target>
<Target Name="_BuildSingleConfiguration">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=$(Configuration);OutputPath=%(Projects.BuildOutputPath)$(Configuration)\%(Projects.Subfolder)" />
</Target>
</Project>
Try to do it using Project metadata
<ItemGroup>
<Projects Include="project1.csproj">
<ChildFolder/>
</Project>
<Projects Include="project2.csproj">
<ChildFolder/>
</Project>
<Projects Include="project3.csproj">
<ChildFolder>Child</ChildFolder>
</Project>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)%(Project.ChildFolder)" />

How to create copying items from property values?

Let's say I have a list of sub paths such as
<PropertyGroup>
<subPaths>$(path1)\**\*; $(path2)\**\*; $(path3)\file3.txt; </subPaths>
</PropertyGroup>
I want to copy these files from folder A to folder B (surely we already have all the sub folders/files in A). What I try was:
<Target Name="Replace" DependsOnTargets="Replace_Init; Replace_Copy1Path">
</Target>
<Target Name="Replace_Init">
<PropertyGroup>
<subPaths>$(path1)\**\*; $(path2)\**\*; $(path3)\file3.txt; </subPaths>
</PropertyGroup>
<ItemGroup>
<subPathItems Include="$(subPathFiles.Split(';'))" />
</ItemGroup>
</Target>
<Target Name="Replace_Copy1Path" Outputs="%(subPathItems.Identity)">
<PropertyGroup>
<src>$(folderA)\%(subPathItems.Identity)</src>
<dest>$(folderB)\%(subPathItems.Identity)</dest>
</PropertyGroup>
<Copy SourceFiles="$(src)" DestinationFiles="$(dest)" />
</Target>
But the Copy task didn't work. It doesn't translate the **\* to files. What did I do wrong? Please help!
I don't think you can do something like that.
$(subPathFiles.Split(';')) returns a property where value are separated by semicolon, so this call is useless.
If you want to keep this mechanism you should use the task StringToItemCol from MSBuild Extension Pack :
<Target Name="Replace_Init">
<PropertyGroup>
<subPaths>$(path1)\**\*; $(path2)\**\*; $(path3)\file3.txt; </subPaths>
</PropertyGroup>
<MsBuildHelper TaskAction="StringToItemCol"
ItemString="$(subPaths)" Separator=";">
<Output TaskParameter="OutputItems" ItemName="subPathItems "/>
</MsBuildHelper>
</Target>
Otherwise, you could directly pass items with folderA and subPaths embedded :
<ItemGroup>
<subPathIt Include="$(folderA)\$(path1)\**\*"/>
<subPathIt Include="$(folderA)\$(path2)\**\*"/>
<subPathIt Include="$(folderA)\$(path3)\file3.txt" Condition="Exists('$(path3)\file3.txt')"/>
</ItemGroup>
<Target Name="Replace_Copy1Path">
<Copy SourceFiles="#(subPathItems )"
DestinationFiles="$(folderB)\%(RecursiveDir)\%(Filename)%(Extension)" />
</Target>

TFS Build SourceTfs.Checkout

I am trying to get my build to checkout some files (using Microsoft.Sdc.Common.tasks) and then to check them in after the build has finished.
But I can't seem to get this working at all, let alone before and after the build.
Whaereabouts should this sort of code live?
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild;MyProjectDbUpdate" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<PropertyGroup>
<TasksPath>C:\Program Files\MSBuild\sdc\</TasksPath>
</PropertyGroup>
<Import Project="$(TasksPath)\Microsoft.Sdc.Common.tasks" />
<Target Name="MyProjectDbUpdate">
<Message Text="MyProjectDbUpdate checkin start"/>
<SourceTfs.Checkout Path="$/MyProject/Code/MyProjectDbUpdate" TfsVersion="2008" workingDirectory="C:\buildagent\MyProject\ContinuousIntegration\Sources\Code" />
<SourceTfs.Checkin Path="$/MyProject/Code/MyProjectDbUpdate" workingDirectory="C:\buildagent\MyProject\ContinuousIntegration\Sources\Code" Comments="Build checkout/checkin." TfsVersion="2008" Override="Build overrides checkin policy" />
<Message Text="MyProjectDbUpdate checkin complete"/>
</Target>
<ProjectExtensions>
<ProjectFileVersion>2</ProjectFileVersion>
<Description>Build</Description>
<BuildMachine>MYSERVER</BuildMachine>
</ProjectExtensions>
<PropertyGroup>
<TeamProject>MyProject</TeamProject>
<BuildDirectoryPath>c:\buildagent\MyProject\ContinuousIntegration</BuildDirectoryPath>
<DropLocation>\\UNKNOWN\drops</DropLocation>
<RunTest>false</RunTest>
<RunCodeAnalysis>Never</RunCodeAnalysis>
<WorkItemType>Bug</WorkItemType>
<WorkItemFieldValues>System.Reason=Build Failure;System.Description=Start the build using Team Build</WorkItemFieldValues>
<WorkItemTitle>Build failure in build:</WorkItemTitle>
<DescriptionText>This work item was created by Team Build on a build failure.</DescriptionText>
<BuildlogText>The build log file is at:</BuildlogText>
<ErrorWarningLogText>The errors/warnings log file is at:</ErrorWarningLogText>
<UpdateAssociatedWorkItems>true</UpdateAssociatedWorkItems>
<AdditionalVCOverrides></AdditionalVCOverrides>
<CustomPropertiesForClean></CustomPropertiesForClean>
<CustomPropertiesForBuild></CustomPropertiesForBuild>
</PropertyGroup>
<ItemGroup>
<SolutionToBuild Include="$(BuildProjectFolderPath)/../../Code/MyProject.sln">
<Targets></Targets>
<Properties></Properties>
</SolutionToBuild>
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<ItemGroup>
</ItemGroup>
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
Specifying your target as a default target is not going to call it as Team build explicity sets the target it is going to call.
Try renaming the Target to AfterGet or overriding the the GetDependsOn property to include your target
<GetDependsOn>
$(GetDependsOn)
MyProjectDbUpdate;
</GetDependsOn>