16

I have the following piece of msbuild code:

  <PropertyGroup>
    <DirA>C:\DirA\</DirA>
    <DirB>C:\DirB\</DirB>
  </PropertyGroup>

  <Target Name="CopyToDirA"
          Condition="Exists('$(DirA)') AND '@(FilesToCopy)' != ''"
          Inputs="@(FilesToCopy)"
          Outputs="@(FilesToCopy -> '$(DirA)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(DirA)" />
  </Target>

  <Target Name="CopyToDirB"
          Condition="Exists('$(DirB)') AND '@(FilesToCopy)' != ''"
          Inputs="@(FilesToCopy)"
          Outputs="@(FilesToCopy -> '$(DirB)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(DirB)" />
  </Target>

  <Target Name="CopyFiles" DependsOnTargets="CopyToDirA;CopyToDirB"/>

So invoking the target CopyFiles copies the relevant files to $(DirA) and $(DirB), provided they are not already there and up-to-date.

But the targets CopyToDirA and CopyToDirB look identical except one copies to $(DirA) and the other - to $(DirB). Is it possible to unify them into one target first invoked with $(DirA) and then with $(DirB)?

Thanks.

alexandrul
  • 12,856
  • 13
  • 72
  • 99
mark
  • 59,016
  • 79
  • 296
  • 580

3 Answers3

15

You should be able to generate an ItemGroup containing the Dirs and then % on that.

<ItemGroup>
    <Dirs Include="C:\DirA\;C:\DirB\">
</ItemGroup>
<Target Name="CopyFiles"
    Condition="Exists('%(Dirs)') AND '@(FilesToCopy)' != ''"
    Inputs="@(FilesToCopy)"
    Outputs="@(FilesToCopy -> '%(Dirs)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(FilesToCopy)" DestinationFolder="%(Dirs)" />
</Target>

Or you can do 2 explicit calls:

<Target Name="CopyFiles">
    <MsBuild Projects="$(MSBuildProjectFullPath)" Targets="CopyASetOfFiles" Properties="FilesToCopy=@(FilesToCopy);DestDir=$(DirA)" />
    <MsBuild Projects="$(MSBuildProjectFullPath)" Targets="CopyASetOfFiles" Properties="FilesToCopy=@(FilesToCopy);DestDir=$(DirB)" />
</Target>

<Target Name="CopyASetOfFiles"
    Condition="Exists('$(DestDir)') AND '@(FilesToCopy)' != ''"
    Inputs="@(FilesToCopy)"
    Outputs="@(FilesToCopy -> '$(DestDir)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(DestDir)" />
</Target>

I haven't tested either syntax, but am relatively more confident of the second.

(The answer, if there is one, is in my Sayed Hashimi book on my desk - you'll have to wait until the first of:

  1. Get the book
  2. I get bored
  3. Sayed finds this post and comes up with a brilliant tested answer)
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • Hi Ruben. Could you expand the ... in your response? I am not that familiar with parameterized targets to understand what you mean. Thanks. – mark Aug 19 '09 at 08:40
  • @mark: done. Reason I didnt do it before is twofold 1. Didnt have a nice editor to hand. 2. not sure on syntax of first sample. Hope this helps. Highly recommend the Inside MSBuild book – Ruben Bartelink Aug 19 '09 at 09:36
  • I think that there is a problem with solution no. 2. If your build script is invoked with specific properties from the command line, those properties are not passed when you use the task. – Pete Oct 04 '12 at 07:31
  • I've replaced `$(MSBuildProjectFile)` with `$(MSBuildProjectFullPath)` to make it clearer that the CopyFiles target is forking another MSBuild to call the `Target` directly below *and hence (unless there are `InitialTargets` in play), the only target that will ever run is `CopyASetOfFiles`, which only requires the two `Properties` that are passed (viz. `FilesToCopy` and `DestDir`)*. The key thing that jumps out wrt polishing the answer for me is to `BuildInParallel="true"` and `%`-batch over the `Dirs` to make them parallelizable. Hope you agree with my reasoning and I'm not missing something! – Ruben Bartelink Oct 04 '12 at 08:54
  • @Pete: The main problem with solution #2 is that it's pointless frivolity if the only concern is to copy files and/or do anything that can be batched within the `Target` - the cases where one needs to have copy-and-paste-and-slightly-modify `Targets` or fork another MSBuild are a lot rarer than one might think -- i.e., #1 is more idiomatic :D – Ruben Bartelink Oct 04 '12 at 08:55
9

As someone already mentiond the answer is batching.

Here are some links:

Sayed Ibrahim Hashimi
  • 43,864
  • 17
  • 144
  • 178
4

Yes, what you want is called batching in MSBuild. The

;%(Dirs.Identity)

Defined in the Outputs will cause this task to be executed for each item in the Dirs ItemGroup.

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="CopyFiles"
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003" 
   ToolsVersion="3.5">

<ItemGroup>
    <Dirs Include="C:\DirA" />
    <Dirs Include="C:\DirB" />
</ItemGroup>

<Target Name="CopyFiles"
    Inputs="@(FilesToCopy);@(Dirs)"
    Outputs="@(FilesToCopy -> '%(Dirs.Identity)%(Filename)%(Extension)');%(Dirs.Identity)" >
    <Message Text="%(Dirs.Identity)" />
</Target>

</Project>

Outputs:

Build started 8/19/2009 10:11:57 PM.
Project "D:\temp\test.proj" on node 0 (default targets).
  C:\DirA
CopyFiles:
  C:\DirB
Done Building Project "D:\temp\test.proj" (default targets).

Change the Message task to Copy task with the following condition and you are done:

Condition="Exists('%(Dirs.Identity)') AND '@(FilesToCopy)' != ''"
Todd
  • 5,017
  • 1
  • 25
  • 16