Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use MSBuild Copy Task to Copy To Multiple Destination Folders?

I'm trying to copy a folder recursively to multiple destination folders using MSBuild's Copy task. I've seen the following question which gave me a good start, but I must be missing something:

Msbuild copy to several locations based on list of destination parameter?

A snippet from my build file is below:

<ItemGroup>
    <DeployPath Include="\\server1\path" />
    <DeployPath Include="\\server2\path" />
</Item Group>

<Target Name="Deploy">
    <Message Text="%(DeployPath.Identity)" />
    <Copy SourceFiles="@(ItemsToCopy)" DestinationFolder="%(DeployPath.Identity)\%(RecursiveDir)" />
</Target>

When I run this, the "Message" task, as I would expect, spits out 2 lines:

\\server1\path
\\server2\path

The problem is, the "Copy" task appears to only run once, and copies the files to the root of the current hard drive and not the specified network paths:

Copies to C:\file1.txt instead of \\server1\path\file1.txt

I'm fairly new to MSBuild, so I feel like I'm missing something pretty basic here.

Any help would be greatly appreciated.

like image 625
WayneC Avatar asked Sep 11 '10 03:09

WayneC


2 Answers

What you are dealing with here is known as batching. I have blogged quite a bit about batching. You can find my blogs listed at http://sedotech.com/Resources#Batching. Batching is a way to do a loop without really doing one in MSBuild. You can split groups into values with a common metadata value. Metadata could be values like Identity, FullPath, Filename, etc. You can even make your own metadata. In any case when you batch on more than 1 value they are batched independently of each other. Take a look at the example that I created. The result of executing the target is shown after the script.

<Project ToolsVersion="4.0" DefaultTargets="Demo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <ItemsToCopy Include="src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt"/>
  </ItemGroup>

  <ItemGroup>
    <DeployPath Include="C:\temp\path01\" />
    <DeployPath Include="C:\temp\path02\" />
  </ItemGroup>

  <!--
    Target batching is happening here because there is a 
    %() expression inside the Outputs attribute. So that 
    means that this target will be repeated once per
    uinque batch of %(DeployPath.Identity). Identity is
    the value that is passed in the Include= attribute.
    Since we know there are two values we know that
    this target will be executed twice, and on each 
    pass the DeployPath item will only look to contain
    a single value. If there were duplicates then the list
    could contain more than 1 value.
  -->
  <Target Name="Demo" Outputs="%(DeployPath.Identity)">
    <Message Text="DeployPath.Identity: %(DeployPath.Identity)" />

    <Message Text="======================================" Importance="high"/>
    <Message Text="ItemsToCopy1: @(ItemsToCopy)|| DeployPath.Identity: %(DeployPath.Identity)" />
    <Message Text="======================================" Importance="high"/>
    <!--
      In the next emample you are batching on both the DeployPath item list as well as
      the ItemsToCopy item. When two batched items are in the same expression they are
      matched individually, so you ge a value for DeployPath metadata but not ItemsToCopy
      metadata. That is why your copy only copied to one location.
    -->
    <Message Text="ItemsToCopy2: @(ItemsToCopy)|| DeployPath.Identity-RecursiveDir: %(DeployPath.Identity)\%(RecursiveDir)" />
    <Message Text="======================================" Importance="high"/>
    <!-- 
      In this example I create a property and assign it the value of 
      %(DeployPath.Identity). We know there will only be one such
      value. Because there should only be one value with Identity 
      when this target is executed so it is safe to 
      convert item to property
      
      Because we are not batching on both items we will get the values for both vaules
      to be correct becuase the target is repeated for the other
      DeployPath values.
    -->
    <PropertyGroup>
      <_DeployPathIdentity>%(DeployPath.Identity)</_DeployPathIdentity>
    </PropertyGroup>
    <Message Text="ItemsToCopy3: @(ItemsToCopy)|| _DeployPathIdentity-RecursiveDir: $(_DeployPathIdentity)\%(RecursiveDir)" />

    <!-- 
      I've always preferred to use DestinationFiles so my sample
      below uses that. But you could change the target to use
      DestinationFolder instead.
    -->
    <Copy SourceFiles="@(ItemsToCopy)"
          DestinationFiles="@(ItemsToCopy->'$(_DeployPathIdentity)%(RecursiveDir)%(Filename)%(Extension)')" />
  </Target>

</Project>

Output

Build started 9/10/2010 9:31:28 PM.
Project "I:\Development\My Code\Community\MSBuild\CopyFiles01.proj" on node 1 (default targets).
Demo:
  DeployPath.Identity: C:\temp\path01\
  ======================================
  ItemsToCopy1: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| DeployPath.I
  dentity: C:\temp\path01\
  ======================================
  ItemsToCopy2: || DeployPath.Identity-RecursiveDir: C:\temp\path01\\
  ItemsToCopy2: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| DeployPath.I
  dentity-RecursiveDir: \
  ======================================
  ItemsToCopy3: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| _DeployPathI
  dentity-RecursiveDir: C:\temp\path01\\
  Creating directory "C:\temp\path01".
  Copying file from "src\0001.txt" to "C:\temp\path01\0001.txt".
  Copying file from "src\0002.txt" to "C:\temp\path01\0002.txt".
  Copying file from "src\sub\sub-0001.txt" to "C:\temp\path01\sub-0001.txt".
  Copying file from "src\sub\sub-0002.txt" to "C:\temp\path01\sub-0002.txt".
Demo:
  DeployPath.Identity: C:\temp\path02\
  ======================================
  ItemsToCopy1: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| DeployPath.I
  dentity: C:\temp\path02\
  ======================================
  ItemsToCopy2: || DeployPath.Identity-RecursiveDir: C:\temp\path02\\
  ItemsToCopy2: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| DeployPath.I
  dentity-RecursiveDir: \
  ======================================
  ItemsToCopy3: src\0001.txt;src\0002.txt;src\sub\sub-0001.txt;src\sub\sub-0002.txt|| _DeployPathI
  dentity-RecursiveDir: C:\temp\path02\\
  Creating directory "C:\temp\path02".
  Copying file from "src\0001.txt" to "C:\temp\path02\0001.txt".
  Copying file from "src\0002.txt" to "C:\temp\path02\0002.txt".
  Copying file from "src\sub\sub-0001.txt" to "C:\temp\path02\sub-0001.txt".
  Copying file from "src\sub\sub-0002.txt" to "C:\temp\path02\sub-0002.txt".
Done Building Project "I:\Development\My Code\Community\MSBuild\CopyFiles01.proj" (default targets
).


Build succeeded.
like image 149
Sayed Ibrahim Hashimi Avatar answered Nov 13 '22 15:11

Sayed Ibrahim Hashimi


The most important missing piece in the puzzle seems to be the Outputs attribute on the Target element without which you'll always only execute the target for one item of the whole list. The other piece is the new property you need to define on the way.

The solution to your problem might look like so:

<ItemGroup>
    <DeployPath Include="\\server1\path" />
    <DeployPath Include="\\server2\path" />
</ItemGroup>

<Target Name="Deploy" Outputs="%(DeployPath.Identity)">
    <PropertyGroup>
        <Destination>%(DeployPath.Identity)</Destination>
    </PropertyGroup>
    <Message Text="Processing: '$(Destination)" />
    <Copy SourceFiles="@(ItemsToCopy)"
          DestinationFolder="%(DeployPath.Identity)\%(RecursiveDir)" />
</Target>
like image 22
Oliver Avatar answered Nov 13 '22 16:11

Oliver